diff options
-rw-r--r-- | .pyenchant_pylint_custom_dict.txt | 4 | ||||
-rw-r--r-- | doc/user_guide/configuration/all-options.rst | 2 | ||||
-rw-r--r-- | doc/whatsnew/fragments/7957.feature | 3 | ||||
-rw-r--r-- | pylint/checkers/imports.py | 14 | ||||
-rw-r--r-- | tests/checkers/unittest_imports.py | 56 | ||||
-rw-r--r-- | tests/regrtest_data/preferred_module/unpreferred_submodule.py | 3 |
6 files changed, 78 insertions, 4 deletions
diff --git a/.pyenchant_pylint_custom_dict.txt b/.pyenchant_pylint_custom_dict.txt index a82b2e1bf..47a222f8e 100644 --- a/.pyenchant_pylint_custom_dict.txt +++ b/.pyenchant_pylint_custom_dict.txt @@ -242,6 +242,7 @@ params paren parens passthru +pathlib positionals png pragma @@ -303,6 +304,8 @@ subcommands subdicts subgraphs sublists +submodule +submodules subparsers subparts subprocess @@ -348,6 +351,7 @@ unicode Uninferable uninferable unittest +untriggered # prefix for string ur ureport diff --git a/doc/user_guide/configuration/all-options.rst b/doc/user_guide/configuration/all-options.rst index b83a2ca08..d8df0ac1d 100644 --- a/doc/user_guide/configuration/all-options.rst +++ b/doc/user_guide/configuration/all-options.rst @@ -993,7 +993,7 @@ Standard Checkers --preferred-modules """"""""""""""""""" -*Couples of modules and preferred modules, separated by a comma.* +*Couples of modules and preferred modules, separated by a comma. Submodules may also be specified using '.' syntax (for ex. 'os.path').* **Default:** ``()`` diff --git a/doc/whatsnew/fragments/7957.feature b/doc/whatsnew/fragments/7957.feature new file mode 100644 index 000000000..325f61d13 --- /dev/null +++ b/doc/whatsnew/fragments/7957.feature @@ -0,0 +1,3 @@ +Adds new functionality with preferred-modules configuration to detect submodules. + +Refs #7957 diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index d29056b8c..7270ea368 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -914,11 +914,21 @@ class ImportsChecker(DeprecatedMixin, BaseChecker): def _check_preferred_module(self, node: ImportNode, mod_path: str) -> None: """Check if the module has a preferred replacement.""" - if mod_path in self.preferred_modules: + + mod_compare = [mod_path] + # build a comparison list of possible names using importfrom + if isinstance(node, astroid.nodes.node_classes.ImportFrom): + mod_compare = [f"{node.modname}.{name[0]}" for name in node.names] + + # find whether there are matches with the import vs preferred_modules keys + matches = [k for k in self.preferred_modules for mod in mod_compare if k in mod] + + # if we have matches, add message + if matches: self.add_message( "preferred-module", node=node, - args=(self.preferred_modules[mod_path], mod_path), + args=(self.preferred_modules[matches[0]], matches[0]), ) def _check_import_as_rename(self, node: ImportNode) -> None: diff --git a/tests/checkers/unittest_imports.py b/tests/checkers/unittest_imports.py index 40aa8432a..8aa062cf4 100644 --- a/tests/checkers/unittest_imports.py +++ b/tests/checkers/unittest_imports.py @@ -123,7 +123,7 @@ class TestImportsChecker(CheckerTestCase): # test preferred-modules case with base module import Run( [ - f"{os.path.join(REGR_DATA, 'preferred_module')}", + f"{os.path.join(REGR_DATA, 'preferred_module/unpreferred_module.py')}", "-d all", "-e preferred-module", # prefer sys instead of os (for triggering test) @@ -138,6 +138,60 @@ class TestImportsChecker(CheckerTestCase): # assert there were no errors assert len(errors) == 0 + # test preferred-modules trigger case with submodules + Run( + [ + f"{os.path.join(REGR_DATA, 'preferred_module/unpreferred_submodule.py')}", + "-d all", + "-e preferred-module", + # prefer os.path instead of pathlib (for triggering test) + "--preferred-modules=os.path:pathlib", + ], + exit=False, + ) + output, errors = capsys.readouterr() + + # assert that we saw preferred-modules triggered + assert "Prefer importing 'pathlib' instead of 'os.path'" in output + # assert there were no errors + assert len(errors) == 0 + + # test preferred-modules ignore case with submodules + Run( + [ + f"{os.path.join(REGR_DATA, 'preferred_module/unpreferred_submodule.py')}", + "-d all", + "-e preferred-module", + # prefer pathlib instead of os.stat (for untriggered test) + "--preferred-modules=os.stat:pathlib", + ], + exit=False, + ) + output, errors = capsys.readouterr() + + # assert that we didn't see preferred-modules triggered + assert "Your code has been rated at 10.00/10" in output + # assert there were no errors + assert len(errors) == 0 + + # test preferred-modules base module for implemented submodule (from ... import) + Run( + [ + f"{os.path.join(REGR_DATA, 'preferred_module/unpreferred_submodule.py')}", + "-d all", + "-e preferred-module", + # prefer pathlib instead of os (for triggering test) + "--preferred-modules=os:pathlib", + ], + exit=False, + ) + output, errors = capsys.readouterr() + + # assert that we saw preferred-modules triggered with base module + assert "Prefer importing 'pathlib' instead of 'os'" in output + # assert there were no errors + assert len(errors) == 0 + @staticmethod def test_allow_reexport_package(capsys: CaptureFixture[str]) -> None: """Test --allow-reexport-from-package option.""" diff --git a/tests/regrtest_data/preferred_module/unpreferred_submodule.py b/tests/regrtest_data/preferred_module/unpreferred_submodule.py new file mode 100644 index 000000000..22c61ffbe --- /dev/null +++ b/tests/regrtest_data/preferred_module/unpreferred_submodule.py @@ -0,0 +1,3 @@ +from os import path, getrandom + +path(".") |