Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --allow-reexport-from-package option #8124

Merged
merged 4 commits into from
Jan 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions doc/data/messages/u/useless-import-alias/details.rst
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions doc/data/messages/u/useless-import-alias/related.rst
Original file line number Diff line number Diff line change
@@ -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>`_
2 changes: 1 addition & 1 deletion doc/user_guide/checkers/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ Exceptions checker Messages
program errors, use ``except Exception:`` (bare except is equivalent to
``except BaseException:``).
:broad-exception-raised (W0719): *Raising too general exception: %s*
Raising exceptions that are too generic force you to catch exception
Raising exceptions that are too generic force you to catch exceptions
generically too. It will force you to use a naked ``except Exception:``
clause. You might then end up catching exceptions other than the ones you
expect to catch. This can hide bugs or make it harder to debug programs when
Expand Down
9 changes: 9 additions & 0 deletions doc/user_guide/configuration/all-options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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__.*
Expand Down Expand Up @@ -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 = []
Expand Down
5 changes: 5 additions & 0 deletions doc/whatsnew/fragments/6006.feature
Original file line number Diff line number Diff line change
@@ -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
22 changes: 20 additions & 2 deletions pylint/checkers/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -461,6 +470,7 @@ def open(self) -> None:
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'}
Expand All @@ -470,6 +480,7 @@ def open(self) -> None:
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)
Expand All @@ -495,6 +506,10 @@ def deprecated_modules(self) -> set[str]:
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)
Expand Down Expand Up @@ -917,8 +932,11 @@ def _check_import_as_rename(self, node: ImportNode) -> None:
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",
Expand Down
3 changes: 3 additions & 0 deletions pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 43 additions & 0 deletions tests/checkers/unittest_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,46 @@ def test_preferred_module(capsys: CaptureFixture[str]) -> None:
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
14 changes: 7 additions & 7 deletions tests/functional/i/import_aliasing.txt
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions tests/regrtest_data/allow_reexport/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import os as os
2 changes: 2 additions & 0 deletions tests/regrtest_data/allow_reexport/file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# pylint: disable=unused-import
import os as os