Skip to content

Commit c4aad6e

Browse files
authored
Tried adding autodoc support (#19)
1 parent 8c76668 commit c4aad6e

File tree

4 files changed

+101
-13
lines changed

4 files changed

+101
-13
lines changed

scanpydoc/elegant_typehints/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def x() -> Tuple[int, float]:
5353
from sphinx.application import Sphinx
5454
from sphinx.config import Config
5555
from docutils.parsers.rst import roles
56+
from sphinx.ext.autodoc import ClassDocumenter
5657

5758
from .. import _setup_sig, metadata
5859

@@ -99,6 +100,12 @@ def setup(app: Sphinx) -> Dict[str, Any]:
99100
name, partial(_role_annot, additional_classes=name.split("-"))
100101
)
101102

103+
from .autodoc_patch import dir_head_adder
104+
105+
ClassDocumenter.add_directive_header = dir_head_adder(
106+
qualname_overrides, ClassDocumenter.add_directive_header
107+
)
108+
102109
from .return_tuple import process_docstring # , process_signature
103110

104111
app.connect("autodoc-process-docstring", process_docstring)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from functools import wraps
2+
from typing import Mapping, Callable, Tuple
3+
4+
from docutils.statemachine import StringList
5+
from sphinx.ext.autodoc import ClassDocumenter
6+
7+
8+
def dir_head_adder(
9+
qualname_overrides: Mapping[str, str],
10+
orig: Callable[[ClassDocumenter, str], None],
11+
):
12+
@wraps(orig)
13+
def add_directive_header(self: ClassDocumenter, sig: str) -> None:
14+
orig(self, sig)
15+
lines: StringList = self.directive.result
16+
role, direc = (
17+
("exc", "exception")
18+
if issubclass(self.object, BaseException)
19+
else ("class", "class")
20+
)
21+
for old, new in qualname_overrides.items():
22+
# Currently, autodoc doesn’t link to bases using :exc:
23+
lines.replace(f":class:`{old}`", f":{role}:`{new}`")
24+
# But maybe in the future it will
25+
lines.replace(f":{role}:`{old}`", f":{role}:`{new}`")
26+
old_mod, old_cls = old.rsplit(".", 1)
27+
new_mod, new_cls = new.rsplit(".", 1)
28+
replace_multi_suffix(
29+
lines,
30+
(f".. py:{direc}:: {old_cls}", f" :module: {old_mod}"),
31+
(f".. py:{direc}:: {new_cls}", f" :module: {new_mod}"),
32+
)
33+
34+
return add_directive_header
35+
36+
37+
def replace_multi_suffix(lines: StringList, old: Tuple[str, str], new: Tuple[str, str]):
38+
if len(old) != len(new) != 2:
39+
raise NotImplementedError("Only supports replacing 2 lines")
40+
for l, line in enumerate(lines):
41+
start = line.find(old[0])
42+
if start == -1:
43+
continue
44+
prefix = line[:start]
45+
suffix = line[start + len(old[0]) :]
46+
if lines[l + 1].startswith(prefix + old[1]):
47+
break
48+
else:
49+
return
50+
lines[l + 0] = prefix + new[0] + suffix
51+
lines[l + 1] = prefix + new[1]

tests/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from docutils.writers import Writer
77
from sphinx.application import Sphinx
88
from sphinx.io import read_doc
9-
from sphinx.testing.fixtures import make_app, test_params
9+
from sphinx.testing.fixtures import make_app, test_params # noqa
1010
from sphinx.testing.path import path as STP
1111
from sphinx.util import rst
1212
from sphinx.util.docutils import sphinx_domains

tests/test_elegant_typehints.py

+42-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import inspect
22
import re
3+
import sys
34
import typing as t
5+
from pathlib import Path
6+
from types import ModuleType
47

58
try:
69
from typing import Literal
@@ -19,23 +22,28 @@
1922
from scanpydoc.elegant_typehints.return_tuple import process_docstring
2023

2124

22-
TestCls = type("Class", (), {})
23-
TestCls.__module__ = "_testmod"
24-
TestExc = type("Excep", (RuntimeError,), {})
25-
TestExc.__module__ = "_testmod"
25+
_testmod = sys.modules["_testmod"] = ModuleType("_testmod")
26+
_testmod.Class = type("Class", (), dict(__module__="_testmod"))
27+
_testmod.SubCl = type("SubCl", (_testmod.Class,), dict(__module__="_testmod"))
28+
_testmod.Excep = type("Excep", (RuntimeError,), dict(__module__="_testmod"))
29+
_testmod.Excep2 = type("Excep2", (_testmod.Excep,), dict(__module__="_testmod"))
2630

2731

2832
@pytest.fixture
2933
def app(make_app_setup) -> Sphinx:
3034
return make_app_setup(
35+
master_doc="index",
3136
extensions=[
37+
"sphinx.ext.autodoc",
3238
"sphinx.ext.napoleon",
3339
"sphinx_autodoc_typehints",
3440
"scanpydoc.elegant_typehints",
3541
],
3642
qualname_overrides={
3743
"_testmod.Class": "test.Class",
44+
"_testmod.SubCl": "test.SubCl",
3845
"_testmod.Excep": "test.Excep",
46+
"_testmod.Excep2": "test.Excep2",
3947
},
4048
)
4149

@@ -143,20 +151,20 @@ def test_literal(app):
143151

144152

145153
def test_qualname_overrides_class(app):
146-
assert TestCls.__module__ == "_testmod"
147-
assert _format_terse(TestCls) == ":py:class:`~test.Class`"
154+
assert _testmod.Class.__module__ == "_testmod"
155+
assert _format_terse(_testmod.Class) == ":py:class:`~test.Class`"
148156

149157

150158
def test_qualname_overrides_exception(app):
151-
assert TestExc.__module__ == "_testmod"
152-
assert _format_terse(TestExc) == ":py:exc:`~test.Excep`"
159+
assert _testmod.Excep.__module__ == "_testmod"
160+
assert _format_terse(_testmod.Excep) == ":py:exc:`~test.Excep`"
153161

154162

155163
def test_qualname_overrides_recursive(app):
156-
assert _format_terse(t.Union[TestCls, str]) == (
164+
assert _format_terse(t.Union[_testmod.Class, str]) == (
157165
r":py:class:`~test.Class`, :py:class:`str`"
158166
)
159-
assert _format_full(t.Union[TestCls, str]) == (
167+
assert _format_full(t.Union[_testmod.Class, str]) == (
160168
r":py:data:`~typing.Union`\["
161169
r":py:class:`~test.Class`, "
162170
r":py:class:`str`"
@@ -165,10 +173,10 @@ def test_qualname_overrides_recursive(app):
165173

166174

167175
def test_fully_qualified(app):
168-
assert _format_terse(t.Union[TestCls, str], True) == (
176+
assert _format_terse(t.Union[_testmod.Class, str], True) == (
169177
r":py:class:`test.Class`, :py:class:`str`"
170178
)
171-
assert _format_full(t.Union[TestCls, str], True) == (
179+
assert _format_full(t.Union[_testmod.Class, str], True) == (
172180
r":py:data:`typing.Union`\[" r":py:class:`test.Class`, " r":py:class:`str`" r"]"
173181
)
174182

@@ -222,6 +230,28 @@ def test_typing_class_nested(app):
222230
)
223231

224232

233+
@pytest.mark.parametrize(
234+
"direc,base,sub",
235+
[
236+
("autoclass", "Class", "SubCl"),
237+
("autoexception", "Excep", "Excep2"),
238+
],
239+
)
240+
def test_autodoc(app, direc, base, sub):
241+
Path(app.srcdir, "index.rst").write_text(
242+
f"""\
243+
.. {direc}:: _testmod.{sub}
244+
:show-inheritance:
245+
"""
246+
)
247+
app.build()
248+
out = Path(app.outdir, "index.html").read_text()
249+
assert not app._warning.getvalue(), app._warning.getvalue()
250+
assert re.search(rf"<code[^>]*>test\.</code><code[^>]*>{sub}</code>", out), out
251+
assert f'<a class="headerlink" href="#test.{sub}"' in out, out
252+
assert re.search(rf"Bases: <code[^>]*><span[^>]*>test\.{base}", out), out
253+
254+
225255
@pytest.mark.parametrize(
226256
"docstring",
227257
[

0 commit comments

Comments
 (0)