Skip to content

Commit 599737b

Browse files
authored
Merge pull request #212 from machow/feat-automodule
Feat automodule
2 parents d89c03f + 77f07fd commit 599737b

File tree

14 files changed

+512
-39
lines changed

14 files changed

+512
-39
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
exclude: ".*\\.(csv)|(md)|(json)"
1+
exclude: ".*\\.(csv)|(md)|(json)|(ambr)"
22
repos:
33
- repo: https://github.com/pre-commit/pre-commit-hooks
44
rev: v2.4.0

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ docs/examples/%: examples/%/_site
3030
rm -rf docs/examples/$*
3131
cp -rv $< $@
3232

33-
docs-build-examples: docs/examples/single-page docs/examples/pkgdown
33+
docs-build-examples: docs/examples/single-page docs/examples/pkgdown docs/examples/auto-package
3434

3535
docs-build: docs-build-examples
3636
cd docs && quarto add --no-prompt ..

docs/_quarto.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ project:
44
resources:
55
- examples/single-page
66
- examples/pkgdown
7+
- examples/auto-package
78

89
metadata-files:
910
- api/_sidebar.yml

examples/auto-package/_quarto.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
project:
2+
type: website
3+
resources:
4+
- objects.json
5+
6+
website:
7+
title: pkgdown example
8+
navbar:
9+
left:
10+
- href: https://machow.github.io/quartodoc/
11+
text: quartodoc home
12+
- file: reference/index.qmd
13+
text: "Reference"
14+
right:
15+
- icon: github
16+
href: https://github.com/machow/quartodoc/tree/main/examples/pkgdown
17+
18+
format:
19+
html:
20+
toc: true
21+
22+
quartodoc:
23+
style: pkgdown
24+
dir: reference
25+
package: quartodoc

quartodoc/__init__.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
1+
"""quartodoc is a package for building delightful python API documentation.
2+
"""
3+
14
# flake8: noqa
25

3-
from .autosummary import (
4-
get_function,
5-
get_object,
6-
Builder,
7-
BuilderPkgdown,
8-
BuilderSinglePage,
9-
)
6+
from .autosummary import get_function, get_object, Builder
107
from .renderers import MdRenderer
118
from .inventory import convert_inventory, create_inventory
129
from .ast import preview
1310
from .builder.blueprint import blueprint
1411
from .builder.collect import collect
1512
from .layout import Auto
13+
14+
__all__ = (
15+
"Auto",
16+
"blueprint",
17+
"collect",
18+
"convert_inventory",
19+
"create_inventory",
20+
"get_object",
21+
"preview",
22+
"Builder",
23+
"BuilderPkgdown",
24+
"BuilderSinglePage",
25+
"MdRenderer",
26+
)

quartodoc/autosummary.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,8 @@ def __init_subclass__(cls, **kwargs):
416416
def __init__(
417417
self,
418418
package: str,
419-
sections: "list[Any]",
419+
# TODO: correct typing
420+
sections: "list[Any]" = tuple(),
420421
version: "str | None" = None,
421422
dir: str = "reference",
422423
title: str = "Function reference",
@@ -426,6 +427,7 @@ def __init__(
426427
rewrite_all_pages=False,
427428
source_dir: "str | None" = None,
428429
dynamic: bool | None = None,
430+
parser="numpy",
429431
):
430432
self.layout = self.load_layout(sections=sections, package=package)
431433

@@ -434,6 +436,7 @@ def __init__(
434436
self.dir = dir
435437
self.title = title
436438
self.sidebar = sidebar
439+
self.parser = parser
437440

438441
self.renderer = Renderer.from_config(renderer)
439442

@@ -451,10 +454,12 @@ def load_layout(self, sections: dict, package: str):
451454
try:
452455
return layout.Layout(sections=sections, package=package)
453456
except ValidationError as e:
454-
msg = 'Configuration error for YAML:\n - '
457+
msg = "Configuration error for YAML:\n - "
455458
errors = [fmt(err) for err in e.errors() if fmt(err)]
456-
first_error = errors[0] # we only want to show one error at a time b/c it is confusing otherwise
457-
msg += first_error
459+
first_error = errors[
460+
0
461+
] # we only want to show one error at a time b/c it is confusing otherwise
462+
msg += first_error
458463
raise ValueError(msg) from None
459464

460465
# building ----------------------------------------------------------------
@@ -480,7 +485,7 @@ def build(self, filter: str = "*"):
480485
# shaping and collection ----
481486

482487
_log.info("Generating blueprint.")
483-
blueprint = blueprint(self.layout, dynamic=self.dynamic)
488+
blueprint = blueprint(self.layout, dynamic=self.dynamic, parser=self.parser)
484489

485490
_log.info("Collecting pages and inventory items.")
486491
pages, items = collect(blueprint, base_dir=self.dir)

quartodoc/builder/blueprint.py

Lines changed: 131 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from __future__ import annotations
22

33
import logging
4+
import json
5+
import yaml
46

57
from griffe import dataclasses as dc
68
from griffe.loader import GriffeLoader
79
from griffe.collections import ModulesCollection, LinesCollection
810
from griffe.docstrings.parsers import Parser
911
from functools import partial
12+
from textwrap import indent
1013

1114
from plum import dispatch
1215

@@ -31,6 +34,65 @@
3134
_log = logging.getLogger(__name__)
3235

3336

37+
def _auto_package(mod: dc.Module) -> list[Section]:
38+
"""Create default sections for the given package."""
39+
40+
import griffe.docstrings.dataclasses as ds
41+
42+
# get module members for content ----
43+
contents = []
44+
for name, member in mod.members.items():
45+
external_alias = _is_external_alias(member, mod)
46+
if external_alias or member.is_module or name.startswith("__"):
47+
continue
48+
49+
contents.append(Auto(name=name))
50+
51+
# try to fetch a description of the module ----
52+
mod_summary = mod.docstring.parsed[0]
53+
if isinstance(mod_summary, ds.DocstringSectionText):
54+
desc = mod_summary.value
55+
else:
56+
desc = ""
57+
58+
return [Section(title=mod.path, desc=desc, contents=contents)]
59+
60+
61+
def _is_external_alias(obj: dc.Alias | dc.Object, mod: dc.Module):
62+
package_name = mod.path.split(".")[0]
63+
64+
if not isinstance(obj, dc.Alias):
65+
return False
66+
67+
crnt_target = obj
68+
69+
while crnt_target.is_alias:
70+
if not crnt_target.target_path.startswith(package_name):
71+
return True
72+
73+
try:
74+
new_target = crnt_target.modules_collection[crnt_target.target_path]
75+
76+
if new_target is crnt_target:
77+
raise Exception(f"Cyclic Alias: {new_target}")
78+
79+
crnt_target = new_target
80+
81+
except KeyError:
82+
# assumes everything from module was loaded, so target must
83+
# be outside module
84+
return True
85+
86+
return False
87+
88+
89+
def _to_simple_dict(el):
90+
# round-trip to json, so we can take advantage of pydantic
91+
# dumping Enums, etc.. There may be a simple way to do
92+
# this in pydantic v2.
93+
return json.loads(el.json(exclude_unset=True))
94+
95+
3496
class BlueprintTransformer(PydanticTransformer):
3597
def __init__(self, get_object=None, parser="numpy"):
3698

@@ -64,6 +126,13 @@ def get_object_fixed(self, path, **kwargs):
64126
f" Does an object with the path {path} exist?"
65127
)
66128

129+
@staticmethod
130+
def _clean_member_path(path, new):
131+
if ":" in new:
132+
return new.replace(":", ".")
133+
134+
return new
135+
67136
@dispatch
68137
def visit(self, el):
69138
# TODO: use a context handler
@@ -80,6 +149,40 @@ def visit(self, el):
80149
finally:
81150
self.crnt_package = old
82151

152+
@dispatch
153+
def enter(self, el: Layout):
154+
if not el.sections:
155+
# TODO: should be shown all the time, not just logged,
156+
# but also want to be able to disable (similar to pins)
157+
print("Autogenerating contents (since no contents specified in config)")
158+
159+
package = el.package
160+
161+
mod = self.get_object_fixed(package)
162+
sections = _auto_package(mod)
163+
164+
if not sections:
165+
# TODO: informative message. When would this occur?
166+
raise ValueError()
167+
168+
new_el = el.copy()
169+
new_el.sections = sections
170+
171+
print(
172+
"Use the following configuration to recreate the automatically",
173+
" generated site:\n\n\n",
174+
"quartodoc:\n",
175+
indent(
176+
yaml.safe_dump(_to_simple_dict(new_el), sort_keys=False), " " * 2
177+
),
178+
"\n",
179+
sep="",
180+
)
181+
182+
return super().enter(new_el)
183+
184+
return super().enter(el)
185+
83186
@dispatch
84187
def exit(self, el: Section):
85188
"""Transform top-level sections, so their contents are all Pages."""
@@ -109,8 +212,10 @@ def enter(self, el: Auto):
109212
pkg = self.crnt_package
110213
if pkg is None:
111214
path = el.name
215+
elif ":" in pkg or ":" in el.name:
216+
path = f"{pkg}.{el.name}"
112217
else:
113-
path = f"{pkg}.{el.name}" if ":" in el.name else f"{pkg}:{el.name}"
218+
path = f"{pkg}:{el.name}"
114219

115220
_log.info(f"Getting object for {path}")
116221

@@ -128,19 +233,27 @@ def enter(self, el: Auto):
128233
# but the actual objects on the target.
129234
# On the other hand, we've wired get_object up to make sure getting
130235
# the member of an Alias also returns an Alias.
131-
member_path = self._append_member_path(path, entry)
132-
obj_member = self.get_object_fixed(member_path, dynamic=dynamic)
236+
# member_path = self._append_member_path(path, entry)
237+
relative_path = self._clean_member_path(path, entry)
238+
239+
# create Doc element for member ----
240+
# TODO: when a member is a Class, it is currently created using
241+
# defaults, and there is no way to override those.
242+
doc = self.visit(Auto(name=relative_path, dynamic=dynamic, package=path))
133243

134244
# do no document submodules
135-
if obj_member.kind.value == "module":
245+
if (
246+
_is_external_alias(doc.obj, obj.package)
247+
or doc.obj.kind.value == "module"
248+
):
136249
continue
137250

138-
# create element for child ----
139-
doc = Doc.from_griffe(obj_member.name, obj_member)
251+
# obj_member = self.get_object_fixed(member_path, dynamic=dynamic)
252+
# doc = Doc.from_griffe(obj_member.name, obj_member)
140253

141254
# Case 1: make each member entry its own page
142255
if el.children == ChoicesChildren.separate:
143-
res = MemberPage(path=obj_member.path, contents=[doc])
256+
res = MemberPage(path=doc.obj.path, contents=[doc])
144257
# Case2: use just the Doc element, so it gets embedded directly
145258
# into the class being documented
146259
elif el.children in {ChoicesChildren.embedded, ChoicesChildren.flat}:
@@ -149,8 +262,9 @@ def enter(self, el: Auto):
149262
# if the page for the member is not created somewhere else, then it
150263
# won't exist in the documentation (but its summary will still be in
151264
# the table).
265+
# TODO: we shouldn't even bother blueprinting these members.
152266
elif el.children == ChoicesChildren.linked:
153-
res = Link(name=obj_member.path, obj=obj_member)
267+
res = Link(name=doc.obj.path, obj=doc.obj)
154268
else:
155269
raise ValueError(f"Unsupported value of children: {el.children}")
156270

@@ -175,11 +289,14 @@ def _fetch_members(el: Auto, obj: dc.Object | dc.Alias):
175289
if not el.include_private:
176290
options = {k: v for k, v in options.items() if not k.startswith("_")}
177291

292+
if not el.include_imports:
293+
options = {k: v for k, v in options.items() if not v.is_alias}
294+
178295
# for modules, remove any Alias objects, since they were imported from
179296
# other places. Sphinx has a flag for this behavior, so may be good
180297
# to do something similar.
181-
if obj.is_module:
182-
options = {k: v for k, v in options.items() if not v.is_alias}
298+
# if obj.is_module:
299+
# options = {k: v for k, v in options.items() if not v.is_alias}
183300

184301
return sorted(options)
185302

@@ -205,7 +322,9 @@ def blueprint(el: Auto, package: str) -> Doc:
205322
...
206323

207324

208-
def blueprint(el: _Base, package: str = None, dynamic: None | bool = None) -> _Base:
325+
def blueprint(
326+
el: _Base, package: str = None, dynamic: None | bool = None, parser="numpy"
327+
) -> _Base:
209328
"""Convert a configuration element to something that is ready to render.
210329
211330
Parameters
@@ -230,7 +349,7 @@ def blueprint(el: _Base, package: str = None, dynamic: None | bool = None) -> _B
230349
231350
"""
232351

233-
trans = BlueprintTransformer()
352+
trans = BlueprintTransformer(parser=parser)
234353

235354
if package is not None:
236355
trans.crnt_package = package

quartodoc/layout.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class Layout(_Structural):
4646
The package being documented.
4747
"""
4848

49-
sections: list[Union[SectionElement, Section]]
49+
sections: list[Union[SectionElement, Section]] = []
5050
package: Union[str, None, MISSING] = MISSING()
5151

5252

@@ -196,6 +196,8 @@ class Auto(_Base):
196196
A list of members, such as attributes or methods on a class, to document.
197197
include_private:
198198
Whether to include members starting with "_"
199+
include_imports:
200+
Whether to include members that were imported from somewhere else.
199201
include:
200202
(Not implemented). A list of members to include.
201203
exclude:
@@ -216,6 +218,7 @@ class Auto(_Base):
216218
name: str
217219
members: Optional[list[str]] = None
218220
include_private: bool = False
221+
include_imports: bool = False
219222
include: Optional[str] = None
220223
exclude: Optional[str] = None
221224
dynamic: Union[None, bool, str] = None

0 commit comments

Comments
 (0)