Skip to content

Commit 71501f4

Browse files
authored
More robust paths (#78)
1 parent 76f168b commit 71501f4

File tree

6 files changed

+46
-33
lines changed

6 files changed

+46
-33
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ jobs:
2828
- name: dependencies
2929
run: |
3030
pip install --upgrade pip wheel
31-
pip install -e .[test,typehints]
31+
pip install .[test,typehints]
3232
- name: tests
3333
run: pytest --color=yes

docs/conf.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import sys
21
from datetime import datetime
32
from importlib.metadata import metadata
43
from pathlib import Path
@@ -8,9 +7,6 @@
87

98
HERE = Path(__file__).parent
109

11-
# necessary for rtd_gh_links’ linkcode support
12-
sys.path.insert(0, HERE.parent / "src")
13-
1410
# Clean build env
1511
for file in HERE.glob("scanpydoc.*.rst"):
1612
file.unlink()
@@ -49,6 +45,7 @@
4945

5046
# Generate .rst stubs for modules using autosummary
5147
autosummary_generate = True
48+
autosummary_ignore_module_all = False
5249
# Don’t add module paths to documented functions’ names
5350
add_module_names = False
5451

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ source = 'vcs'
6262
version-file = 'src/scanpydoc/_version.py'
6363

6464
[tool.hatch.envs.docs]
65+
python = '3.11'
6566
features = ['doc']
6667
[tool.hatch.envs.docs.scripts]
6768
build = 'sphinx-build -M html docs docs/_build'

src/scanpydoc/elegant_typehints/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ def x() -> Tuple[int, float]:
6060
from .example import example_func
6161

6262

63+
__all__ = ["example_func", "setup"]
64+
65+
6366
HERE = Path(__file__).parent.resolve()
6467

6568
qualname_overrides_default = {
@@ -88,9 +91,6 @@ def _init_vars(app: Sphinx, config: Config):
8891
config.html_static_path.append(str(HERE / "static"))
8992

9093

91-
example_func.__module__ = "scanpydoc.elegant_typehints" # Make it show up here
92-
93-
9494
@_setup_sig
9595
def setup(app: Sphinx) -> dict[str, Any]:
9696
"""Patches :mod:`sphinx_autodoc_typehints` for a more elegant display."""

src/scanpydoc/rtd_github_links/__init__.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,6 @@
88
99
.. code:: python
1010
11-
import sys
12-
from pathlib import Path
13-
14-
HERE = Path(__file__).parent
15-
# make sure modules are import from the right place
16-
sys.path.insert(0, HERE.parent / "src")
17-
1811
extensions = [
1912
"scanpydoc",
2013
"sphinx.ext.linkcode",
@@ -67,6 +60,7 @@
6760

6861
import inspect
6962
import sys
63+
from importlib import import_module
7064
from pathlib import Path, PurePosixPath
7165
from types import ModuleType
7266
from typing import Any
@@ -108,14 +102,18 @@ def _get_obj_module(qualname: str) -> tuple[Any, ModuleType]:
108102
# retrieve object and find original module name
109103
mod = sys.modules[modname]
110104
obj = None
105+
del modname
111106
for attr_name in attr_path:
112107
try:
113108
thing = getattr(mod if obj is None else obj, attr_name)
114-
except AttributeError:
109+
except AttributeError as e:
115110
if is_dataclass(obj):
116111
thing = next(f for f in fields(obj) if f.name == attr_name)
117112
else:
118-
raise
113+
try:
114+
thing = import_module(f"{mod.__name__}.{attr_name}")
115+
except ImportError:
116+
raise e from None
119117
if isinstance(thing, ModuleType):
120118
mod = thing
121119
else:
@@ -137,12 +135,15 @@ def _get_linenos(obj):
137135
return start, start + len(lines) - 1
138136

139137

140-
def _module_path(module: ModuleType) -> PurePosixPath:
141-
stem = PurePosixPath(*module.__name__.split("."))
142-
if Path(module.__file__).name == "__init__.py":
143-
return stem / "__init__.py"
144-
else:
145-
return stem.with_suffix(".py")
138+
def _module_path(obj: Any, module: ModuleType) -> PurePosixPath:
139+
"""Relative module path to parent directory of toplevel module."""
140+
try:
141+
file = Path(inspect.getabsfile(obj))
142+
except TypeError:
143+
file = Path(module.__file__)
144+
offset = -1 if file.name == "__init__.py" else 0
145+
parts = module.__name__.split(".")
146+
return PurePosixPath(*file.parts[offset - len(parts) :])
146147

147148

148149
def github_url(qualname: str) -> str:
@@ -159,7 +160,7 @@ def github_url(qualname: str) -> str:
159160
except Exception:
160161
print(f"Error in github_url({qualname!r}):", file=sys.stderr)
161162
raise
162-
path = rtd_links_prefix / _module_path(module)
163+
path = rtd_links_prefix / _module_path(obj, module)
163164
start, end = _get_linenos(obj)
164165
fragment = f"#L{start}-L{end}" if start and end else ""
165166
return f"{github_base_url}/{path}{fragment}"

tests/test_rtd_github_links.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from dataclasses import Field
2+
from importlib import import_module
23
from pathlib import Path, PurePosixPath
34

45
import pytest
@@ -16,17 +17,30 @@ def _env(monkeypatch: MonkeyPatch) -> None:
1617

1718

1819
@pytest.fixture(params=[".", "src"])
19-
def pfx(monkeypatch: MonkeyPatch, _env, request):
20+
def prefix(monkeypatch: MonkeyPatch, _env, request) -> PurePosixPath:
2021
pfx = PurePosixPath(request.param)
2122
monkeypatch.setattr("scanpydoc.rtd_github_links.rtd_links_prefix", pfx)
22-
return pfx
23-
24-
25-
def test_as_function(pfx):
26-
pth = "x" / pfx / "scanpydoc/rtd_github_links/__init__.py"
27-
assert github_url("scanpydoc.rtd_github_links") == str(pth)
28-
s, e = _get_linenos(github_url)
29-
assert github_url("scanpydoc.rtd_github_links.github_url") == f"{pth}#L{s}-L{e}"
23+
return "x" / pfx / "scanpydoc"
24+
25+
26+
@pytest.mark.parametrize(
27+
("module", "name", "obj_path"),
28+
[
29+
pytest.param(
30+
*("rtd_github_links", "github_url", "rtd_github_links/__init__.py"),
31+
id="basic",
32+
),
33+
pytest.param(
34+
*("elegant_typehints", "example_func", "elegant_typehints/example.py"),
35+
id="reexport",
36+
),
37+
],
38+
)
39+
def test_as_function(prefix, module, name, obj_path):
40+
assert github_url(f"scanpydoc.{module}") == str(prefix / module / "__init__.py")
41+
obj = getattr(import_module(f"scanpydoc.{module}"), name)
42+
s, e = _get_linenos(obj)
43+
assert github_url(f"scanpydoc.{module}.{name}") == f"{prefix}/{obj_path}#L{s}-L{e}"
3044

3145

3246
def test_get_obj_module():

0 commit comments

Comments
 (0)