From 9fa662ca9c1b20bd2781e564d6a78d31111e5bb0 Mon Sep 17 00:00:00 2001 From: Daniel Mouritzen Date: Tue, 4 Apr 2023 05:44:15 +0900 Subject: Handle patterns in ignored-modules when checking for no-name-in-module (#7579) Co-authored-by: Pierre Sassoulas Co-authored-by: Jared Proffitt --- doc/whatsnew/fragments/7578.bugfix | 3 +++ pylint/checkers/imports.py | 22 ++++-------------- pylint/checkers/typecheck.py | 2 +- pylint/checkers/utils.py | 46 +++++++++++++++---------------------- pylint/checkers/variables.py | 5 +++- tests/functional/i/import_error.py | 6 +++++ tests/functional/i/import_error.rc | 2 +- tests/functional/i/import_error.txt | 2 +- 8 files changed, 40 insertions(+), 48 deletions(-) create mode 100644 doc/whatsnew/fragments/7578.bugfix diff --git a/doc/whatsnew/fragments/7578.bugfix b/doc/whatsnew/fragments/7578.bugfix new file mode 100644 index 000000000..2b01149ca --- /dev/null +++ b/doc/whatsnew/fragments/7578.bugfix @@ -0,0 +1,3 @@ +The ``ignored-modules`` option will now be correctly taken into account for ``no-name-in-module``. + +Closes #7578 diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index 9f4b0e379..6cc5d2164 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -23,6 +23,7 @@ from pylint.checkers.utils import ( get_import_name, in_type_checking_block, is_from_fallback_block, + is_module_ignored, is_sys_guard, node_ignores_exception, ) @@ -84,18 +85,6 @@ DEPRECATED_MODULES = { } -def _qualified_names(modname: str | None) -> list[str]: - """Split the names of the given module into subparts. - - For example, - _qualified_names('pylint.checkers.ImportsChecker') - returns - ['pylint', 'pylint.checkers', 'pylint.checkers.ImportsChecker'] - """ - names = modname.split(".") if modname is not None else "" - return [".".join(names[0 : i + 1]) for i in range(len(names))] - - def _get_first_import( node: ImportNode, context: nodes.LocalsDictNodeNG, @@ -153,12 +142,11 @@ def _get_first_import( def _ignore_import_failure( node: ImportNode, - modname: str | None, + modname: str, ignored_modules: Sequence[str], ) -> bool: - for submodule in _qualified_names(modname): - if submodule in ignored_modules: - return True + if is_module_ignored(modname, ignored_modules): + return True # Ignore import failure if part of guarded import block # I.e. `sys.version_info` or `typing.TYPE_CHECKING` @@ -852,7 +840,7 @@ class ImportsChecker(DeprecatedMixin, BaseChecker): return std_imports, external_imports, local_imports def _get_imported_module( - self, importnode: ImportNode, modname: str | None + self, importnode: ImportNode, modname: str ) -> nodes.Module | None: try: return importnode.do_import_module(modname) diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 90604261d..42131f936 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -124,7 +124,7 @@ def _is_owner_ignored( matches any name from the *ignored_classes* or if its qualified name can be found in *ignored_classes*. """ - if is_module_ignored(owner.root(), ignored_modules): + if is_module_ignored(owner.root().qname(), ignored_modules): return True # Match against ignored classes. diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 4b316909e..38321bc11 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -12,7 +12,6 @@ import itertools import numbers import re import string -from collections import deque from collections.abc import Iterable, Iterator from functools import lru_cache, partial from re import Match @@ -2050,37 +2049,30 @@ def is_augmented_assign(node: nodes.Assign) -> tuple[bool, str]: return False, "" +def _qualified_name_parts(qualified_module_name: str) -> list[str]: + """Split the names of the given module into subparts. + + For example, + _qualified_name_parts('pylint.checkers.ImportsChecker') + returns + ['pylint', 'pylint.checkers', 'pylint.checkers.ImportsChecker'] + """ + names = qualified_module_name.split(".") + return [".".join(names[0 : i + 1]) for i in range(len(names))] + + def is_module_ignored( - module: nodes.Module, - ignored_modules: Iterable[str], + qualified_module_name: str, ignored_modules: Iterable[str] ) -> bool: ignored_modules = set(ignored_modules) - module_name = module.name - module_qname = module.qname() - - for ignore in ignored_modules: - # Try to match the module name / fully qualified name directly - if module_qname in ignored_modules or module_name in ignored_modules: + for current_module in _qualified_name_parts(qualified_module_name): + # Try to match the module name directly + if current_module in ignored_modules: return True - - # Try to see if the ignores pattern match against the module name. - if fnmatch.fnmatch(module_qname, ignore): - return True - - # Otherwise, we might have a root module name being ignored, - # and the qualified owner has more levels of depth. - parts = deque(module_name.split(".")) - current_module = "" - - while parts: - part = parts.popleft() - if not current_module: - current_module = part - else: - current_module += f".{part}" - if current_module in ignored_modules: + for ignore in ignored_modules: + # Try to see if the ignores pattern match against the module name. + if fnmatch.fnmatch(current_module, ignore): return True - return False diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 0a24de370..79bdb6a97 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -25,6 +25,7 @@ from astroid.typing import InferenceResult from pylint.checkers import BaseChecker, utils from pylint.checkers.utils import ( in_type_checking_block, + is_module_ignored, is_postponed_evaluation_enabled, is_sys_guard, overridden_method, @@ -2941,7 +2942,9 @@ class VariablesChecker(BaseChecker): if not isinstance(module, nodes.Module): return None except astroid.NotFoundError: - if module.name in self._ignored_modules: + # Unable to import `name` from `module`. Since `name` may itself be a + # module, we first check if it matches the ignored modules. + if is_module_ignored(f"{module.qname()}.{name}", self._ignored_modules): return None self.add_message( "no-name-in-module", args=(name, module.name), node=node diff --git a/tests/functional/i/import_error.py b/tests/functional/i/import_error.py index 97e9dddbf..474a53b5a 100644 --- a/tests/functional/i/import_error.py +++ b/tests/functional/i/import_error.py @@ -80,6 +80,12 @@ import foo, bar # [multiple-imports] import foo import bar +import mymodule_ignored +import mymodule.something_ignored +from mymodule.something_ignored import anything +import sys.something_ignored +from sys.something_ignored import anything + # Issues with contextlib.suppress reported in # https://github.com/pylint-dev/pylint/issues/7270 import contextlib diff --git a/tests/functional/i/import_error.rc b/tests/functional/i/import_error.rc index d8f2b25e2..7278ff8f1 100644 --- a/tests/functional/i/import_error.rc +++ b/tests/functional/i/import_error.rc @@ -3,7 +3,7 @@ disable=C,R,W enable=multiple-imports [TYPECHECK] -ignored-modules=external_module,fake_module.submodule,foo,bar +ignored-modules=external_module,fake_module.submodule,foo,bar,*_ignored [testoptions] # TODO: PY3.9: This does pass on PyPy 3.9 diff --git a/tests/functional/i/import_error.txt b/tests/functional/i/import_error.txt index 80a5732f0..49cc0a673 100644 --- a/tests/functional/i/import_error.txt +++ b/tests/functional/i/import_error.txt @@ -3,4 +3,4 @@ import-error:21:4:21:26::Unable to import 'maybe_missing_2':UNDEFINED no-name-in-module:33:0:33:49::No name 'syntax_error' in module 'functional.s.syntax':UNDEFINED syntax-error:33:0:None:None::Cannot import 'functional.s.syntax.syntax_error' due to 'invalid syntax (, line 1)':HIGH multiple-imports:78:0:78:15::Multiple imports on one line (foo, bar):UNDEFINED -import-error:90:4:90:15::Unable to import 'foo2':UNDEFINED +import-error:96:4:96:15::Unable to import 'foo2':UNDEFINED -- cgit v1.2.1