Skip to content

Tried adding autodoc support #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions scanpydoc/elegant_typehints/__init__.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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