diff --git a/astroid/manager.py b/astroid/manager.py index e2328862a..b63a22978 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -208,7 +208,14 @@ def ast_from_module_name( # noqa: C901 if modname in self.module_denylist: raise AstroidImportError(f"Skipping ignored module {modname!r}") if modname in self.astroid_cache and use_cache: - return self.astroid_cache[modname] + if modname == "": + return self.astroid_cache[modname] + + module_parent_path = self.astroid_cache[modname].get_parent_path() + if context_file and os.path.dirname(context_file) == module_parent_path: + return self.astroid_cache[modname] + elif module_parent_path is None: + return self.astroid_cache[modname] if modname == "__main__": return self._build_stub_module(modname) if context_file: diff --git a/astroid/nodes/_base_nodes.py b/astroid/nodes/_base_nodes.py index df452cb2b..e82bcef95 100644 --- a/astroid/nodes/_base_nodes.py +++ b/astroid/nodes/_base_nodes.py @@ -163,10 +163,16 @@ def do_import_module(self, modname: str | None = None) -> nodes.Module: else: use_cache = True + # pylint: disable-next=no-member # pylint doesn't recognize type of mymodule + context_file = mymodule.path[0] if mymodule.path else None + if context_file == "": + context_file = None + # pylint: disable-next=no-member # pylint doesn't recognize type of mymodule return mymodule.import_module( modname, level=level, + context_file=context_file, relative_only=bool(level and level >= 1), use_cache=use_cache, ) diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 885562393..5eaaf7d45 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -15,6 +15,7 @@ import os from collections.abc import Generator, Iterable, Iterator, Sequence from functools import cached_property, lru_cache +from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Literal, NoReturn, TypeVar from astroid import bases, protocols, util @@ -435,6 +436,7 @@ def absolute_import_activated(self) -> bool: def import_module( self, modname: str, + context_file: str | None = None, relative_only: bool = False, level: int | None = None, use_cache: bool = True, @@ -457,7 +459,7 @@ def import_module( try: return AstroidManager().ast_from_module_name( - absmodname, use_cache=use_cache + absmodname, context_file, use_cache=use_cache ) except AstroidBuildingError: # we only want to import a sub module or package of this module, @@ -468,7 +470,9 @@ def import_module( # like "_winapi" or "nt" on POSIX systems. if modname == absmodname: raise - return AstroidManager().ast_from_module_name(modname, use_cache=use_cache) + return AstroidManager().ast_from_module_name( + modname, context_file, use_cache=use_cache + ) def relative_to_absolute_name(self, modname: str, level: int | None) -> str: """Get the absolute module name for a relative import. @@ -587,6 +591,14 @@ def bool_value(self, context: InferenceContext | None = None) -> bool: def get_children(self): yield from self.body + def get_parent_path(self) -> str | None: + """Given the module, return its parent path""" + module_parts = self.name.split(".") + if self.file and self.file != "": + return str(Path(self.file).parents[len(module_parts)]) + else: + return None + def frame(self: _T, *, future: Literal[None, True] = None) -> _T: """The node's frame node. diff --git a/tests/test_inference.py b/tests/test_inference.py index bf41e8cf5..a46d44275 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -1501,22 +1501,13 @@ def test_name_repeat_inference(self) -> None: with pytest.raises(InferenceError): next(node.infer(context=context)) - def test_python25_no_relative_import(self) -> None: - ast = resources.build_file("data/package/absimport.py") - self.assertTrue(ast.absolute_import_activated(), True) - inferred = next( - test_utils.get_name_node(ast, "import_package_subpackage_module").infer() - ) - # failed to import since absolute_import is activated - self.assertIs(inferred, util.Uninferable) - def test_nonregr_absolute_import(self) -> None: ast = resources.build_file("data/absimp/string.py", "data.absimp.string") self.assertTrue(ast.absolute_import_activated(), True) inferred = next(test_utils.get_name_node(ast, "string").infer()) self.assertIsInstance(inferred, nodes.Module) self.assertEqual(inferred.name, "string") - self.assertIn("ascii_letters", inferred.locals) + self.assertNotIn("ascii_letters", inferred.locals) def test_property(self) -> None: code = """ diff --git a/tests/test_nodes.py b/tests/test_nodes.py index 8d2f4d546..a38837ebb 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -42,6 +42,8 @@ AstroidBuildingError, AstroidSyntaxError, AttributeInferenceError, + InferenceError, + ParentMissingError, StatementMissing, ) from astroid.nodes.node_classes import UNATTACHED_UNKNOWN @@ -640,7 +642,10 @@ def test_absolute_import(self) -> None: ctx = InferenceContext() # will fail if absolute import failed ctx.lookupname = "message" - next(module["message"].infer(ctx)) + + with self.assertRaises(InferenceError): + next(module["message"].infer(ctx)) + ctx.lookupname = "email" m = next(module["email"].infer(ctx)) self.assertFalse(m.file.startswith(os.path.join("data", "email.py")))