Compare commits

...

9 Commits

Author SHA1 Message Date
Ashley Whetter
9e19e4af60 Version 3.1.0a4 2024-03-24 15:24:17 -07:00
Ashley Whetter
5c0f650913 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.
2024-03-24 15:20:37 -07:00
Jorge Martinez
d5807f4dee Recursive rendering of children with their own page 2024-03-24 15:20:37 -07:00
Ashley Whetter
35be997aaa Basic tests for single page rendering 2024-03-24 15:20:37 -07:00
Jorge Martinez Garrido
fcb0320776 Initial implementation of customisable single page output 2024-03-24 15:20:37 -07:00
Ashley Whetter
c4db7eb14a Fix IndexError when a module docstring contains only a heading 2024-02-18 21:17:06 -08:00
Ashley Whetter
422004ea91 Made links in the README less confusing 2024-02-18 16:28:37 -08:00
laggykiller
823c146b3a Correct handling of __init__.pyi
Closes #405
2024-02-18 16:01:48 -08:00
Ashley Whetter
bbb50f68ae Reformatted to latest stable black style 2024-02-18 15:57:31 -08:00
48 changed files with 1776 additions and 434 deletions

View File

@ -5,6 +5,27 @@ Versions follow `Semantic Versioning <https://semver.org/>`_ (``<major>.<minor>.
.. towncrier release notes start
v3.1.0a4 (2024-03-24)
---------------------
Bugfixes
^^^^^^^^
- Children not present in `__all__` are not rendered. (#226)
- Fix emitting ignore event twice for methods. (#226)
- Corrected documentation around `imported-members` to reflect that it
applies only to objects imported into a package, not modules. (#226)
- Fix path error on Windows. (#226)
- Fix submodule with `__init__.pyi` documented as `__init__` instead of submodule name (#398)
- Fix IndexError when a module docstring contains only a heading (#412)
Misc
^^^^
- #388
v3.0.0 (2023-09-26)
-------------------

View File

@ -28,13 +28,14 @@ In contrast to the traditional `Sphinx autodoc <https://www.sphinx-doc.org/en/ma
which requires manual authoring and uses code imports,
AutoAPI finds and generates documentation by parsing source code.
For more information, see `the full documentation <https://sphinx-autoapi.readthedocs.org>`_.
Getting Started
---------------
The following steps will walk through how to add AutoAPI to an existing Sphinx project.
For instructions on how to set up a Sphinx project,
see Sphinx's documentation on
`Getting Started <https://www.sphinx-doc.org/en/master/usage/quickstart.html>`_.
see `Sphinx's documentation <https://www.sphinx-doc.org/en/master/usage/quickstart.html>`_.
Installation
~~~~~~~~~~~~

View File

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

View File

@ -41,7 +41,6 @@ class AutoapiSummary(Autosummary):
class NestedParse(Directive):
"""Nested parsing to remove the first heading of included rST
This is used to handle the case where we like to remove user supplied
@ -49,7 +48,7 @@ class NestedParse(Directive):
duplicate headings on sections.
"""
has_content = 1
has_content = True
required_arguments = 0
optional_arguments = 0
final_argument_whitespace = False
@ -59,9 +58,10 @@ class NestedParse(Directive):
node.document = self.state.document
nested_parse_with_titles(self.state, self.content, node)
try:
title_node = node[0][0]
if isinstance(title_node, nodes.title):
del node[0][0]
if isinstance(node[0], nodes.section) and isinstance(
node[0][0], nodes.title
):
node.children = node[0][1:] + node.children[1:]
except IndexError:
pass
return node.children

View File

@ -2,6 +2,7 @@
This extension allows you to automagically generate API documentation from your project.
"""
import io
import os
import shutil
@ -34,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."""
@ -74,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,7 +112,7 @@ def run_autoapi(app):
RemovedInAutoAPI3Warning,
)
sphinx_mapper_obj = PythonSphinxMapper(
app, template_dir=template_dir, url_root=url_root
app, template_dir=template_dir, dir_root=normalized_root, url_root=url_root
)
if app.config.autoapi_file_patterns:
@ -127,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):
@ -270,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)

View File

@ -1,9 +1,9 @@
import os
import fnmatch
from collections import OrderedDict, namedtuple
import fnmatch
import os
import pathlib
import re
import anyascii
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
import sphinx
import sphinx.util
@ -12,29 +12,35 @@ from sphinx.util.display import status_iterator
from sphinx.util.osutil import ensuredir
import sphinx.util.logging
from ..settings import API_ROOT, TEMPLATE_DIR
from ..settings import TEMPLATE_DIR
LOGGER = sphinx.util.logging.getLogger(__name__)
_OWN_PAGE_LEVELS = [
"package",
"module",
"exception",
"class",
"function",
"method",
"property",
"data",
"attribute",
]
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.
@ -44,25 +50,21 @@ class PythonMapperBase:
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):
def __init__(self, obj, jinja_env, app, url_root, 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.url_root = url_root
self.name = None
self.qual_name = None
self.id = None
def __getstate__(self):
@ -90,10 +92,15 @@ class PythonMapperBase:
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,
}
@ -111,28 +118,19 @@ class PythonMapperBase:
"""Shorten name property"""
return self.name.split(".")[-1]
@property
def pathname(self):
"""Sluggified path for filenames
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)
Slugs to a filename using the follow steps
def output_filename(self):
"""The name of the file to render into, without a file suffix."""
filename = self.qual_name
if filename == "index":
filename = ".index"
* 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)
return filename
@property
def include_path(self):
@ -141,9 +139,7 @@ class PythonMapperBase:
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)
return str(self.output_dir(self.url_root) / self.output_filename())
@property
def display(self):
@ -163,14 +159,13 @@ class PythonMapperBase:
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):
def __init__(self, app, template_dir=None, dir_root=None, url_root=None):
self.app = app
template_paths = [TEMPLATE_DIR]
@ -192,18 +187,21 @@ class SphinxMapperBase:
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 = OrderedDict()
# Mapping of {object id -> Python Object}
self.objects = OrderedDict()
self.objects_to_render = 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."""
@ -281,12 +279,17 @@ class SphinxMapperBase:
Args:
obj: Instance of a AutoAPI object
"""
self.objects[obj.id] = obj
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 map(self, options=None):
@ -308,31 +311,32 @@ class SphinxMapperBase:
"""
raise NotImplementedError
def output_rst(self, root, source_suffix):
def output_rst(self, source_suffix):
for _, obj in status_iterator(
self.objects.items(),
self.objects_to_render.items(),
colorize("bold", "[AutoAPI] ") + "Rendering Data... ",
length=len(self.objects),
length=len(self.objects_to_render),
verbosity=1,
stringify_func=(lambda x: x[0]),
):
rst = obj.render()
rst = obj.render(is_own_page=True)
if not rst:
continue
detail_dir = obj.include_dir(root=root)
ensuredir(detail_dir)
path = os.path.join(detail_dir, f"index{source_suffix}")
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(root)
self._output_top_rst()
def _output_top_rst(self, root):
def _output_top_rst(self):
# Render Top Index
top_level_index = os.path.join(root, "index.rst")
pages = self.objects.values()
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"))

View File

@ -24,6 +24,7 @@ from .objects import (
PythonAttribute,
PythonData,
PythonException,
TopLevelPythonPythonMapper,
)
LOGGER = sphinx.util.logging.getLogger(__name__)
@ -61,12 +62,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 +169,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"]
@ -216,8 +220,7 @@ def _link_objs(value):
class PythonSphinxMapper(SphinxMapperBase):
"""Auto API domain handler for Python
"""AutoAPI domain handler for Python
Parses directly from Python files.
@ -240,8 +243,8 @@ 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):
super().__init__(app, template_dir, dir_root, url_root)
self.jinja_env.filters["link_objs"] = _link_objs
self._use_implicit_namespace = (
@ -269,6 +272,7 @@ class PythonSphinxMapper(SphinxMapperBase):
dir_root = dir_
if (
os.path.exists(os.path.join(dir_, "__init__.py"))
or os.path.exists(os.path.join(dir_, "__init__.pyi"))
or self._use_implicit_namespace
):
dir_root = os.path.abspath(os.path.join(dir_, os.pardir))
@ -339,25 +343,44 @@ 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():
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):
@ -378,9 +401,9 @@ class PythonSphinxMapper(SphinxMapperBase):
options=self.app.config.autoapi_options,
jinja_env=self.jinja_env,
app=self.app,
url_root=self.url_root,
**kwargs,
)
obj.url_root = self.url_root
for child_data in data.get("children", []):
for child_obj in self.create_class(

View File

@ -1,4 +1,5 @@
import functools
import pathlib
from typing import List, Optional
import sphinx.util.logging
@ -38,11 +39,13 @@ class PythonPythonMapper(PythonMapperBase):
language = "python"
is_callable = False
member_order = 0
type: str
def __init__(self, obj, class_content="class", **kwargs) -> None:
super().__init__(obj, **kwargs)
self.name = obj["name"]
self.qual_name = obj["qual_name"]
self.id = obj.get("full_name", self.name)
# Optional
@ -55,6 +58,7 @@ class PythonPythonMapper(PythonMapperBase):
:type: bool
"""
self._hide = obj.get("hide", False)
# For later
self._class_content = class_content
@ -80,6 +84,16 @@ class PythonPythonMapper(PythonMapperBase):
self._docstring = value
self._docstring_resolved = True
@property
def is_top_level_object(self):
"""Whether this object is at the very top level (True) or not (False).
This will be False for subpackages and submodules.
:type: bool
"""
return "." not in self.id
@property
def is_undoc_member(self):
"""Whether this object has a docstring (False) or not (True).
@ -143,12 +157,17 @@ 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
@ -228,11 +247,10 @@ class PythonMethod(PythonFunction):
"""
def _should_skip(self): # type: () -> bool
skip = super()._should_skip() or self.name in (
return super()._should_skip() or self.name in (
"__new__",
"__init__",
)
return self._ask_ignore(skip)
class PythonProperty(PythonPythonMapper):
@ -299,14 +317,6 @@ class TopLevelPythonPythonMapper(PythonPythonMapper):
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
"""
self.subpackages = []
self.submodules = []
self.all = obj["all"]
@ -334,6 +344,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."""

View File

@ -15,16 +15,20 @@ 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)
module_parts = []
if filename != "__init__.py":
if filename != "__init__.py" and filename != "__init__.pyi":
module_part = os.path.splitext(filename)[0]
module_parts = [module_part]
module_parts = collections.deque(module_parts)
@ -40,7 +44,10 @@ class Parser:
def parse_file(self, file_path):
return self._parse_file(
file_path,
lambda directory: os.path.isfile(os.path.join(directory, "__init__.py")),
lambda directory: (
os.path.isfile(os.path.join(directory, "__init__.py"))
or os.path.isfile(os.path.join(directory, "__init__.pyi"))
),
)
def parse_file_in_namespace(self, file_path, dir_root):
@ -88,6 +95,7 @@ class Parser:
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,
@ -108,6 +116,7 @@ class Parser:
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)),
@ -116,7 +125,8 @@ class Parser:
"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()):
@ -145,7 +155,8 @@ class Parser:
overridden.update(seen)
self._name_stack.pop()
self._qual_name_stack.pop()
self._full_name_stack.pop()
return [data]
@ -182,6 +193,7 @@ 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)),
@ -206,14 +218,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,
}
@ -230,12 +247,13 @@ 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": [],

View File

@ -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>`_

View File

@ -1,60 +1,104 @@
{% if obj.display %}
.. py:{{ obj.type }}:: {{ obj.short_name }}{% if obj.args %}({{ obj.args }}){% endif %}
{% if is_own_page %}
:class:`{{ obj.id }}`
========={{ "=" * obj.id | length }}
{% for (args, return_annotation) in obj.overloads %}
{% 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 %}
{% 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 %}
{% 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 %}
{% 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 %}
:private-bases:
{% endif %}
{% endif %}
{% 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 %}
:private-bases:
{% 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) }}
{% 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_properties = obj.properties|selectattr("display")|list %}
{% else %}
{% set visible_properties = obj.properties|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 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) }}
{% 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 %}
{% endif %}
{% for method in visible_methods %}
{{ method.render()|indent(3) }}
{% endfor %}
{% endif %}

View File

@ -1,37 +1,42 @@
{% if obj.display %}
.. py:{{ obj.type }}:: {{ obj.name }}
{%- if obj.annotation is not none %}
{% if is_own_page %}
:py:{{ obj.type|truncate(4, True, "", 0) }}:`{{ 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 is string and 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 %}
{% if obj.value is string %}
:value: {{ "%r" % obj.value|string|truncate(100) }}
{% else %}
:value: {{ obj.value|string|truncate(100) }}
{% endif %}
{% endif %}
{% endif %}
{% if obj.docstring %}
{{ obj.docstring|indent(3) }}
{% endif %}
{% endif %}

View File

@ -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 %}
:py:func:`{{ 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 %}

View File

@ -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 %}
:py:meth:`{{ 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 %}

View File

@ -1,114 +1,170 @@
{% if not obj.display %}
:orphan:
{% endif %}
:py:mod:`{{ obj.name }}`
=========={{ "=" * obj.name|length }}
{% if obj.display %}
{% if is_own_page %}
:py:mod:`{{ 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
{% for subpackage in visible_subpackages %}
{{ subpackage.short_name }}/index.rst
{% endfor %}
{% 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 subpackage in visible_subpackages %}
{{ subpackage.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 %}
{% endif %}
{% endblock %}
{% block submodules %}
{% set visible_submodules = obj.submodules|selectattr("display")|list %}
{% if visible_submodules %}
Submodules
----------
.. toctree::
:maxdepth: 1
{% for submodule in visible_submodules %}
{{ submodule.include_path }}
{% endfor %}
{% 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
----------
{% if "attribute" in own_page_types %}
.. toctree::
:hidden:
{% for attribute in visible_attributes %}
{{ attribute.include_path }}
{% endfor %}
{% endif %}
.. autoapisummary::
{% for attribute in visible_attributes %}
{{ attribute.id }}
{% endfor %}
{% endif %}
{% 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
----------
{% if "exception" in own_page_types %}
.. toctree::
:hidden:
{% for exception in visible_exceptions %}
{{ exception.include_path }}
{% endfor %}
{% endif %}
.. autoapisummary::
{% for exception in visible_exceptions %}
{{ exception.id }}
{% endfor %}
{% endif %}
{% 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
-------
{% if "class" in own_page_types %}
.. toctree::
:hidden:
{% for klass in visible_classes %}
{{ klass.include_path }}
{% endfor %}
{% endif %}
.. autoapisummary::
{% for klass in visible_classes %}
{{ klass.id }}
{% endfor %}
{% endif %}
{% 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 }}---------
{% 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
~~~~~~~
.. autoapisummary::
{% for klass in visible_classes %}
{{ klass.id }}
{% endfor %}
{% endif %}
{% endblock %}
{% block functions scoped %}
{% if visible_functions %}
Functions
~~~~~~~~~
.. autoapisummary::
{% for function in visible_functions %}
{{ function.id }}
{% endfor %}
{% endif %}
{% endblock %}
{% block attributes scoped %}
{% if visible_attributes %}
Attributes
~~~~~~~~~~
.. autoapisummary::
{% for attribute in visible_attributes %}
{{ attribute.id }}
{% endfor %}
{% endif %}
{% endblock %}
{% endif %}
{% for obj_item in visible_children %}
{% 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 %}

View File

@ -1,13 +1,18 @@
{%- if obj.display %}
.. py:property:: {{ obj.short_name }}
{% if obj.display %}
{% if is_own_page %}
:py:property:`{{ 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) }}

View File

@ -0,0 +1 @@
Made links to the documention in the README less confusing.

View File

@ -0,0 +1 @@
Reformatted to latest stable black style.

1
docs/changes/398.bugfix Normal file
View File

@ -0,0 +1 @@
Fix submodule with `__init__.pyi` documented as `__init__` instead of submodule name

1
docs/changes/412.bugfix Normal file
View File

@ -0,0 +1 @@
Fix IndexError when a module docstring contains only a heading

View File

@ -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`.

View File

@ -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
~~~~~~

View File

@ -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

View File

@ -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.pydocstyle]
convention = "google"

View File

@ -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

68
tests/python/conftest.py Normal file
View File

@ -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

View File

@ -51,26 +51,21 @@ mixed_list: List[Union[str, int]] = [1, "two", 3]
"This is mixed"
def f2(not_yet_a: "A") -> int:
...
def f2(not_yet_a: "A") -> int: ...
def f3(imported: B) -> B:
...
def f3(imported: B) -> B: ...
class MyGeneric(Generic[T, U]):
...
class MyGeneric(Generic[T, U]): ...
@overload
def overloaded_func(a: float) -> float:
...
def overloaded_func(a: float) -> float: ...
@typing.overload
def overloaded_func(a: str) -> str:
...
def overloaded_func(a: str) -> str: ...
def overloaded_func(a: Union[float, str]) -> Union[float, str]:
@ -79,8 +74,7 @@ def overloaded_func(a: Union[float, str]) -> Union[float, str]:
@overload
def undoc_overloaded_func(a: str) -> str:
...
def undoc_overloaded_func(a: str) -> str: ...
def undoc_overloaded_func(a: str) -> str:
@ -112,33 +106,28 @@ class A:
return "method"
@overload
def overloaded_method(self, a: float) -> float:
...
def overloaded_method(self, a: float) -> float: ...
@typing.overload
def overloaded_method(self, a: str) -> str:
...
def overloaded_method(self, a: str) -> str: ...
def overloaded_method(self, a: Union[float, str]) -> Union[float, str]:
"""Overloaded method"""
return a * 2
@overload
def undoc_overloaded_method(self, a: float) -> float:
...
def undoc_overloaded_method(self, a: float) -> float: ...
def undoc_overloaded_method(self, a: float) -> float:
return a * 2
@typing.overload
@classmethod
def overloaded_class_method(cls, a: float) -> float:
...
def overloaded_class_method(cls, a: float) -> float: ...
@overload
@classmethod
def overloaded_class_method(cls, a: str) -> str:
...
def overloaded_class_method(cls, a: str) -> str: ...
@classmethod
def overloaded_class_method(cls, a: Union[float, str]) -> Union[float, str]:
@ -148,23 +137,18 @@ class A:
class C:
@overload
def __init__(self, a: int) -> None:
...
def __init__(self, a: int) -> None: ...
@typing.overload
def __init__(self, a: float) -> None:
...
def __init__(self, a: float) -> None: ...
def __init__(self, a: str):
...
def __init__(self, a: str): ...
class D(C):
class Da:
...
class Da: ...
class DB(Da):
...
class DB(Da): ...
...
@ -184,5 +168,4 @@ async def async_function(wait: bool) -> int:
global_a: A = A()
class SomeMetaclass(type):
...
class SomeMetaclass(type): ...

View File

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

View File

@ -26,12 +26,15 @@ class Foo(object):
def foo():
"""The foo class method"""
...
def __init__(self, attr):
"""Constructor docstring"""
...
def method_okay(self, foo=None, bar=None):
"""This method should parse okay"""
...
def method_multiline(self, foo=None, bar=None, baz=None):
"""This is on multiple lines, but should parse okay too
@ -39,4 +42,5 @@ class Foo(object):
definitions are covered in the way we're anticipating here
"""
...
def method_without_docstring(self): ...

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
templates_path = ["_templates"]
source_suffix = ".rst"
master_doc = "index"
project = "pyisubmoduleinit"
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 = "pyisubmoduleinitdoc"
extensions = ["sphinx.ext.autodoc", "autoapi.extension"]
autoapi_dirs = ["example"]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""Example __init__ in submodule foo
Documentation generated for this file
should be titled submodule_foo instead of __init__
This is a description
"""
class Foo(object):
"""
This is a description
"""
def bar(self, a: int) -> None:
"""
This is a description
"""
...

View File

@ -0,0 +1,15 @@
Welcome to pyisubmoduleinit's documentation!
============================================
.. toctree::
autoapi/index
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -1,7 +1,12 @@
"""
This heading will be removed
============================
"""
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

View File

@ -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"]

View File

@ -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

View File

@ -1,3 +1,11 @@
"""
This heading will be removed
============================
This docstring will not be removed.
"""
def public_chain():
"""Part of a public resolution chain."""
return 5

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -0,0 +1,32 @@
"""This is a docstring."""
from . import submodule
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."""

View File

@ -3,19 +3,25 @@
This is a description
"""
DATA = 42
class Foo(object):
class_var = 42 #: Class var docstring
another_class_var = 42
"""Another class var docstring"""
def function(foo, bar):
"""A module level function"""
class Meta(object):
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 +67,7 @@ class Foo(object):
int: The sum of foo and bar.
"""
return foo + bar
class MyException(Exception):
"""This is an exception."""

View File

@ -0,0 +1,5 @@
"""This is a docstring."""
def function(foo, bar):
"""A module level function"""

View File

@ -0,0 +1,33 @@
"""Example module
This is a description
"""
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."""

View File

@ -0,0 +1,950 @@
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")
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")
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")
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")
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")
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")
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")
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",
"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",
"function.html",
"index.html",
]
_, dirs, files = next(os.walk("_build/html/autoapi/package/subpackage"))
assert dirs == ["submodule"]
assert sorted(files) == ["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",
]
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")
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")
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",
"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",
"function.html",
"index.html",
]
_, dirs, files = next(os.walk("_build/html/autoapi/package/subpackage"))
assert dirs == ["submodule"]
assert sorted(files) == ["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",
]
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.
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.
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",
"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",
"function.html",
"index.html",
]
_, dirs, files = next(os.walk("_build/html/autoapi/package/subpackage"))
assert dirs == ["submodule"]
assert sorted(files) == ["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",
]
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,
},
)

View File

@ -1,7 +1,6 @@
import io
import os
import pathlib
import shutil
import sys
from unittest.mock import Mock, call
@ -13,7 +12,6 @@ from autoapi.mappers.python import (
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 (
@ -300,6 +244,20 @@ class TestSimpleStubModuleNotPreferred:
assert foo_sig
class TestStubInitModuleInSubmodule:
@pytest.fixture(autouse=True, scope="class")
def built(self, builder):
builder("pyisubmoduleinit", warningiserror=True)
def test_integration(self, parse):
example_file = parse("_build/html/autoapi/example/index.html")
# Documentation should list
# submodule_foo instead of __init__
assert example_file.find(title="submodule_foo")
assert not example_file.find(title="__init__")
class TestPy3Module:
@pytest.fixture(autouse=True, scope="class")
def built(self, builder):
@ -656,26 +614,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):
@ -724,14 +682,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):
@ -963,7 +921,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")
@ -974,12 +932,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")
@ -991,6 +949,13 @@ class TestComplexPackage:
assert foo_file.find(id="complex.unicode_data.unicode_str")
def test_nested_parse_directive(self, parse):
package_file = parse("_build/html/autoapi/complex/index.html")
complex = package_file.find(id="complex")
assert "This heading will be removed" not in complex.parent.text
assert complex.parent.find("section")["id"] != "this-heading-will-be-removed"
class TestComplexPackageParallel(TestComplexPackage):
@pytest.fixture(autouse=True, scope="class")
@ -998,7 +963,7 @@ class TestComplexPackageParallel(TestComplexPackage):
builder("pypackagecomplex", parallel=2)
def test_caching(builder):
def test_caching(builder, rebuild):
mtimes = (0, 0)
def record_mtime():
@ -1104,25 +1069,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