Skip to content

Commit

Permalink
Simplify typehints (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
flying-sheep authored Nov 21, 2023
1 parent 20f0dd4 commit ed7dd0c
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 205 deletions.
8 changes: 3 additions & 5 deletions src/scanpydoc/elegant_typehints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def x() -> Tuple[int, float]:
.. _sphinx issue 4826: https://github.com/sphinx-doc/sphinx/issues/4826
.. _sphinx-autodoc-typehints issue 38: https://github.com/agronholm/sphinx-autodoc-typehints/issues/38
.. _sphinx-autodoc-typehints issue 38: https://github.com/tox-dev/sphinx-autodoc-typehints/issues/38
""" # noqa: D300, D301

Expand Down Expand Up @@ -97,7 +97,6 @@ def _init_vars(_app: Sphinx, config: Config) -> None:
if config.typehints_defaults is None and config.annotate_defaults:
# override default for “typehints_defaults”
config.typehints_defaults = "braces"
config.html_static_path.append(str(HERE / "static"))


@dataclass
Expand All @@ -112,12 +111,11 @@ def setup(app: Sphinx) -> dict[str, Any]:
"""Patches :mod:`sphinx_autodoc_typehints` for a more elegant display."""
app.add_config_value("qualname_overrides", default={}, rebuild="html")
app.add_config_value("annotate_defaults", default=True, rebuild="html")
app.add_css_file("typehints.css")
app.connect("config-inited", _init_vars)

from ._formatting import _role_annot, format_annotation
from ._formatting import _role_annot, typehints_formatter

app.config["typehints_formatter"] = PickleableCallable(format_annotation)
app.config["typehints_formatter"] = PickleableCallable(typehints_formatter)
for name in ["annotation-terse", "annotation-full"]:
roles.register_canonical_role(
name,
Expand Down
94 changes: 16 additions & 78 deletions src/scanpydoc/elegant_typehints/_formatting.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
from __future__ import annotations

import inspect
from collections.abc import Callable, Iterable, Mapping, Sequence
from functools import partial
from typing import TYPE_CHECKING, Any, Literal, Union, get_args, get_origin
from typing import Callable as t_Callable
from typing import Dict as t_Dict # noqa: UP035
from typing import Mapping as t_Mapping # noqa: UP035
from typing import TYPE_CHECKING, Any, get_origin


try:
Expand All @@ -18,17 +13,30 @@
from docutils.parsers.rst.roles import set_classes
from docutils.parsers.rst.states import Inliner, Struct
from docutils.utils import SystemMessage, unescape
from sphinx_autodoc_typehints import format_annotation as _format_orig

from scanpydoc import elegant_typehints


if TYPE_CHECKING:
from collections.abc import Iterable, Sequence

from docutils.nodes import Node
from sphinx.config import Config


def _format_full(annotation: type[Any], config: Config) -> str | None:
def typehints_formatter(annotation: type[Any], config: Config) -> str | None:
"""Generate reStructuredText containing links to the types.
Can be used as ``typehints_formatter`` for :mod:`sphinx_autodoc_typehints`,
to respect the ``qualname_overrides`` option.
Args:
annotation: A type or class used as type annotation.
config: Sphinx config containing ``sphinx-autodoc-typehints``’s options.
Returns:
reStructuredText describing the type
"""
if inspect.isclass(annotation) and annotation.__module__ == "builtins":
return None

Expand All @@ -53,72 +61,6 @@ def _format_full(annotation: type[Any], config: Config) -> str | None:
return None


def _format_terse(annotation: type[Any], config: Config) -> str:
origin = get_origin(annotation)
args = get_args(annotation)
tilde = "" if config.typehints_fully_qualified else "~"
fmt = partial(_format_terse, config=config)

# display `Union[A, B]` as `A | B`
if origin in ({Union, UnionType} - {None}):
# Never use the `Optional` keyword in the displayed docs.
# Use `| None` instead, similar to other large numerical packages.
return " | ".join(map(fmt, args))

# do not show the arguments of Mapping
if origin in (Mapping, t_Mapping):
return f":py:class:`{tilde}collections.abc.Mapping`"

# display dict as {k: v}
if origin in (dict, t_Dict) and len(args) == 2: # noqa: UP006, PLR2004
k, v = args
return f"{{{fmt(k)}: {fmt(v)}}}"

# display Callable[[a1, a2], r] as (a1, a2) -> r
if origin in (Callable, t_Callable) and len(args) == 2: # noqa: PLR2004
params, ret = args
params = ["…"] if params is Ellipsis else map(fmt, params)
return f"({', '.join(params)}) → {fmt(ret)}"

# display Literal as {'a', 'b', ...}
if origin is Literal:
return f"{{{', '.join(map(repr, args))}}}"

return _format_full(annotation, config) or _format_orig(annotation, config)


def format_annotation(annotation: type[Any], config: Config) -> str | None:
"""Generate reStructuredText containing links to the types.
Unlike :func:`sphinx_autodoc_typehints.format_annotation`,
it tries to achieve a simpler style as seen in numeric packages like numpy.
Args:
annotation: A type or class used as type annotation.
config: Sphinx config containing ``sphinx-autodoc-typehints``’s options.
Returns:
reStructuredText describing the type
"""
curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe, 2)
if calframe[2].function in {"process_docstring", "_inject_signature"} or (
calframe[2].function == "_inject_types_to_docstring"
and calframe[3].function == "process_docstring"
):
return format_both(annotation, config)
# recursive use
return _format_full(annotation, config)


def format_both(annotation: type[Any], config: Config) -> str:
terse = _format_terse(annotation, config)
full = _format_full(annotation, config) or _format_orig(annotation, config)
if terse == full:
return terse
return f":annotation-terse:`{_escape(terse)}`\\ :annotation-full:`{_escape(full)}`"


def _role_annot( # noqa: PLR0913
name: str, # noqa: ARG001
rawtext: str,
Expand Down Expand Up @@ -148,10 +90,6 @@ def _role_annot( # noqa: PLR0913
return [node], messages


def _escape(rst: str) -> str:
return rst.replace("`", "\\`")


def _unescape(rst: str) -> str:
# IDK why the [ part is necessary.
return unescape(rst).replace("\\`", "`").replace("[", "\\[")
4 changes: 2 additions & 2 deletions src/scanpydoc/elegant_typehints/_return_tuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
except ImportError:
UnionType = None

from ._formatting import format_both
from sphinx_autodoc_typehints import format_annotation


if TYPE_CHECKING:
Expand Down Expand Up @@ -71,7 +71,7 @@ def process_docstring( # noqa: PLR0913
idxs_ret_names = _get_idxs_ret_names(lines)
if len(idxs_ret_names) == len(ret_types):
for l, rt in zip(idxs_ret_names, ret_types):
typ = format_both(rt, app.config)
typ = format_annotation(rt, app.config)
lines[l : l + 1] = [f"{lines[l]} : {typ}"]


Expand Down
6 changes: 0 additions & 6 deletions src/scanpydoc/elegant_typehints/static/typehints.css

This file was deleted.

Loading

0 comments on commit ed7dd0c

Please sign in to comment.