|
| 1 | +from functools import partial |
| 2 | +from optparse import Values |
| 3 | +from typing import Any, Tuple |
| 4 | +from unittest.mock import patch |
| 5 | + |
| 6 | +import sphinx.domains.python |
| 7 | +import sphinx.ext.autodoc |
| 8 | +from docutils.parsers.rst import Parser as RstParser |
| 9 | +from docutils.utils import new_document |
| 10 | +from sphinx.addnodes import desc_signature |
| 11 | +from sphinx.application import Sphinx |
| 12 | +from sphinx.domains.python import PyAttribute |
| 13 | +from sphinx.ext.autodoc import AttributeDocumenter |
| 14 | + |
| 15 | +# Defensively check for the things we want to patch |
| 16 | +_parse_annotation = getattr(sphinx.domains.python, "_parse_annotation", None) |
| 17 | + |
| 18 | +# We want to patch: |
| 19 | +# * sphinx.ext.autodoc.stringify_typehint (in sphinx < 6.1) |
| 20 | +# * sphinx.ext.autodoc.stringify_annotation (in sphinx >= 6.1) |
| 21 | +STRINGIFY_PATCH_TARGET = "" |
| 22 | +for target in ["stringify_typehint", "stringify_annotation"]: |
| 23 | + if hasattr(sphinx.ext.autodoc, target): |
| 24 | + STRINGIFY_PATCH_TARGET = f"sphinx.ext.autodoc.{target}" |
| 25 | + break |
| 26 | + |
| 27 | +# If we didn't locate both patch targets, we will just do nothing. |
| 28 | +OKAY_TO_PATCH = bool(_parse_annotation and STRINGIFY_PATCH_TARGET) |
| 29 | + |
| 30 | +# A label we inject to the type string so we know not to try to treat it as a |
| 31 | +# type annotation |
| 32 | +TYPE_IS_RST_LABEL = "--is-rst--" |
| 33 | + |
| 34 | + |
| 35 | +orig_add_directive_header = AttributeDocumenter.add_directive_header |
| 36 | +orig_handle_signature = PyAttribute.handle_signature |
| 37 | + |
| 38 | + |
| 39 | +def stringify_annotation(app: Sphinx, annotation: Any, mode: str = "") -> str: # noqa: U100 |
| 40 | + """Format the annotation with sphinx-autodoc-typehints and inject our |
| 41 | + magic prefix to tell our patched PyAttribute.handle_signature to treat |
| 42 | + it as rst.""" |
| 43 | + from . import format_annotation |
| 44 | + |
| 45 | + return TYPE_IS_RST_LABEL + format_annotation(annotation, app.config) |
| 46 | + |
| 47 | + |
| 48 | +def patch_attribute_documenter(app: Sphinx) -> None: |
| 49 | + """Instead of using stringify_typehint in |
| 50 | + `AttributeDocumenter.add_directive_header`, use `format_annotation` |
| 51 | + """ |
| 52 | + |
| 53 | + def add_directive_header(*args: Any, **kwargs: Any) -> Any: |
| 54 | + with patch(STRINGIFY_PATCH_TARGET, partial(stringify_annotation, app)): |
| 55 | + return orig_add_directive_header(*args, **kwargs) |
| 56 | + |
| 57 | + AttributeDocumenter.add_directive_header = add_directive_header # type:ignore[assignment] |
| 58 | + |
| 59 | + |
| 60 | +def rst_to_docutils(settings: Values, rst: str) -> Any: |
| 61 | + """Convert rst to a sequence of docutils nodes""" |
| 62 | + doc = new_document("", settings) |
| 63 | + RstParser().parse(rst, doc) |
| 64 | + # Remove top level paragraph node so that there is no line break. |
| 65 | + return doc.children[0].children |
| 66 | + |
| 67 | + |
| 68 | +def patched_parse_annotation(settings: Values, typ: str, env: Any) -> Any: |
| 69 | + # if typ doesn't start with our label, use original function |
| 70 | + if not typ.startswith(TYPE_IS_RST_LABEL): |
| 71 | + return _parse_annotation(typ, env) # type: ignore |
| 72 | + # Otherwise handle as rst |
| 73 | + typ = typ[len(TYPE_IS_RST_LABEL) :] |
| 74 | + return rst_to_docutils(settings, typ) |
| 75 | + |
| 76 | + |
| 77 | +def patched_handle_signature(self: PyAttribute, sig: str, signode: desc_signature) -> Tuple[str, str]: |
| 78 | + target = "sphinx.domains.python._parse_annotation" |
| 79 | + new_func = partial(patched_parse_annotation, self.state.document.settings) |
| 80 | + with patch(target, new_func): |
| 81 | + return orig_handle_signature(self, sig, signode) |
| 82 | + |
| 83 | + |
| 84 | +def patch_attribute_handling(app: Sphinx) -> None: |
| 85 | + """Use format_signature to format class attribute type annotations""" |
| 86 | + if not OKAY_TO_PATCH: |
| 87 | + return |
| 88 | + PyAttribute.handle_signature = patched_handle_signature # type:ignore[assignment] |
| 89 | + patch_attribute_documenter(app) |
| 90 | + |
| 91 | + |
| 92 | +__all__ = ["patch_attribute_handling"] |
0 commit comments