From b7f934de0d5ded5a120685d92ae07c2eb54b9ff1 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sun, 6 Dec 2015 18:41:34 +0200 Subject: Added a new error, 'relative-beyond-top-level'. This is emitted when a relative import was attempted beyond the top level package. For instance, if a package has X levels, trying to climb X + n levels with a relative import, as in `from ..stuff import Stuff`, will result in an error. Closes issue #588. --- ChangeLog | 12 ++++++++--- pylint/checkers/imports.py | 26 ++++++++++++++++++------ pylint/test/regrtest_data/beyond_top/__init__.py | 6 ++++++ pylint/test/regrtest_data/beyond_top/data.py | 1 + pylint/test/unittest_checker_imports.py | 22 +++++++++++++++++++- 5 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 pylint/test/regrtest_data/beyond_top/__init__.py create mode 100644 pylint/test/regrtest_data/beyond_top/data.py diff --git a/ChangeLog b/ChangeLog index 082c558..c9410a9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,10 +2,16 @@ ChangeLog for Pylint -------------------- -- - * Accept only functions and methods for the deprecated-method checker. + + * Added a new error, 'relative-beyond-top-level', which is emitted + when a relative import was attempted beyond the top level package. - This prevents a crash which can occur when an object doesn't have - .qname() method after the inference. + Closes issue #588. + + * Accept only functions and methods for the deprecated-method checker. + + This prevents a crash which can occur when an object doesn't have + .qname() method after the inference. * Don't emit super-on-old-class on classes with unknown bases. Closes issue #721. diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index 5863130..2fdcc85 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -88,6 +88,14 @@ def _get_first_import(node, context, name, base, level): if found and not are_exclusive(first, node): return first + +def _ignore_import_failure(node, modname, ignored_modules): + for submodule in _qualified_names(modname): + if submodule in ignored_modules: + return True + + return node_ignores_exception(node, ImportError) + # utilities to represents import dependencies as tree and dot graph ########### def _make_tree_defs(mod_files_list): @@ -161,6 +169,10 @@ MSGS = { 'import-error', 'Used when pylint has been unable to import a module.', {'old_names': [('F0401', 'import-error')]}), + 'E0402': ('Attempted relative import beyond top-level package', + 'relative-beyond-top-level', + 'Used when a relative import tries to access too many levels ' + 'in the current package.'), 'R0401': ('Cyclic import (%s)', 'cyclic-import', 'Used when a cyclic import between two or more modules is \ @@ -461,16 +473,18 @@ given file (report RP0402 must not be disabled)'} def _get_imported_module(self, importnode, modname): try: return importnode.do_import_module(modname) - except astroid.AstroidBuildingException as exc: - for submodule in _qualified_names(modname): - if submodule in self._ignored_modules: - return None + except astroid.TooManyLevelsError: + if _ignore_import_failure(importnode, modname, self._ignored_modules): + return None + + self.add_message('relative-beyond-top-level', node=importnode) - if node_ignores_exception(importnode, ImportError): + except astroid.AstroidBuildingException as exc: + if _ignore_import_failure(importnode, modname, self._ignored_modules): return None dotted_modname = _get_import_name(importnode, modname) - self.add_message("import-error", args=repr(dotted_modname), + self.add_message('import-error', args=repr(dotted_modname), node=importnode) def _check_relative_import(self, modnode, importnode, importedmodnode, diff --git a/pylint/test/regrtest_data/beyond_top/__init__.py b/pylint/test/regrtest_data/beyond_top/__init__.py new file mode 100644 index 0000000..401fe06 --- /dev/null +++ b/pylint/test/regrtest_data/beyond_top/__init__.py @@ -0,0 +1,6 @@ +from ... import Something +from . import data +try: + from ... import Lala +except ImportError: + pass \ No newline at end of file diff --git a/pylint/test/regrtest_data/beyond_top/data.py b/pylint/test/regrtest_data/beyond_top/data.py new file mode 100644 index 0000000..9ec64d2 --- /dev/null +++ b/pylint/test/regrtest_data/beyond_top/data.py @@ -0,0 +1 @@ +Anything = 42 \ No newline at end of file diff --git a/pylint/test/unittest_checker_imports.py b/pylint/test/unittest_checker_imports.py index f5e6bbb..0069be2 100644 --- a/pylint/test/unittest_checker_imports.py +++ b/pylint/test/unittest_checker_imports.py @@ -1,6 +1,8 @@ """Unit tests for the imports checker.""" +import os import unittest +import astroid from astroid import test_utils from pylint.checkers import imports from pylint.testutils import CheckerTestCase, Message, set_config @@ -62,7 +64,7 @@ class ImportsCheckerTC(CheckerTestCase): with self.assertNoMessages(): self.checker.visit_import(node) - def test_visit_importfrom(self): + def test_reimported_same_line(self): """ Test that duplicate imports on single line raise 'reimported'. """ @@ -71,5 +73,23 @@ class ImportsCheckerTC(CheckerTestCase): with self.assertAddsMessages(msg): self.checker.visit_importfrom(node) + def test_relative_beyond_top_level(self): + here = os.path.abspath(os.path.dirname(__file__)) + path = os.path.join(here, 'regrtest_data', 'beyond_top', '__init__.py') + with open(path) as stream: + data = stream.read() + module = astroid.parse(data, module_name='beyond_top', path=path) + import_from = module.body[0] + + msg = Message(msg_id='relative-beyond-top-level', + node=import_from) + with self.assertAddsMessages(msg): + self.checker.visit_importfrom(import_from) + with self.assertNoMessages(): + self.checker.visit_importfrom(module.body[1]) + with self.assertNoMessages(): + self.checker.visit_importfrom(module.body[2].body[0]) + + if __name__ == '__main__': unittest.main() -- cgit v1.2.1