diff --git a/ChangeLog b/ChangeLog index ec63e2dade..3f7a0a1f12 100644 --- a/ChangeLog +++ b/ChangeLog @@ -15,6 +15,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 + * Reduce file system access in ``ast_from_file()``. * Reduce time to ``import astroid`` by delaying ``astroid_bootstrapping()`` until 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..a7270c4e6a 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, WIN32 from astroid.interpreter._import import spec from . import resources @@ -268,6 +268,19 @@ 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: + realpath = str(resources.RESOURCE_PATH) + if WIN32: + # Escape backslashes. + realpath = realpath.replace("\\", "\\\\") + sys.path.insert(0, realpath) + 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)"""