Skip to content
7 changes: 7 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ Release date: TBA

Closes #4301

* Improved recognition of namespace packages. They should no longer be added to ``sys.path``
which should prevent various issues with false positives.
Because of the difficulties with mirroring the python import mechanics we would gladly
hear feedback on this change.

Closes #5226, Closes #2648

* ``BaseChecker`` classes now require the ``linter`` argument to be passed.

* Fix a failure to respect inline disables for ``fixme`` occurring on the last line
Expand Down
7 changes: 7 additions & 0 deletions doc/whatsnew/2.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ Other Changes

Closes #4301

* Improved recognition of namespace packages. They should no longer be added to ``sys.path``
which should prevent various issues with false positives.
Because of the difficulties with mirroring the python import mechanics we would gladly
hear feedback on this change.

Closes #5226, Closes #2648

* Update ``invalid-slots-object`` message to show bad object rather than its inferred value.

Closes #6101
Expand Down
19 changes: 19 additions & 0 deletions pylint/lint/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from datetime import datetime
from pathlib import Path

from astroid import modutils

from pylint.config import PYLINT_HOME
from pylint.lint.expand_modules import get_python_path

Expand Down Expand Up @@ -71,12 +73,29 @@ def get_fatal_error_message(filepath: str, issue_template_path: Path) -> str:
)


def _is_part_of_namespace_package(filename: str) -> bool:
"""Check if a file is part of a namespace package."""
try:
modname = modutils.modpath_from_file(filename)
except ImportError:
modname = [Path(filename).stem]

try:
spec = modutils.file_info_from_modpath(modname)
except ImportError:
return False

return modutils.is_namespace(spec)


def _patch_sys_path(args: Sequence[str]) -> list[str]:
original = list(sys.path)
changes = []
seen = set()
for arg in args:
path = get_python_path(arg)
if _is_part_of_namespace_package(path):
continue
if path not in seen:
changes.append(path)
seen.add(path)
Expand Down
21 changes: 21 additions & 0 deletions tests/lint/test_namespace_packages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt

"""Tests related to linting of namespace packages."""

from io import StringIO

from pylint.reporters.text import TextReporter
from pylint.testutils._run import _Run as Run


def test_namespace_package_sys_path() -> None:
"""Test that we do not append namespace packages to sys.path.

The test package is based on https://github.com/PyCQA/pylint/issues/2648.
"""
pylint_output = StringIO()
reporter = TextReporter(pylint_output)
Run(["tests/regrtest_data/namespace_import_self/"], reporter=reporter, exit=False)
assert "Module import itself" not in pylint_output.getvalue()
Comment thread
jacobtylerwalls marked this conversation as resolved.
Outdated
9 changes: 9 additions & 0 deletions tests/lint/unittest_expand_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ def test__is_in_ignore_list_re_match() -> None:
"path": str(TEST_DIRECTORY / "lint/test_pylinter.py"),
}

test_namespace_packages = {
"basename": "lint",
"basepath": INIT_PATH,
"isarg": False,
"name": "lint.test_namespace_packages",
"path": str(TEST_DIRECTORY / "lint/test_namespace_packages.py"),
}

init_of_package = {
"basename": "lint",
"basepath": INIT_PATH,
Expand Down Expand Up @@ -98,6 +106,7 @@ class Checker(BaseChecker):
[str(Path(__file__).parent)],
[
init_of_package,
test_namespace_packages,
test_pylinter,
test_utils,
this_file_from_init,
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from existing import my_func # type: ignore[attr-defined]

my_func()