Compare commits

...

11 Commits

Author SHA1 Message Date
Ashley Whetter 53de7fd42d Version 3.1.0b0 2 months ago
Ashley Whetter f23b079e7d Stop using xrefs in page titles
Closes #427
2 months ago
Ashley Whetter 3b037c7643 Moved bugfix announcement into a separate news item
Closes #224
2 months ago
Ashley Whetter bc71226c3b Render PEP-695 type aliases as TypeAlias assignments
Partially addresses #414
2 months ago
Ashley Whetter 007077a7db Refactor mapper classes into their bases
This separate used to exist to support parsing multiple languages,
which we no longer do.
2 months ago
Ashley Whetter a6558dcfc2 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.
Children not present in `__all__` are not rendered.
Fixed emitting ignore event twice for methods.
Corrected documentation around `imported-members` to reflect that it
applies only to objects imported into a package, not modules.
Fixed path error on Windows.
2 months ago
Jorge Martinez 2a603b8ac0 Recursive rendering of children with their own page 2 months ago
Ashley Whetter 93fb571a7e Basic tests for single page rendering 2 months ago
Jorge Martinez Garrido ecd47ea456 Initial implementation of customisable single page output 2 months ago
George Zhang 0d69974c19 Preserve strings inside Literal type annotations 2 months ago
George Zhang 34a96700ea Replace usage of deprecated docutils.nodes.Node.traverse 2 months ago

@ -5,6 +5,33 @@ Versions follow `Semantic Versioning <https://semver.org/>`_ (``<major>.<minor>.
.. towncrier release notes start
v3.1.0.b0 (2024-04-12)
----------------------
Features
^^^^^^^^
- Objects can render to their own page (#226)
- Render PEP-695 type aliases as TypeAlias assignments. (#414)
Bugfixes
^^^^^^^^
- Values are always rendered for TypeAlises and PEP-695 type aliases. (#224)
- Fix submodule with `__init__.pyi` documented as `__init__` instead of submodule name (#398)
- Fix IndexError when a module docstring contains only a heading (#412)
- Preserve strings inside Literal type annotations (#423)
- Stopped using xrefs in page titles (#427)
- Fix emitting ignore event twice for methods.
Misc
^^^^
- #388
v3.0.0 (2023-09-26)
-------------------

@ -3,5 +3,5 @@
from .extension import setup
__all__ = ("setup",)
__version__ = "3.0.0"
__version_info__ = (3, 0, 0)
__version__ = "3.1.0b0"
__version_info__ = (3, 1, 0)

@ -4,11 +4,7 @@ import re
import astroid
import astroid.nodes
import astroid.nodes.node_classes
import sphinx.util.logging
_LOGGER = sphinx.util.logging.getLogger(__name__)
def resolve_import_alias(name, import_names):
@ -120,27 +116,39 @@ def get_full_basenames(node):
yield _resolve_annotation(base)
def _get_const_values(node):
value = None
def _get_const_value(node):
if isinstance(node, astroid.nodes.Const):
if isinstance(node.value, str) and "\n" in node.value:
return '"""{0}"""'.format(node.value)
if isinstance(node, (astroid.nodes.List, astroid.nodes.Tuple)):
new_value = []
for element in node.elts:
if isinstance(element, astroid.nodes.Const):
new_value.append(element.value)
elif isinstance(element, (astroid.nodes.List, astroid.nodes.Tuple)):
new_value.append(_get_const_values(element))
else:
break
else:
value = new_value
class NotConstException(Exception):
pass
def _inner(node):
if isinstance(node, (astroid.nodes.List, astroid.nodes.Tuple)):
new_value = []
for element in node.elts:
new_value.append(_inner(element))
if isinstance(node, astroid.nodes.Tuple):
return tuple(new_value)
if isinstance(node, astroid.nodes.Tuple):
value = tuple(new_value)
elif isinstance(node, astroid.nodes.Const):
value = node.value
return new_value
elif isinstance(node, astroid.nodes.Const):
# Don't allow multi-line strings inside a data structure.
if isinstance(node.value, str) and "\n" in node.value:
raise NotConstException()
return value
return node.value
raise NotConstException()
try:
result = _inner(node)
except NotConstException:
return None
return repr(result)
def get_assign_value(node):
@ -153,8 +161,9 @@ def get_assign_value(node):
to get the assignment value from.
Returns:
tuple(str, object or None) or None: The name that is assigned
to, and the value assigned to the name (if it can be converted).
tuple(str, str or None) or None: The name that is assigned
to, and the string representation of the value assigned to the name
(if it can be converted).
"""
try:
targets = node.targets
@ -169,7 +178,7 @@ def get_assign_value(node):
name = target.attrname
else:
return None
return (name, _get_const_values(node.value))
return (name, _get_const_value(node.value))
return None
@ -416,7 +425,20 @@ def _resolve_annotation(annotation):
# astroid.Index was removed in astroid v3
if hasattr(astroid, "Index") and isinstance(slice_node, astroid.Index):
slice_node = slice_node.value
if isinstance(slice_node, astroid.Tuple):
if value == "Literal":
if isinstance(slice_node, astroid.Tuple):
elts = slice_node.elts
else:
elts = [slice_node]
slice_ = ", ".join(
(
elt.as_string()
if isinstance(elt, astroid.Const)
else _resolve_annotation(elt)
)
for elt in elts
)
elif isinstance(slice_node, astroid.Tuple):
slice_ = ", ".join(_resolve_annotation(elt) for elt in slice_node.elts)
else:
slice_ = _resolve_annotation(slice_node)

@ -1,20 +1,23 @@
import collections
import copy
import fnmatch
import operator
import os
import re
from jinja2 import Environment, FileSystemLoader
import sphinx
import sphinx.environment
from sphinx.errors import ExtensionError
import sphinx.util
import sphinx.util.logging
from sphinx.util.console import colorize
from sphinx.util.display import status_iterator
import sphinx.util.docstrings
import sphinx.util.logging
from sphinx.util.osutil import ensuredir
from ..base import SphinxMapperBase
from .parser import Parser
from .objects import (
from ._parser import Parser
from ._objects import (
PythonClass,
PythonFunction,
PythonModule,
@ -24,7 +27,9 @@ from .objects import (
PythonAttribute,
PythonData,
PythonException,
TopLevelPythonPythonMapper,
)
from .settings import OWN_PAGE_LEVELS, TEMPLATE_DIR
LOGGER = sphinx.util.logging.getLogger(__name__)
@ -61,12 +66,14 @@ def _expand_wildcard_placeholder(original_module, originals_map, placeholder):
placeholders = []
for original in originals:
new_full_name = placeholder["full_name"].replace("*", original["name"])
new_qual_name = placeholder["qual_name"].replace("*", original["name"])
new_original_path = placeholder["original_path"].replace("*", original["name"])
if "original_path" in original:
new_original_path = original["original_path"]
new_placeholder = dict(
placeholder,
name=original["name"],
qual_name=new_qual_name,
full_name=new_full_name,
original_path=new_original_path,
)
@ -166,6 +173,7 @@ def _resolve_placeholder(placeholder, original):
assert original["type"] != "placeholder"
# The name remains the same.
new["name"] = placeholder["name"]
new["qual_name"] = placeholder["qual_name"]
new["full_name"] = placeholder["full_name"]
# Record where the placeholder originally came from.
new["original_path"] = original["full_name"]
@ -215,13 +223,11 @@ def _link_objs(value):
return result[:-2]
class PythonSphinxMapper(SphinxMapperBase):
"""Auto API domain handler for Python
Parses directly from Python files.
class Mapper:
"""Base class for mapping `PythonMapperBase` objects to Sphinx.
Args:
app: Sphinx application passed in as part of the extension
app: Sphinx application instance
"""
_OBJ_MAP = {
@ -239,14 +245,143 @@ class PythonSphinxMapper(SphinxMapperBase):
)
}
def __init__(self, app, template_dir=None, url_root=None):
super().__init__(app, template_dir, url_root)
def __init__(self, app, template_dir=None, dir_root=None, url_root=None):
self.app = app
template_paths = [TEMPLATE_DIR]
if template_dir:
# Put at the front so it's loaded first
template_paths.insert(0, template_dir)
self.jinja_env = Environment(
loader=FileSystemLoader(template_paths),
trim_blocks=True,
lstrip_blocks=True,
)
def _wrapped_prepare(value):
return value
self.jinja_env.filters["prepare_docstring"] = _wrapped_prepare
if self.app.config.autoapi_prepare_jinja_env:
self.app.config.autoapi_prepare_jinja_env(self.jinja_env)
own_page_level = self.app.config.autoapi_own_page_level
desired_page_level = OWN_PAGE_LEVELS.index(own_page_level)
self.own_page_types = set(OWN_PAGE_LEVELS[: desired_page_level + 1])
self.dir_root = dir_root
self.url_root = url_root
# Mapping of {filepath -> raw data}
self.paths = collections.OrderedDict()
# Mapping of {object id -> Python Object}
self.objects_to_render = collections.OrderedDict()
# Mapping of {object id -> Python Object}
self.all_objects = collections.OrderedDict()
# Mapping of {namespace id -> Python Object}
self.namespaces = collections.OrderedDict()
self.jinja_env.filters["link_objs"] = _link_objs
self._use_implicit_namespace = (
self.app.config.autoapi_python_use_implicit_namespaces
)
@staticmethod
def find_files(patterns, dirs, ignore):
if not ignore:
ignore = []
pattern_regexes = []
for pattern in patterns:
regex = re.compile(fnmatch.translate(pattern).replace(".*", "(.*)"))
pattern_regexes.append((pattern, regex))
for _dir in dirs:
for root, _, filenames in os.walk(_dir):
seen = set()
for pattern, pattern_re in pattern_regexes:
for filename in fnmatch.filter(filenames, pattern):
skip = False
match = re.match(pattern_re, filename)
norm_name = match.groups()
if norm_name in seen:
continue
# Skip ignored files
for ignore_pattern in ignore:
if fnmatch.fnmatch(
os.path.join(root, filename), ignore_pattern
):
LOGGER.info(
colorize("bold", "[AutoAPI] ")
+ colorize(
"darkgreen", f"Ignoring {root}/{filename}"
)
)
skip = True
if skip:
continue
# Make sure the path is full
if not os.path.isabs(filename):
filename = os.path.join(root, filename)
yield filename
seen.add(norm_name)
def add_object(self, obj):
"""Add object to local and app environment storage
Args:
obj: Instance of a AutoAPI object
"""
display = obj.display
if display and obj.type in self.own_page_types:
self.objects_to_render[obj.id] = obj
self.all_objects[obj.id] = obj
child_stack = list(obj.children)
while child_stack:
child = child_stack.pop()
self.all_objects[child.id] = child
if display and child.type in self.own_page_types:
self.objects_to_render[child.id] = child
child_stack.extend(getattr(child, "children", ()))
def output_rst(self, source_suffix):
for _, obj in status_iterator(
self.objects_to_render.items(),
colorize("bold", "[AutoAPI] ") + "Rendering Data... ",
length=len(self.objects_to_render),
verbosity=1,
stringify_func=(lambda x: x[0]),
):
rst = obj.render(is_own_page=True)
if not rst:
continue
output_dir = obj.output_dir(self.dir_root)
ensuredir(output_dir)
output_path = output_dir / obj.output_filename()
path = f"{output_path}{source_suffix}"
with open(path, "wb+") as detail_file:
detail_file.write(rst.encode("utf-8"))
if self.app.config.autoapi_add_toctree_entry:
self._output_top_rst()
def _output_top_rst(self):
# Render Top Index
top_level_index = os.path.join(self.dir_root, "index.rst")
pages = [obj for obj in self.objects_to_render.values() if obj.display]
with open(top_level_index, "wb") as top_level_file:
content = self.jinja_env.get_template("index.rst")
top_level_file.write(content.render(pages=pages).encode("utf-8"))
def _need_to_load(self, files):
last_files = getattr(self.app.env, "autoapi_source_files", [])
self.app.env.autoapi_source_files = files
@ -339,28 +474,54 @@ class PythonSphinxMapper(SphinxMapperBase):
visit_path = collections.OrderedDict()
_resolve_module_placeholders(modules, module_name, visit_path, resolved)
def _hide_yo_kids(self):
"""For all direct children of a module/package, hide them if needed."""
for module in self.paths.values():
if module["all"] is not None:
all_names = set(module["all"])
for child in module["children"]:
if child["qual_name"] not in all_names:
child["hide"] = True
elif module["type"] == "module":
for child in module["children"]:
if child.get("imported"):
child["hide"] = True
def map(self, options=None):
self._resolve_placeholders()
self._hide_yo_kids()
self.app.env.autoapi_annotations = {}
super().map(options)
parents = {obj.name: obj for obj in self.objects.values()}
for obj in self.objects.values():
for _, data in status_iterator(
self.paths.items(),
colorize("bold", "[AutoAPI] ") + "Mapping Data... ",
length=len(self.paths),
stringify_func=(lambda x: x[0]),
):
for obj in self.create_class(data, options=options):
self.add_object(obj)
top_level_objects = {
obj.id: obj
for obj in self.all_objects.values()
if isinstance(obj, TopLevelPythonPythonMapper)
}
parents = {obj.name: obj for obj in top_level_objects.values()}
for obj in top_level_objects.values():
parent_name = obj.name.rsplit(".", 1)[0]
if parent_name in parents and parent_name != obj.name:
parent = parents[parent_name]
attr = f"sub{obj.type}s"
getattr(parent, attr).append(obj)
for obj in self.objects.values():
for obj in top_level_objects.values():
obj.submodules.sort()
obj.subpackages.sort()
self.app.env.autoapi_objects = self.objects
self.app.env.autoapi_objects = self.objects_to_render
self.app.env.autoapi_all_objects = self.all_objects
def create_class(self, data, options=None, **kwargs):
def create_class(self, data, options=None):
"""Create a class from the passed in data
Args:
@ -378,14 +539,11 @@ class PythonSphinxMapper(SphinxMapperBase):
options=self.app.config.autoapi_options,
jinja_env=self.jinja_env,
app=self.app,
**kwargs,
url_root=self.url_root,
)
obj.url_root = self.url_root
for child_data in data.get("children", []):
for child_obj in self.create_class(
child_data, options=options, **kwargs
):
for child_obj in self.create_class(child_data, options=options):
obj.children.append(child_obj)
# Some objects require children to establish their docstring

@ -1,10 +1,14 @@
from __future__ import annotations
import functools
from typing import List, Optional
import pathlib
from typing import List, Optional, Tuple
import sphinx
import sphinx.util
import sphinx.util.logging
from ..base import PythonMapperBase
from .settings import OWN_PAGE_LEVELS
LOGGER = sphinx.util.logging.getLogger(__name__)
@ -26,92 +30,176 @@ def _format_args(args_info, include_annotations=True, ignore_self=None):
return ", ".join(result)
class PythonPythonMapper(PythonMapperBase):
"""A base class for all types of representations of Python objects.
class PythonObject:
"""A class representing an entity from the parsed source code.
This class turns the dictionaries output by the parser into an object.
Attributes:
name (str): The name given to this object.
id (str): A unique identifier for this object.
children (list(PythonPythonMapper)): The members of this object.
Args:
obj: JSON object representing this object
jinja_env: A template environment for rendering this object
"""
language = "python"
is_callable = False
member_order = 0
"""The ordering of objects when doing "groupwise" sorting."""
type: str
def __init__(
self, obj, jinja_env, app, url_root, options=None, class_content="class"
):
self.app = app
self.obj = obj
self.options = options
self.jinja_env = jinja_env
self.url_root = url_root
self.name: str = obj["name"]
"""The name of the object, as named in the parsed source code.
This name will have no periods in it.
"""
self.qual_name: str = obj["qual_name"]
"""The qualified name for this object."""
self.id: str = obj.get("full_name", self.name)
"""A globally unique identifier for this object.
def __init__(self, obj, class_content="class", **kwargs) -> None:
super().__init__(obj, **kwargs)
self.name = obj["name"]
self.id = obj.get("full_name", self.name)
This is the same as the fully qualified name of the object.
"""
# Optional
self.children: List[PythonPythonMapper] = []
self._docstring = obj["doc"]
self._docstring_resolved = False
self.imported = "original_path" in obj
self.inherited = obj.get("inherited", False)
"""Whether this was inherited from an ancestor of the parent class.
self.children: List[PythonObject] = []
"""The members of this object.
:type: bool
For example, the classes and functions defined in the parent module.
"""
self._docstring: str = obj["doc"]
self.imported: bool = "original_path" in obj
"""Whether this object was imported from another module."""
self.inherited: bool = obj.get("inherited", False)
"""Whether this was inherited from an ancestor of the parent class."""
self._hide = obj.get("hide", False)
# For later
self._class_content = class_content
self._display_cache: Optional[bool] = None
def __getstate__(self):
"""Obtains serialisable data for pickling."""
__dict__ = self.__dict__.copy()
__dict__.update(app=None, jinja_env=None) # clear unpickable attributes
return __dict__
def render(self, **kwargs):
LOGGER.log("VERBOSE", "Rendering %s", self.id)
template = self.jinja_env.get_template(f"python/{self.type}.rst")
ctx = {}
ctx.update(**self.get_context_data())
ctx.update(**kwargs)
return template.render(**ctx)
@property
def docstring(self):
def rendered(self):
"""Shortcut to render an object in templates."""
return self.render()
def get_context_data(self):
own_page_level = self.app.config.autoapi_own_page_level
desired_page_level = OWN_PAGE_LEVELS.index(own_page_level)
own_page_types = set(OWN_PAGE_LEVELS[: desired_page_level + 1])
return {
"autoapi_options": self.app.config.autoapi_options,
"include_summaries": self.app.config.autoapi_include_summaries,
"obj": self,
"own_page_types": own_page_types,
"sphinx_version": sphinx.version_info,
}
def __lt__(self, other):
"""Object sorting comparison"""
if not isinstance(other, PythonObject):
return NotImplemented
return self.id < other.id
def __str__(self) -> str:
return f"<{self.__class__.__name__} {self.id}>"
@property
def short_name(self) -> str:
"""Shorten name property"""
return self.name.split(".")[-1]
def output_dir(self, root):
"""The directory to render this object."""
module = self.id[: -(len("." + self.qual_name))]
parts = [root] + module.split(".")
return pathlib.PurePosixPath(*parts)
def output_filename(self) -> str:
"""The name of the file to render into, without a file suffix."""
filename = self.qual_name
if filename == "index":
filename = ".index"
return filename
@property
def include_path(self) -> str:
"""Return 'absolute' path without regarding OS path separator
This is used in ``toctree`` directives, as Sphinx always expects Unix
path separators
"""
return str(self.output_dir(self.url_root) / self.output_filename())
@property
def docstring(self) -> str:
"""The docstring for this object.
If a docstring did not exist on the object,
this will be the empty string.
For classes this will also depend on the
For classes, this will also depend on the
:confval:`autoapi_python_class_content` option.
:type: str
"""
return self._docstring
@docstring.setter
def docstring(self, value):
def docstring(self, value: str) -> None:
self._docstring = value
self._docstring_resolved = True
@property
def is_undoc_member(self):
"""Whether this object has a docstring (False) or not (True).
def is_top_level_object(self) -> bool:
"""Whether this object is at the very top level (True) or not (False).
:type: bool
This will be False for subpackages and submodules.
"""
return not bool(self.docstring)
return "." not in self.id
@property
def is_private_member(self):
"""Whether this object is private (True) or not (False).
def is_undoc_member(self) -> bool:
"""Whether this object has a docstring (False) or not (True)."""
return not bool(self.docstring)
:type: bool
"""
@property
def is_private_member(self) -> bool:
"""Whether this object is private (True) or not (False)."""
return self.short_name.startswith("_") and not self.short_name.endswith("__")
@property
def is_special_member(self):
"""Whether this object is a special member (True) or not (False).
:type: bool
"""
def is_special_member(self) -> bool:
"""Whether this object is a special member (True) or not (False)."""
return self.short_name.startswith("__") and self.short_name.endswith("__")
@property
def display(self):
def display(self) -> bool:
"""Whether this object should be displayed in documentation.
This attribute depends on the configuration options given in
:confval:`autoapi_options` and the result of :event:`autoapi-skip-member`.
:type: bool
"""
if self._display_cache is None:
self._display_cache = not self._ask_ignore(self._should_skip())
@ -119,13 +207,11 @@ class PythonPythonMapper(PythonMapperBase):
return self._display_cache
@property
def summary(self):
def summary(self) -> str:
"""The summary line of the docstring.
The summary line is the first non-empty line, as-per :pep:`257`.
This will be the empty string if the object does not have a docstring.
:type: str
"""
for line in self.docstring.splitlines():
line = line.strip()
@ -134,7 +220,7 @@ class PythonPythonMapper(PythonMapperBase):
return ""
def _should_skip(self): # type: () -> bool
def _should_skip(self) -> bool:
skip_undoc_member = self.is_undoc_member and "undoc-members" not in self.options
skip_private_member = (
self.is_private_member and "private-members" not in self.options
@ -143,69 +229,66 @@ class PythonPythonMapper(PythonMapperBase):
self.is_special_member and "special-members" not in self.options
)
skip_imported_member = self.imported and "imported-members" not in self.options
skip_inherited_member = (
self.inherited and "inherited-members" not in self.options
)
return (
skip_undoc_member
self._hide
or skip_undoc_member
or skip_private_member
or skip_special_member
or skip_imported_member
or skip_inherited_member
)
def _ask_ignore(self, skip): # type: (bool) -> bool
def _ask_ignore(self, skip: bool) -> bool:
ask_result = self.app.emit_firstresult(
"autoapi-skip-member", self.type, self.id, self, skip, self.options
)
return ask_result if ask_result is not None else skip
def _children_of_type(self, type_):
def _children_of_type(self, type_: str) -> List[PythonObject]:
return list(child for child in self.children if child.type == type_)
class PythonFunction(PythonPythonMapper):
class PythonFunction(PythonObject):
"""The representation of a function."""
type = "function"
is_callable = True
member_order = 30
def __init__(self, obj, **kwargs):
super().__init__(obj, **kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
autodoc_typehints = getattr(self.app.config, "autodoc_typehints", "signature")
show_annotations = autodoc_typehints != "none" and not (
autodoc_typehints == "description" and not obj["overloads"]
autodoc_typehints == "description" and not self.obj["overloads"]
)
self.args = _format_args(obj["args"], show_annotations)
"""The arguments to this object, formatted as a string.
:type: str
"""
self.args: str = _format_args(self.obj["args"], show_annotations)
"""The arguments to this object, formatted as a string."""
self.return_annotation = obj["return_annotation"] if show_annotations else None
self.return_annotation: Optional[str] = (
self.obj["return_annotation"] if show_annotations else None
)
"""The type annotation for the return type of this function.
This will be ``None`` if an annotation
or annotation comment was not given.
:type: str or None
"""
self.properties = obj["properties"]
self.properties: List[str] = self.obj["properties"]
"""The properties that describe what type of function this is.
Can be only be: async
:type: list(str)
Can be only be: async.
"""
self.overloads = [
self.overloads: List[Tuple[str, str]] = [
(_format_args(args), return_annotation)
for args, return_annotation in obj["overloads"]
for args, return_annotation in self.obj["overloads"]
]
"""The overloaded signatures of this function.
Each tuple is a tuple of ``(args, return_annotation)``
:type: list(tuple(str, str))
"""
@ -213,74 +296,61 @@ class PythonMethod(PythonFunction):
"""The representation of a method."""
type = "method"
is_callable = True
member_order = 50
def __init__(self, obj, **kwargs):
super().__init__(obj, **kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.properties = obj["properties"]
self.properties: List[str] = self.obj["properties"]
"""The properties that describe what type of method this is.
Can be any of: abstractmethod, async, classmethod, property, staticmethod
:type: list(str)
Can be any of: abstractmethod, async, classmethod, property, staticmethod.
"""
def _should_skip(self): # type: () -> bool
skip = super()._should_skip() or self.name in (
def _should_skip(self) -> bool:
return super()._should_skip() or self.name in (
"__new__",
"__init__",
)
return self._ask_ignore(skip)
class PythonProperty(PythonPythonMapper):
class PythonProperty(PythonObject):
"""The representation of a property on a class."""
type = "property"
member_order = 60
def __init__(self, obj, **kwargs):
super().__init__(obj, **kwargs)
self.annotation = obj["return_annotation"]
"""The type annotation of this property.
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
:type: str or None
"""
self.properties = obj["properties"]
self.annotation: Optional[str] = self.obj["return_annotation"]
"""The type annotation of this property."""
self.properties: List[str] = self.obj["properties"]
"""The properties that describe what type of property this is.
Can be any of: abstractmethod, classmethod
:type: list(str)
Can be any of: abstractmethod, classmethod.
"""
class PythonData(PythonPythonMapper):
class PythonData(PythonObject):
"""Global, module level data."""
type = "data"
member_order = 40
def __init__(self, obj, **kwargs):
super().__init__(obj, **kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.value = obj.get("value")
self.value: Optional[str] = self.obj.get("value")
"""The value of this attribute.
This will be ``None`` if the value is not constant.
:type: str or None
"""
self.annotation = obj.get("annotation")
self.annotation: Optional[str] = self.obj.get("annotation")
"""The type annotation of this attribute.
This will be ``None`` if an annotation
or annotation comment was not given.
:type: str or None
"""
@ -291,25 +361,15 @@ class PythonAttribute(PythonData):
member_order = 60
class TopLevelPythonPythonMapper(PythonPythonMapper):
class TopLevelPythonPythonMapper(PythonObject):
"""A common base class for modules and packages."""
_RENDER_LOG_LEVEL = "VERBOSE"
def __init__(self, obj, **kwargs):
super().__init__(obj, **kwargs)
self.top_level_object = "." not in self.name
"""Whether this object is at the very top level (True) or not (False).
This will be False for subpackages and submodules.
:type: bool
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.subpackages = []
self.submodules = []
self.all = obj["all"]
self.all = self.obj["all"]
"""The contents of ``__all__`` if assigned to.
Only constants are included.
@ -334,6 +394,15 @@ class TopLevelPythonPythonMapper(PythonPythonMapper):
"""
return self._children_of_type("class")
def output_dir(self, root):
"""The path to the file to render into, without a file suffix."""
parts = [root] + self.name.split(".")
return pathlib.PurePosixPath(*parts)
def output_filename(self):
"""The path to the file to render into, without a file suffix."""
return "index"
class PythonModule(TopLevelPythonPythonMapper):
"""The representation of a module."""
@ -347,27 +416,23 @@ class PythonPackage(TopLevelPythonPythonMapper):
type = "package"
class PythonClass(PythonPythonMapper):
class PythonClass(PythonObject):
"""The representation of a class."""
type = "class"
member_order = 20
def __init__(self, obj, **kwargs):
super().__init__(obj, **kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.bases = obj["bases"]
"""The fully qualified names of all base classes.
self.bases: List[str] = self.obj["bases"]
"""The fully qualified names of all base classes."""
:type: list(str)
"""
self._docstring_resolved: bool = False
@property
def args(self):
"""The arguments to this object, formatted as a string.
:type: str
"""
def args(self) -> str:
"""The arguments to this object, formatted as a string."""
args = ""
if self.constructor:
@ -383,7 +448,7 @@ class PythonClass(PythonPythonMapper):
return args
@property
def overloads(self):
def overloads(self) -> List[Tuple[str, str]]:
overloads = []
if self.constructor:
@ -403,8 +468,8 @@ class PythonClass(PythonPythonMapper):
return overloads
@property
def docstring(self):
docstring = super().docstring
def docstring(self) -> str:
docstring = self._docstring
if not self._docstring_resolved and self._class_content in ("both", "init"):
constructor_docstring = self.constructor_docstring
@ -418,8 +483,9 @@ class PythonClass(PythonPythonMapper):
return docstring
@docstring.setter
def docstring(self, value):
super(PythonClass, self.__class__).docstring.fset(self, value)
def docstring(self, value: str) -> None:
self._docstring = value
self._docstring_resolved = True
@property
def methods(self):
@ -447,7 +513,7 @@ class PythonClass(PythonPythonMapper):
return None
@property
def constructor_docstring(self):
def constructor_docstring(self) -> str:
docstring = ""
constructor = self.constructor

@ -6,7 +6,7 @@ import astroid
import astroid.builder
import sphinx.util.docstrings
from . import astroid_utils
from . import _astroid_utils
def _prepare_docstring(doc):
@ -15,11 +15,15 @@ def _prepare_docstring(doc):
class Parser:
def __init__(self):
self._name_stack = []
self._qual_name_stack = []
self._full_name_stack = []
self._encoding = None
def _get_qual_name(self, name):
return ".".join(self._qual_name_stack + [name])
def _get_full_name(self, name):
return ".".join(self._name_stack + [name])
return ".".join(self._full_name_stack + [name])
def _parse_file(self, file_path, condition):
directory, filename = os.path.split(file_path)
@ -76,21 +80,24 @@ class Parser:
type_ = "data"
if isinstance(
node.scope(), astroid.nodes.ClassDef
) or astroid_utils.is_constructor(node.scope()):
) or _astroid_utils.is_constructor(node.scope()):
type_ = "attribute"
assign_value = astroid_utils.get_assign_value(node)
assign_value = _astroid_utils.get_assign_value(node)
if not assign_value:
return []
target = assign_value[0]
value = assign_value[1]
annotation = astroid_utils.get_assign_annotation(node)
annotation = _astroid_utils.get_assign_annotation(node)
if annotation in ("TypeAlias", "typing.TypeAlias"):
value = node.value.as_string()
data = {
"type": type_,
"name": target,
"qual_name": self._get_qual_name(target),
"full_name": self._get_full_name(target),
"doc": _prepare_docstring(doc),
"value": value,
@ -103,23 +110,25 @@ class Parser:
def parse_classdef(self, node, data=None):
type_ = "class"
if astroid_utils.is_exception(node):
if _astroid_utils.is_exception(node):
type_ = "exception"
basenames = list(astroid_utils.get_full_basenames(node))
basenames = list(_astroid_utils.get_full_basenames(node))
data = {
"type": type_,
"name": node.name,
"qual_name": self._get_qual_name(node.name),
"full_name": self._get_full_name(node.name),
"bases": basenames,
"doc": _prepare_docstring(astroid_utils.get_class_docstring(node)),
"doc": _prepare_docstring(_astroid_utils.get_class_docstring(node)),
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
"children": [],
}
self._name_stack.append(node.name)
self._qual_name_stack.append(node.name)
self._full_name_stack.append(node.name)
overridden = set()
overloads = {}
for base in itertools.chain(iter((node,)), node.ancestors()):
@ -133,7 +142,7 @@ class Parser:
for child in base.get_children():
name = getattr(child, "name", None)
if isinstance(child, (astroid.Assign, astroid.AnnAssign)):
assign_value = astroid_utils.get_assign_value(child)
assign_value = _astroid_utils.get_assign_value(child)
if not assign_value:
continue
name = assign_value[0]
@ -148,7 +157,8 @@ class Parser:
overridden.update(seen)
self._name_stack.pop()
self._qual_name_stack.pop()
self._full_name_stack.pop()
return [data]
@ -156,7 +166,7 @@ class Parser:
return self.parse_functiondef(node)
def parse_functiondef(self, node):
if astroid_utils.is_decorated_with_property_setter(node):
if _astroid_utils.is_decorated_with_property_setter(node):
return []
type_ = "method"
@ -167,7 +177,7 @@ class Parser:
if isinstance(node, astroid.AsyncFunctionDef):
properties.append("async")
elif astroid_utils.is_decorated_with_property(node):
elif _astroid_utils.is_decorated_with_property(node):
type_ = "property"
if node.type == "classmethod":
properties.append(node.type)
@ -185,14 +195,15 @@ class Parser:
data = {
"type": type_,
"name": node.name,
"qual_name": self._get_qual_name(node.name),
"full_name": self._get_full_name(node.name),
"args": astroid_utils.get_args_info(node.args),
"doc": _prepare_docstring(astroid_utils.get_func_docstring(node)),
"args": _astroid_utils.get_args_info(node.args),
"doc": _prepare_docstring(_astroid_utils.get_func_docstring(node)),
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
"return_annotation": astroid_utils.get_return_annotation(node),
"return_annotation": _astroid_utils.get_return_annotation(node),
"properties": properties,
"is_overload": astroid_utils.is_decorated_with_overload(node),
"is_overload": _astroid_utils.is_decorated_with_overload(node),
"overloads": [],
}
@ -209,14 +220,19 @@ class Parser:
def _parse_local_import_from(self, node):
result = []
for name, alias in node.names:
is_wildcard = (alias or name) == "*"
full_name = self._get_full_name(alias or name)
original_path = astroid_utils.get_full_import_name(node, alias or name)
for import_name, alias in node.names:
is_wildcard = (alias or import_name) == "*"
original_path = _astroid_utils.get_full_import_name(
node, alias or import_name
)
name = original_path if is_wildcard else (alias or import_name)
qual_name = self._get_qual_name(alias or import_name)
full_name = self._get_full_name(alias or import_name)
data = {
"type": "placeholder",
"name": original_path if is_wildcard else (alias or name),
"name": name,
"qual_name": qual_name,
"full_name": full_name,
"original_path": original_path,
}
@ -233,24 +249,25 @@ class Parser:
if node.package:
type_ = "package"
self._name_stack = [node.name]
self._full_name_stack = [node.name]
self._encoding = node.file_encoding
data = {
"type": type_,
"name": node.name,
"qual_name": node.name,
"full_name": node.name,
"doc": _prepare_docstring(node.doc_node.value if node.doc_node else ""),
"children": [],
"file_path": path,
"encoding": node.file_encoding,
"all": astroid_utils.get_module_all(node),
"all": _astroid_utils.get_module_all(node),
}
overloads = {}
top_name = node.name.split(".", 1)[0]
for child in node.get_children():
if astroid_utils.is_local_import_from(child, top_name):
if _astroid_utils.is_local_import_from(child, top_name):
child_data = self._parse_local_import_from(child)
else:
child_data = self.parse(child)
@ -259,6 +276,35 @@ class Parser:
return data
def parse_typealias(self, node):
doc = ""
doc_node = node.next_sibling()
if isinstance(doc_node, astroid.nodes.Expr) and isinstance(
doc_node.value, astroid.nodes.Const
):
doc = doc_node.value.value
if isinstance(node.name, astroid.nodes.AssignName):
name = node.name.name
elif isinstance(node.name, astroid.nodes.AssignAttr):
name = node.name.attrname
else:
return []
data = {
"type": "data",
"name": name,
"qual_name": self._get_qual_name(name),
"full_name": self._get_full_name(name),
"doc": _prepare_docstring(doc),
"value": node.value.as_string(),
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
"annotation": "TypeAlias",
}
return [data]
def parse(self, node):
data = {}

@ -6,7 +6,7 @@ from docutils import nodes
from sphinx.ext.autosummary import Autosummary, mangle_signature
from sphinx.util.nodes import nested_parse_with_titles
from .mappers.python.objects import PythonFunction
from ._objects import PythonFunction
class AutoapiSummary(Autosummary):

@ -2,7 +2,7 @@ import re
from sphinx.ext import autodoc
from .mappers.python import (
from ._objects import (
PythonFunction,
PythonClass,
PythonMethod,

@ -19,7 +19,7 @@ from docutils.parsers.rst import directives
from . import documenters
from .directives import AutoapiSummary, NestedParse
from .inheritance_diagrams import AutoapiInheritanceDiagram
from .mappers import PythonSphinxMapper
from ._mapper import Mapper
from .settings import API_ROOT
LOGGER = sphinx.util.logging.getLogger(__name__)
@ -35,6 +35,13 @@ _DEFAULT_OPTIONS = [
"special-members",
"imported-members",
]
_VALID_PAGE_LEVELS = [
"module",
"class",
"function",
"method",
"attribute",
]
_VIEWCODE_CACHE: Dict[str, Tuple[str, Dict]] = {}
"""Caches a module's parse results for use in viewcode."""
@ -75,6 +82,10 @@ def run_autoapi(app):
if app.config.autoapi_include_summaries:
app.config.autoapi_options.append("show-module-summary")
own_page_level = app.config.autoapi_own_page_level
if own_page_level not in _VALID_PAGE_LEVELS:
raise ValueError(f"Invalid autoapi_own_page_level '{own_page_level}")
# Make sure the paths are full
normalised_dirs = _normalise_autoapi_dirs(app.config.autoapi_dirs, app.srcdir)
for _dir in normalised_dirs:
@ -100,8 +111,8 @@ def run_autoapi(app):
"relative to where sphinx-build is run\n",
RemovedInAutoAPI3Warning,
)
sphinx_mapper_obj = PythonSphinxMapper(
app, template_dir=template_dir, url_root=url_root
sphinx_mapper_obj = Mapper(
app, template_dir=template_dir, dir_root=normalized_root, url_root=url_root
)
if app.config.autoapi_file_patterns:
@ -128,7 +139,7 @@ def run_autoapi(app):
sphinx_mapper_obj.map(options=app.config.autoapi_options)
if app.config.autoapi_generate_api_docs:
sphinx_mapper_obj.output_rst(root=normalized_root, source_suffix=out_suffix)
sphinx_mapper_obj.output_rst(source_suffix=out_suffix)
def build_finished(app, exception):
@ -156,7 +167,7 @@ def doctree_read(app, doctree):
if app.env.docname == "index":
all_docs = set()
insert = True
nodes = list(doctree.traverse(toctree))
nodes = list(doctree.findall(toctree))
toc_entry = f"{app.config.autoapi_root}/index"
add_entry = (
nodes
@ -271,6 +282,7 @@ def setup(app):
app.add_config_value("autoapi_python_class_content", "class", "html")
app.add_config_value("autoapi_generate_api_docs", True, "html")
app.add_config_value("autoapi_prepare_jinja_env", None, "html")
app.add_config_value("autoapi_own_page_level", "module", "html")
app.add_autodocumenter(documenters.AutoapiFunctionDocumenter)
app.add_autodocumenter(documenters.AutoapiPropertyDocumenter)
app.add_autodocumenter(documenters.AutoapiDecoratorDocumenter)

@ -1,3 +0,0 @@
from .python import PythonSphinxMapper
__all__ = ("PythonSphinxMapper",)

@ -1,336 +0,0 @@
import os
import fnmatch
from collections import OrderedDict, namedtuple
import re
import anyascii
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
import sphinx
import sphinx.util
from sphinx.util.console import colorize
from sphinx.util.display import status_iterator
from sphinx.util.osutil import ensuredir
import sphinx.util.logging
from ..settings import API_ROOT, TEMPLATE_DIR
LOGGER = sphinx.util.logging.getLogger(__name__)
Path = namedtuple("Path", ["absolute", "relative"])
class PythonMapperBase:
"""Base object for JSON -> Python object mapping.
Subclasses of this object will handle their language specific JSON input,
and map that onto this standard Python object.
Subclasses may also include language-specific attributes on this object.
Arguments:
Args:
obj: JSON object representing this object
jinja_env: A template environment for rendering this object
Required attributes:
Attributes:
id (str): A globally unique identifier for this object.
Generally a fully qualified name, including namespace.
name (str): A short "display friendly" name for this object.
docstring (str): The documentation for this object
imports (list): Imports in this object
children (list): Children of this object
parameters (list): Parameters to this object
methods (list): Methods on this object
Optional attributes:
"""
language = "base"
type = "base"
# Create a page in the output for this object.
top_level_object = False
_RENDER_LOG_LEVEL = "VERBOSE"
def __init__(self, obj, jinja_env, app, options=None):
self.app = app
self.obj = obj
self.options = options
self.jinja_env = jinja_env
self.url_root = os.path.join("/", API_ROOT)
self.name = None
self.id = None
def __getstate__(self):
"""Obtains serialisable data for pickling."""
__dict__ = self.__dict__.copy()
__dict__.update(app=None, jinja_env=None) # clear unpickable attributes
return __dict__
def render(self, **kwargs):
LOGGER.log(self._RENDER_LOG_LEVEL, "Rendering %s", self.id)
ctx = {}
try:
template = self.jinja_env.get_template(f"{self.language}/{self.type}.rst")
except TemplateNotFound:
template = self.jinja_env.get_template(f"base/{self.type}.rst")
ctx.update(**self.get_context_data())
ctx.update(**kwargs)
return template.render(**ctx)
@property
def rendered(self):
"""Shortcut to render an object in templates."""
return self.render()
def get_context_data(self):
return {
"autoapi_options": self.app.config.autoapi_options,
"include_summaries": self.app.config.autoapi_include_summaries,
"obj": self,
"sphinx_version": sphinx.version_info,
}
def __lt__(self, other):
"""Object sorting comparison"""
if isinstance(other, PythonMapperBase):
return self.id < other.id
return super().__lt__(other)
def __str__(self):
return f"<{self.__class__.__name__} {self.id}>"
@property
def short_name(self):
"""Shorten name property"""
return self.name.split(".")[-1]
@property
def pathname(self):
"""Sluggified path for filenames
Slugs to a filename using the follow steps
* Decode unicode to approximate ascii
* Remove existing hyphens
* Substitute hyphens for non-word characters
* Break up the string as paths
"""
slug = self.name
slug = anyascii.anyascii(slug)
slug = slug.replace("-", "")
slug = re.sub(r"[^\w\.]+", "-", slug).strip("-")
return os.path.join(*slug.split("."))
def include_dir(self, root):
"""Return directory of file"""
parts = [root]
parts.extend(self.pathname.split(os.path.sep))
return "/".join(parts)
@property
def include_path(self):
"""Return 'absolute' path without regarding OS path separator
This is used in ``toctree`` directives, as Sphinx always expects Unix
path separators
"""
parts = [self.include_dir(root=self.url_root)]
parts.append("index")
return "/".join(parts)
@property
def display(self):
"""Whether to display this object or not.
:type: bool
"""
return True
@property
def ref_type(self):
return self.type
@property
def ref_directive(self):
return self.type
class SphinxMapperBase:
"""Base class for mapping `PythonMapperBase` objects to Sphinx.
Args:
app: Sphinx application instance
"""
def __init__(self, app, template_dir=None, url_root=None):
self.app = app
template_paths = [TEMPLATE_DIR]
if template_dir:
# Put at the front so it's loaded first
template_paths.insert(0, template_dir)
self.jinja_env = Environment(
loader=FileSystemLoader(template_paths),
trim_blocks=True,
lstrip_blocks=True,
)
def _wrapped_prepare(value):
return value
self.jinja_env.filters["prepare_docstring"] = _wrapped_prepare
if self.app.config.autoapi_prepare_jinja_env:
self.app.config.autoapi_prepare_jinja_env(self.jinja_env)
self.url_root = url_root
# Mapping of {filepath -> raw data}
self.paths = OrderedDict()
# Mapping of {object id -> Python Object}
self.objects = OrderedDict()
# Mapping of {object id -> Python Object}
self.all_objects = OrderedDict()
# Mapping of {namespace id -> Python Object}
self.namespaces = OrderedDict()
# Mapping of {namespace id -> Python Object}
self.top_level_objects = OrderedDict()
def load(self, patterns, dirs, ignore=None):
"""Load objects from the filesystem into the ``paths`` dictionary."""
paths = list(self.find_files(patterns=patterns, dirs=dirs, ignore=ignore))
for path in status_iterator(
paths,
colorize("bold", "[AutoAPI] Reading files... "),
"darkgreen",
len(paths),
):
data = self.read_file(path=path)
if data:
self.paths[path] = data
return True
@staticmethod
def find_files(patterns, dirs, ignore):
if not ignore:
ignore = []
pattern_regexes = []
for pattern in patterns:
regex = re.compile(fnmatch.translate(pattern).replace(".*", "(.*)"))
pattern_regexes.append((pattern, regex))
for _dir in dirs:
for root, _, filenames in os.walk(_dir):
seen = set()
for pattern, pattern_re in pattern_regexes:
for filename in fnmatch.filter(filenames, pattern):
skip = False
match = re.match(pattern_re, filename)
norm_name = match.groups()
if norm_name in seen:
continue
# Skip ignored files
for ignore_pattern in ignore:
if fnmatch.fnmatch(
os.path.join(root, filename), ignore_pattern
):
LOGGER.info(
colorize("bold", "[AutoAPI] ")
+ colorize(
"darkgreen", f"Ignoring {root}/{filename}"
)
)
skip = True
if skip:
continue
# Make sure the path is full
if not os.path.isabs(filename):
filename = os.path.join(root, filename)
yield filename
seen.add(norm_name)
def read_file(self, path, **kwargs):
"""Read file input into memory
Args:
path: Path of file to read
"""
# TODO support JSON here
# TODO sphinx way of reporting errors in logs?
raise NotImplementedError
def add_object(self, obj):
"""Add object to local and app environment storage
Args:
obj: Instance of a AutoAPI object
"""
self.objects[obj.id] = obj
self.all_objects[obj.id] = obj
child_stack = list(obj.children)
while child_stack:
child = child_stack.pop()
self.all_objects[child.id] = child
child_stack.extend(getattr(child, "children", ()))
def map(self, options=None):
"""Trigger find of serialized sources and build objects"""
for _, data in status_iterator(
self.paths.items(),
colorize("bold", "[AutoAPI] ") + "Mapping Data... ",
length=len(self.paths),
stringify_func=(lambda x: x[0]),
):
for obj in self.create_class(data, options=options):
self.add_object(obj)
def create_class(self, data, options=None, **kwargs):
"""Create class object.
Args:
data: Instance of a AutoAPI object
"""
raise NotImplementedError
def output_rst(self, root, source_suffix):
for _, obj in status_iterator(
self.objects.items(),
colorize("bold", "[AutoAPI] ") + "Rendering Data... ",
length=len(self.objects),
verbosity=1,
stringify_func=(lambda x: x[0]),
):
rst = obj.render()
if not rst:
continue
detail_dir = obj.include_dir(root=root)
ensuredir(detail_dir)
path = os.path.join(detail_dir, f"index{source_suffix}")
with open(path, "wb+") as detail_file:
detail_file.write(rst.encode("utf-8"))
if self.app.config.autoapi_add_toctree_entry:
self._output_top_rst(root)
def _output_top_rst(self, root):
# Render Top Index
top_level_index = os.path.join(root, "index.rst")
pages = self.objects.values()
with open(top_level_index, "wb") as top_level_file:
content = self.jinja_env.get_template("index.rst")
top_level_file.write(content.render(pages=pages).encode("utf-8"))

@ -1,25 +0,0 @@
from .mapper import PythonSphinxMapper
from .objects import (
PythonAttribute,
PythonClass,
PythonData,
PythonException,
PythonFunction,
PythonMethod,
PythonModule,
PythonPackage,
PythonProperty,
)
__all__ = (
"PythonAttribute",
"PythonClass",
"PythonData",
"PythonException",
"PythonFunction",
"PythonMethod",
"PythonModule",
"PythonPackage",
"PythonProperty",
"PythonSphinxMapper",
)

@ -10,3 +10,14 @@ SITE_ROOT = os.path.dirname(os.path.realpath(__file__))
TEMPLATE_DIR = os.path.join(SITE_ROOT, "templates")
API_ROOT = "autoapi"
OWN_PAGE_LEVELS = [
"package",
"module",
"exception",
"class",
"function",
"method",
"property",
"data",
"attribute",
]

@ -1,7 +0,0 @@
.. {{ obj.type }}:: {{ obj.name }}
{% if summary %}
{{ obj.summary }}
{% endif %}

@ -6,10 +6,8 @@ This page contains auto-generated API reference documentation [#f1]_.
.. toctree::
:titlesonly:
{% for page in pages %}
{% if page.top_level_object and page.display %}
{% for page in pages|selectattr("is_top_level_object") %}
{{ page.include_path }}
{% endif %}
{% endfor %}
.. [#f1] Created with `sphinx-autoapi <https://github.com/readthedocs/sphinx-autoapi>`_

@ -1,60 +1,104 @@
{% if obj.display %}
.. py:{{ obj.type }}:: {{ obj.short_name }}{% if obj.args %}({{ obj.args }}){% endif %}
{% if is_own_page %}
{{ obj.id }}
{{ "=" * obj.id | length }}
{% for (args, return_annotation) in obj.overloads %}
{{ " " * (obj.type | length) }} {{ obj.short_name }}{% if args %}({{ args }}){% endif %}
{% endif %}
{% set visible_children = obj.children|selectattr("display")|list %}
{% set own_page_children = visible_children|selectattr("type", "in", own_page_types)|list %}
{% if is_own_page and own_page_children %}
.. toctree::
:hidden:
{% for child in own_page_children %}
{{ child.include_path }}
{% endfor %}
{% endfor %}
{% endif %}
.. py:{{ obj.type }}:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}{% if obj.args %}({{ obj.args }}){% endif %}
{% for (args, return_annotation) in obj.overloads %}
{{ " " * (obj.type | length) }} {{ obj.short_name }}{% if args %}({{ args }}){% endif %}
{% endfor %}
{% if obj.bases %}
{% if "show-inheritance" in autoapi_options %}
{% if "show-inheritance" in autoapi_options %}
Bases: {% for base in obj.bases %}{{ base|link_objs }}{% if not loop.last %}, {% endif %}{% endfor %}
{% endif %}
{% endif %}
{% if "show-inheritance-diagram" in autoapi_options and obj.bases != ["object"] %}
{% if "show-inheritance-diagram" in autoapi_options and obj.bases != ["object"] %}
.. autoapi-inheritance-diagram:: {{ obj.obj["full_name"] }}
:parts: 1
{% if "private-members" in autoapi_options %}
{% if "private-members" in autoapi_options %}
:private-bases:
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% if obj.docstring %}
{{ obj.docstring|indent(3) }}
{% endif %}
{% if "inherited-members" in autoapi_options %}
{% set visible_classes = obj.classes|selectattr("display")|list %}
{% else %}
{% set visible_classes = obj.classes|rejectattr("inherited")|selectattr("display")|list %}
{% endif %}
{% for klass in visible_classes %}
{{ klass.render()|indent(3) }}
{% endfor %}
{% if "inherited-members" in autoapi_options %}
{% set visible_properties = obj.properties|selectattr("display")|list %}
{% else %}
{% set visible_properties = obj.properties|rejectattr("inherited")|selectattr("display")|list %}
{% endif %}
{% for property in visible_properties %}
{{ property.render()|indent(3) }}
{% endfor %}
{% if "inherited-members" in autoapi_options %}
{% set visible_attributes = obj.attributes|selectattr("display")|list %}
{% else %}
{% set visible_attributes = obj.attributes|rejectattr("inherited")|selectattr("display")|list %}
{% endif %}
{% for attribute in visible_attributes %}
{{ attribute.render()|indent(3) }}
{% for obj_item in visible_children %}
{% if obj_item.type not in own_page_types %}
{{ obj_item.render()|indent(3) }}
{% endif %}
{% endfor %}
{% if "inherited-members" in autoapi_options %}
{% set visible_methods = obj.methods|selectattr("display")|list %}
{% else %}
{% set visible_methods = obj.methods|rejectattr("inherited")|selectattr("display")|list %}
{% if is_own_page and own_page_children %}
{% set visible_attributes = own_page_children|selectattr("type", "equalto", "attribute")|list %}
{% if visible_attributes %}
Attributes
----------
.. autoapisummary::
{% for attribute in visible_attributes %}
{{ attribute.id }}
{% endfor %}
{% endif %}
{% set visible_exceptions = own_page_children|selectattr("type", "equalto", "exception")|list %}
{% if visible_exceptions %}
Exceptions
----------
.. autoapisummary::
{% for exception in visible_exceptions %}
{{ exception.id }}
{% endfor %}
{% endif %}
{% set visible_classes = own_page_children|selectattr("type", "equalto", "class")|list %}
{% if visible_classes %}
Classes
-------
.. autoapisummary::
{% for klass in visible_classes %}
{{ klass.id }}
{% endfor %}
{% endif %}
{% set visible_methods = own_page_children|selectattr("type", "equalto", "method")|list %}
{% if visible_methods %}
Methods
-------
.. autoapisummary::
{% for method in visible_methods %}
{{ method.id }}
{% endfor %}
{% endif %}
{% endif %}
{% for method in visible_methods %}
{{ method.render()|indent(3) }}
{% endfor %}
{% endif %}

@ -1,37 +1,38 @@
{% if obj.display %}
.. py:{{ obj.type }}:: {{ obj.name }}
{%- if obj.annotation is not none %}
{% if is_own_page %}
{{ obj.id }}
{{ "=" * obj.id | length }}
:type: {%- if obj.annotation %} {{ obj.annotation }}{%- endif %}
{% endif %}
.. py:{{ obj.type }}:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.name }}{% endif %}
{% if obj.annotation is not none %}
{%- endif %}
:type: {% if obj.annotation %} {{ obj.annotation }}{% endif %}
{% endif %}
{% if obj.value is not none %}
{%- if obj.value is not none %}
{% if obj.value.splitlines()|count > 1 %}
:value: Multiline-String
:value: {% if obj.value is string and obj.value.splitlines()|count > 1 -%}
Multiline-String
.. raw:: html
.. raw:: html
<details><summary>Show Value</summary>
<details><summary>Show Value</summary>
.. code-block:: python
.. code-block:: python
{{ obj.value|indent(width=6,blank=true) }}
"""{{ obj.value|indent(width=8,blank=true) }}"""
.. raw:: html
.. raw:: html
</details>
</details>
{%- else -%}
{%- if obj.value is string -%}
{{ "%r" % obj.value|string|truncate(100) }}
{%- else -%}
{{ obj.value|string|truncate(100) }}
{%- endif -%}
{%- endif %}
{%- endif %}
{% else %}
:value: {{ obj.value|truncate(100) }}
{% endif %}
{% endif %}
{% if obj.docstring %}
{{ obj.docstring|indent(3) }}
{% endif %}
{% endif %}

@ -1,15 +1,21 @@
{% if obj.display %}
.. py:function:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %}
{% if is_own_page %}
{{ obj.id }}
{{ "=" * obj.id | length }}
{% for (args, return_annotation) in obj.overloads %}
{{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %}
{% endif %}
.. py:function:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %}
{% for (args, return_annotation) in obj.overloads %}
{% endfor %}
{%+ if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %}
{% endfor %}
{% for property in obj.properties %}
:{{ property }}:
{% endfor %}
{% if obj.docstring %}
{{ obj.docstring|indent(3) }}
{% endif %}
{% endif %}

@ -1,19 +1,21 @@
{%- if obj.display %}
.. py:method:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %}
{% if obj.display %}
{% if is_own_page %}
{{ obj.id }}
{{ "=" * obj.id | length }}
{% for (args, return_annotation) in obj.overloads %}
{{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %}
{% endif %}
.. py:method:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %}
{% for (args, return_annotation) in obj.overloads %}
{% endfor %}
{% if obj.properties %}
{%+ if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %}
{% endfor %}
{% for property in obj.properties %}
:{{ property }}:
{% endfor %}
{% else %}
{% endif %}
{% if obj.docstring %}
{{ obj.docstring|indent(3) }}
{% endif %}
{% endif %}

@ -1,114 +1,170 @@
{% if not obj.display %}
:orphan:
{% endif %}
:py:mod:`{{ obj.name }}`
=========={{ "=" * obj.name|length }}
{% if obj.display %}
{% if is_own_page %}
{{ obj.id }}
{{ "=" * obj.id|length }}
.. py:module:: {{ obj.name }}
{% if obj.docstring %}
{% if obj.docstring %}
.. autoapi-nested-parse::
{{ obj.docstring|indent(3) }}
{% endif %}
{% endif %}
{% block subpackages %}
{% set visible_subpackages = obj.subpackages|selectattr("display")|list %}
{% if visible_subpackages %}
{% block subpackages %}
{% set visible_subpackages = obj.subpackages|selectattr("display")|list %}
{% if visible_subpackages %}
Subpackages
-----------
.. toctree::
:titlesonly:
:maxdepth: 3
:maxdepth: 1
{% for subpackage in visible_subpackages %}
{{ subpackage.short_name }}/index.rst
{% endfor %}
{% for subpackage in visible_subpackages %}
{{ subpackage.include_path }}
{% endfor %}
{% endif %}
{% endblock %}
{% block submodules %}
{% set visible_submodules = obj.submodules|selectattr("display")|list %}
{% if visible_submodules %}
{% endif %}
{% endblock %}
{% block submodules %}
{% set visible_submodules = obj.submodules|selectattr("display")|list %}
{% if visible_submodules %}
Submodules
----------
.. toctree::
:titlesonly:
:maxdepth: 1
{% for submodule in visible_submodules %}
{{ submodule.short_name }}/index.rst
{% endfor %}
{% for submodule in visible_submodules %}
{{ submodule.include_path }}
{% endfor %}
{% endif %}
{% endblock %}
{% block content %}
{% if obj.all is not none %}
{% set visible_children = obj.children|selectattr("short_name", "in", obj.all)|list %}
{% elif obj.type is equalto("package") %}
{% set visible_children = obj.children|selectattr("display")|list %}
{% else %}
{% set visible_children = obj.children|selectattr("display")|rejectattr("imported")|list %}
{% endif %}
{% if visible_children %}
{{ obj.type|title }} Contents
{{ "-" * obj.type|length }}---------
{% endif %}
{% endblock %}
{% block content %}
{% set visible_children = obj.children|selectattr("display")|list %}
{% if visible_children %}
{% set visible_attributes = visible_children|selectattr("type", "equalto", "data")|list %}
{% if visible_attributes %}
{% if "attribute" in own_page_types or "show-module-summary" in autoapi_options %}
Attributes
----------
{% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %}
{% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %}
{% set visible_attributes = visible_children|selectattr("type", "equalto", "data")|list %}
{% if "show-module-summary" in autoapi_options and (visible_classes or visible_functions) %}
{% block classes scoped %}
{% if visible_classes %}
Classes
~~~~~~~
{% if "attribute" in own_page_types %}
.. toctree::
:hidden:
{% for attribute in visible_attributes %}
{{ attribute.include_path }}
{% endfor %}
{% endif %}
.. autoapisummary::
{% for klass in visible_classes %}
{{ klass.id }}
{% endfor %}
{% for attribute in visible_attributes %}
{{ attribute.id }}
{% endfor %}
{% endif %}
{% endif %}
{% endblock %}
{% endif %}
{% set visible_exceptions = visible_children|selectattr("type", "equalto", "exception")|list %}
{% if visible_exceptions %}
{% if "exception" in own_page_types or "show-module-summary" in autoapi_options %}
Exceptions
----------
{% block functions scoped %}
{% if visible_functions %}
Functions
~~~~~~~~~
{% if "exception" in own_page_types %}
.. toctree::
:hidden:
{% for exception in visible_exceptions %}
{{ exception.include_path }}
{% endfor %}
{% endif %}
.. autoapisummary::
{% for function in visible_functions %}
{{ function.id }}
{% endfor %}
{% for exception in visible_exceptions %}
{{ exception.id }}
{% endfor %}
{% endif %}
{% endif %}
{% endblock %}
{% endif %}
{% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %}
{% if visible_classes %}
{% if "class" in own_page_types or "show-module-summary" in autoapi_options %}
Classes
-------
{% block attributes scoped %}
{% if visible_attributes %}
Attributes
~~~~~~~~~~
{% if "class" in own_page_types %}
.. toctree::
:hidden:
{% for klass in visible_classes %}
{{ klass.include_path }}
{% endfor %}
{% endif %}
.. autoapisummary::
{% for attribute in visible_attributes %}
{{ attribute.id }}
{% endfor %}
{% for klass in visible_classes %}
{{ klass.id }}
{% endfor %}
{% endif %}
{% endif %}
{% endblock %}
{% endif %}
{% for obj_item in visible_children %}
{% endif %}
{% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %}
{% if visible_functions %}
{% if "function" in own_page_types or "show-module-summary" in autoapi_options %}
Functions
---------
{% if "function" in own_page_types %}
.. toctree::
:hidden:
{% for function in visible_functions %}
{{ function.include_path }}
{% endfor %}
{% endif %}
.. autoapisummary::
{% for function in visible_functions %}
{{ function.id }}
{% endfor %}
{% endif %}
{% endif %}
{% set this_page_children = visible_children|rejectattr("type", "in", own_page_types)|list %}
{% if this_page_children %}
{{ obj.type|title }} Contents
{{ "-" * obj.type|length }}---------
{% for obj_item in this_page_children %}
{{ obj_item.render()|indent(0) }}
{% endfor %}
{% endfor %}
{% endif %}
{% endif %}
{% endblock %}
{% else %}
.. py:module:: {{ obj.name }}
{% if obj.docstring %}
.. autoapi-nested-parse::
{{ obj.docstring|indent(6) }}
{% endif %}
{% for obj_item in visible_children %}
{{ obj_item.render()|indent(3) }}
{% endfor %}
{% endif %}
{% endif %}
{% endblock %}

@ -1,13 +1,18 @@
{%- if obj.display %}
.. py:property:: {{ obj.short_name }}
{% if obj.display %}
{% if is_own_page %}
{{ obj.id }}
{{ "=" * obj.id | length }}
{% endif %}
.. py:property:: {% if is_own_page %}{{ obj.id}}{% else %}{{ obj.short_name }}{% endif %}
{% if obj.annotation %}
:type: {{ obj.annotation }}
{% endif %}
{% if obj.properties %}
{% for property in obj.properties %}
:{{ property }}:
{% endfor %}
{% endif %}
{% if obj.docstring %}
{{ obj.docstring|indent(3) }}

@ -0,0 +1 @@
Fix emitting ignore event twice for methods.

@ -0,0 +1 @@
Refactor mapper classes into their bases

@ -0,0 +1 @@
Values are always rendered for TypeAlises and PEP-695 type aliases.

@ -0,0 +1 @@
Objects can render to their own page

@ -0,0 +1 @@
Render PEP-695 type aliases as TypeAlias assignments.

@ -0,0 +1 @@
Preserve strings inside Literal type annotations

@ -0,0 +1 @@
Stopped using xrefs in page titles

@ -2,7 +2,7 @@ How-to Guides
=============
These guides will take you through the steps to perform common actions
or solve common problems in AutoAPI.
or solve common problems in AutoAPI.
They will assume that you already have a Sphinx project with AutoAPI
set up already.
If you don't know how to do this then read the :doc:`tutorials`.

@ -79,11 +79,9 @@ Customisation Options
and requires `Graphviz <https://graphviz.org/>`_ to be installed.
* ``show-module-summary``: Whether to include autosummary directives
in generated module documentation.
* ``imported-members``: Display objects imported from the same
top level package or module.
The default module template does not include imported objects,
even with this option enabled.
The default package template does.
* ``imported-members``: For objects imported into a package,
display objects imported from the same top level package or module.
This option does not effect objects imported into a module.
.. confval:: autoapi_ignore
@ -181,6 +179,26 @@ Customisation Options
:noindex:
.. confval:: autoapi_own_page_level
Default: ``'module'``
This configuration value specifies the level at which objects are rendered on
a single page. Valid levels, in descending order of hierarchy, are as
follows:
* ``module``: Packages, modules, subpackages, and submodules.
* ``class``: Classes, exceptions, and all object types mentioned above.
* ``function``: Functions, and all object types mentioned above.
* ``method``: Methods, and all object types mentioned above.
* ``attribute``: Class and module level attributes, properties,
and all object types mentioned above.
Events
~~~~~~

@ -55,7 +55,7 @@ Inheritance Diagrams
For example:
.. autoapi-inheritance-diagram:: autoapi.mappers.python.objects.PythonModule autoapi.mappers.python.objects.PythonPackage
.. autoapi-inheritance-diagram:: autoapi._objects.PythonModule autoapi._objects.PythonPackage
:parts: 1
:mod:`sphinx.ext.inheritance_diagram` makes use of the

@ -49,6 +49,8 @@ This contains:
* ``include_summaries``: The value of the :confval:`autoapi_include_summaries`
configuration option.
* ``obj``: A Python object derived from :class:`PythonPythonMapper`.
* ``own_page_types``: A set of strings that contains the object types that
render on their own page.
* ``sphinx_version``: The contents of :attr:`sphinx.version_info`.
The object in ``obj`` has a number of standard attributes

@ -10,11 +10,6 @@ ignore_missing_imports = true
module = "autoapi.documenters"
ignore_errors = true
[[tool.mypy.overrides]]
# https://github.com/anyascii/anyascii/issues/19
module = "anyascii"
ignore_missing_imports = true
[tool.ruff.lint.pydocstyle]
convention = "google"

@ -33,7 +33,6 @@ packages = find:
include_package_data = True
python_requires = >=3.8
install_requires =
anyascii
astroid>=2.7;python_version<"3.12"
astroid>=3.0.0a1;python_version>="3.12"
Jinja2

@ -0,0 +1,68 @@
import io
import os
import pathlib
import shutil
from unittest.mock import call
from bs4 import BeautifulSoup
import pytest
from sphinx.application import Sphinx
@pytest.fixture(scope="session")
def rebuild():
def _rebuild(confdir=".", **kwargs):
app = Sphinx(
srcdir=".",
confdir=confdir,
outdir="_build/html",
doctreedir="_build/.doctrees",
buildername="html",
pdb=True,
**kwargs,
)
app.build()
return _rebuild
@pytest.fixture(scope="class")
def builder(rebuild):
cwd = os.getcwd()
def build(test_dir, **kwargs):
if kwargs.get("warningiserror"):
# Add any warnings raised when using `Sphinx` more than once
# in a Python session.
confoverrides = kwargs.setdefault("confoverrides", {})
confoverrides.setdefault("suppress_warnings", [])
suppress = confoverrides["suppress_warnings"]
suppress.append("app.add_node")
suppress.append("app.add_directive")
suppress.append("app.add_role")
os.chdir("tests/python/{0}".format(test_dir))
rebuild(**kwargs)
yield build
try:
shutil.rmtree("_build")
if (pathlib.Path("autoapi") / "index.rst").exists():
shutil.rmtree("autoapi")
finally:
os.chdir(cwd)
@pytest.fixture(scope="class")
def parse():
cache = {}
def parser(path):
if path not in cache:
with io.open(path, encoding="utf8") as file_handle:
cache[path] = BeautifulSoup(file_handle, features="html.parser")
return cache[path]
yield parser

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
templates_path = ["_templates"]
source_suffix = ".rst"
master_doc = "index"
project = "pyexample"
copyright = "2015, readthedocs"
author = "readthedocs"
version = "0.1"
release = "0.1"
language = "en"
exclude_patterns = ["_build"]
pygments_style = "sphinx"
todo_include_todos = False
html_theme = "alabaster"
htmlhelp_basename = "pyexampledoc"
extensions = ["sphinx.ext.intersphinx", "sphinx.ext.autodoc", "autoapi.extension"]
intersphinx_mapping = {"python": ("https://docs.python.org/3.10", None)}
autoapi_dirs = ["example"]
autoapi_file_pattern = "*.py"

@ -0,0 +1,4 @@
from typing import TypeAlias
MyTypeAliasA: TypeAlias = tuple[str, int]
type MyTypeAliasB = tuple[str, int]

@ -0,0 +1,26 @@
.. pyexample documentation master file, created by
sphinx-quickstart on Fri May 29 13:34:37 2015.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to pyexample's documentation!
=====================================
.. toctree::
autoapi/index
Contents:
.. toctree::
:maxdepth: 2
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

@ -17,3 +17,4 @@ htmlhelp_basename = "pyexampledoc"
extensions = ["sphinx.ext.autodoc", "autoapi.extension"]
autoapi_dirs = ["example"]
autoapi_file_pattern = "*.py"
autoapi_keep_files = True

@ -7,6 +7,6 @@ from .subpackage import public_chain
from .subpackage.submodule import public_multiple_imports
def module_level_method(foo, bar):
def module_level_function(foo, bar):
"""A module level method"""
pass

@ -4,7 +4,7 @@ This is a description
"""
from ._private_module import PrivateClass as PublicClass
from ._subpackage import module_level_method
from ._subpackage import module_level_function
__all__ = ["PublicClass", "Foo"]

@ -3,6 +3,6 @@ from .submodule import _private_made_public as now_public_function
from .submodule import public_multiple_imports
def module_level_method(foo, bar):
def module_level_function(foo, bar):
"""A module level method"""
pass

@ -4,7 +4,7 @@ __all__ = [
"SimpleClass",
"simple_function",
"public_chain",
"module_level_method",
"module_level_function",
"does_not_exist",
]

@ -1,2 +1,2 @@
from ..wildcard import module_level_method
from ..wildcard import module_level_function
from ..wildcard import public_chain

@ -15,6 +15,6 @@ todo_include_todos = False
html_theme = "alabaster"
htmlhelp_basename = "pypackageexampledoc"
extensions = ["autoapi.extension"]
autoapi_dirs = ["example"]
autoapi_dirs = ["package"]
autoapi_file_pattern = "*.py"
autoapi_keep_files = True

@ -1,8 +0,0 @@
"""This is a docstring."""
from . import foo
def module_level_method(foo, bar):
"""A module level method"""
pass

@ -0,0 +1,42 @@
"""This is a docstring."""
from . import submodule
from .subpackage.submodule import function as aliased_function
from .subpackage.submodule import not_in_all_function
__all__ = (
"aliased_function",
"Class",
"DATA",
"function",
"MyException",
)
DATA = 42
def function(foo, bar):
"""A module level function"""
class Class(object):
"""This is a class."""
class_var = 42
"""Class var docstring"""
class NestedClass(object):
"""A nested class just to test things out"""
@classmethod
def a_classmethod():
"""A class method"""
return True
def method_okay(self, foo=None, bar=None):
"""This method should parse okay"""
return True
class MyException(Exception):
"""This is an exception."""

@ -3,19 +3,36 @@
This is a description
"""
from .subpackage.submodule import function as aliased_function
from .subpackage.submodule import not_in_all_function
class Foo(object):
class_var = 42 #: Class var docstring
__all__ = (
"aliased_function",
"Class",
"DATA",
"function",
"MyException",
)
another_class_var = 42
"""Another class var docstring"""
DATA = 42
class Meta(object):
def function(foo, bar):
"""A module level function"""
class Class(object):
"""This is a class."""
class_var = 42
"""Class var docstring"""
class NestedClass(object):
"""A nested class just to test things out"""
@classmethod
def foo():
"""The foo class method"""
def a_classmethod():
"""A class method"""
return True
def method_okay(self, foo=None, bar=None):
@ -61,3 +78,7 @@ class Foo(object):
int: The sum of foo and bar.
"""
return foo + bar
class MyException(Exception):
"""This is an exception."""

@ -0,0 +1,13 @@
"""This is a docstring."""
from .submodule import function as aliased_function
from .submodule import not_in_all_function
__all__ = (
"aliased_function",
"function",
)
def function(foo, bar):
"""A module level function"""

@ -0,0 +1,41 @@
"""Example module
This is a description
"""
DATA = 42
def function(foo, bar):
"""A module level function"""
def _private_function():
"""A function that shouldn't get rendered."""
def not_in_all_function():
"""A function that doesn't exist in __all__ when imported."""
class Class(object):
"""This is a class."""
class_var = 42
"""Class var docstring"""
class NestedClass(object):
"""A nested class just to test things out"""
@classmethod
def a_classmethod():
"""A class method"""
return True
def method_okay(self, foo=None, bar=None):
"""This method should parse okay"""
return True
class MyException(Exception):
"""This is an exception."""

@ -0,0 +1,997 @@
import os
import pytest
class TestModule:
@pytest.fixture(autouse=True, scope="class")
def built(self, builder):
builder(
"pypackageexample",
warningiserror=True,
confoverrides={
"autoapi_own_page_level": "module",
"autoapi_options": [
"members",
"undoc-members",
"show-inheritance",
"imported-members",
],
},
)
def test_package(self, parse):
package_path = "_build/html/autoapi/package/index.html"
package_file = parse(package_path)
docstring = package_file.find("p")
assert docstring.text == "This is a docstring."
# There should be links to the children with their own page
subpackages = package_file.find(id="subpackages")
assert subpackages
assert subpackages.find("a", string="package.subpackage")
submodules = package_file.find(id="submodules")
assert submodules
assert submodules.find("a", string="package.submodule")
# There should not be links to the children without their own page
assert not package_file.find(id="attributes")
assert not package_file.find(id="exceptions")
assert not package_file.find(id="classes")
assert not package_file.find(id="functions")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
contents = package_file.find(id="package-contents")
assert contents.find(id="package.DATA")
assert contents.find(id="package.MyException")
assert contents.find(id="package.Class")
assert contents.find(id="package.Class.class_var")
assert contents.find(id="package.Class.NestedClass")
assert contents.find(id="package.Class.method_okay")
assert contents.find(id="package.Class.NestedClass")
assert contents.find(id="package.Class.NestedClass.a_classmethod")
assert contents.find(id="package.function")
assert contents.find(id="package.aliased_function")
# Hidden children are never rendered.
assert not contents.find(id="package.not_in_all_function")
def test_subpackage(self, parse):
subpackage_path = "_build/html/autoapi/package/subpackage/index.html"
subpackage_file = parse(subpackage_path)
docstring = subpackage_file.find("p")
assert docstring.text == "This is a docstring."
# There should be links to the children with their own page
assert not subpackage_file.find(id="subpackages")
submodules = subpackage_file.find(id="submodules")
assert submodules
assert submodules.find("a", string="package.subpackage.submodule")
# There should not be links to the children without their own page
assert not subpackage_file.find(id="attributes")
assert not subpackage_file.find(id="exceptions")
assert not subpackage_file.find(id="classes")
assert not subpackage_file.find(id="functions")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
contents = subpackage_file.find(id="package-contents")
assert contents.find(id="package.subpackage.function")
assert contents.find(id="package.subpackage.aliased_function")
# Hidden children are never rendered.
assert not contents.find(id="package.subpackage.not_in_all_function")
def test_module(self, parse):
submodule_path = "_build/html/autoapi/package/submodule/index.html"
submodule_file = parse(submodule_path)
docstring = submodule_file.find("p")
assert docstring.text == "Example module"
# There should be links to the children with their own page
pass # there are no children with their own page
# There should not be links to the children without their own page
assert not submodule_file.find(id="submodules")
assert not submodule_file.find(id="subpackages")
assert not submodule_file.find(id="attributes")
assert not submodule_file.find(id="exceptions")
assert not submodule_file.find(id="classes")
assert not submodule_file.find(id="functions")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
contents = submodule_file.find(id="module-contents")
assert contents.find(id="package.submodule.DATA")
assert contents.find(id="package.submodule.MyException")
assert contents.find(id="package.submodule.Class")
assert contents.find(id="package.submodule.Class.class_var")
assert contents.find(id="package.submodule.Class.NestedClass")
assert contents.find(id="package.submodule.Class.method_okay")
assert contents.find(id="package.submodule.Class.NestedClass")
assert contents.find(id="package.submodule.Class.NestedClass.a_classmethod")
assert contents.find(id="package.submodule.function")
assert contents.find(id="package.submodule.aliased_function")
# Hidden children are never rendered.
assert not contents.find(id="package.submodule.not_in_all_function")
def test_rendered_only_expected_pages(self):
_, dirs, files = next(os.walk("_build/html/autoapi/package"))
assert sorted(dirs) == ["submodule", "subpackage"]
assert files == ["index.html"]
_, dirs, files = next(os.walk("_build/html/autoapi/package/submodule"))
assert not dirs
assert files == ["index.html"]
_, dirs, files = next(os.walk("_build/html/autoapi/package/subpackage"))
assert dirs == ["submodule"]
assert files == ["index.html"]
_, dirs, files = next(
os.walk("_build/html/autoapi/package/subpackage/submodule")
)
assert not dirs
assert files == ["index.html"]
def test_index(self, parse):
index_path = "_build/html/autoapi/index.html"
index_file = parse(index_path)
top_links = index_file.find_all(class_="toctree-l1")
top_hrefs = sorted(link.a["href"] for link in top_links)
assert top_hrefs == [
"#",
"package/index.html",
]
class TestClass:
@pytest.fixture(autouse=True, scope="class")
def built(self, builder):
builder(
"pypackageexample",
warningiserror=True,
confoverrides={
"autoapi_own_page_level": "class",
"autoapi_options": [
"members",
"undoc-members",
"show-inheritance",
"imported-members",
],
},
)
def test_package(self, parse):
package_path = "_build/html/autoapi/package/index.html"
package_file = parse(package_path)
docstring = package_file.find("p")
assert docstring.text == "This is a docstring."
# There should be links to the children with their own page
subpackages = package_file.find(id="subpackages")
assert subpackages
assert subpackages.find("a", string="package.subpackage")
submodules = package_file.find(id="submodules")
assert submodules
assert submodules.find("a", string="package.submodule")
exceptions = package_file.find(id="exceptions")
assert exceptions
assert exceptions.find("a", title="package.MyException")
classes = package_file.find(id="classes")
assert classes
assert classes.find("a", title="package.Class")
assert not classes.find("a", title="package.Class.NestedClass")
# There should not be links to the children without their own page
assert not package_file.find(id="attributes")
assert not package_file.find(id="functions")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
contents = package_file.find(id="package-contents")
assert contents.find(id="package.DATA")
assert not contents.find(id="package.MyException")
assert not contents.find(id="package.Class")
assert not contents.find(id="package.Class.class_var")
assert not contents.find(id="package.Class.NestedClass")
assert not contents.find(id="package.Class.method_okay")
assert not contents.find(id="package.Class.NestedClass")
assert not contents.find(id="package.Class.NestedClass.a_classmethod")
assert contents.find(id="package.function")
assert contents.find(id="package.aliased_function")
# Hidden children are never rendered.
assert not contents.find(id="package.not_in_all_function")
def test_module(self, parse):
submodule_path = "_build/html/autoapi/package/submodule/index.html"
submodule_file = parse(submodule_path)
docstring = submodule_file.find("p")
assert docstring.text == "Example module"
# There should be links to the children with their own page
exceptions = submodule_file.find(id="exceptions")
assert exceptions
assert exceptions.find("a", title="package.submodule.MyException")
classes = submodule_file.find(id="classes")
assert classes
assert classes.find("a", title="package.submodule.Class")
assert not classes.find("a", title="package.submodule.Class.NestedClass")
# There should not be links to the children without their own page
assert not submodule_file.find(id="submodules")
assert not submodule_file.find(id="subpackages")
assert not submodule_file.find(id="attributes")
assert not submodule_file.find(id="functions")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
contents = submodule_file.find(id="module-contents")
assert contents.find(id="package.submodule.DATA")
assert not contents.find(id="package.submodule.MyException")
assert not contents.find(id="package.submodule.Class")
assert not contents.find(id="package.submodule.Class.class_var")
assert not contents.find(id="package.submodule.Class.NestedClass")
assert not contents.find(id="package.submodule.Class.method_okay")
assert not contents.find(id="package.submodule.Class.NestedClass")
assert not contents.find(id="package.submodule.Class.NestedClass.a_classmethod")
assert contents.find(id="package.submodule.function")
assert contents.find(id="package.submodule.aliased_function")
# Hidden children are never rendered.
assert not contents.find(id="package.submodule.not_in_all_function")
def test_class(self, parse):
class_path = "_build/html/autoapi/package/Class.html"
class_file = parse(class_path)
class_sig = class_file.find(id="package.Class")
assert class_sig
class_ = class_sig.parent
docstring = class_.find_all("p")[1]
assert docstring.text == "This is a class."
# There should be links to the children with their own page
classes = class_file.find(id="classes")
assert classes
assert classes.find("a", title="package.Class.NestedClass")
# There should not be links to the children without their own page
assert not class_file.find(id="attributes")
assert not class_file.find(id="exceptions")
assert not class_file.find(id="methods")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
assert class_.find(id="package.Class.class_var")
assert class_.find(id="package.Class.method_okay")
nested_class_path = "_build/html/autoapi/package/Class.NestedClass.html"
nested_class_file = parse(nested_class_path)
nested_class_sig = nested_class_file.find(id="package.Class.NestedClass")
assert nested_class_sig
nested_class = nested_class_sig.parent
# There should be links to the children with their own page
pass # there are no children with their own page
# There should not be links to the children without their own page
assert not class_file.find(id="attributes")
assert not class_file.find(id="exceptions")
assert not class_file.find(id="methods")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
assert nested_class.find(id="package.Class.NestedClass.a_classmethod")
def test_exception(self, parse):
exception_path = "_build/html/autoapi/package/MyException.html"
exception_file = parse(exception_path)
exception_sig = exception_file.find(id="package.MyException")
assert exception_sig
exception = exception_sig.parent
docstring = exception.find_all("p")[1]
assert docstring.text == "This is an exception."
# There should be links to the children with their own page
pass # there are no children with their own page
# There should not be links to the children without their own page
assert not exception_file.find(id="attributes")
assert not exception_file.find(id="exceptions")
assert not exception_file.find(id="methods")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
pass # there are no children without their own page
def test_rendered_only_expected_pages(self):
_, dirs, files = next(os.walk("_build/html/autoapi/package"))
assert sorted(dirs) == ["submodule", "subpackage"]
assert sorted(files) == [
"Class.NestedClass.html",
"Class.html",
"MyException.html",
"index.html",
]
_, dirs, files = next(os.walk("_build/html/autoapi/package/submodule"))
assert not dirs
assert sorted(files) == [
"Class.NestedClass.html",
"Class.html",
"MyException.html",
"index.html",
]
_, dirs, files = next(os.walk("_build/html/autoapi/package/subpackage"))
assert dirs == ["submodule"]
assert files == ["index.html"]
_, dirs, files = next(
os.walk("_build/html/autoapi/package/subpackage/submodule")
)
assert not dirs
assert sorted(files) == [
"Class.NestedClass.html",
"Class.html",
"MyException.html",
"index.html",
]
def test_index(self, parse):
index_path = "_build/html/autoapi/index.html"
index_file = parse(index_path)
top_links = index_file.find_all(class_="toctree-l1")
top_hrefs = sorted(link.a["href"] for link in top_links)
assert top_hrefs == [
"#",
"package/index.html",
]
class TestFunction:
@pytest.fixture(autouse=True, scope="class")
def built(self, builder):
builder(
"pypackageexample",
warningiserror=True,
confoverrides={
"autoapi_own_page_level": "function",
"autoapi_options": [
"members",
"undoc-members",
"show-inheritance",
"imported-members",
],
},
)
def test_package(self, parse):
package_path = "_build/html/autoapi/package/index.html"
package_file = parse(package_path)
docstring = package_file.find("p")
assert docstring.text == "This is a docstring."
# There should be links to the children with their own page
subpackages = package_file.find(id="subpackages")
assert subpackages
assert subpackages.find("a", string="package.subpackage")
submodules = package_file.find(id="submodules")
assert submodules
assert submodules.find("a", string="package.submodule")
classes = package_file.find(id="classes")
assert classes
assert classes.find("a", title="package.Class")
exceptions = package_file.find(id="exceptions")
assert exceptions
assert exceptions.find("a", title="package.MyException")
functions = package_file.find(id="functions")
assert functions
assert functions.find("a", title="package.function")
# There should not be links to the children without their own page
assert not package_file.find(id="attributes")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
contents = package_file.find(id="package-contents")
assert contents.find(id="package.DATA")
assert not contents.find(id="package.MyException")
assert not contents.find(id="package.Class")
assert not contents.find(id="package.Class.class_var")
assert not contents.find(id="package.Class.NestedClass")
assert not contents.find(id="package.Class.method_okay")
assert not contents.find(id="package.Class.NestedClass")
assert not contents.find(id="package.Class.NestedClass.a_classmethod")
assert not contents.find(id="package.function")
assert not contents.find(id="package.aliased_function")
# Hidden children are never rendered.
assert not contents.find(id="package.not_in_all_function")
def test_module(self, parse):
submodule_path = "_build/html/autoapi/package/submodule/index.html"
submodule_file = parse(submodule_path)
docstring = submodule_file.find("p")
assert docstring.text == "Example module"
# There should be links to the children with their own page
exceptions = submodule_file.find(id="exceptions")
assert exceptions
assert exceptions.find("a", title="package.submodule.MyException")
classes = submodule_file.find(id="classes")
assert classes
assert classes.find("a", title="package.submodule.Class")
assert not classes.find("a", title="package.submodule.Class.NestedClass")
functions = submodule_file.find(id="functions")
assert functions
assert functions.find("a", title="package.submodule.function")
# There should not be links to the children without their own page
assert not submodule_file.find(id="submodules")
assert not submodule_file.find(id="subpackages")
assert not submodule_file.find(id="attributes")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
contents = submodule_file.find(id="module-contents")
assert contents.find(id="package.submodule.DATA")
assert not contents.find(id="package.submodule.MyException")
assert not contents.find(id="package.submodule.Class")
assert not contents.find(id="package.submodule.Class.class_var")
assert not contents.find(id="package.submodule.Class.NestedClass")
assert not contents.find(id="package.submodule.Class.method_okay")
assert not contents.find(id="package.submodule.Class.NestedClass")
assert not contents.find(id="package.submodule.Class.NestedClass.a_classmethod")
assert not contents.find(id="package.submodule.function")
assert not contents.find(id="package.submodule.aliased_function")
# Hidden children are never rendered.
assert not contents.find(id="package.submodule.not_in_all_function")
def test_class(self, parse):
class_path = "_build/html/autoapi/package/Class.html"
class_file = parse(class_path)
class_sig = class_file.find(id="package.Class")
assert class_sig
class_ = class_sig.parent
docstring = class_.find_all("p")[1]
assert docstring.text == "This is a class."
# There should be links to the children with their own page
classes = class_file.find(id="classes")
assert classes
assert classes.find("a", title="package.Class.NestedClass")
# There should not be links to the children without their own page
assert not class_file.find(id="attributes")
assert not class_file.find(id="exceptions")
assert not class_file.find(id="methods")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
assert class_.find(id="package.Class.class_var")
assert class_.find(id="package.Class.method_okay")
def test_function(self, parse):
function_path = "_build/html/autoapi/package/function.html"
function_file = parse(function_path)
function_sig = function_file.find(id="package.function")
assert function_sig
function_path = "_build/html/autoapi/package/submodule/function.html"
function_file = parse(function_path)
assert function_file.find(id="package.submodule.function")
def test_rendered_only_expected_pages(self):
_, dirs, files = next(os.walk("_build/html/autoapi/package"))
assert sorted(dirs) == ["submodule", "subpackage"]
assert sorted(files) == [
"Class.NestedClass.html",
"Class.html",
"MyException.html",
"aliased_function.html",
"function.html",
"index.html",
]
_, dirs, files = next(os.walk("_build/html/autoapi/package/submodule"))
assert not dirs
assert sorted(files) == [
"Class.NestedClass.html",
"Class.html",
"MyException.html",
"aliased_function.html",
"function.html",
"index.html",
]
_, dirs, files = next(os.walk("_build/html/autoapi/package/subpackage"))
assert dirs == ["submodule"]
assert sorted(files) == ["aliased_function.html", "function.html", "index.html"]
_, dirs, files = next(
os.walk("_build/html/autoapi/package/subpackage/submodule")
)
assert not dirs
assert sorted(files) == [
"Class.NestedClass.html",
"Class.html",
"MyException.html",
"function.html",
"index.html",
"not_in_all_function.html",
]
def test_index(self, parse):
index_path = "_build/html/autoapi/index.html"
index_file = parse(index_path)
top_links = index_file.find_all(class_="toctree-l1")
top_hrefs = sorted(link.a["href"] for link in top_links)
assert top_hrefs == [
"#",
"package/index.html",
]
class TestMethod:
@pytest.fixture(autouse=True, scope="class")
def built(self, builder):
builder(
"pypackageexample",
warningiserror=True,
confoverrides={
"autoapi_own_page_level": "method",
"autoapi_options": [
"members",
"undoc-members",
"show-inheritance",
"imported-members",
],
},
)
def test_package(self, parse):
package_path = "_build/html/autoapi/package/index.html"
package_file = parse(package_path)
docstring = package_file.find("p")
assert docstring.text == "This is a docstring."
# There should be links to the children with their own page
subpackages = package_file.find(id="subpackages")
assert subpackages
assert subpackages.find("a", string="package.subpackage")
submodules = package_file.find(id="submodules")
assert submodules
assert submodules.find("a", string="package.submodule")
classes = package_file.find(id="classes")
assert classes
assert classes.find("a", title="package.Class")
exceptions = package_file.find(id="exceptions")
assert exceptions
assert exceptions.find("a", title="package.MyException")
functions = package_file.find(id="functions")
assert functions
assert functions.find("a", title="package.function")
# There should not be links to the children without their own page
assert not package_file.find(id="attributes")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
contents = package_file.find(id="package-contents")
assert contents.find(id="package.DATA")
assert not contents.find(id="package.MyException")
assert not contents.find(id="package.Class")
assert not contents.find(id="package.Class.class_var")
assert not contents.find(id="package.Class.NestedClass")
assert not contents.find(id="package.Class.method_okay")
assert not contents.find(id="package.Class.NestedClass")
assert not contents.find(id="package.Class.NestedClass.a_classmethod")
assert not contents.find(id="package.function")
assert not contents.find(id="package.aliased_function")
# Hidden children are never rendered.
assert not contents.find(id="package.not_in_all_function")
def test_module(self, parse):
submodule_path = "_build/html/autoapi/package/submodule/index.html"
submodule_file = parse(submodule_path)
docstring = submodule_file.find("p")
assert docstring.text == "Example module"
# There should be links to the children with their own page
exceptions = submodule_file.find(id="exceptions")
assert exceptions
assert exceptions.find("a", title="package.submodule.MyException")
classes = submodule_file.find(id="classes")
assert classes
assert classes.find("a", title="package.submodule.Class")
assert not classes.find("a", title="package.submodule.Class.NestedClass")
functions = submodule_file.find(id="functions")
assert functions
assert functions.find("a", title="package.submodule.function")
# There should not be links to the children without their own page
assert not submodule_file.find(id="submodules")
assert not submodule_file.find(id="subpackages")
assert not submodule_file.find(id="attributes")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
contents = submodule_file.find(id="module-contents")
assert contents.find(id="package.submodule.DATA")
assert not contents.find(id="package.submodule.MyException")
assert not contents.find(id="package.submodule.Class")
assert not contents.find(id="package.submodule.Class.class_var")
assert not contents.find(id="package.submodule.Class.NestedClass")
assert not contents.find(id="package.submodule.Class.method_okay")
assert not contents.find(id="package.submodule.Class.NestedClass")
assert not contents.find(id="package.submodule.Class.NestedClass.a_classmethod")
assert not contents.find(id="package.submodule.function")
assert not contents.find(id="package.submodule.aliased_function")
# Hidden children are never rendered.
assert not contents.find(id="package.submodule.not_in_all_function")
def test_class(self, parse):
class_path = "_build/html/autoapi/package/Class.html"
class_file = parse(class_path)
class_sig = class_file.find(id="package.Class")
assert class_sig
class_ = class_sig.parent
docstring = class_.find_all("p")[1]
assert docstring.text == "This is a class."
# There should be links to the children with their own page
classes = class_file.find(id="classes")
assert classes
assert classes.find("a", title="package.Class.NestedClass")
methods = class_file.find(id="methods")
assert methods
assert methods.find("a", title="package.Class.method_okay")
# There should not be links to the children without their own page
assert not class_file.find(id="attributes")
assert not class_file.find(id="exceptions")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
assert class_.find(id="package.Class.class_var")
assert not class_.find(id="package.Class.method_okay")
def test_function(self, parse):
function_path = "_build/html/autoapi/package/function.html"
function_file = parse(function_path)
function_sig = function_file.find(id="package.function")
assert function_sig
function_path = "_build/html/autoapi/package/submodule/function.html"
function_file = parse(function_path)
assert function_file.find(id="package.submodule.function")
def test_method(self, parse):
method_path = "_build/html/autoapi/package/Class.method_okay.html"
method_file = parse(method_path)
method_sig = method_file.find(id="package.Class.method_okay")
assert method_sig
method_path = "_build/html/autoapi/package/submodule/Class.method_okay.html"
method_file = parse(method_path)
assert method_file.find(id="package.submodule.Class.method_okay")
def test_rendered_only_expected_pages(self):
_, dirs, files = next(os.walk("_build/html/autoapi/package"))
assert sorted(dirs) == ["submodule", "subpackage"]
assert sorted(files) == [
"Class.NestedClass.a_classmethod.html",
"Class.NestedClass.html",
"Class.html",
"Class.method_okay.html",
"MyException.html",
"aliased_function.html",
"function.html",
"index.html",
]
_, dirs, files = next(os.walk("_build/html/autoapi/package/submodule"))
assert not dirs
assert sorted(files) == [
"Class.NestedClass.a_classmethod.html",
"Class.NestedClass.html",
"Class.html",
"Class.method_google_docs.html",
"Class.method_multiline.html",
"Class.method_okay.html",
"Class.method_sphinx_docs.html",
"Class.method_tricky.html",
"MyException.html",
"aliased_function.html",
"function.html",
"index.html",
]
_, dirs, files = next(os.walk("_build/html/autoapi/package/subpackage"))
assert dirs == ["submodule"]
assert sorted(files) == ["aliased_function.html", "function.html", "index.html"]
_, dirs, files = next(
os.walk("_build/html/autoapi/package/subpackage/submodule")
)
assert not dirs
assert sorted(files) == [
"Class.NestedClass.a_classmethod.html",
"Class.NestedClass.html",
"Class.html",
"Class.method_okay.html",
"MyException.html",
"function.html",
"index.html",
"not_in_all_function.html",
]
def test_index(self, parse):
index_path = "_build/html/autoapi/index.html"
index_file = parse(index_path)
top_links = index_file.find_all(class_="toctree-l1")
top_hrefs = sorted(link.a["href"] for link in top_links)
assert top_hrefs == [
"#",
"package/index.html",
]
class TestAttribute:
@pytest.fixture(autouse=True, scope="class")
def built(self, builder):
builder(
"pypackageexample",
warningiserror=True,
confoverrides={
"autoapi_own_page_level": "attribute",
"autoapi_options": [
"members",
"undoc-members",
"show-inheritance",
"imported-members",
],
},
)
# TODO: Include a test for a property
def test_package(self, parse):
package_path = "_build/html/autoapi/package/index.html"
package_file = parse(package_path)
docstring = package_file.find("p")
assert docstring.text == "This is a docstring."
# There should be links to the children with their own page
subpackages = package_file.find(id="subpackages")
assert subpackages
assert subpackages.find("a", string="package.subpackage")
submodules = package_file.find(id="submodules")
assert submodules
assert submodules.find("a", string="package.submodule")
classes = package_file.find(id="classes")
assert classes
assert classes.find("a", title="package.Class")
exceptions = package_file.find(id="exceptions")
assert exceptions
assert exceptions.find("a", title="package.MyException")
functions = package_file.find(id="functions")
assert functions
assert functions.find("a", title="package.function")
attributes = package_file.find(id="attributes")
assert attributes
assert attributes.find("a", title="package.DATA")
# There should not be links to the children without their own page
pass # there are no children without their own page
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
# Hidden children are never rendered.
assert not package_file.find(id="package-contents")
def test_module(self, parse):
submodule_path = "_build/html/autoapi/package/submodule/index.html"
submodule_file = parse(submodule_path)
docstring = submodule_file.find("p")
assert docstring.text == "Example module"
# There should be links to the children with their own page
exceptions = submodule_file.find(id="exceptions")
assert exceptions
assert exceptions.find("a", title="package.submodule.MyException")
classes = submodule_file.find(id="classes")
assert classes
assert classes.find("a", title="package.submodule.Class")
assert not classes.find("a", title="package.submodule.Class.NestedClass")
functions = submodule_file.find(id="functions")
assert functions
assert functions.find("a", title="package.submodule.function")
attributes = submodule_file.find(id="attributes")
assert attributes
assert attributes.find("a", title="package.submodule.DATA")
# There should not be links to the children without their own page
assert not submodule_file.find(id="submodules")
assert not submodule_file.find(id="subpackages")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
# Hidden children are never rendered.
assert not submodule_file.find(id="module-contents")
def test_class(self, parse):
class_path = "_build/html/autoapi/package/Class.html"
class_file = parse(class_path)
class_sig = class_file.find(id="package.Class")
assert class_sig
class_ = class_sig.parent
docstring = class_.find_all("p")[1]
assert docstring.text == "This is a class."
# There should be links to the children with their own page
classes = class_file.find(id="classes")
assert classes
assert classes.find("a", title="package.Class.NestedClass")
methods = class_file.find(id="methods")
assert methods
assert methods.find("a", title="package.Class.method_okay")
attributes = class_file.find(id="attributes")
assert attributes
assert attributes.find("a", title="package.Class.class_var")
# There should not be links to the children without their own page
assert not class_file.find(id="exceptions")
# Children without their own page should be rendered on this page,
# and children with their own page should not be rendered on this page.
assert not class_.find(id="package.Class.class_var")
assert not class_.find(id="package.Class.method_okay")
def test_function(self, parse):
function_path = "_build/html/autoapi/package/function.html"
function_file = parse(function_path)
function_sig = function_file.find(id="package.function")
assert function_sig
function_path = "_build/html/autoapi/package/submodule/function.html"
function_file = parse(function_path)
assert function_file.find(id="package.submodule.function")
def test_method(self, parse):
method_path = "_build/html/autoapi/package/Class.method_okay.html"
method_file = parse(method_path)
method_sig = method_file.find(id="package.Class.method_okay")
assert method_sig
method_path = "_build/html/autoapi/package/submodule/Class.method_okay.html"
method_file = parse(method_path)
assert method_file.find(id="package.submodule.Class.method_okay")
def test_data(self, parse):
data_path = "_build/html/autoapi/package/DATA.html"
data_file = parse(data_path)
data_sig = data_file.find(id="package.DATA")
assert data_sig
def test_attribute(self, parse):
attribute_path = "_build/html/autoapi/package/Class.class_var.html"
attribute_file = parse(attribute_path)
attribute_sig = attribute_file.find(id="package.Class.class_var")
assert attribute_sig
def test_rendered_only_expected_pages(self):
_, dirs, files = next(os.walk("_build/html/autoapi/package"))
assert sorted(dirs) == ["submodule", "subpackage"]
assert sorted(files) == [
"Class.NestedClass.a_classmethod.html",
"Class.NestedClass.html",
"Class.class_var.html",
"Class.html",
"Class.method_okay.html",
"DATA.html",
"MyException.html",
"aliased_function.html",
"function.html",
"index.html",
]
_, dirs, files = next(os.walk("_build/html/autoapi/package/submodule"))
assert not dirs
assert sorted(files) == [
"Class.NestedClass.a_classmethod.html",
"Class.NestedClass.html",
"Class.class_var.html",
"Class.html",
"Class.method_google_docs.html",
"Class.method_multiline.html",
"Class.method_okay.html",
"Class.method_sphinx_docs.html",
"Class.method_tricky.html",
"DATA.html",
"MyException.html",
"aliased_function.html",
"function.html",
"index.html",
]
_, dirs, files = next(os.walk("_build/html/autoapi/package/subpackage"))
assert dirs == ["submodule"]
assert sorted(files) == ["aliased_function.html", "function.html", "index.html"]
_, dirs, files = next(
os.walk("_build/html/autoapi/package/subpackage/submodule")
)
assert not dirs
assert sorted(files) == [
"Class.NestedClass.a_classmethod.html",
"Class.NestedClass.html",
"Class.class_var.html",
"Class.html",
"Class.method_okay.html",
"DATA.html",
"MyException.html",
"function.html",
"index.html",
"not_in_all_function.html",
]
def test_index(self, parse):
index_path = "_build/html/autoapi/index.html"
index_file = parse(index_path)
top_links = index_file.find_all(class_="toctree-l1")
top_hrefs = sorted(link.a["href"] for link in top_links)
assert top_hrefs == [
"#",
"package/index.html",
]
@pytest.mark.parametrize(
"value", ["package", "exception", "property", "data", "not_a_value"]
)
def test_invalid_values(builder, value):
"""Test failure when autoapi_own_page_level is invalid."""
with pytest.raises(ValueError):
builder(
"pypackageexample",
confoverrides={
"autoapi_own_page_level": value,
},
)

@ -1,15 +1,8 @@
# coding=utf8
"""Test Python parser"""
from io import StringIO
import sys
from textwrap import dedent
import astroid
import pytest
from autoapi.mappers.python.parser import Parser
from autoapi._parser import Parser
class TestPythonParser:
@ -32,7 +25,7 @@ class TestPythonParser:
"""
data = self.parse(source)[0]
assert data["name"] == "__all__"
assert data["value"] == ["Foo", 5.0]
assert data["value"] == "['Foo', 5.0]"
def test_parses_all_multiline(self):
source = """
@ -42,7 +35,7 @@ class TestPythonParser:
]
"""
data = self.parse(source)[0]
assert data["value"] == ["foo", "bar"]
assert data["value"] == "['foo', 'bar']"
def test_parses_name(self):
source = "foo.bar"
@ -50,7 +43,7 @@ class TestPythonParser:
def test_parses_list(self):
name = "__all__"
value = [1, 2, 3, 4]
value = "[1, 2, 3, 4]"
source = "{} = {}".format(name, value)
data = self.parse(source)[0]
assert data["name"] == name
@ -58,7 +51,7 @@ class TestPythonParser:
def test_parses_nested_list(self):
name = "__all__"
value = [[1, 2], [3, 4]]
value = "[[1, 2], [3, 4]]"
source = "{} = {}".format(name, value)
data = self.parse(source)[0]
assert data["name"] == name

@ -1,19 +1,17 @@
import io
import os
import pathlib
import shutil
import sys
from unittest.mock import Mock, call
import autoapi.settings
from autoapi.mappers.python import (
from autoapi._objects import (
PythonClass,
PythonData,
PythonFunction,
PythonMethod,
PythonModule,
)
from bs4 import BeautifulSoup
from packaging import version
import pytest
import sphinx
@ -24,60 +22,6 @@ import sphinx.util.logging
sphinx_version = version.parse(sphinx.__version__).release
def rebuild(confdir=".", **kwargs):
app = Sphinx(
srcdir=".",
confdir=confdir,
outdir="_build/html",
doctreedir="_build/.doctrees",
buildername="html",
**kwargs,
)
app.build()
@pytest.fixture(scope="class")
def builder():
cwd = os.getcwd()
def build(test_dir, **kwargs):
if kwargs.get("warningiserror"):
# Add any warnings raised when using `Sphinx` more than once
# in a Python session.
confoverrides = kwargs.setdefault("confoverrides", {})
confoverrides.setdefault("suppress_warnings", [])
suppress = confoverrides["suppress_warnings"]
suppress.append("app.add_node")
suppress.append("app.add_directive")
suppress.append("app.add_role")
os.chdir("tests/python/{0}".format(test_dir))
rebuild(**kwargs)
yield build
try:
shutil.rmtree("_build")
if (pathlib.Path("autoapi") / "index.rst").exists():
shutil.rmtree("autoapi")
finally:
os.chdir(cwd)
@pytest.fixture(scope="class")
def parse():
cache = {}
def parser(path):
if path not in cache:
with io.open(path, encoding="utf8") as file_handle:
cache[path] = BeautifulSoup(file_handle, features="html.parser")
return cache[path]
yield parser
class TestSimpleModule:
@pytest.fixture(autouse=True, scope="class")
def built(self, builder):
@ -200,7 +144,7 @@ class TestSimpleModule:
def test_long_signature(self, parse):
example_file = parse("_build/html/autoapi/example/index.html")
summary_row = example_file.find_all(class_="autosummary")[1].find_all("tr")[-1]
summary_row = example_file.find_all(class_="autosummary")[-1].find_all("tr")[-1]
assert summary_row
cells = summary_row.find_all("td")
assert (
@ -643,6 +587,34 @@ class TestPipeUnionModule:
assert links[1].text == "None"
@pytest.mark.skipif(
sys.version_info < (3, 12), reason="PEP-695 support requires Python >=3.12"
)
class TestPEP695:
@pytest.fixture(autouse=True, scope="class")
def built(self, builder):
builder("pep695", warningiserror=True)
def test_integration(self, parse):
example_file = parse("_build/html/autoapi/example/index.html")
alias = example_file.find(id="example.MyTypeAliasA")
properties = alias.find_all(class_="property")
assert len(properties) == 2
annotation = properties[0].text
assert annotation == ": TypeAlias"
value = properties[1].text
assert value == " = tuple[str, int]"
alias = example_file.find(id="example.MyTypeAliasB")
properties = alias.find_all(class_="property")
assert len(properties) == 2
annotation = properties[0].text
assert annotation == ": TypeAlias"
value = properties[1].text
assert value == " = tuple[str, int]"
def test_napoleon_integration_loaded(builder, parse):
confoverrides = {
"exclude_patterns": ["manualapi.rst"],
@ -670,26 +642,26 @@ class TestSimplePackage:
builder("pypackageexample", warningiserror=True)
def test_integration_with_package(self, parse):
example_file = parse("_build/html/autoapi/example/index.html")
example_file = parse("_build/html/autoapi/package/index.html")
entries = example_file.find_all(class_="toctree-l1")
assert any(entry.text == "example.foo" for entry in entries)
assert example_file.find(id="example.module_level_method")
assert any(entry.text == "package.submodule" for entry in entries)
assert example_file.find(id="package.function")
example_foo_file = parse("_build/html/autoapi/example/foo/index.html")
example_foo_file = parse("_build/html/autoapi/package/submodule/index.html")
foo = example_foo_file.find(id="example.foo.Foo")
assert foo
method_okay = foo.parent.find(id="example.foo.Foo.method_okay")
submodule = example_foo_file.find(id="package.submodule.Class")
assert submodule
method_okay = submodule.parent.find(id="package.submodule.Class.method_okay")
assert method_okay
index_file = parse("_build/html/index.html")
toctree = index_file.select("li > a")
assert any(item.text == "API Reference" for item in toctree)
assert any(item.text == "example.foo" for item in toctree)
assert any(item.text == "Foo" for item in toctree)
assert any(item.text == "module_level_method()" for item in toctree)
assert any(item.text == "package.submodule" for item in toctree)
assert any(item.text == "Class" for item in toctree)
assert any(item.text == "function()" for item in toctree)
def test_simple_no_false_warnings(builder, caplog):
@ -738,14 +710,14 @@ def test_hiding_private_members(builder, parse):
confoverrides = {"autoapi_options": ["members", "undoc-members", "special-members"]}
builder("pypackageexample", warningiserror=True, confoverrides=confoverrides)
example_file = parse("_build/html/autoapi/example/index.html")
example_file = parse("_build/html/autoapi/package/index.html")
entries = example_file.find_all(class_="toctree-l1")
assert all("private" not in entry.text for entry in entries)
private_file = parse("_build/html/autoapi/example/_private_module/index.html")
assert private_file.find(id="example._private_module.PrivateClass.public_method")
assert not pathlib.Path(
"_build/html/autoapi/package/_private_module/index.html"
).exists()
def test_hiding_inheritance(builder, parse):
@ -977,7 +949,7 @@ class TestComplexPackage:
assert wildcard_file.find(id="complex.wildcard.public_chain")
assert wildcard_file.find(id="complex.wildcard.now_public_function")
assert wildcard_file.find(id="complex.wildcard.public_multiple_imports")
assert wildcard_file.find(id="complex.wildcard.module_level_method")
assert wildcard_file.find(id="complex.wildcard.module_level_function")
def test_wildcard_all_imports(self, parse):
wildcard_file = parse("_build/html/autoapi/complex/wildall/index.html")
@ -988,12 +960,12 @@ class TestComplexPackage:
assert wildcard_file.find(id="complex.wildall.SimpleClass")
assert wildcard_file.find(id="complex.wildall.simple_function")
assert wildcard_file.find(id="complex.wildall.public_chain")
assert wildcard_file.find(id="complex.wildall.module_level_method")
assert wildcard_file.find(id="complex.wildall.module_level_function")
def test_no_imports_in_module_with_all(self, parse):
foo_file = parse("_build/html/autoapi/complex/foo/index.html")
assert not foo_file.find(id="complex.foo.module_level_method")
assert not foo_file.find(id="complex.foo.module_level_function")
def test_all_overrides_import_in_module_with_all(self, parse):
foo_file = parse("_build/html/autoapi/complex/foo/index.html")
@ -1019,7 +991,7 @@ class TestComplexPackageParallel(TestComplexPackage):
builder("pypackagecomplex", parallel=2)
def test_caching(builder):
def test_caching(builder, rebuild):
mtimes = (0, 0)
def record_mtime():
@ -1125,25 +1097,25 @@ def test_string_module_attributes(builder):
".. py:data:: code_snippet",
" :value: Multiline-String",
"",
" .. raw:: html",
" .. raw:: html",
"",
" <details><summary>Show Value</summary>",
" <details><summary>Show Value</summary>",
"",
" .. code-block:: python",
" .. code-block:: python",
"",
' """The following is some code:',
" ", # <--- Line array monstrosity to preserve these leading spaces
" # -*- coding: utf-8 -*-",
" from __future__ import absolute_import, division, print_function, unicode_literals",
" # from future.builtins.disabled import *",
" # from builtins import *",
" ",
""" print("chunky o'block")""",
' """',
' """The following is some code:',
" ", # <--- Line array monstrosity to preserve these leading spaces
" # -*- coding: utf-8 -*-",
" from __future__ import absolute_import, division, print_function, unicode_literals",
" # from future.builtins.disabled import *",
" # from builtins import *",
" ",
""" print("chunky o'block")""",
' """',
"",
" .. raw:: html",
" .. raw:: html",
"",
" </details>",
" </details>",
]
assert "\n".join(code_snippet_contents) in example_file

@ -1,7 +1,8 @@
import sys
import astroid
from autoapi.mappers.python import astroid_utils, objects
from autoapi import _astroid_utils
from autoapi import _objects
import pytest
@ -70,7 +71,7 @@ class TestAstroidUtils:
import_, basename
)
node = astroid.extract_node(source)
basenames = astroid_utils.resolve_qualname(node.bases[0], node.basenames[0])
basenames = _astroid_utils.resolve_qualname(node.bases[0], node.basenames[0])
assert basenames == expected
@pytest.mark.parametrize(
@ -85,21 +86,28 @@ class TestAstroidUtils:
import_, basename
)
node = astroid.extract_node(source)
basenames = astroid_utils.resolve_qualname(node.bases[0], node.basenames[0])
basenames = _astroid_utils.resolve_qualname(node.bases[0], node.basenames[0])
assert basenames == expected
@pytest.mark.parametrize(
("source", "expected"),
[
('a = "a"', ("a", "a")),
("a = 1", ("a", 1)),
('a = "a"', ("a", "'a'")),
("a = 1", ("a", "1")),
("a, b, c = (1, 2, 3)", None),
("a = b = 1", None),
("a = [1, 2, [3, 4]]", ("a", "[1, 2, [3, 4]]")),
("a = [1, 2, variable[subscript]]", ("a", None)),
('a = """multiline\nstring"""', ("a", '"""multiline\nstring"""')),
('a = ["""multiline\nstring"""]', ("a", None)),
("a = (1, 2, 3)", ("a", "(1, 2, 3)")),
("a = (1, 'two', 3)", ("a", "(1, 'two', 3)")),
("a = None", ("a", "None")),
],
)
def test_can_get_assign_values(self, source, expected):
node = astroid.extract_node(source)
value = astroid_utils.get_assign_value(node)
value = _astroid_utils.get_assign_value(node)
assert value == expected
@pytest.mark.parametrize(
@ -160,7 +168,7 @@ class TestAstroidUtils:
)
)
annotations = astroid_utils.get_args_info(node.args)
annotations = _astroid_utils.get_args_info(node.args)
assert annotations == expected
def test_parse_split_type_comments(self):
@ -174,7 +182,7 @@ class TestAstroidUtils:
"""
)
annotations = astroid_utils.get_args_info(node.args)
annotations = _astroid_utils.get_args_info(node.args)
expected = [
(None, "a", "int", None),
@ -209,6 +217,9 @@ class TestAstroidUtils:
),
("a: int, *args, b: str, **kwargs", "a: int, *args, b: str, **kwargs"),
("a: 'A'", "a: A"),
("a: Literal[1]", "a: Literal[1]"),
("a: Literal['x']", "a: Literal['x']"),
("a: Literal['x', 'y', 'z']", "a: Literal['x', 'y', 'z']"),
],
)
def test_format_args(self, signature, expected):
@ -221,6 +232,6 @@ class TestAstroidUtils:
)
)
args_info = astroid_utils.get_args_info(node.args)
formatted = objects._format_args(args_info)
args_info = _astroid_utils.get_args_info(node.args)
formatted = _objects._format_args(args_info)
assert formatted == expected

@ -25,7 +25,7 @@ commands =
pytest {posargs}
[testenv:formatting]
basepython = python3
basepython = python312
skip_install = true
deps =
black

Loading…
Cancel
Save