Skip to content

Commit a74dccf

Browse files
Shyam Dwaraknathpawamoy
Shyam Dwaraknath
authored andcommitted
fix: Fix relative path for native namespace packages
Native namespace packages don't have an `__init__.py` module. This commit adds support for finding relative file path of such packages. References: #19, #22.
1 parent ab1efe5 commit a74dccf

File tree

3 files changed

+38
-12
lines changed

3 files changed

+38
-12
lines changed

Diff for: src/pytkdocs/objects.py

+24-12
Original file line numberDiff line numberDiff line change
@@ -173,20 +173,32 @@ def relative_file_path(self) -> str:
173173
174174
If the relative file path cannot be determined, the value returned is `""` (empty string).
175175
"""
176-
top_package_name = self.path.split(".", 1)[0]
177-
try:
178-
top_package = sys.modules[top_package_name]
179-
except KeyError:
176+
177+
parts = self.path.split(".")
178+
namespaces = [".".join(parts[:l]) for l in range(1, len(parts) + 1)]
179+
# Iterate through all sub namespaces including the last in case it is a module
180+
for namespace in namespaces:
181+
try:
182+
importlib.import_module(namespace)
183+
top_package = sys.modules[namespace]
184+
except (ImportError, ModuleNotFoundError, KeyError):
185+
# ImportError: Triggered if the namespace is not importable
186+
# ModuleNotFoundError: Triggered if the namespace is not a module
187+
# KeyError: Triggered if the imported package isn't referenced under the same fully qualified name
188+
# Namespace packages are importable, so this should work for them
189+
return ""
190+
180191
try:
181-
importlib.import_module(top_package_name)
182-
except ImportError:
192+
top_package_path = Path(inspect.getabsfile(top_package)).parent
193+
return str(Path(self.file_path).relative_to(top_package_path.parent))
194+
except TypeError:
195+
# Triggered if getabsfile() can't be found in the case of a Namespace package
196+
pass
197+
except ValueError:
198+
# Triggered if Path().relative_to can't find an appropriate path
183199
return ""
184-
top_package = sys.modules[top_package_name]
185-
top_package_path = Path(inspect.getabsfile(top_package)).parent
186-
try:
187-
return str(Path(self.file_path).relative_to(top_package_path.parent))
188-
except ValueError:
189-
return ""
200+
201+
return ""
190202

191203
@property
192204
def name_to_check(self) -> str:

Diff for: tests/fixtures/test_namespace/subspace/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"The subspace package docstring."

Diff for: tests/test_loader.py

+13
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
"""Tests for [the `loader` module][pytkdocs.loader]."""
22

33
import sys
4+
from pathlib import Path
45

56
import pytest
67

78
from pytkdocs.loader import Loader, get_object_tree
89

10+
from . import FIXTURES_DIR
11+
912

1013
def test_import_no_path():
1114
with pytest.raises(ValueError):
@@ -82,6 +85,16 @@ def test_loading_package():
8285
assert obj.docstring == "The package docstring."
8386

8487

88+
def test_loading_namespace_package():
89+
loader = Loader()
90+
old_paths = list(sys.path)
91+
sys.path.append(str(Path(FIXTURES_DIR).resolve()))
92+
obj = loader.get_object_documentation("test_namespace.subspace")
93+
assert obj.docstring == "The subspace package docstring."
94+
assert obj.relative_file_path == "subspace/__init__.py"
95+
sys.path = old_paths
96+
97+
8598
def test_loading_module():
8699
loader = Loader()
87100
obj = loader.get_object_documentation("tests.fixtures.the_package.the_module")

0 commit comments

Comments
 (0)