summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc Mueller <30130371+cdce8p@users.noreply.github.com>2023-01-28 18:08:28 +0100
committerGitHub <noreply@github.com>2023-01-28 18:08:28 +0100
commitacb28d8fcefb06179d9e6528ba14dd099e12ecfa (patch)
tree1f8f68dd9a3f00c375ec743010980452e821cbd8
parent2cfc8f5d38a2e0ea46be7b3908729cca8c8fb8c4 (diff)
downloadpylint-git-acb28d8fcefb06179d9e6528ba14dd099e12ecfa.tar.gz
Add `--allow-reexport-from-package` option (#8124)
-rw-r--r--doc/data/messages/u/useless-import-alias/details.rst8
-rw-r--r--doc/data/messages/u/useless-import-alias/related.rst1
-rw-r--r--doc/user_guide/configuration/all-options.rst9
-rw-r--r--doc/whatsnew/fragments/6006.feature5
-rw-r--r--pylint/checkers/imports.py22
-rw-r--r--pylintrc3
-rw-r--r--tests/checkers/unittest_imports.py43
-rw-r--r--tests/functional/i/import_aliasing.txt14
-rw-r--r--tests/regrtest_data/allow_reexport/__init__.py1
-rw-r--r--tests/regrtest_data/allow_reexport/file.py2
10 files changed, 95 insertions, 13 deletions
diff --git a/doc/data/messages/u/useless-import-alias/details.rst b/doc/data/messages/u/useless-import-alias/details.rst
index 5b804e1e9..f1349efe8 100644
--- a/doc/data/messages/u/useless-import-alias/details.rst
+++ b/doc/data/messages/u/useless-import-alias/details.rst
@@ -1,8 +1,8 @@
Known issue
-----------
-If you prefer to use "from-as" to explicitly reexport in API (`from fruit import orange as orange`)
-instead of using `__all__` this message will be a false positive.
+If you prefer to use "from-as" to explicitly reexport in API (``from fruit import orange as orange``)
+instead of using ``__all__`` this message will be a false positive.
-If that's the case use `pylint: disable=useless-import-alias` before your imports in your API files.
-`False positive 'useless-import-alias' error for mypy-compatible explicit re-exports #6006 <https://github.com/PyCQA/pylint/issues/6006>`_
+Use ``--allow-reexport-from-package`` to allow explicit reexports by alias
+in package ``__init__`` files.
diff --git a/doc/data/messages/u/useless-import-alias/related.rst b/doc/data/messages/u/useless-import-alias/related.rst
index 4693aeeb8..04e084d9c 100644
--- a/doc/data/messages/u/useless-import-alias/related.rst
+++ b/doc/data/messages/u/useless-import-alias/related.rst
@@ -1,3 +1,4 @@
+- :ref:`--allow-reexport-from-package<imports-options>`
- `PEP 8, Import Guideline <https://peps.python.org/pep-0008/#imports>`_
- :ref:`Pylint block-disable <block_disables>`
- `mypy --no-implicit-reexport <https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-no-implicit-reexport>`_
diff --git a/doc/user_guide/configuration/all-options.rst b/doc/user_guide/configuration/all-options.rst
index d284d1194..b83a2ca08 100644
--- a/doc/user_guide/configuration/all-options.rst
+++ b/doc/user_guide/configuration/all-options.rst
@@ -935,6 +935,13 @@ Standard Checkers
**Default:** ``()``
+--allow-reexport-from-package
+"""""""""""""""""""""""""""""
+*Allow explicit reexports by alias from a package __init__.*
+
+**Default:** ``False``
+
+
--allow-wildcard-with-all
"""""""""""""""""""""""""
*Allow wildcard imports from modules that define __all__.*
@@ -1004,6 +1011,8 @@ Standard Checkers
[tool.pylint.imports]
allow-any-import-level = []
+ allow-reexport-from-package = false
+
allow-wildcard-with-all = false
deprecated-modules = []
diff --git a/doc/whatsnew/fragments/6006.feature b/doc/whatsnew/fragments/6006.feature
new file mode 100644
index 000000000..4d89e80b9
--- /dev/null
+++ b/doc/whatsnew/fragments/6006.feature
@@ -0,0 +1,5 @@
+Add ``--allow-reexport-from-package`` option to configure the
+``useless-import-alias`` check not to emit a warning if a name
+is reexported from a package.
+
+Closes #6006
diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py
index 48f308f2a..653bd85c9 100644
--- a/pylint/checkers/imports.py
+++ b/pylint/checkers/imports.py
@@ -439,6 +439,15 @@ class ImportsChecker(DeprecatedMixin, BaseChecker):
"help": "Allow wildcard imports from modules that define __all__.",
},
),
+ (
+ "allow-reexport-from-package",
+ {
+ "default": False,
+ "type": "yn",
+ "metavar": "<y or n>",
+ "help": "Allow explicit reexports by alias from a package __init__.",
+ },
+ ),
)
def __init__(self, linter: PyLinter) -> None:
@@ -461,6 +470,7 @@ class ImportsChecker(DeprecatedMixin, BaseChecker):
self.linter.stats = self.linter.stats
self.import_graph = defaultdict(set)
self._module_pkg = {} # mapping of modules to the pkg they belong in
+ self._current_module_package = False
self._excluded_edges: defaultdict[str, set[str]] = defaultdict(set)
self._ignored_modules: Sequence[str] = self.linter.config.ignored_modules
# Build a mapping {'module': 'preferred-module'}
@@ -470,6 +480,7 @@ class ImportsChecker(DeprecatedMixin, BaseChecker):
if ":" in module
)
self._allow_any_import_level = set(self.linter.config.allow_any_import_level)
+ self._allow_reexport_package = self.linter.config.allow_reexport_from_package
def _import_graph_without_ignored_edges(self) -> defaultdict[str, set[str]]:
filtered_graph = copy.deepcopy(self.import_graph)
@@ -495,6 +506,10 @@ class ImportsChecker(DeprecatedMixin, BaseChecker):
all_deprecated_modules = all_deprecated_modules.union(mod_set)
return all_deprecated_modules
+ def visit_module(self, node: nodes.Module) -> None:
+ """Store if current module is a package, i.e. an __init__ file."""
+ self._current_module_package = node.package
+
def visit_import(self, node: nodes.Import) -> None:
"""Triggered when an import statement is seen."""
self._check_reimport(node)
@@ -917,8 +932,11 @@ class ImportsChecker(DeprecatedMixin, BaseChecker):
if import_name != aliased_name:
continue
- if len(splitted_packages) == 1:
- self.add_message("useless-import-alias", node=node)
+ if len(splitted_packages) == 1 and (
+ self._allow_reexport_package is False
+ or self._current_module_package is False
+ ):
+ self.add_message("useless-import-alias", node=node, confidence=HIGH)
elif len(splitted_packages) == 2:
self.add_message(
"consider-using-from-import",
diff --git a/pylintrc b/pylintrc
index 1328db768..26fde0c79 100644
--- a/pylintrc
+++ b/pylintrc
@@ -477,6 +477,9 @@ allow-any-import-level=
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
+# Allow explicit reexports by alias from a package __init__.
+allow-reexport-from-package=no
+
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
diff --git a/tests/checkers/unittest_imports.py b/tests/checkers/unittest_imports.py
index ed5c13805..40aa8432a 100644
--- a/tests/checkers/unittest_imports.py
+++ b/tests/checkers/unittest_imports.py
@@ -137,3 +137,46 @@ class TestImportsChecker(CheckerTestCase):
assert "Prefer importing 'sys' 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."""
+
+ # Option disabled - useless-import-alias should always be emitted
+ Run(
+ [
+ f"{os.path.join(REGR_DATA, 'allow_reexport')}",
+ "--allow-reexport-from-package=no",
+ "-sn",
+ ],
+ exit=False,
+ )
+ output, errors = capsys.readouterr()
+ assert len(output.split("\n")) == 5
+ assert (
+ "__init__.py:1:0: C0414: Import alias does not rename original package (useless-import-alias)"
+ in output
+ )
+ assert (
+ "file.py:2:0: C0414: Import alias does not rename original package (useless-import-alias)"
+ in output
+ )
+ assert len(errors) == 0
+
+ # Option enabled - useless-import-alias should only be emitted for 'file.py'
+ Run(
+ [
+ f"{os.path.join(REGR_DATA, 'allow_reexport')}",
+ "--allow-reexport-from-package=yes",
+ "-sn",
+ ],
+ exit=False,
+ )
+ output, errors = capsys.readouterr()
+ assert len(output.split("\n")) == 3
+ assert "__init__.py" not in output
+ assert (
+ "file.py:2:0: C0414: Import alias does not rename original package (useless-import-alias)"
+ in output
+ )
+ assert len(errors) == 0
diff --git a/tests/functional/i/import_aliasing.txt b/tests/functional/i/import_aliasing.txt
index a79bd962f..94e57d418 100644
--- a/tests/functional/i/import_aliasing.txt
+++ b/tests/functional/i/import_aliasing.txt
@@ -1,10 +1,10 @@
-useless-import-alias:6:0:6:50::Import alias does not rename original package:UNDEFINED
+useless-import-alias:6:0:6:50::Import alias does not rename original package:HIGH
consider-using-from-import:8:0:8:22::Use 'from os import path' instead:UNDEFINED
consider-using-from-import:10:0:10:31::Use 'from foo.bar import foobar' instead:UNDEFINED
-useless-import-alias:14:0:14:24::Import alias does not rename original package:UNDEFINED
-useless-import-alias:17:0:17:28::Import alias does not rename original package:UNDEFINED
-useless-import-alias:18:0:18:38::Import alias does not rename original package:UNDEFINED
-useless-import-alias:20:0:20:38::Import alias does not rename original package:UNDEFINED
-useless-import-alias:21:0:21:38::Import alias does not rename original package:UNDEFINED
-useless-import-alias:23:0:23:36::Import alias does not rename original package:UNDEFINED
+useless-import-alias:14:0:14:24::Import alias does not rename original package:HIGH
+useless-import-alias:17:0:17:28::Import alias does not rename original package:HIGH
+useless-import-alias:18:0:18:38::Import alias does not rename original package:HIGH
+useless-import-alias:20:0:20:38::Import alias does not rename original package:HIGH
+useless-import-alias:21:0:21:38::Import alias does not rename original package:HIGH
+useless-import-alias:23:0:23:36::Import alias does not rename original package:HIGH
relative-beyond-top-level:26:0:26:27::Attempted relative import beyond top-level package:UNDEFINED
diff --git a/tests/regrtest_data/allow_reexport/__init__.py b/tests/regrtest_data/allow_reexport/__init__.py
new file mode 100644
index 000000000..0ee6e5127
--- /dev/null
+++ b/tests/regrtest_data/allow_reexport/__init__.py
@@ -0,0 +1 @@
+import os as os
diff --git a/tests/regrtest_data/allow_reexport/file.py b/tests/regrtest_data/allow_reexport/file.py
new file mode 100644
index 000000000..a1adccd46
--- /dev/null
+++ b/tests/regrtest_data/allow_reexport/file.py
@@ -0,0 +1,2 @@
+# pylint: disable=unused-import
+import os as os