From a6558dcfc225e9ec840c6436da17e5e46e2709d5 Mon Sep 17 00:00:00 2001 From: Ashley Whetter Date: Sun, 21 Jan 2024 21:06:01 -0800 Subject: [PATCH] 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. --- autoapi/extension.py | 15 +- autoapi/mappers/base.py | 139 +-- autoapi/mappers/python/mapper.py | 33 +- autoapi/mappers/python/objects.py | 40 +- autoapi/mappers/python/parser.py | 35 +- autoapi/templates/index.rst | 4 +- autoapi/templates/python/class.rst | 151 ++- autoapi/templates/python/data.rst | 54 +- autoapi/templates/python/function.rst | 19 +- autoapi/templates/python/method.rst | 25 +- autoapi/templates/python/module.rst | 124 +-- autoapi/templates/python/property.rst | 16 +- docs/changes/+8cd14c4f.bugfix | 1 + docs/changes/226.feature | 1 + docs/how_to.rst | 2 +- docs/reference/config.rst | 23 +- pyproject.toml | 5 - setup.cfg | 1 - tests/python/conftest.py | 68 ++ .../pyannotationcommentsexample/conf.py | 1 + tests/python/pypackageexample/conf.py | 2 +- .../pypackageexample/example/__init__.py | 8 - .../pypackageexample/package/__init__.py | 42 + .../_private_submodule.py} | 0 .../{example/foo.py => package/submodule.py} | 35 +- .../package/subpackage/__init__.py | 13 + .../package/subpackage/submodule.py | 41 + tests/python/test_own_page_option.py | 997 ++++++++++++++++++ tests/python/test_pyintegration.py | 411 +------- 29 files changed, 1545 insertions(+), 761 deletions(-) create mode 100644 docs/changes/+8cd14c4f.bugfix create mode 100644 docs/changes/226.feature create mode 100644 tests/python/conftest.py delete mode 100644 tests/python/pypackageexample/example/__init__.py create mode 100644 tests/python/pypackageexample/package/__init__.py rename tests/python/pypackageexample/{example/_private_module.py => package/_private_submodule.py} (100%) rename tests/python/pypackageexample/{example/foo.py => package/submodule.py} (72%) create mode 100644 tests/python/pypackageexample/package/subpackage/__init__.py create mode 100644 tests/python/pypackageexample/package/subpackage/submodule.py create mode 100644 tests/python/test_own_page_option.py diff --git a/autoapi/extension.py b/autoapi/extension.py index fef422f..00a1ffd 100644 --- a/autoapi/extension.py +++ b/autoapi/extension.py @@ -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: @@ -101,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: @@ -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): diff --git a/autoapi/mappers/base.py b/autoapi/mappers/base.py index a7ca4db..e55413e 100644 --- a/autoapi/mappers/base.py +++ b/autoapi/mappers/base.py @@ -1,10 +1,9 @@ -import os -import fnmatch from collections import OrderedDict, namedtuple +import fnmatch +import os +import pathlib import re -import anyascii -from docutils.parsers.rst import convert_directive_function from jinja2 import Environment, FileSystemLoader, TemplateNotFound import sphinx import sphinx.util @@ -13,7 +12,7 @@ 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 = [ @@ -24,8 +23,8 @@ _OWN_PAGE_LEVELS = [ "function", "method", "property", - "attribute", "data", + "attribute", ] Path = namedtuple("Path", ["absolute", "relative"]) @@ -38,14 +37,10 @@ class PythonMapperBase: 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. @@ -55,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): @@ -103,7 +94,7 @@ class PythonMapperBase: 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]) + own_page_types = set(_OWN_PAGE_LEVELS[: desired_page_level + 1]) return { "autoapi_options": self.app.config.autoapi_options, @@ -127,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): @@ -157,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): @@ -185,7 +165,7 @@ class SphinxMapperBase: 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] @@ -209,8 +189,9 @@ class SphinxMapperBase: 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.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} @@ -298,7 +279,8 @@ class SphinxMapperBase: Args: obj: Instance of a AutoAPI object """ - if obj.type in self.own_page_types: + 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 @@ -306,6 +288,8 @@ class SphinxMapperBase: 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): @@ -327,51 +311,7 @@ class SphinxMapperBase: """ raise NotImplementedError - def output_child_rst(self, obj, obj_parent, detail_dir, source_suffix): - - if not obj.display: - return - - # Skip nested cases like functions in functions or clases in clases - if obj.type == obj_parent.type: - return - - obj_child_page_level = _OWN_PAGE_LEVELS.index(obj.type) - desired_page_level = _OWN_PAGE_LEVELS.index(self.app.config.autoapi_own_page_level) - is_own_page = obj_child_page_level <= desired_page_level - if not is_own_page: - return - - obj_child_rst = obj.render( - is_own_page=is_own_page, - ) - if not obj_child_rst: - return - - function_page_level = _OWN_PAGE_LEVELS.index("function") - is_level_beyond_function = function_page_level < desired_page_level - if obj.type in ["exception", "class"]: - if not is_level_beyond_function: - outfile = f"{obj.short_name}{source_suffix}" - path = os.path.join(detail_dir, outfile) - else: - outdir = os.path.join(detail_dir, obj.short_name) - ensuredir(outdir) - path = os.path.join(outdir, f"index{source_suffix}") - else: - is_parent_in_detail_dir = detail_dir.endswith(obj_parent.short_name) - outdir = detail_dir if is_parent_in_detail_dir else os.path.join(detail_dir, obj_parent.short_name) - ensuredir(outdir) - path = os.path.join(outdir, f"{obj.short_name}{source_suffix}") - - with open(path, "wb+") as obj_child_detail_file: - obj_child_detail_file.write(obj_child_rst.encode("utf-8")) - - for obj_child in obj.children: - child_detail_dir = os.path.join(detail_dir, obj.name) - self.output_child_rst(obj_child, obj, child_detail_dir, source_suffix) - - def output_rst(self, root, source_suffix): + def output_rst(self, source_suffix): for _, obj in status_iterator( self.objects_to_render.items(), colorize("bold", "[AutoAPI] ") + "Rendering Data... ", @@ -379,29 +319,24 @@ class SphinxMapperBase: verbosity=1, stringify_func=(lambda x: x[0]), ): - if not obj.display: - continue - 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")) - - for child in obj.children: - self.output_child_rst(child, obj, detail_dir, source_suffix) 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_to_render.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")) diff --git a/autoapi/mappers/python/mapper.py b/autoapi/mappers/python/mapper.py index 23d84e0..34e972a 100644 --- a/autoapi/mappers/python/mapper.py +++ b/autoapi/mappers/python/mapper.py @@ -62,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, ) @@ -167,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"] @@ -217,7 +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 = ( @@ -340,15 +343,33 @@ 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) - top_level_objects = {obj.id: obj for obj in self.all_objects.values() if isinstance(obj, TopLevelPythonPythonMapper)} + 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 self.objects_to_render.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] @@ -380,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( diff --git a/autoapi/mappers/python/objects.py b/autoapi/mappers/python/objects.py index 7b8b722..42abf88 100644 --- a/autoapi/mappers/python/objects.py +++ b/autoapi/mappers/python/objects.py @@ -1,4 +1,5 @@ import functools +import pathlib from typing import List, Optional import sphinx.util.logging @@ -44,6 +45,7 @@ class PythonPythonMapper(PythonMapperBase): super().__init__(obj, **kwargs) self.name = obj["name"] + self.qual_name = obj["qual_name"] self.id = obj.get("full_name", self.name) # Optional @@ -56,6 +58,7 @@ class PythonPythonMapper(PythonMapperBase): :type: bool """ + self._hide = obj.get("hide", False) # For later self._class_content = class_content @@ -81,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). @@ -144,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 @@ -229,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): @@ -300,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"] @@ -335,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.""" diff --git a/autoapi/mappers/python/parser.py b/autoapi/mappers/python/parser.py index 1504d9d..22bf8d9 100644 --- a/autoapi/mappers/python/parser.py +++ b/autoapi/mappers/python/parser.py @@ -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) @@ -91,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, @@ -111,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)), @@ -119,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()): @@ -148,7 +155,8 @@ class Parser: overridden.update(seen) - self._name_stack.pop() + self._qual_name_stack.pop() + self._full_name_stack.pop() return [data] @@ -185,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)), @@ -209,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, } @@ -233,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": [], diff --git a/autoapi/templates/index.rst b/autoapi/templates/index.rst index 95d0ad8..2a45554 100644 --- a/autoapi/templates/index.rst +++ b/autoapi/templates/index.rst @@ -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 `_ diff --git a/autoapi/templates/python/class.rst b/autoapi/templates/python/class.rst index f494efe..b211ef5 100644 --- a/autoapi/templates/python/class.rst +++ b/autoapi/templates/python/class.rst @@ -1,105 +1,104 @@ {% if obj.display %} -{% if is_own_page %} -{{ obj.name }} -{{ "=" * obj.name | length }} -{% endif %} - -.. 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 %} - {{ " " * (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 %} - {# TODO: Rendering of all children below this line must be conditional on own_page_types #} - {% 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(own_page_types=[])|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 %} - {% endif %} - {% if "property" in own_page_types and visible_properties %} + {% if is_own_page and own_page_children %} + {% set visible_attributes = own_page_children|selectattr("type", "equalto", "attribute")|list %} + {% if visible_attributes %} +Attributes +---------- - Properties - ---------- +.. autoapisummary:: - .. toctree:: - :hidden: + {% for attribute in visible_attributes %} + {{ attribute.id }} + {% endfor %} - {% for property in visible_properties %} - {{ property.name }} - {% endfor %} - {% else %} - {% for property in visible_properties %} - {{ property.render()|indent(3) }} - {% endfor %} - {% endif %} - {% 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 %} - {% if "attribute" in own_page_types and visible_attributes %} - Attributes - ---------- + {% endif %} + {% set visible_exceptions = own_page_children|selectattr("type", "equalto", "exception")|list %} + {% if visible_exceptions %} +Exceptions +---------- - .. toctree:: - :hidden: +.. autoapisummary:: - {% for attribute in visible_attributes %} - {{ attribute.name }} - {% endfor %} - {% else %} - {% for attribute in visible_attributes %} - {{ attribute.render()|indent(3) }} - {% endfor %} - {% endif %} - {% 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 %} - {% if "method" in own_page_types and visible_methods %} + {% for exception in visible_exceptions %} + {{ exception.id }} + {% endfor %} - Methods - ------- - .. toctree:: - :hidden: + {% endif %} + {% set visible_classes = own_page_children|selectattr("type", "equalto", "class")|list %} + {% if visible_classes %} +Classes +------- - {% for method in visible_methods %} - {{ method.name }} - {% endfor %} - {% else %} - {% for method in visible_methods %} - {{ method.render()|indent(3) }} - {% endfor %} +.. 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 %} {% endif %} diff --git a/autoapi/templates/python/data.rst b/autoapi/templates/python/data.rst index 3851d6c..845a2ae 100644 --- a/autoapi/templates/python/data.rst +++ b/autoapi/templates/python/data.rst @@ -1,42 +1,42 @@ {% if obj.display %} -{% if is_own_page %} -{{ obj.name }} -{{ "=" * obj.name | length }} + {% if is_own_page %} +:py:{{ obj.type|truncate(4, True, "", 0) }}:`{{ obj.id }}` +==========={{ "=" * obj.id | length }} -{% endif %} -.. py:{{ obj.type }}:: {{ obj.name }} - {%- if obj.annotation is not none %} - - :type: {%- if obj.annotation %} {{ obj.annotation }}{%- endif %} - - {%- endif %} + {% endif %} +.. py:{{ obj.type }}:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.name }}{% endif %} + {% if obj.annotation is not none %} - {%- if obj.value is not none %} + :type: {% if obj.annotation %} {{ obj.annotation }}{% endif %} + {% endif %} + {% if obj.value is not none %} - :value: {% if obj.value is string and obj.value.splitlines()|count > 1 -%} - Multiline-String + {% if obj.value is string and obj.value.splitlines()|count > 1 %} + :value: Multiline-String - .. raw:: html + .. raw:: html -
Show Value +
Show Value - .. code-block:: python + .. code-block:: python - """{{ obj.value|indent(width=8,blank=true) }}""" + """{{ obj.value|indent(width=6,blank=true) }}""" - .. raw:: html + .. raw:: html -
+
- {%- 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 %} diff --git a/autoapi/templates/python/function.rst b/autoapi/templates/python/function.rst index 2d58b1e..3be1d9d 100644 --- a/autoapi/templates/python/function.rst +++ b/autoapi/templates/python/function.rst @@ -1,20 +1,21 @@ {% if obj.display %} -{% if is_own_page %} -{{ obj.name }} -{{ "=" * obj.name | length }} + {% if is_own_page %} +:py:func:`{{ obj.id }}` +==========={{ "=" * obj.id | length }} -{% endif %} -.. py:function:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} - -{% 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 %} diff --git a/autoapi/templates/python/method.rst b/autoapi/templates/python/method.rst index f6e008f..4b159f0 100644 --- a/autoapi/templates/python/method.rst +++ b/autoapi/templates/python/method.rst @@ -1,24 +1,21 @@ -{%- if obj.display %} -{% if is_own_page %} -{{ obj.name }} -{{ "=" * obj.name | length }} +{% if obj.display %} + {% if is_own_page %} +:py:meth:`{{ obj.id }}` +==========={{ "=" * obj.id | length }} -{% endif %} -.. py:method:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} - -{% 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 %} diff --git a/autoapi/templates/python/module.rst b/autoapi/templates/python/module.rst index 0e7b769..8769308 100644 --- a/autoapi/templates/python/module.rst +++ b/autoapi/templates/python/module.rst @@ -1,90 +1,67 @@ {% if obj.display %} {% if is_own_page %} -:py:mod:`{{ obj.name }}` -=========={{ "=" * obj.name|length }} +:py:mod:`{{ obj.id }}` +=========={{ "=" * obj.id|length }} - {% endif %} .. 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:: - :hidden: - - {% for subpackage in visible_subpackages %} - {{ subpackage.short_name }}/index.rst - {% endfor %} -.. autoapisummary:: +.. toctree:: + :maxdepth: 1 - {% for subpackage in visible_subpackages %} - {{ subpackage.id }} - {% 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 %} - {% if "module" in own_page_types %} + {% endif %} + {% endblock %} + {% block submodules %} + {% set visible_submodules = obj.submodules|selectattr("display")|list %} + {% if visible_submodules %} Submodules ---------- + .. toctree:: - :hidden: + :maxdepth: 1 {% for submodule in visible_submodules %} - {{ submodule.short_name }}/index.rst + {{ submodule.include_path }} {% endfor %} -.. autoapisummary:: - {% for submodule in visible_submodules %} - {{ submodule.id }} - {% endfor %} - {% else %} - {% for submodule in visible_submodules %} - {{ submodule.render() }} - {% endfor %} {% endif %} - - - {% 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") %} + {% endblock %} + {% block content %} {% set visible_children = obj.children|selectattr("display")|list %} - {% else %} - {% set visible_children = obj.children|selectattr("display")|rejectattr("imported")|list %} - {% endif %} - {% if visible_children %} - {% if is_own_page %} + {% 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.short_name }} + {{ attribute.include_path }} {% endfor %} - {% endif%} + {% endif %} .. autoapisummary:: {% for attribute in visible_attributes %} @@ -99,12 +76,13 @@ Attributes {% 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.short_name }}/index.rst + {{ exception.include_path }} {% endfor %} {% endif %} @@ -122,23 +100,13 @@ Exceptions {% 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 %} - {# - The set own_page_types sometimes is not ordered! This changes the value of - its last element. Thus, the best way to check is to verify if 'function' - lies within the list - Do -> if 'function' not in own_page_types - Instead of -> if "class" == (own_page_types | list | last) - #} - {% if "method" not in own_page_types %} - {{ klass.short_name }}.rst - {% else %} - {{ klass.short_name }}/index.rst - {% endif %} + {{ klass.include_path }} {% endfor %} {% endif %} @@ -156,12 +124,13 @@ Classes {% 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.short_name }}.rst + {{ function.include_path }} {% endfor %} {% endif %} @@ -174,21 +143,28 @@ Functions {% 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 visible_children %} - {% if obj_item.type not in own_page_types %} + {% for obj_item in this_page_children %} {{ obj_item.render()|indent(0) }} - {% endif %} - {% endfor %} - {% else %} - {# If this is not its own page, the children won't have their own page either. #} - {# So render them as normal, without needing to check if they have their own page. #} - {% for obj_item in visible_children %} - {{ obj_item.render()|indent(3) }} - {% endfor %} + {% endfor %} + {% endif %} {% endif %} + {% endblock %} + {% else %} +.. py:module:: {{ obj.name }} + + {% if obj.docstring %} + .. autoapi-nested-parse:: + + {{ obj.docstring|indent(6) }} + {% endif %} - {% endblock %} + {% for obj_item in visible_children %} + {{ obj_item.render()|indent(3) }} + {% endfor %} + {% endif %} {% endif %} diff --git a/autoapi/templates/python/property.rst b/autoapi/templates/python/property.rst index d2eb0b8..0d02925 100644 --- a/autoapi/templates/python/property.rst +++ b/autoapi/templates/python/property.rst @@ -1,18 +1,18 @@ -{%- if obj.display %} -{% if is_own_page %} -{{ obj.name }} -{{ "=" * obj.name | length }} +{% if obj.display %} + {% if is_own_page %} +:py:property:`{{ obj.id }}` +==============={{ "=" * obj.id | length }} -{% endif %} -.. py:property:: {{ obj.short_name }} + {% 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) }} diff --git a/docs/changes/+8cd14c4f.bugfix b/docs/changes/+8cd14c4f.bugfix new file mode 100644 index 0000000..10756f9 --- /dev/null +++ b/docs/changes/+8cd14c4f.bugfix @@ -0,0 +1 @@ +Fix emitting ignore event twice for methods. \ No newline at end of file diff --git a/docs/changes/226.feature b/docs/changes/226.feature new file mode 100644 index 0000000..ad59393 --- /dev/null +++ b/docs/changes/226.feature @@ -0,0 +1 @@ +Objects can render to their own page \ No newline at end of file diff --git a/docs/how_to.rst b/docs/how_to.rst index 5bf211b..061b68c 100644 --- a/docs/how_to.rst +++ b/docs/how_to.rst @@ -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`. diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 26b47bb..08eed5e 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -79,11 +79,9 @@ Customisation Options and requires `Graphviz `_ 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 @@ -189,19 +187,16 @@ Customisation Options a single page. Valid levels, in descending order of hierarchy, are as follows: - * Package + * ``module``: Packages, modules, subpackages, and submodules. - * Module + * ``class``: Classes, exceptions, and all object types mentioned above. - * Class + * ``function``: Functions, and all object types mentioned above. - * Function + * ``method``: Methods, and all object types mentioned above. - * Method - - * Attribute - - * Data + * ``attribute``: Class and module level attributes, properties, + and all object types mentioned above. Events diff --git a/pyproject.toml b/pyproject.toml index 1fa1f93..4ab46dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/setup.cfg b/setup.cfg index fc10528..b160686 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/tests/python/conftest.py b/tests/python/conftest.py new file mode 100644 index 0000000..a973b85 --- /dev/null +++ b/tests/python/conftest.py @@ -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 diff --git a/tests/python/pyannotationcommentsexample/conf.py b/tests/python/pyannotationcommentsexample/conf.py index f2a68ec..f6f68a9 100644 --- a/tests/python/pyannotationcommentsexample/conf.py +++ b/tests/python/pyannotationcommentsexample/conf.py @@ -17,3 +17,4 @@ htmlhelp_basename = "pyexampledoc" extensions = ["sphinx.ext.autodoc", "autoapi.extension"] autoapi_dirs = ["example"] autoapi_file_pattern = "*.py" +autoapi_keep_files = True diff --git a/tests/python/pypackageexample/conf.py b/tests/python/pypackageexample/conf.py index 9bb3325..c1405a2 100644 --- a/tests/python/pypackageexample/conf.py +++ b/tests/python/pypackageexample/conf.py @@ -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 diff --git a/tests/python/pypackageexample/example/__init__.py b/tests/python/pypackageexample/example/__init__.py deleted file mode 100644 index 1b58595..0000000 --- a/tests/python/pypackageexample/example/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""This is a docstring.""" - -from . import foo - - -def module_level_function(foo, bar): - """A module level method""" - pass diff --git a/tests/python/pypackageexample/package/__init__.py b/tests/python/pypackageexample/package/__init__.py new file mode 100644 index 0000000..827d0d4 --- /dev/null +++ b/tests/python/pypackageexample/package/__init__.py @@ -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.""" diff --git a/tests/python/pypackageexample/example/_private_module.py b/tests/python/pypackageexample/package/_private_submodule.py similarity index 100% rename from tests/python/pypackageexample/example/_private_module.py rename to tests/python/pypackageexample/package/_private_submodule.py diff --git a/tests/python/pypackageexample/example/foo.py b/tests/python/pypackageexample/package/submodule.py similarity index 72% rename from tests/python/pypackageexample/example/foo.py rename to tests/python/pypackageexample/package/submodule.py index 0a87d32..061ad52 100644 --- a/tests/python/pypackageexample/example/foo.py +++ b/tests/python/pypackageexample/package/submodule.py @@ -3,21 +3,36 @@ This is a description """ -MODULE_DATA = 42 +from .subpackage.submodule import function as aliased_function +from .subpackage.submodule import not_in_all_function +__all__ = ( + "aliased_function", + "Class", + "DATA", + "function", + "MyException", +) -class Foo(object): - class_var = 42 #: Class var docstring +DATA = 42 - another_class_var = 42 - """Another class var docstring""" - 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): @@ -63,3 +78,7 @@ class Foo(object): int: The sum of foo and bar. """ return foo + bar + + +class MyException(Exception): + """This is an exception.""" diff --git a/tests/python/pypackageexample/package/subpackage/__init__.py b/tests/python/pypackageexample/package/subpackage/__init__.py new file mode 100644 index 0000000..3bc54ce --- /dev/null +++ b/tests/python/pypackageexample/package/subpackage/__init__.py @@ -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""" diff --git a/tests/python/pypackageexample/package/subpackage/submodule.py b/tests/python/pypackageexample/package/subpackage/submodule.py new file mode 100644 index 0000000..0bf556e --- /dev/null +++ b/tests/python/pypackageexample/package/subpackage/submodule.py @@ -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.""" diff --git a/tests/python/test_own_page_option.py b/tests/python/test_own_page_option.py new file mode 100644 index 0000000..b469833 --- /dev/null +++ b/tests/python/test_own_page_option.py @@ -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, + }, + ) diff --git a/tests/python/test_pyintegration.py b/tests/python/test_pyintegration.py index dcdc7b6..e106fec 100644 --- a/tests/python/test_pyintegration.py +++ b/tests/python/test_pyintegration.py @@ -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 ( @@ -670,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_function") + 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_function()" 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 +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): @@ -1019,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(): @@ -1125,25 +1069,25 @@ def test_string_module_attributes(builder): ".. py:data:: code_snippet", " :value: Multiline-String", "", - " .. raw:: html", + " .. raw:: html", "", - "
Show Value", + "
Show Value", "", - " .. 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", "", - "
", + "
", ] assert "\n".join(code_snippet_contents) in example_file @@ -1208,298 +1152,3 @@ class TestMemberOrder: method_sphinx_docs = example_file.find(id="example.Foo.method_sphinx_docs") assert method_tricky.sourceline < method_sphinx_docs.sourceline - - -# TODO: This might be easier to understand with its own test case. -# Eg make a package named "package", subpackage named "subpackage", -# submodule named "submodule", etc. -class TestOwnPageLevel: - def test_package(self, builder, parse): - builder( - "pypackageexample", - warningiserror=True, - confoverrides={"autoapi_own_page_level": "package"}, - ) - - example_path = "_build/html/autoapi/example/index.html" - example_file = parse(example_path) - - # TODO: Look for expected contents - - # subpackage_path = "_build/html/autoapi/example/subpackage/index.html" - # subpackage_file = parse(subpackage_path) - - # TODO: Look for expected contents - - assert not os.path.exists("_build/html/autoapi/example/foo/index.html") - assert not os.path.exists("_build/html/autoapi/example/foo/FooError.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo.html") - assert not os.path.exists("_build/html/autoapi/example/foo/module_level_function.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/method_okay.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/property.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/class_var.html") - assert not os.path.exists("_build/html/autoapi/example/foo/MODULE_DATA.html") - - def test_module(self, builder, parse): - builder( - "pypackageexample", - warningiserror=True, - confoverrides={"autoapi_own_page_level": "module"}, - ) - - example_path = "_build/html/autoapi/example/index.html" - example_file = parse(example_path) - - # TODO: Look for expected contents - - #subpackage_path = "_build/html/autoapi/example/subpackage/index.html" - #subpackage_file = parse(subpackage_path) - - # TODO: Look for expected contents - - foo_path = "_build/html/autoapi/example/foo/index.html" - foo_file = parse(foo_path) - - # TODO: Look for expected contents - - assert not os.path.exists("_build/html/autoapi/example/foo/FooError.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo.html") - assert not os.path.exists("_build/html/autoapi/example/foo/module_level_function.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/method_okay.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/property.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/class_var.html") - assert not os.path.exists("_build/html/autoapi/example/foo/MODULE_DATA.html") - - def test_class(self, builder, parse): - builder( - "pypackageexample", - warningiserror=True, - confoverrides={"autoapi_own_page_level": "class"}, - ) - example_path = "_build/html/autoapi/example/index.html" - example_file = parse(example_path) - - # TODO: Look for expected contents - - #subpackage_path = "_build/html/autoapi/example/subpackage/index.html" - #subpackage_file = parse(subpackage_path) - - # TODO: Look for expected contents - - foo_path = "_build/html/autoapi/example/foo/index.html" - foo_file = parse(foo_path) - - # TODO: Look for expected contents - - #error_path = "_build/html/autoapi/example/foo/FooError.html" - #error_file = parse(error_path) - - # TODO: Look for expected contents - - foo_path = "_build/html/autoapi/example/foo/Foo.html" - foo_file = parse(foo_path) - - # TODO: Look for expected contents - - assert not os.path.exists("_build/html/autoapi/example/foo/module_level_function.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/method_okay.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/property.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/class_var.html") - assert not os.path.exists("_build/html/autoapi/example/foo/MODULE_DATA.html") - - def test_function(self, builder, parse): - builder( - "pypackageexample", - warningiserror=True, - confoverrides={"autoapi_own_page_level": "function"}, - ) - example_path = "_build/html/autoapi/example/index.html" - example_file = parse(example_path) - - # TODO: Look for expected contents - - # subpackage_path = "_build/html/autoapi/example/subpackage/index.html" - # subpackage_file = parse(subpackage_path) - - # TODO: Look for expected contents - - foo_path = "_build/html/autoapi/example/foo/index.html" - foo_file = parse(foo_path) - - # TODO: Look for expected contents - - # error_path = "_build/html/autoapi/example/foo/FooError.html" - # error_file = parse(error_path) - - # TODO: Look for expected contents - - foo_path = "_build/html/autoapi/example/foo/Foo/index.html" - foo_file = parse(foo_path) - - # TODO: Look for expected contents - - func_path = "_build/html/autoapi/example/module_level_function.html" - func_file = parse(func_path) - - # TODO: Look for expected contents - - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/method_okay.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/property.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/class_var.html") - assert not os.path.exists("_build/html/autoapi/example/foo/MODULE_DATA.html") - - def test_method(self, builder, parse): - builder( - "pypackageexample", - warningiserror=True, - confoverrides={"autoapi_own_page_level": "method"}, - ) - example_path = "_build/html/autoapi/example/index.html" - example_file = parse(example_path) - - # TODO: Look for expected contents - - # subpackage_path = "_build/html/autoapi/example/subpackage/index.html" - # subpackage_file = parse(subpackage_path) - - # TODO: Look for expected contents - - foo_path = "_build/html/autoapi/example/foo/index.html" - foo_file = parse(foo_path) - - # TODO: Look for expected contents - - error_path = "_build/html/autoapi/example/foo/FooError.html" - error_file = parse(error_path) - - # TODO: Look for expected contents - - foo_path = "_build/html/autoapi/example/foo/Foo.html" - foo_file = parse(foo_path) - - # TODO: Look for expected contents - - func_path = "_build/html/autoapi/example/foo/module_level_function.html" - func_file = parse(func_path) - - # TODO: Look for expected contents - - method_path = "_build/html/autoapi/example/foo/Foo/method_okay.html" - method_file = parse(method_path) - - # TODO: Look for expected contents - - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/property.html") - assert not os.path.exists("_build/html/autoapi/example/foo/Foo/class_var.html") - assert not os.path.exists("_build/html/autoapi/example/foo/MODULE_DATA.html") - - def test_attribute(self, builder, parse): - builder( - "pypackageexample", - warningiserror=True, - confoverrides={"autoapi_own_page_level": "attribute"}, - ) - example_path = "_build/html/autoapi/example/index.html" - example_file = parse(example_path) - - # TODO: Look for expected contents - - # subpackage_path = "_build/html/autoapi/example/subpackage/index.html" - # subpackage_file = parse(subpackage_path) - - # TODO: Look for expected contents - - foo_path = "_build/html/autoapi/example/foo/index.html" - foo_file = parse(foo_path) - - # TODO: Look for expected contents - - error_path = "_build/html/autoapi/example/foo/FooError.html" - error_file = parse(error_path) - - # TODO: Look for expected contents - - foo_path = "_build/html/autoapi/example/foo/Foo.html" - foo_file = parse(foo_path) - - # TODO: Look for expected contents - - func_path = "_build/html/autoapi/example/foo/module_level_function.html" - func_file = parse(func_path) - - # TODO: Look for expected contents - - method_path = "_build/html/autoapi/example/foo/Foo/method_okay.html" - method_file = parse(method_path) - - # TODO: Look for expected contents - - property_path = "_build/html/autoapi/example/foo/Foo/property.html" - property_file = parse(property_path) - - # TODO: Look for expected contents - - attribute_path = "_build/html/autoapi/example/foo/Foo/class_var.html" - attribute_file = parse(attribute_path) - - # TODO: Look for expected contents - - assert not os.path.exists("_build/html/autoapi/example/foo/MODULE_DATA.html") - - - def test_data(self, builder, parse): - builder( - "pypackageexample", - warningiserror=True, - confoverrides={"autoapi_own_page_level": "data"}, - ) - - example_path = "_build/html/autoapi/example/index.html" - example_file = parse(example_path) - - # TODO: Look for expected contents - - # subpackage_path = "_build/html/autoapi/example/subpackage/index.html" - # subpackage_file = parse(subpackage_path) - - # TODO: Look for expected contents - - foo_path = "_build/html/autoapi/example/foo/index.html" - foo_file = parse(foo_path) - - # TODO: Look for expected contents - - error_path = "_build/html/autoapi/example/foo/FooError.html" - error_file = parse(error_path) - - # TODO: Look for expected contents - - foo_path = "_build/html/autoapi/example/foo/Foo.html" - foo_file = parse(foo_path) - - # TODO: Look for expected contents - - func_path = "_build/html/autoapi/example/foo/module_level_function.html" - func_file = parse(func_path) - - # TODO: Look for expected contents - - method_path = "_build/html/autoapi/example/foo/Foo/method_okay.html" - method_file = parse(method_path) - - # TODO: Look for expected contents - - property_path = "_build/html/autoapi/example/foo/Foo/property.html" - property_file = parse(property_path) - - # TODO: Look for expected contents - - attribute_path = "_build/html/autoapi/example/foo/Foo/class_var.html" - attribute_file = parse(attribute_path) - - # TODO: Look for expected contents - - data_path = "_build/html/autoapi/example/foo/MODULE_DATA.html" - data_file = parse(data_path) - - # TODO: Look for expected contents