1
1
from __future__ import annotations
2
2
3
3
import logging
4
+ import json
5
+ import yaml
4
6
5
7
from griffe import dataclasses as dc
6
8
from griffe .loader import GriffeLoader
7
9
from griffe .collections import ModulesCollection , LinesCollection
8
10
from griffe .docstrings .parsers import Parser
9
11
from functools import partial
12
+ from textwrap import indent
10
13
11
14
from plum import dispatch
12
15
31
34
_log = logging .getLogger (__name__ )
32
35
33
36
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
+
34
96
class BlueprintTransformer (PydanticTransformer ):
35
97
def __init__ (self , get_object = None , parser = "numpy" ):
36
98
@@ -64,6 +126,13 @@ def get_object_fixed(self, path, **kwargs):
64
126
f" Does an object with the path { path } exist?"
65
127
)
66
128
129
+ @staticmethod
130
+ def _clean_member_path (path , new ):
131
+ if ":" in new :
132
+ return new .replace (":" , "." )
133
+
134
+ return new
135
+
67
136
@dispatch
68
137
def visit (self , el ):
69
138
# TODO: use a context handler
@@ -80,6 +149,40 @@ def visit(self, el):
80
149
finally :
81
150
self .crnt_package = old
82
151
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
+
83
186
@dispatch
84
187
def exit (self , el : Section ):
85
188
"""Transform top-level sections, so their contents are all Pages."""
@@ -109,8 +212,10 @@ def enter(self, el: Auto):
109
212
pkg = self .crnt_package
110
213
if pkg is None :
111
214
path = el .name
215
+ elif ":" in pkg or ":" in el .name :
216
+ path = f"{ pkg } .{ el .name } "
112
217
else :
113
- path = f"{ pkg } . { el . name } " if ":" in el . name else f" { pkg } :{ el .name } "
218
+ path = f"{ pkg } :{ el .name } "
114
219
115
220
_log .info (f"Getting object for { path } " )
116
221
@@ -128,19 +233,27 @@ def enter(self, el: Auto):
128
233
# but the actual objects on the target.
129
234
# On the other hand, we've wired get_object up to make sure getting
130
235
# 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 ))
133
243
134
244
# 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
+ ):
136
249
continue
137
250
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)
140
253
141
254
# Case 1: make each member entry its own page
142
255
if el .children == ChoicesChildren .separate :
143
- res = MemberPage (path = obj_member .path , contents = [doc ])
256
+ res = MemberPage (path = doc . obj .path , contents = [doc ])
144
257
# Case2: use just the Doc element, so it gets embedded directly
145
258
# into the class being documented
146
259
elif el .children in {ChoicesChildren .embedded , ChoicesChildren .flat }:
@@ -149,8 +262,9 @@ def enter(self, el: Auto):
149
262
# if the page for the member is not created somewhere else, then it
150
263
# won't exist in the documentation (but its summary will still be in
151
264
# the table).
265
+ # TODO: we shouldn't even bother blueprinting these members.
152
266
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 )
154
268
else :
155
269
raise ValueError (f"Unsupported value of children: { el .children } " )
156
270
@@ -175,11 +289,14 @@ def _fetch_members(el: Auto, obj: dc.Object | dc.Alias):
175
289
if not el .include_private :
176
290
options = {k : v for k , v in options .items () if not k .startswith ("_" )}
177
291
292
+ if not el .include_imports :
293
+ options = {k : v for k , v in options .items () if not v .is_alias }
294
+
178
295
# for modules, remove any Alias objects, since they were imported from
179
296
# other places. Sphinx has a flag for this behavior, so may be good
180
297
# 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}
183
300
184
301
return sorted (options )
185
302
@@ -205,7 +322,9 @@ def blueprint(el: Auto, package: str) -> Doc:
205
322
...
206
323
207
324
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 :
209
328
"""Convert a configuration element to something that is ready to render.
210
329
211
330
Parameters
@@ -230,7 +349,7 @@ def blueprint(el: _Base, package: str = None, dynamic: None | bool = None) -> _B
230
349
231
350
"""
232
351
233
- trans = BlueprintTransformer ()
352
+ trans = BlueprintTransformer (parser = parser )
234
353
235
354
if package is not None :
236
355
trans .crnt_package = package
0 commit comments