diff --git a/ChangeLog b/ChangeLog index 52e064edff..b117deafc6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -40,6 +40,11 @@ Release date: TBA Closes #1780 Refs #2140 +* 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 + * Remove the ``inference`` module. Node inference methods are now in the module defining the node, rather than being associated to the node afterward. diff --git a/astroid/interpreter/_import/spec.py b/astroid/interpreter/_import/spec.py index 3c21fd73b4..e1df3d56d7 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 EXT_LIB_DIRS, STD_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..ecb21829d7 100644 --- a/tests/test_modutils.py +++ b/tests/test_modutils.py @@ -20,7 +20,7 @@ import astroid from astroid import modutils -from astroid.const import PY310_PLUS +from astroid.const import PY310_PLUS, PY311_PLUS, WIN32 from astroid.interpreter._import import spec from . import resources @@ -268,6 +268,21 @@ def test_std_lib(self) -> None: os.path.realpath(os.path.__file__.replace(".pyc", ".py")), ) + @pytest.mark.skipif( + WIN32 and not PY311_PLUS, + reason="Fails on Windows below 3.11 for what seems like a test setup/isolation issue " + "rather than a functional issue. Possibly related: " + "https://github.com/python/cpython/pull/93653 (other surrounding tests add '.' to sys.path)", + ) + def test_std_lib_found_before_same_named_package_on_path(self) -> None: + sys.path.insert(0, str(resources.RESOURCE_PATH)) + 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)"""