diff options
author | Marc Mueller <30130371+cdce8p@users.noreply.github.com> | 2023-01-28 18:08:28 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-28 18:08:28 +0100 |
commit | acb28d8fcefb06179d9e6528ba14dd099e12ecfa (patch) | |
tree | 1f8f68dd9a3f00c375ec743010980452e821cbd8 | |
parent | 2cfc8f5d38a2e0ea46be7b3908729cca8c8fb8c4 (diff) | |
download | pylint-git-acb28d8fcefb06179d9e6528ba14dd099e12ecfa.tar.gz |
Add `--allow-reexport-from-package` option (#8124)
-rw-r--r-- | doc/data/messages/u/useless-import-alias/details.rst | 8 | ||||
-rw-r--r-- | doc/data/messages/u/useless-import-alias/related.rst | 1 | ||||
-rw-r--r-- | doc/user_guide/configuration/all-options.rst | 9 | ||||
-rw-r--r-- | doc/whatsnew/fragments/6006.feature | 5 | ||||
-rw-r--r-- | pylint/checkers/imports.py | 22 | ||||
-rw-r--r-- | pylintrc | 3 | ||||
-rw-r--r-- | tests/checkers/unittest_imports.py | 43 | ||||
-rw-r--r-- | tests/functional/i/import_aliasing.txt | 14 | ||||
-rw-r--r-- | tests/regrtest_data/allow_reexport/__init__.py | 1 | ||||
-rw-r--r-- | tests/regrtest_data/allow_reexport/file.py | 2 |
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", @@ -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 |