Skip to content

Commit 0051bf4

Browse files
authored
Use formatter option (#44)
1 parent ddd3631 commit 0051bf4

File tree

6 files changed

+53
-93
lines changed

6 files changed

+53
-93
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ doc = [
3636
'scanpydoc[typehints]',
3737
'sphinx-rtd-theme',
3838
]
39-
typehints = ['sphinx-autodoc-typehints>=1.14']
39+
typehints = ['sphinx-autodoc-typehints>=1.15.1']
4040
theme = ['sphinx-rtd-theme']
4141

4242
[tool.flit.entrypoints.'sphinx.html_themes']

scanpydoc/elegant_typehints/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ def x() -> Tuple[int, float]:
4949
from pathlib import Path
5050
from typing import Any, Dict
5151

52-
import sphinx_autodoc_typehints
5352
from docutils.parsers.rst import roles
5453
from sphinx.application import Sphinx
5554
from sphinx.config import Config
@@ -102,7 +101,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
102101

103102
from .formatting import _role_annot, format_annotation
104103

105-
sphinx_autodoc_typehints.format_annotation = format_annotation
104+
app.config.typehints_formatter = format_annotation
106105
for name in ["annotation-terse", "annotation-full"]:
107106
roles.register_canonical_role(
108107
name, partial(_role_annot, additional_classes=name.split("-"))

scanpydoc/elegant_typehints/formatting.py

Lines changed: 20 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import collections.abc as cabc
22
import inspect
33
from functools import partial
4-
from typing import Any, Dict, Iterable, List, Sequence, Tuple, Type, Union
4+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Type, Union
5+
6+
from sphinx.config import Config
57

68

79
try: # 3.8 additions
810
from typing import Literal, get_args, get_origin
911
except ImportError:
1012
from typing_extensions import Literal, get_args, get_origin
1113

12-
import sphinx_autodoc_typehints
1314
from docutils import nodes
1415
from docutils.nodes import Node
1516
from docutils.parsers.rst.roles import set_classes
@@ -20,20 +21,16 @@
2021
from scanpydoc import elegant_typehints
2122

2223

23-
def _format_full(
24-
annotation: Type[Any],
25-
fully_qualified: bool = False,
26-
simplify_optional_unions: bool = True,
27-
):
24+
def _format_full(annotation: Type[Any], config: Config) -> Optional[str]:
2825
if inspect.isclass(annotation) and annotation.__module__ == "builtins":
29-
return _format_orig(annotation, fully_qualified, simplify_optional_unions)
26+
return None
3027

3128
origin = get_origin(annotation)
32-
tilde = "" if fully_qualified else "~"
29+
tilde = "" if config.typehints_fully_qualified else "~"
3330

3431
annotation_cls = annotation if inspect.isclass(annotation) else type(annotation)
3532
if annotation_cls.__module__ == "typing":
36-
return _format_orig(annotation, fully_qualified, simplify_optional_unions)
33+
return None
3734

3835
# Only if this is a real class we override sphinx_autodoc_typehints
3936
if inspect.isclass(annotation) or inspect.isclass(origin):
@@ -43,22 +40,14 @@ def _format_full(
4340
if override is not None:
4441
return f":py:{role}:`{tilde}{override}`"
4542

46-
return _format_orig(annotation, fully_qualified, simplify_optional_unions)
43+
return None
4744

4845

49-
def _format_terse(
50-
annotation: Type[Any],
51-
fully_qualified: bool = False,
52-
simplify_optional_unions: bool = True,
53-
) -> str:
46+
def _format_terse(annotation: Type[Any], config: Config) -> str:
5447
origin = get_origin(annotation)
5548
args = get_args(annotation)
56-
tilde = "" if fully_qualified else "~"
57-
fmt = partial(
58-
_format_terse,
59-
fully_qualified=fully_qualified,
60-
simplify_optional_unions=simplify_optional_unions,
61-
)
49+
tilde = "" if config.typehints_fully_qualified else "~"
50+
fmt = partial(_format_terse, config=config)
6251

6352
# display `Union[A, B]` as `A | B`
6453
if origin is Union:
@@ -85,51 +74,34 @@ def _format_terse(
8574
if origin is Literal:
8675
return f"{{{', '.join(map(repr, args))}}}"
8776

88-
return _format_full(annotation, fully_qualified, simplify_optional_unions)
77+
return _format_full(annotation, config) or _format_orig(annotation, config)
8978

9079

91-
def format_annotation(
92-
annotation: Type[Any],
93-
fully_qualified: bool = False,
94-
simplify_optional_unions: bool = True,
95-
) -> str:
80+
def format_annotation(annotation: Type[Any], config: Config) -> Optional[str]:
9681
r"""Generate reStructuredText containing links to the types.
9782
9883
Unlike :func:`sphinx_autodoc_typehints.format_annotation`,
9984
it tries to achieve a simpler style as seen in numeric packages like numpy.
10085
10186
Args:
10287
annotation: A type or class used as type annotation.
103-
fully_qualified: If links should be formatted as fully qualified
104-
(e.g. ``:py:class:`foo.Bar```) or not (e.g. ``:py:class:`~foo.Bar```).
105-
simplify_optional_unions: If Unions should be minimized if they contain
106-
3 or more elements one of which is ``None``. (If ``True``, e.g.
107-
``Optional[Union[str, int]]`` becomes ``Union[str, int, None]``)
88+
config: Sphinx config containing ``sphinx-autodoc-typehints``’s options.
10889
10990
Returns:
11091
reStructuredText describing the type
11192
"""
112-
if sphinx_autodoc_typehints.format_annotation is not format_annotation:
113-
raise RuntimeError(
114-
"This function is not guaranteed to work correctly without overriding"
115-
"`sphinx_autodoc_typehints.format_annotation` with it."
116-
)
11793

11894
curframe = inspect.currentframe()
11995
calframe = inspect.getouterframes(curframe, 2)
120-
if calframe[1][3] == "process_docstring":
121-
return format_both(annotation, fully_qualified, simplify_optional_unions)
96+
if calframe[2].function == "process_docstring":
97+
return format_both(annotation, config)
12298
else: # recursive use
123-
return _format_full(annotation, fully_qualified, simplify_optional_unions)
99+
return _format_full(annotation, config)
124100

125101

126-
def format_both(
127-
annotation: Type[Any],
128-
fully_qualified: bool = False,
129-
simplify_optional_unions: bool = True,
130-
) -> str:
131-
terse = _format_terse(annotation, fully_qualified, simplify_optional_unions)
132-
full = _format_full(annotation, fully_qualified, simplify_optional_unions)
102+
def format_both(annotation: Type[Any], config: Config) -> str:
103+
terse = _format_terse(annotation, config)
104+
full = _format_full(annotation, config) or _format_orig(annotation, config)
133105
return f":annotation-terse:`{_escape(terse)}`\\ :annotation-full:`{_escape(full)}`"
134106

135107

scanpydoc/elegant_typehints/return_tuple.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def process_docstring(
8989

9090
if len(idxs_ret_names) == len(ret_types):
9191
for l, rt in zip(idxs_ret_names, ret_types):
92-
typ = format_both(rt, app.config.typehints_fully_qualified)
92+
typ = format_both(rt, app.config)
9393
lines[l : l + 1] = [f"{lines[l]} : {typ}"]
9494

9595

tests/conftest.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import importlib.util
2+
import linecache
23
import sys
34
import typing as t
45
from tempfile import NamedTemporaryFile
56
from textwrap import dedent
7+
from uuid import uuid4
68

79
import pytest
810
from docutils.nodes import document
@@ -55,14 +57,19 @@ def _render(app: Sphinx, doc: document) -> str:
5557

5658

5759
@pytest.fixture
58-
def make_module():
60+
def make_module(tmp_path):
5961
added_modules = []
6062

6163
def make_module(name, code):
64+
code = dedent(code)
6265
assert name not in sys.modules
6366
spec = importlib.util.spec_from_loader(name, loader=None)
6467
mod = sys.modules[name] = importlib.util.module_from_spec(spec)
65-
exec(dedent(code), mod.__dict__)
68+
path = tmp_path / f"{name}_{str(uuid4()).replace('-', '_')}.py"
69+
path.write_text(code)
70+
mod.__file__ = str(path)
71+
exec(code, mod.__dict__)
72+
linecache.updatecache(str(path), mod.__dict__)
6673
added_modules.append(name)
6774
return mod
6875

tests/test_elegant_typehints.py

Lines changed: 21 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ def app(make_app_setup) -> Sphinx:
5656

5757
@pytest.fixture
5858
def process_doc(app):
59-
app.config.typehints_fully_qualified = True
60-
6159
def process(fn: t.Callable) -> t.List[str]:
6260
lines = inspect.getdoc(fn).split("\n")
6361
sat.process_docstring(app, "function", fn.__name__, fn, None, lines)
@@ -73,7 +71,7 @@ def test_app(app):
7371

7472

7573
def test_default(app):
76-
assert format_annotation(str) == ":py:class:`str`"
74+
assert format_annotation(str, app.config) is None
7775

7876

7977
def test_alternatives(process_doc):
@@ -125,26 +123,24 @@ def fn_test(m: t.Mapping[str, int] = {}):
125123

126124
assert process_doc(fn_test) == [
127125
":type m: "
128-
r":annotation-terse:`:py:class:\`typing.Mapping\``\ "
126+
r":annotation-terse:`:py:class:\`~typing.Mapping\``\ "
129127
r":annotation-full:`"
130-
r":py:class:\`typing.Mapping\`\[:py:class:\`str\`, :py:class:\`int\`]"
128+
r":py:class:\`~typing.Mapping\`\[:py:class:\`str\`, :py:class:\`int\`]"
131129
"` (default: ``{}``)",
132130
":param m: Test M",
133131
]
134132

135133

136134
def test_mapping(app):
137-
assert _format_terse(t.Mapping[str, t.Any]) == ":py:class:`~typing.Mapping`"
138-
assert _format_full(t.Mapping[str, t.Any]) == (
139-
r":py:class:`~typing.Mapping`\["
140-
r":py:class:`str`, "
141-
r":py:data:`~typing.Any`"
142-
r"]"
135+
assert (
136+
_format_terse(t.Mapping[str, t.Any], app.config)
137+
== ":py:class:`~typing.Mapping`"
143138
)
139+
assert _format_full(t.Mapping[str, t.Any], app.config) is None
144140

145141

146142
def test_dict(app):
147-
assert _format_terse(t.Dict[str, t.Any]) == (
143+
assert _format_terse(t.Dict[str, t.Any], app.config) == (
148144
"{:py:class:`str`: :py:data:`~typing.Any`}"
149145
)
150146

@@ -160,45 +156,37 @@ def test_dict(app):
160156
],
161157
)
162158
def test_callable_terse(app, annotation, expected):
163-
assert _format_terse(annotation) == expected
159+
assert _format_terse(annotation, app.config) == expected
164160

165161

166162
def test_literal(app):
167-
assert _format_terse(Literal["str", 1, None]) == "{'str', 1, None}"
168-
assert _format_full(Literal["str", 1, None]) == (
169-
r":py:data:`~typing.Literal`\['str', 1, None]"
170-
)
163+
assert _format_terse(Literal["str", 1, None], app.config) == "{'str', 1, None}"
164+
assert _format_full(Literal["str", 1, None], app.config) is None
171165

172166

173167
def test_qualname_overrides_class(app, _testmod):
174168
assert _testmod.Class.__module__ == "_testmod"
175-
assert _format_terse(_testmod.Class) == ":py:class:`~test.Class`"
169+
assert _format_terse(_testmod.Class, app.config) == ":py:class:`~test.Class`"
176170

177171

178172
def test_qualname_overrides_exception(app, _testmod):
179173
assert _testmod.Excep.__module__ == "_testmod"
180-
assert _format_terse(_testmod.Excep) == ":py:exc:`~test.Excep`"
174+
assert _format_terse(_testmod.Excep, app.config) == ":py:exc:`~test.Excep`"
181175

182176

183177
def test_qualname_overrides_recursive(app, _testmod):
184-
assert _format_terse(t.Union[_testmod.Class, str]) == (
178+
assert _format_terse(t.Union[_testmod.Class, str], app.config) == (
185179
r":py:class:`~test.Class` | :py:class:`str`"
186180
)
187-
assert _format_full(t.Union[_testmod.Class, str]) == (
188-
r":py:data:`~typing.Union`\["
189-
r":py:class:`~test.Class`, "
190-
r":py:class:`str`"
191-
r"]"
192-
)
181+
assert _format_full(t.Union[_testmod.Class, str], app.config) is None
193182

194183

195184
def test_fully_qualified(app, _testmod):
196-
assert _format_terse(t.Union[_testmod.Class, str], True) == (
185+
app.config.typehints_fully_qualified = True
186+
assert _format_terse(t.Union[_testmod.Class, str], app.config) == (
197187
r":py:class:`test.Class` | :py:class:`str`"
198188
)
199-
assert _format_full(t.Union[_testmod.Class, str], True) == (
200-
r":py:data:`typing.Union`\[" r":py:class:`test.Class`, " r":py:class:`str`" r"]"
201-
)
189+
assert _format_full(t.Union[_testmod.Class, str], app.config) is None
202190

203191

204192
def test_classes_get_added(app, parse):
@@ -228,6 +216,7 @@ def test_classes_get_added(app, parse):
228216
ids=lambda p: str(p).replace("typing.", ""),
229217
)
230218
def test_typing_classes(app, annotation, formatter):
219+
app.config.typehints_fully_qualified = True
231220
name = (
232221
getattr(annotation, "_name", None)
233222
or getattr(annotation, "__name__", None)
@@ -240,15 +229,8 @@ def test_typing_classes(app, annotation, formatter):
240229
args = get_args(annotation)
241230
if name == "Union" and len(args) == 2 and type(None) in args:
242231
name = "Optional"
243-
assert formatter(annotation, True).startswith(f":py:data:`typing.{name}")
244-
245-
246-
def test_typing_class_nested(app):
247-
assert _format_full(t.Optional[t.Tuple[int, str]]) == (
248-
":py:data:`~typing.Optional`\\["
249-
":py:data:`~typing.Tuple`\\[:py:class:`int`, :py:class:`str`]"
250-
"]"
251-
)
232+
output = formatter(annotation, app.config)
233+
assert output is None or output.startswith(f":py:data:`typing.{name}")
252234

253235

254236
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)