Skip to content

Commit 6d0fbca

Browse files
authored
Merge pull request #46 from machow/feat-narrow-notes
Feat narrow notes
2 parents a716a86 + ac4bba7 commit 6d0fbca

File tree

5 files changed

+201
-77
lines changed

5 files changed

+201
-77
lines changed

docs/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ api
1212
# interlinks
1313
_inv
1414
objects.json
15+
_sidebar.yml
16+
_extensions

examples/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
objects.json
2+
_extensions
3+
_inv
4+
_sidebar.yml

quartodoc/ast.py

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,109 @@
1+
from enum import Enum
2+
from dataclasses import dataclass
13
from griffe.docstrings import dataclasses as ds
24
from griffe import dataclasses as dc
35
from plum import dispatch
46
from typing import Union
57

6-
# TODO: these classes are created to wrap some tuple outputs
7-
# we should consolidate logic for transforming the griffe
8-
# docstring here (or open a griffe issue).
9-
from .renderers import tuple_to_data, docstring_section_narrow, ExampleCode, ExampleText
8+
9+
# Transform and patched-in classes ============================================
10+
# TODO: annotate transform return types. make sure subtypes inherit from correct
11+
# griffe base objects.
12+
# TODO: it seems like transform should happen on the root, not individual elements.
13+
14+
15+
def transform(el):
16+
"""Return a more specific docstring element, or simply return the original one."""
17+
18+
if isinstance(el, tuple):
19+
try:
20+
return tuple_to_data(el)
21+
except ValueError:
22+
pass
23+
elif isinstance(el, ds.DocstringSection):
24+
return _DocstringSectionPatched.transform(el)
25+
26+
return el
27+
28+
29+
# Patch DocstringSection ------------------------------------------------------
30+
31+
32+
class DocstringSectionKindPatched(Enum):
33+
see_also = "see also"
34+
notes = "notes"
35+
warnings = "warnings"
36+
37+
38+
class _DocstringSectionPatched(ds.DocstringSection):
39+
_registry: "dict[Enum, _DocstringSectionPatched]" = {}
40+
41+
def __init__(self, value: str, title: "str | None" = None):
42+
self.value = value
43+
super().__init__(title)
44+
45+
def __init_subclass__(cls, **kwargs):
46+
super().__init_subclass__(**kwargs)
47+
48+
if cls.kind.value in cls._registry:
49+
raise KeyError(f"A section for kind {cls.kind} already exists")
50+
51+
cls._registry[cls.kind] = cls
52+
53+
@classmethod
54+
def transform(cls, el: ds.DocstringSection) -> ds.DocstringSection:
55+
"""Attempt to cast DocstringSection element to more specific section type.
56+
57+
Note that this is meant to patch cases where the general DocstringSectionText
58+
class represents a section like See Also, etc..
59+
"""
60+
61+
if isinstance(el, ds.DocstringSectionText):
62+
for kind, sub_cls in cls._registry.items():
63+
prefix = kind.value.title() + "\n---"
64+
if el.value.lstrip("\n").startswith(prefix):
65+
stripped = el.value.replace(prefix, "", 1).lstrip("-\n")
66+
return sub_cls(stripped, el.title)
67+
68+
return el
69+
70+
71+
class DocstringSectionSeeAlso(_DocstringSectionPatched):
72+
kind = DocstringSectionKindPatched.see_also
73+
74+
75+
class DocstringSectionNotes(_DocstringSectionPatched):
76+
kind = DocstringSectionKindPatched.notes
77+
78+
79+
class DocstringSectionWarnings(_DocstringSectionPatched):
80+
kind = DocstringSectionKindPatched.warnings
81+
82+
83+
# Patch Example elements ------------------------------------------------------
84+
85+
86+
@dataclass
87+
class ExampleCode:
88+
value: str
89+
90+
91+
@dataclass
92+
class ExampleText:
93+
value: str
94+
95+
96+
def tuple_to_data(el: "tuple[ds.DocstringSectionKind, str]"):
97+
"""Re-format funky tuple setup in example section to be a class."""
98+
assert len(el) == 2
99+
100+
kind, value = el
101+
if kind.value == "examples":
102+
return ExampleCode(value)
103+
elif kind.value == "text":
104+
return ExampleText(value)
105+
106+
raise ValueError(f"Unsupported first element in tuple: {kind}")
10107

11108

12109
# Tree previewer ==============================================================
@@ -106,7 +203,7 @@ def __init__(self, string_max_length: int = 50, max_depth=999):
106203
def format(self, call, depth=0, pad=0):
107204
"""Return a Symbolic or Call back as a nice tree, with boxes for nodes."""
108205

109-
call = self.transform(call)
206+
call = transform(call)
110207

111208
crnt_fields = fields(call)
112209

@@ -147,19 +244,6 @@ def get_field(self, obj, k):
147244

148245
return getattr(obj, k)
149246

150-
def transform(self, obj):
151-
# TODO: currently this transform happens here, and in the renderer.
152-
# let's consolidate this into one step (when getting the object)
153-
if isinstance(obj, tuple):
154-
try:
155-
return tuple_to_data(obj)
156-
except ValueError:
157-
pass
158-
elif isinstance(obj, ds.DocstringSectionText):
159-
return docstring_section_narrow(obj)
160-
161-
return obj
162-
163247
def fmt_pipe(self, x, is_final=False, pad=0):
164248
if not is_final:
165249
connector = self.icon_connector if not is_final else " "

quartodoc/renderers.py

Lines changed: 22 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,14 @@
1+
import quartodoc.ast as qast
12
import re
23

3-
from enum import Enum
44
from griffe.docstrings import dataclasses as ds
55
from griffe import dataclasses as dc
6-
from dataclasses import dataclass
76
from tabulate import tabulate
87
from plum import dispatch
98
from typing import Tuple, Union
109

1110

12-
# Docstring rendering =========================================================
13-
1411
# utils -----------------------------------------------------------------------
15-
# these largely re-format the output of griffe
16-
17-
18-
def tuple_to_data(el: "tuple[ds.DocstringSectionKind, str]"):
19-
"""Re-format funky tuple setup in example section to be a class."""
20-
assert len(el) == 2
21-
22-
kind, value = el
23-
if kind.value == "examples":
24-
return ExampleCode(value)
25-
elif kind.value == "text":
26-
return ExampleText(value)
27-
28-
raise ValueError(f"Unsupported first element in tuple: {kind}")
29-
30-
31-
def docstring_section_narrow(el: ds.DocstringSection) -> ds.DocstringSection:
32-
# attempt to narrow down text sections
33-
prefix = "See Also\n---"
34-
if isinstance(el, ds.DocstringSectionText) and el.value.startswith(prefix):
35-
stripped = el.value.replace(prefix, "", 1).lstrip("-\n")
36-
return DocstringSectionSeeAlso(stripped, el.title)
37-
38-
return el
39-
40-
41-
class DocstringSectionKindPatched(Enum):
42-
see_also = "see also"
43-
44-
45-
class DocstringSectionSeeAlso(ds.DocstringSection):
46-
kind = DocstringSectionKindPatched.see_also
47-
48-
def __init__(self, value: str, title: "str | None"):
49-
self.value = value
50-
super().__init__(title)
51-
52-
53-
@dataclass
54-
class ExampleCode:
55-
value: str
56-
57-
58-
@dataclass
59-
class ExampleText:
60-
value: str
6112

6213

6314
def escape(val: str):
@@ -230,7 +181,7 @@ def render(self, el: Union[dc.Object, dc.Alias]):
230181
pass
231182
else:
232183
for section in el.docstring.parsed:
233-
new_el = docstring_section_narrow(section)
184+
new_el = qast.transform(section)
234185
title = new_el.kind.value
235186
body = self.render(new_el)
236187

@@ -281,7 +232,7 @@ def render(self, el: dc.Parameter):
281232
# or a section with a header not included in the numpydoc standard
282233
@dispatch
283234
def render(self, el: ds.DocstringSectionText):
284-
new_el = docstring_section_narrow(el)
235+
new_el = qast.transform(el)
285236
if isinstance(new_el, ds.DocstringSectionText):
286237
# ensures we don't recurse forever
287238
return el.value
@@ -318,27 +269,43 @@ def render(self, el: ds.DocstringAttribute):
318269
annotation = self._render_annotation(el.annotation)
319270
return el.name, self.render(annotation), el.description
320271

272+
# warnings ----
273+
274+
@dispatch
275+
def render(self, el: qast.DocstringSectionWarnings):
276+
return el.value
277+
321278
# see also ----
322279

323280
@dispatch
324-
def render(self, el: DocstringSectionSeeAlso):
281+
def render(self, el: qast.DocstringSectionSeeAlso):
325282
# TODO: attempt to parse See Also sections
326283
return convert_rst_link_to_md(el.value)
327284

285+
# notes ----
286+
287+
@dispatch
288+
def render(self, el: qast.DocstringSectionNotes):
289+
return el.value
290+
328291
# examples ----
329292

330293
@dispatch
331294
def render(self, el: ds.DocstringSectionExamples):
332295
# its value is a tuple: DocstringSectionKind["text" | "examples"], str
333-
data = map(tuple_to_data, el.value)
296+
data = map(qast.transform, el.value)
334297
return "\n\n".join(list(map(self.render, data)))
335298

336299
@dispatch
337-
def render(self, el: ExampleCode):
300+
def render(self, el: qast.ExampleCode):
338301
return f"""```python
339302
{el.value}
340303
```"""
341304

305+
@dispatch
306+
def render(self, el: qast.ExampleText):
307+
return el.value
308+
342309
# returns ----
343310

344311
@dispatch
@@ -355,10 +322,6 @@ def render(self, el: Union[ds.DocstringReturn, ds.DocstringRaise]):
355322

356323
# unsupported parts ----
357324

358-
@dispatch
359-
def render(self, el: ExampleText):
360-
return el.value
361-
362325
@dispatch.multi(
363326
(ds.DocstringAdmonition,),
364327
(ds.DocstringDeprecated,),

quartodoc/tests/test_ast.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import quartodoc.ast as qast
2+
import pytest
3+
4+
from griffe.docstrings import dataclasses as ds
5+
from griffe import dataclasses as dc
6+
from griffe.docstrings.parsers import parse_numpy
7+
8+
from quartodoc import get_object
9+
10+
11+
@pytest.mark.parametrize(
12+
"el, body",
13+
[
14+
("See Also\n---", ""),
15+
("See Also\n---\n", ""),
16+
("See Also\n--------", ""),
17+
("\n\nSee Also\n---\n", ""),
18+
("See Also\n---\nbody text", "body text"),
19+
("See Also\n---\nbody text", "body text"),
20+
],
21+
)
22+
def test_transform_docstring_section(el, body):
23+
src = ds.DocstringSectionText(el, title=None)
24+
res = qast._DocstringSectionPatched.transform(src)
25+
26+
assert isinstance(res, qast.DocstringSectionSeeAlso)
27+
assert res.value == body
28+
29+
30+
@pytest.mark.parametrize(
31+
"el, cls",
32+
[
33+
("See Also\n---", qast.DocstringSectionSeeAlso),
34+
("Warnings\n---", qast.DocstringSectionWarnings),
35+
("Notes\n---", qast.DocstringSectionNotes),
36+
],
37+
)
38+
def test_transform_docstring_section_subtype(el, cls):
39+
# using transform method ----
40+
src = ds.DocstringSectionText(el, title=None)
41+
res = qast._DocstringSectionPatched.transform(src)
42+
43+
assert isinstance(res, cls)
44+
45+
# using transform function ----
46+
parsed = parse_numpy(dc.Docstring(el))
47+
assert len(parsed) == 1
48+
49+
res2 = qast.transform(parsed[0])
50+
assert isinstance(res2, cls)
51+
52+
53+
@pytest.mark.xfail(reason="TODO: sections get grouped into single element")
54+
def test_transform_docstring_section_clump():
55+
docstring = "See Also---\n\nWarnings\n---\n\nNotes---\n\n"
56+
parsed = parse_numpy(dc.Docstring(docstring))
57+
58+
assert len(parsed) == 1
59+
60+
# res = transform(parsed[0])
61+
62+
# what to do here? this should more reasonably be handled when transform
63+
# operates on the root.
64+
65+
66+
def test_preview_no_fail(capsys):
67+
qast.preview(get_object("quartodoc", "get_object"))
68+
69+
res = capsys.readouterr()
70+
71+
assert "get_object" in res.out

0 commit comments

Comments
 (0)