Skip to content

Commit 180f652

Browse files
committed
Various fixes for own page output
Also added tests for own page output. Fix some inherited members always being rendered. Own page members of an entity are linked to after the docstring of the parent entity. Fix entities below the "class" level that have their own page from rendering incorrectly. Rename "single page output" to "own page output". An entity does not have a "single page" when its members are spread across their own pages. Properties are linked to on their parent classes page.
1 parent 61d8abe commit 180f652

File tree

30 files changed

+1496
-789
lines changed

30 files changed

+1496
-789
lines changed

autoapi/directives.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ def get_items(self, names):
4141

4242

4343
class NestedParse(Directive):
44-
4544
"""Nested parsing to remove the first heading of included rST
4645
4746
This is used to handle the case where we like to remove user supplied

autoapi/extension.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
This extension allows you to automagically generate API documentation from your project.
44
"""
5+
56
import io
67
import os
78
import shutil
@@ -34,6 +35,13 @@
3435
"special-members",
3536
"imported-members",
3637
]
38+
_VALID_PAGE_LEVELS = [
39+
"module",
40+
"class",
41+
"function",
42+
"method",
43+
"attribute",
44+
]
3745
_VIEWCODE_CACHE: Dict[str, Tuple[str, Dict]] = {}
3846
"""Caches a module's parse results for use in viewcode."""
3947

@@ -74,6 +82,10 @@ def run_autoapi(app):
7482
if app.config.autoapi_include_summaries:
7583
app.config.autoapi_options.append("show-module-summary")
7684

85+
own_page_level = app.config.autoapi_own_page_level
86+
if own_page_level not in _VALID_PAGE_LEVELS:
87+
raise ValueError(f"Invalid autoapi_own_page_level '{own_page_level}")
88+
7789
# Make sure the paths are full
7890
normalised_dirs = _normalise_autoapi_dirs(app.config.autoapi_dirs, app.srcdir)
7991
for _dir in normalised_dirs:
@@ -100,7 +112,7 @@ def run_autoapi(app):
100112
RemovedInAutoAPI3Warning,
101113
)
102114
sphinx_mapper_obj = PythonSphinxMapper(
103-
app, template_dir=template_dir, url_root=url_root
115+
app, template_dir=template_dir, dir_root=normalized_root, url_root=url_root
104116
)
105117

106118
if app.config.autoapi_file_patterns:
@@ -127,7 +139,7 @@ def run_autoapi(app):
127139
sphinx_mapper_obj.map(options=app.config.autoapi_options)
128140

129141
if app.config.autoapi_generate_api_docs:
130-
sphinx_mapper_obj.output_rst(root=normalized_root, source_suffix=out_suffix)
142+
sphinx_mapper_obj.output_rst(source_suffix=out_suffix)
131143

132144

133145
def build_finished(app, exception):

autoapi/mappers/base.py

Lines changed: 37 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import os
2-
import fnmatch
31
from collections import OrderedDict, namedtuple
2+
import fnmatch
3+
import os
4+
import pathlib
45
import re
56

6-
import anyascii
7-
from docutils.parsers.rst import convert_directive_function
87
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
98
import sphinx
109
import sphinx.util
@@ -13,7 +12,7 @@
1312
from sphinx.util.osutil import ensuredir
1413
import sphinx.util.logging
1514

16-
from ..settings import API_ROOT, TEMPLATE_DIR
15+
from ..settings import TEMPLATE_DIR
1716

1817
LOGGER = sphinx.util.logging.getLogger(__name__)
1918
_OWN_PAGE_LEVELS = [
@@ -24,29 +23,24 @@
2423
"function",
2524
"method",
2625
"property",
27-
"attribute",
2826
"data",
27+
"attribute",
2928
]
3029

3130
Path = namedtuple("Path", ["absolute", "relative"])
3231

3332

3433
class PythonMapperBase:
35-
3634
"""Base object for JSON -> Python object mapping.
3735
3836
Subclasses of this object will handle their language specific JSON input,
3937
and map that onto this standard Python object.
4038
Subclasses may also include language-specific attributes on this object.
4139
42-
Arguments:
43-
4440
Args:
4541
obj: JSON object representing this object
4642
jinja_env: A template environment for rendering this object
4743
48-
Required attributes:
49-
5044
Attributes:
5145
id (str): A globally unique identifier for this object.
5246
Generally a fully qualified name, including namespace.
@@ -56,25 +50,21 @@ class PythonMapperBase:
5650
children (list): Children of this object
5751
parameters (list): Parameters to this object
5852
methods (list): Methods on this object
59-
60-
Optional attributes:
61-
6253
"""
6354

6455
language = "base"
6556
type = "base"
66-
# Create a page in the output for this object.
67-
top_level_object = False
6857
_RENDER_LOG_LEVEL = "VERBOSE"
6958

70-
def __init__(self, obj, jinja_env, app, options=None):
59+
def __init__(self, obj, jinja_env, app, url_root, options=None):
7160
self.app = app
7261
self.obj = obj
7362
self.options = options
7463
self.jinja_env = jinja_env
75-
self.url_root = os.path.join("/", API_ROOT)
64+
self.url_root = url_root
7665

7766
self.name = None
67+
self.qual_name = None
7868
self.id = None
7969

8070
def __getstate__(self):
@@ -104,7 +94,7 @@ def rendered(self):
10494
def get_context_data(self):
10595
own_page_level = self.app.config.autoapi_own_page_level
10696
desired_page_level = _OWN_PAGE_LEVELS.index(own_page_level)
107-
own_page_types = set(_OWN_PAGE_LEVELS[:desired_page_level+1])
97+
own_page_types = set(_OWN_PAGE_LEVELS[: desired_page_level + 1])
10898

10999
return {
110100
"autoapi_options": self.app.config.autoapi_options,
@@ -128,28 +118,19 @@ def short_name(self):
128118
"""Shorten name property"""
129119
return self.name.split(".")[-1]
130120

131-
@property
132-
def pathname(self):
133-
"""Sluggified path for filenames
121+
def output_dir(self, root):
122+
"""The directory to render this object."""
123+
module = self.id[: -(len("." + self.qual_name))]
124+
parts = [root] + module.split(".")
125+
return pathlib.PosixPath(*parts)
134126

135-
Slugs to a filename using the follow steps
127+
def output_filename(self):
128+
"""The name of the file to render into, without a file suffix."""
129+
filename = self.qual_name
130+
if filename == "index":
131+
filename = ".index"
136132

137-
* Decode unicode to approximate ascii
138-
* Remove existing hyphens
139-
* Substitute hyphens for non-word characters
140-
* Break up the string as paths
141-
"""
142-
slug = self.name
143-
slug = anyascii.anyascii(slug)
144-
slug = slug.replace("-", "")
145-
slug = re.sub(r"[^\w\.]+", "-", slug).strip("-")
146-
return os.path.join(*slug.split("."))
147-
148-
def include_dir(self, root):
149-
"""Return directory of file"""
150-
parts = [root]
151-
parts.extend(self.pathname.split(os.path.sep))
152-
return "/".join(parts)
133+
return filename
153134

154135
@property
155136
def include_path(self):
@@ -158,9 +139,7 @@ def include_path(self):
158139
This is used in ``toctree`` directives, as Sphinx always expects Unix
159140
path separators
160141
"""
161-
parts = [self.include_dir(root=self.url_root)]
162-
parts.append("index")
163-
return "/".join(parts)
142+
return str(self.output_dir(self.url_root) / self.output_filename())
164143

165144
@property
166145
def display(self):
@@ -180,14 +159,13 @@ def ref_directive(self):
180159

181160

182161
class SphinxMapperBase:
183-
184162
"""Base class for mapping `PythonMapperBase` objects to Sphinx.
185163
186164
Args:
187165
app: Sphinx application instance
188166
"""
189167

190-
def __init__(self, app, template_dir=None, url_root=None):
168+
def __init__(self, app, template_dir=None, dir_root=None, url_root=None):
191169
self.app = app
192170

193171
template_paths = [TEMPLATE_DIR]
@@ -211,8 +189,9 @@ def _wrapped_prepare(value):
211189

212190
own_page_level = self.app.config.autoapi_own_page_level
213191
desired_page_level = _OWN_PAGE_LEVELS.index(own_page_level)
214-
self.own_page_types = set(_OWN_PAGE_LEVELS[:desired_page_level+1])
192+
self.own_page_types = set(_OWN_PAGE_LEVELS[: desired_page_level + 1])
215193

194+
self.dir_root = dir_root
216195
self.url_root = url_root
217196

218197
# Mapping of {filepath -> raw data}
@@ -300,14 +279,17 @@ def add_object(self, obj):
300279
Args:
301280
obj: Instance of a AutoAPI object
302281
"""
303-
if obj.type in self.own_page_types:
282+
display = obj.display
283+
if display and obj.type in self.own_page_types:
304284
self.objects_to_render[obj.id] = obj
305285

306286
self.all_objects[obj.id] = obj
307287
child_stack = list(obj.children)
308288
while child_stack:
309289
child = child_stack.pop()
310290
self.all_objects[child.id] = child
291+
if display and child.type in self.own_page_types:
292+
self.objects_to_render[child.id] = child
311293
child_stack.extend(getattr(child, "children", ()))
312294

313295
def map(self, options=None):
@@ -329,81 +311,32 @@ def create_class(self, data, options=None, **kwargs):
329311
"""
330312
raise NotImplementedError
331313

332-
def output_child_rst(self, obj, obj_parent, detail_dir, source_suffix):
333-
334-
if not obj.display:
335-
return
336-
337-
# Skip nested cases like functions in functions or clases in clases
338-
if obj.type == obj_parent.type:
339-
return
340-
341-
obj_child_page_level = _OWN_PAGE_LEVELS.index(obj.type)
342-
desired_page_level = _OWN_PAGE_LEVELS.index(self.app.config.autoapi_own_page_level)
343-
is_own_page = obj_child_page_level <= desired_page_level
344-
if not is_own_page:
345-
return
346-
347-
obj_child_rst = obj.render(
348-
is_own_page=is_own_page,
349-
)
350-
if not obj_child_rst:
351-
return
352-
353-
function_page_level = _OWN_PAGE_LEVELS.index("function")
354-
is_level_beyond_function = function_page_level < desired_page_level
355-
if obj.type in ["exception", "class"]:
356-
if not is_level_beyond_function:
357-
outfile = f"{obj.short_name}{source_suffix}"
358-
path = os.path.join(detail_dir, outfile)
359-
else:
360-
outdir = os.path.join(detail_dir, obj.short_name)
361-
ensuredir(outdir)
362-
path = os.path.join(outdir, f"index{source_suffix}")
363-
else:
364-
is_parent_in_detail_dir = detail_dir.endswith(obj_parent.short_name)
365-
outdir = detail_dir if is_parent_in_detail_dir else os.path.join(detail_dir, obj_parent.short_name)
366-
ensuredir(outdir)
367-
path = os.path.join(outdir, f"{obj.short_name}{source_suffix}")
368-
369-
with open(path, "wb+") as obj_child_detail_file:
370-
obj_child_detail_file.write(obj_child_rst.encode("utf-8"))
371-
372-
for obj_child in obj.children:
373-
child_detail_dir = os.path.join(detail_dir, obj.name)
374-
self.output_child_rst(obj_child, obj, child_detail_dir, source_suffix)
375-
376-
def output_rst(self, root, source_suffix):
314+
def output_rst(self, source_suffix):
377315
for _, obj in status_iterator(
378316
self.objects_to_render.items(),
379317
colorize("bold", "[AutoAPI] ") + "Rendering Data... ",
380318
length=len(self.objects_to_render),
381319
verbosity=1,
382320
stringify_func=(lambda x: x[0]),
383321
):
384-
if not obj.display:
385-
continue
386-
387322
rst = obj.render(is_own_page=True)
388323
if not rst:
389324
continue
390325

391-
detail_dir = obj.include_dir(root=root)
392-
ensuredir(detail_dir)
393-
path = os.path.join(detail_dir, f"index{source_suffix}")
326+
output_dir = obj.output_dir(self.dir_root)
327+
ensuredir(output_dir)
328+
output_path = output_dir / obj.output_filename()
329+
path = f"{output_path}{source_suffix}"
394330
with open(path, "wb+") as detail_file:
395331
detail_file.write(rst.encode("utf-8"))
396-
397-
for child in obj.children:
398-
self.output_child_rst(child, obj, detail_dir, source_suffix)
399332

400333
if self.app.config.autoapi_add_toctree_entry:
401-
self._output_top_rst(root)
334+
self._output_top_rst()
402335

403-
def _output_top_rst(self, root):
336+
def _output_top_rst(self):
404337
# Render Top Index
405-
top_level_index = os.path.join(root, "index.rst")
406-
pages = self.objects_to_render.values()
338+
top_level_index = os.path.join(self.dir_root, "index.rst")
339+
pages = [obj for obj in self.objects_to_render.values() if obj.display]
407340
with open(top_level_index, "wb") as top_level_file:
408341
content = self.jinja_env.get_template("index.rst")
409342
top_level_file.write(content.render(pages=pages).encode("utf-8"))

0 commit comments

Comments
 (0)