Skip to content

Commit 9a3f80a

Browse files
committed
feat: support Interlaced doc layouts
1 parent f28d5b4 commit 9a3f80a

File tree

2 files changed

+89
-5
lines changed

2 files changed

+89
-5
lines changed

quartodoc/layout.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class Section(_Base):
6060
title: str
6161
desc: str
6262
package: Union[str, None, MISSING] = MISSING()
63-
contents: list[Union[ContentElement, Doc, _AutoDefault]]
63+
contents: ContentList
6464

6565

6666
class SummaryDetails(_Base):
@@ -79,7 +79,7 @@ class Page(_Base):
7979
summary: Optional[SummaryDetails] = None
8080
flatten: bool = False
8181

82-
contents: list[Union[ContentElement, Doc, _AutoDefault]]
82+
contents: ContentList
8383

8484
@property
8585
def obj(self):
@@ -99,6 +99,32 @@ class MemberPage(Page):
9999
contents: list[Doc]
100100

101101

102+
class Interlaced(BaseModel):
103+
"""A group of objects, whose documentation will be interlaced.
104+
105+
Rather than list each object's documentation in sequence, this element indicates
106+
that each piece of documentation (e.g. signatures, examples) should be grouped
107+
together.
108+
"""
109+
110+
kind: Literal["interlaced"] = "interlaced"
111+
package: Union[str, None, MISSING] = MISSING()
112+
113+
# note that this is similar to a ContentList, except it cannot include
114+
# elements like Pages, etc..
115+
contents: list[Union[Auto, Doc, _AutoDefault]]
116+
117+
@property
118+
def name(self):
119+
if not self.contents:
120+
raise AttributeError(
121+
f"Cannot get property name for object of type {type(self)}."
122+
" There are no content elements."
123+
)
124+
125+
return self.contents[0].name
126+
127+
102128
class Text(_Base):
103129
kind: Literal["text"] = "text"
104130
contents: str
@@ -224,16 +250,19 @@ class DocAttribute(Doc):
224250
class DocModule(Doc):
225251
kind: Literal["module"] = "module"
226252
members: list[Union[MemberPage, Doc, Link]] = tuple()
253+
flat: bool
227254

228255

229256
SectionElement = Annotated[Union[Section, Page], Field(discriminator="kind")]
230257
"""Entry in the sections list."""
231258

232259
ContentElement = Annotated[
233-
Union[Page, Section, Text, Auto], Field(discriminator="kind")
260+
Union[Page, Section, Interlaced, Text, Auto], Field(discriminator="kind")
234261
]
235262
"""Entry in the contents list."""
236263

264+
ContentList = list[Union[ContentElement, Doc, _AutoDefault]]
265+
237266
# Item ----
238267

239268

@@ -254,3 +283,4 @@ class Config:
254283
Page.update_forward_refs()
255284
Auto.update_forward_refs()
256285
MemberPage.update_forward_refs()
286+
Interlaced.update_forward_refs()

quartodoc/renderers/md_renderer.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,53 @@ def render(self, el: layout.Section):
181181
body = list(map(self.render, el.contents))
182182

183183
return "\n\n".join([section_top, *body])
184+
185+
@dispatch
186+
def render(self, el: layout.Interlaced):
187+
# render a sequence of objects with like-sections together.
188+
# restrict its behavior to documenting functions for now ----
189+
for doc in el.contents:
190+
if not isinstance(doc, (layout.DocFunction, layout.DocAttribute)):
191+
raise NotImplementedError(
192+
"Can only render Interlaced elements if all content elements"
193+
" are function or attribute docs."
194+
f" Found an element of type {type(doc)}, with name {doc.name}"
195+
)
196+
197+
# render ----
198+
# currently, we use everything from the first function, and just render
199+
# the signatures together
200+
first_doc = el.contents[0]
201+
objs = [doc.obj for doc in el.contents]
202+
203+
if first_doc.obj.docstring is None:
204+
raise ValueError("The first element of Interlaced must have a docstring.")
205+
206+
207+
str_title = self.render_header(first_doc)
208+
str_sig = "\n\n".join(map(self.signature, objs))
209+
str_body = []
210+
211+
# TODO: we should also interlace parameters and examples
212+
# parsed = map(qast.transform, [x.docstring.parsed for x in objs if x.docstring])
213+
214+
# TODO: this is copied from the render method for dc.Object
215+
for section in qast.transform(first_doc.obj.docstring.parsed):
216+
title = section.title or section.kind.value
217+
body = self.render(section)
218+
219+
if title != "text":
220+
header = f"{'#' * (self.crnt_header_level + 1)} {title.title()}"
221+
str_body.append("\n\n".join([header, body]))
222+
else:
223+
str_body.append(body)
224+
225+
if self.show_signature:
226+
parts = [str_title, str_sig, *str_body]
227+
else:
228+
parts = [str_title, *str_body]
229+
230+
return "\n\n".join(parts)
184231

185232
@dispatch
186233
def render(self, el: layout.Doc):
@@ -226,7 +273,8 @@ def render(self, el: Union[layout.DocClass, layout.DocModule]):
226273
extra_parts.append(meths)
227274

228275
# TODO use context manager, or context variable?
229-
with self._increment_header(2):
276+
n_incr = 1 if el.flat else 2
277+
with self._increment_header(n_incr):
230278
meth_docs = [self.render(x) for x in raw_meths if isinstance(x, layout.Doc)]
231279

232280
body = self.render(el.obj)
@@ -311,7 +359,7 @@ def render(self, el: dc.Parameter):
311359
elif has_default:
312360
res = f"{glob}{el.name}={el.default}"
313361
else:
314-
res = el.name
362+
res = f"{glob}{el.name}"
315363

316364
return sanitize(res)
317365

@@ -477,6 +525,12 @@ def summarize(self, el: layout.Page):
477525
def summarize(self, el: layout.MemberPage):
478526
# TODO: model should validate these only have a single entry
479527
return self.summarize(el.contents[0], el.path, shorten = True)
528+
529+
@dispatch
530+
def summarize(self, el: layout.Interlaced, *args, **kwargs):
531+
rows = [self.summarize(doc, *args, **kwargs) for doc in el.contents]
532+
533+
return "\n".join(rows)
480534

481535
@dispatch
482536
def summarize(self, el: layout.Doc, path: Optional[str] = None, shorten: bool = False):

0 commit comments

Comments
 (0)