Skip to content

Commit

Permalink
Tried adding autodoc support (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
flying-sheep committed Sep 24, 2020
1 parent 8c76668 commit c4aad6e
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 13 deletions.
7 changes: 7 additions & 0 deletions scanpydoc/elegant_typehints/__init__.py
Expand Up @@ -53,6 +53,7 @@ def x() -> Tuple[int, float]:
from sphinx.application import Sphinx
from sphinx.config import Config
from docutils.parsers.rst import roles
from sphinx.ext.autodoc import ClassDocumenter

from .. import _setup_sig, metadata

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

from .autodoc_patch import dir_head_adder

ClassDocumenter.add_directive_header = dir_head_adder(
qualname_overrides, ClassDocumenter.add_directive_header
)

from .return_tuple import process_docstring # , process_signature

app.connect("autodoc-process-docstring", process_docstring)
Expand Down
51 changes: 51 additions & 0 deletions scanpydoc/elegant_typehints/autodoc_patch.py
@@ -0,0 +1,51 @@
from functools import wraps
from typing import Mapping, Callable, Tuple

from docutils.statemachine import StringList
from sphinx.ext.autodoc import ClassDocumenter


def dir_head_adder(
qualname_overrides: Mapping[str, str],
orig: Callable[[ClassDocumenter, str], None],
):
@wraps(orig)
def add_directive_header(self: ClassDocumenter, sig: str) -> None:
orig(self, sig)
lines: StringList = self.directive.result
role, direc = (
("exc", "exception")
if issubclass(self.object, BaseException)
else ("class", "class")
)
for old, new in qualname_overrides.items():
# Currently, autodoc doesn’t link to bases using :exc:
lines.replace(f":class:`{old}`", f":{role}:`{new}`")
# But maybe in the future it will
lines.replace(f":{role}:`{old}`", f":{role}:`{new}`")
old_mod, old_cls = old.rsplit(".", 1)
new_mod, new_cls = new.rsplit(".", 1)
replace_multi_suffix(
lines,
(f".. py:{direc}:: {old_cls}", f" :module: {old_mod}"),
(f".. py:{direc}:: {new_cls}", f" :module: {new_mod}"),
)

return add_directive_header


def replace_multi_suffix(lines: StringList, old: Tuple[str, str], new: Tuple[str, str]):
if len(old) != len(new) != 2:
raise NotImplementedError("Only supports replacing 2 lines")
for l, line in enumerate(lines):
start = line.find(old[0])
if start == -1:
continue
prefix = line[:start]
suffix = line[start + len(old[0]) :]
if lines[l + 1].startswith(prefix + old[1]):
break
else:
return
lines[l + 0] = prefix + new[0] + suffix
lines[l + 1] = prefix + new[1]
2 changes: 1 addition & 1 deletion tests/conftest.py
Expand Up @@ -6,7 +6,7 @@
from docutils.writers import Writer
from sphinx.application import Sphinx
from sphinx.io import read_doc
from sphinx.testing.fixtures import make_app, test_params
from sphinx.testing.fixtures import make_app, test_params # noqa
from sphinx.testing.path import path as STP
from sphinx.util import rst
from sphinx.util.docutils import sphinx_domains
Expand Down
54 changes: 42 additions & 12 deletions tests/test_elegant_typehints.py
@@ -1,6 +1,9 @@
import inspect
import re
import sys
import typing as t
from pathlib import Path
from types import ModuleType

try:
from typing import Literal
Expand All @@ -19,23 +22,28 @@
from scanpydoc.elegant_typehints.return_tuple import process_docstring


TestCls = type("Class", (), {})
TestCls.__module__ = "_testmod"
TestExc = type("Excep", (RuntimeError,), {})
TestExc.__module__ = "_testmod"
_testmod = sys.modules["_testmod"] = ModuleType("_testmod")
_testmod.Class = type("Class", (), dict(__module__="_testmod"))
_testmod.SubCl = type("SubCl", (_testmod.Class,), dict(__module__="_testmod"))
_testmod.Excep = type("Excep", (RuntimeError,), dict(__module__="_testmod"))
_testmod.Excep2 = type("Excep2", (_testmod.Excep,), dict(__module__="_testmod"))


@pytest.fixture
def app(make_app_setup) -> Sphinx:
return make_app_setup(
master_doc="index",
extensions=[
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx_autodoc_typehints",
"scanpydoc.elegant_typehints",
],
qualname_overrides={
"_testmod.Class": "test.Class",
"_testmod.SubCl": "test.SubCl",
"_testmod.Excep": "test.Excep",
"_testmod.Excep2": "test.Excep2",
},
)

Expand Down Expand Up @@ -143,20 +151,20 @@ def test_literal(app):


def test_qualname_overrides_class(app):
assert TestCls.__module__ == "_testmod"
assert _format_terse(TestCls) == ":py:class:`~test.Class`"
assert _testmod.Class.__module__ == "_testmod"
assert _format_terse(_testmod.Class) == ":py:class:`~test.Class`"


def test_qualname_overrides_exception(app):
assert TestExc.__module__ == "_testmod"
assert _format_terse(TestExc) == ":py:exc:`~test.Excep`"
assert _testmod.Excep.__module__ == "_testmod"
assert _format_terse(_testmod.Excep) == ":py:exc:`~test.Excep`"


def test_qualname_overrides_recursive(app):
assert _format_terse(t.Union[TestCls, str]) == (
assert _format_terse(t.Union[_testmod.Class, str]) == (
r":py:class:`~test.Class`, :py:class:`str`"
)
assert _format_full(t.Union[TestCls, str]) == (
assert _format_full(t.Union[_testmod.Class, str]) == (
r":py:data:`~typing.Union`\["
r":py:class:`~test.Class`, "
r":py:class:`str`"
Expand All @@ -165,10 +173,10 @@ def test_qualname_overrides_recursive(app):


def test_fully_qualified(app):
assert _format_terse(t.Union[TestCls, str], True) == (
assert _format_terse(t.Union[_testmod.Class, str], True) == (
r":py:class:`test.Class`, :py:class:`str`"
)
assert _format_full(t.Union[TestCls, str], True) == (
assert _format_full(t.Union[_testmod.Class, str], True) == (
r":py:data:`typing.Union`\[" r":py:class:`test.Class`, " r":py:class:`str`" r"]"
)

Expand Down Expand Up @@ -222,6 +230,28 @@ def test_typing_class_nested(app):
)


@pytest.mark.parametrize(
"direc,base,sub",
[
("autoclass", "Class", "SubCl"),
("autoexception", "Excep", "Excep2"),
],
)
def test_autodoc(app, direc, base, sub):
Path(app.srcdir, "index.rst").write_text(
f"""\
.. {direc}:: _testmod.{sub}
:show-inheritance:
"""
)
app.build()
out = Path(app.outdir, "index.html").read_text()
assert not app._warning.getvalue(), app._warning.getvalue()
assert re.search(rf"<code[^>]*>test\.</code><code[^>]*>{sub}</code>", out), out
assert f'<a class="headerlink" href="#test.{sub}"' in out, out
assert re.search(rf"Bases: <code[^>]*><span[^>]*>test\.{base}", out), out


@pytest.mark.parametrize(
"docstring",
[
Expand Down

0 comments on commit c4aad6e

Please sign in to comment.