diff --git a/src/griffe/agents/inspector.py b/src/griffe/agents/inspector.py index eec072e0..636d61da 100644 --- a/src/griffe/agents/inspector.py +++ b/src/griffe/agents/inspector.py @@ -35,6 +35,7 @@ from griffe.expressions import safe_get_annotation from griffe.extensions.base import Extensions, load_extensions from griffe.importer import dynamic_import +from griffe.logger import get_logger if TYPE_CHECKING: from pathlib import Path @@ -43,6 +44,7 @@ from griffe.expressions import Expr +logger = get_logger(__name__) empty = Signature.empty @@ -218,9 +220,34 @@ def generic_inspect(self, node: ObjectNode) -> None: before_inspector.inspect(node) for child in node.children: - target_path = child.alias_target_path - if target_path: - self.current.set_member(child.name, Alias(child.name, target_path)) + if target_path := child.alias_target_path: + # If the child is an actual submodule of the current module, + # and has no `__file__` set, we won't find it on the disk so we must inspect it now. + # For that we instantiate a new inspector and use it to inspect the submodule, + # then assign the submodule as member of the current module. + # If the submodule has a `__file__` set, the loader should find it on the disk, + # so we skip it here (no member, no alias, just skip it). + if child.is_module and target_path == f"{self.current.path}.{child.name}": + if not hasattr(child.obj, "__file__"): + logger.debug(f"Module {target_path} is not discoverable on disk, inspecting right now") + inspector = Inspector( + child.name, + filepath=None, + parent=self.current.module, + extensions=self.extensions, + docstring_parser=self.docstring_parser, + docstring_options=self.docstring_options, + lines_collection=self.lines_collection, + modules_collection=self.modules_collection, + ) + try: + inspector.inspect_module(child) + finally: + self.extensions.attach_inspector(self) + self.current.set_member(child.name, inspector.current.module) + # Otherwise, alias the object. + else: + self.current.set_member(child.name, Alias(child.name, target_path)) else: self.inspect(child) diff --git a/tests/test_inspector.py b/tests/test_inspector.py index 3b22ab48..a527c083 100644 --- a/tests/test_inspector.py +++ b/tests/test_inspector.py @@ -110,3 +110,40 @@ def test_inspecting_package_and_module_with_same_names() -> None: with temporary_pypackage("package", {"package.py": "a = 0"}) as tmp_package: inspect("package.package", filepath=tmp_package.path / "package.py", import_paths=[tmp_package.tmpdir]) _clear_sys_modules("package") + + +def test_inspecting_module_with_submodules() -> None: + """Inspecting a module shouldn't register any of its submodules if they're not imported.""" + with temporary_pypackage("pkg", ["mod.py"]) as tmp_package: + pkg = inspect("pkg", filepath=tmp_package.path / "__init__.py") + assert "mod" not in pkg.members + _clear_sys_modules("pkg") + + +def test_inspecting_module_with_imported_submodules() -> None: + """When inspecting a package on the disk, direct submodules should be skipped entirely.""" + with temporary_pypackage( + "pkg", + { + "__init__.py": "from pkg import subpkg\nfrom pkg.subpkg import mod", + "subpkg/__init__.py": "a = 0", + "subpkg/mod.py": "b = 0", + }, + ) as tmp_package: + pkg = inspect("pkg", filepath=tmp_package.path / "__init__.py") + assert "subpkg" not in pkg.members + assert "mod" in pkg.members + assert pkg["mod"].is_alias + assert pkg["mod"].target_path == "pkg.subpkg.mod" + _clear_sys_modules("pkg") + + +def test_inspecting_objects_from_private_builtin_stdlib_moduless() -> None: + """Inspect objects from private built-in modules in the standard library.""" + ast = inspect("ast") + assert "Assign" in ast.members + assert not ast["Assign"].is_alias + + ast = inspect("_ast") + assert "Assign" in ast.members + assert not ast["Assign"].is_alias