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