Skip to content

Commit bb405a6

Browse files
authored
Support annotation-only attributes (#85)
1 parent 86f284e commit bb405a6

File tree

3 files changed

+40
-7
lines changed

3 files changed

+40
-7
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ classifiers = [
1919
requires-python = '>=3.8.1'
2020
dependencies = [
2121
'sphinx>=3.0',
22+
'get-annotations; python_version < "3.10"',
2223
]
2324

2425
[project.optional-dependencies]

src/scanpydoc/rtd_github_links/__init__.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,23 @@ def _init_vars(app: Sphinx, config: Config):
9191
rtd_links_prefix = PurePosixPath(config.rtd_links_prefix)
9292

9393

94+
def _get_annotations(obj: Any) -> dict[str, Any]:
95+
try:
96+
from inspect import get_annotations
97+
except ImportError:
98+
from get_annotations import get_annotations
99+
100+
try:
101+
return get_annotations(obj)
102+
except TypeError:
103+
return {}
104+
105+
94106
def _get_obj_module(qualname: str) -> tuple[Any, ModuleType]:
95-
"""Get a module/class/attribute and its original module by qualname."""
107+
"""Get a module/class/attribute and its original module by qualname.
108+
109+
Returns `None` as `obj` if it’s an annotated field without value.
110+
"""
96111
modname = qualname
97112
attr_path = []
98113
while modname not in sys.modules:
@@ -109,6 +124,8 @@ def _get_obj_module(qualname: str) -> tuple[Any, ModuleType]:
109124
except AttributeError as e:
110125
if is_dataclass(obj):
111126
thing = next(f for f in fields(obj) if f.name == attr_name)
127+
elif attr_name in _get_annotations(thing):
128+
thing = None
112129
else:
113130
try:
114131
thing = import_module(f"{mod.__name__}.{attr_name}")
@@ -158,9 +175,10 @@ def github_url(qualname: str) -> str:
158175
"""
159176
try:
160177
obj, module = _get_obj_module(qualname)
161-
except Exception:
162-
print(f"Error in github_url({qualname!r}):", file=sys.stderr)
163-
raise
178+
except Exception as e:
179+
if hasattr(e, "__notes__"):
180+
e.__notes__.append(f"Qualname: {qualname!r}")
181+
raise e
164182
path = rtd_links_prefix / _module_path(obj, module)
165183
start, end = _get_linenos(obj)
166184
fragment = f"#L{start}-L{end}" if start and end else ""
@@ -220,5 +238,8 @@ def setup(app: Sphinx) -> dict[str, Any]:
220238
from dataclasses import dataclass, field, fields, is_dataclass
221239

222240
@dataclass
223-
class _TestCls:
241+
class _TestDataCls:
224242
test_attr: dict[str, str] = field(default_factory=dict)
243+
244+
class _TestCls:
245+
test_anno: int

tests/test_rtd_github_links.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ def test_as_function(prefix, module, name, obj_path):
4343
assert github_url(f"scanpydoc.{module}.{name}") == f"{prefix}/{obj_path}#L{s}-L{e}"
4444

4545

46+
def test_get_github_url_only_annotation(prefix):
47+
"""Doesn’t really work but shouldn’t crash either."""
48+
url = github_url("scanpydoc.rtd_github_links._TestCls.test_anno")
49+
assert url == f"{prefix}/rtd_github_links/__init__.py"
50+
51+
4652
def test_get_obj_module():
4753
import sphinx.application as sa
4854

@@ -51,6 +57,11 @@ def test_get_obj_module():
5157
assert mod is sa
5258

5359

54-
def test_get_obj_module_anntation():
55-
obj, mod = _get_obj_module("scanpydoc.rtd_github_links._TestCls.test_attr")
60+
def test_get_obj_module_dataclass_field():
61+
obj, mod = _get_obj_module("scanpydoc.rtd_github_links._TestDataCls.test_attr")
5662
assert isinstance(obj, Field)
63+
64+
65+
def test_get_obj_module_only_annotation():
66+
obj, mod = _get_obj_module("scanpydoc.rtd_github_links._TestCls.test_anno")
67+
assert obj is None

0 commit comments

Comments
 (0)