diff --git a/ChangeLog b/ChangeLog index ec63e2dade..5e03d9a4c0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -181,6 +181,11 @@ What's New in astroid 2.15.6? ============================= Release date: TBA +* Prefer standard library modules over same-named modules on sys.path. For example + ``import copy`` now finds ``copy`` instead of ``copy.py``. Solves ``no-member`` issues. + + Closes pylint-dev/pylint#6535 + * Harden ``get_module_part()`` against ``"."``. Closes pylint-dev/pylint#8749 diff --git a/astroid/interpreter/_import/spec.py b/astroid/interpreter/_import/spec.py index 3c21fd73b4..0940c26ba4 100644 --- a/astroid/interpreter/_import/spec.py +++ b/astroid/interpreter/_import/spec.py @@ -20,7 +20,7 @@ from typing import Any, Literal, NamedTuple, Protocol from astroid.const import PY310_PLUS -from astroid.modutils import EXT_LIB_DIRS +from astroid.modutils import STD_LIB_DIRS, EXT_LIB_DIRS from . import util @@ -157,6 +157,19 @@ def find_module( location=getattr(spec.loader_state, "filename", None), type=ModuleType.PY_FROZEN, ) + if ( + spec + and isinstance(spec.loader, importlib.machinery.SourceFileLoader) + and any(spec.origin.startswith(std_lib) for std_lib in STD_LIB_DIRS) + and not spec.origin.endswith("__init__.py") + ): + # Return standard library modules before local modules + # https://github.com/pylint-dev/pylint/issues/6535 + return ModuleSpec( + name=modname, + location=spec.origin, + type=ModuleType.PY_SOURCE, + ) except ValueError: pass submodule_path = sys.path diff --git a/tests/test_modutils.py b/tests/test_modutils.py index 929c58992c..ee5f66fa3e 100644 --- a/tests/test_modutils.py +++ b/tests/test_modutils.py @@ -268,6 +268,15 @@ def test_std_lib(self) -> None: os.path.realpath(os.path.__file__.replace(".pyc", ".py")), ) + def test_std_lib_found_before_same_named_package_on_path(self) -> None: + sys.path.insert(0, resources.find("data")) + self.addCleanup(sys.path.pop, 0) + + file = modutils.file_from_modpath(["copy"]) + + self.assertNotIn("test", file) # tests/testdata/python3/data/copy.py + self.assertTrue(any(stdlib in file for stdlib in modutils.STD_LIB_DIRS)) + def test_builtin(self) -> None: self.assertIsNone(modutils.file_from_modpath(["sys"])) diff --git a/tests/testdata/python3/data/copy.py b/tests/testdata/python3/data/copy.py new file mode 100644 index 0000000000..5f67cbc1ca --- /dev/null +++ b/tests/testdata/python3/data/copy.py @@ -0,0 +1 @@ +"""fake copy module (unlike email, we need one without __init__.py)"""