summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.pyenchant_pylint_custom_dict.txt4
-rw-r--r--doc/user_guide/configuration/all-options.rst2
-rw-r--r--doc/whatsnew/fragments/7957.feature3
-rw-r--r--pylint/checkers/imports.py14
-rw-r--r--tests/checkers/unittest_imports.py56
-rw-r--r--tests/regrtest_data/preferred_module/unpreferred_submodule.py3
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(".")