From a4f798183c04a81fcf355c036a16825dc78d4e23 Mon Sep 17 00:00:00 2001 From: Ashley Whetter Date: Tue, 5 Mar 2019 22:27:57 -0800 Subject: [PATCH] Fixed private submodules displaying when private-members turned off Also fixed whitespace issues in templates. --- .gitignore | 2 + autoapi/mappers/base.py | 5 +- autoapi/mappers/python.py | 737 ------------------ autoapi/mappers/python/__init__.py | 11 + autoapi/mappers/{ => python}/astroid_utils.py | 0 autoapi/mappers/python/mapper.py | 308 ++++++++ autoapi/mappers/python/objects.py | 220 ++++++ autoapi/mappers/python/parser.py | 224 ++++++ autoapi/templates/dotnet/base_detail.rst | 29 +- autoapi/templates/dotnet/base_embed.rst | 19 +- autoapi/templates/dotnet/base_list.rst | 12 +- autoapi/templates/go/base_member.rst | 24 +- autoapi/templates/go/package.rst | 10 +- autoapi/templates/python/class.rst | 34 +- autoapi/templates/python/data.rst | 7 +- autoapi/templates/python/function.rst | 4 +- autoapi/templates/python/module.rst | 89 ++- tests/python/test_parser.py | 2 +- tests/test_astroid_utils.py | 2 +- tests/test_integration.py | 2 +- 20 files changed, 880 insertions(+), 861 deletions(-) delete mode 100644 autoapi/mappers/python.py create mode 100644 autoapi/mappers/python/__init__.py rename autoapi/mappers/{ => python}/astroid_utils.py (100%) create mode 100644 autoapi/mappers/python/mapper.py create mode 100644 autoapi/mappers/python/objects.py create mode 100644 autoapi/mappers/python/parser.py diff --git a/.gitignore b/.gitignore index ce4a6b0..c86c4a9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ _api_ .ropeproject/ .cache .pytest_cache/ +build/ +dist/ diff --git a/autoapi/mappers/base.py b/autoapi/mappers/base.py index f1c6d26..cfeefb3 100644 --- a/autoapi/mappers/base.py +++ b/autoapi/mappers/base.py @@ -177,9 +177,8 @@ class SphinxMapperBase(object): self.jinja_env = Environment( loader=FileSystemLoader(template_paths), - # Kill this to fix rendering for now - # trim_blocks=True, - # lstrip_blocks=True, + trim_blocks=True, + lstrip_blocks=True, ) def _wrapped_prepare(value): diff --git a/autoapi/mappers/python.py b/autoapi/mappers/python.py deleted file mode 100644 index 4a1ca59..0000000 --- a/autoapi/mappers/python.py +++ /dev/null @@ -1,737 +0,0 @@ -import collections -import copy -import os - -import astroid -import sphinx -import sphinx.util.docstrings -import sphinx.util.logging - -from .base import PythonMapperBase, SphinxMapperBase -from . import astroid_utils -from ..utils import slugify - -try: - _TEXT_TYPE = unicode -except NameError: - _TEXT_TYPE = str - -LOGGER = sphinx.util.logging.getLogger(__name__) - - -def _expand_wildcard_placeholder(original_module, originals_map, placeholder): - """Expand a wildcard placeholder to a sequence of named placeholders. - - :param original_module: The data dictionary of the module - that the placeholder is imported from. - :type original_module: dict - :param originals_map: A map of the names of children under the module - to their data dictionaries. - :type originals_map: dict(str, dict) - :param placeholder: The wildcard placeholder to expand. - :type placeholder: dict - - :returns: The placeholders that the wildcard placeholder represents. - :rtype: list(dict) - """ - originals = originals_map.values() - if original_module["all"] is not None: - originals = [] - for name in original_module["all"]: - if name == "__all__": - continue - - if name not in originals_map: - msg = "Invalid __all__ entry {0} in {1}".format( - name, original_module["name"] - ) - LOGGER.warning(msg) - continue - - originals.append(originals_map[name]) - - placeholders = [] - for original in originals: - new_full_name = placeholder["full_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"], - full_name=new_full_name, - original_path=new_original_path, - ) - placeholders.append(new_placeholder) - - return placeholders - - -def _resolve_module_placeholders(modules, module_name, visit_path, resolved): - """Resolve all placeholder children under a module. - - :param modules: A mapping of module names to their data dictionary. - Placeholders are resolved in place. - :type modules: dict(str, dict) - :param module_name: The name of the module to resolve. - :type module_name: str - :param visit_path: An ordered set of visited module names. - :type visited: collections.OrderedDict - :param resolved: A set of already resolved module names. - :type resolved: set(str) - """ - if module_name in resolved: - return - - visit_path[module_name] = True - - module, children = modules[module_name] - for child in list(children.values()): - if child["type"] != "placeholder": - continue - - if child["original_path"] in modules: - module["children"].remove(child) - children.pop(child["name"]) - continue - - imported_from, original_name = child["original_path"].rsplit(".", 1) - if imported_from in visit_path: - msg = "Cannot resolve cyclic import: {0}, {1}".format( - ", ".join(visit_path), imported_from - ) - LOGGER.warning(msg) - module["children"].remove(child) - children.pop(child["name"]) - continue - - if imported_from not in modules: - msg = "Cannot resolve import of unknown module {0} in {1}".format( - imported_from, module_name - ) - LOGGER.warning(msg) - module["children"].remove(child) - children.pop(child["name"]) - continue - - _resolve_module_placeholders(modules, imported_from, visit_path, resolved) - - if original_name == "*": - original_module, originals_map = modules[imported_from] - - # Replace the wildcard placeholder - # with a list of named placeholders. - new_placeholders = _expand_wildcard_placeholder( - original_module, originals_map, child - ) - child_index = module["children"].index(child) - module["children"][child_index : child_index + 1] = new_placeholders - children.pop(child["name"]) - - for new_placeholder in new_placeholders: - if new_placeholder["name"] not in children: - children[new_placeholder["name"]] = new_placeholder - original = originals_map[new_placeholder["name"]] - _resolve_placeholder(new_placeholder, original) - elif original_name not in modules[imported_from][1]: - msg = "Cannot resolve import of {0} in {1}".format( - child["original_path"], module_name - ) - LOGGER.warning(msg) - module["children"].remove(child) - children.pop(child["name"]) - continue - else: - original = modules[imported_from][1][original_name] - _resolve_placeholder(child, original) - - del visit_path[module_name] - resolved.add(module_name) - - -def _resolve_placeholder(placeholder, original): - """Resolve a placeholder to the given original object. - - :param placeholder: The placeholder to resolve, in place. - :type placeholder: dict - :param original: The object that the placeholder represents. - :type original: dict - """ - new = copy.deepcopy(original) - # The name remains the same. - new["name"] = placeholder["name"] - new["full_name"] = placeholder["full_name"] - # Record where the placeholder originally came from. - new["original_path"] = original["full_name"] - # The source lines for this placeholder do not exist in this file. - # The keys might not exist if original is a resolved placeholder. - new.pop("from_line_no", None) - new.pop("to_line_no", None) - - # Resolve the children - stack = list(new.get("children", ())) - while stack: - child = stack.pop() - # Relocate the child to the new location - assert child["full_name"].startswith(original["full_name"]) - suffix = child["full_name"][len(original["full_name"]) :] - child["full_name"] = new["full_name"] + suffix - # The source lines for this placeholder do not exist in this file. - # The keys might not exist if original is a resolved placeholder. - child.pop("from_line_no", None) - child.pop("to_line_no", None) - # Resolve the remaining children - stack.extend(child.get("children", ())) - - placeholder.clear() - placeholder.update(new) - - -class PythonSphinxMapper(SphinxMapperBase): - - """Auto API domain handler for Python - - Parses directly from Python files. - - :param app: Sphinx application passed in as part of the extension - """ - - def load(self, patterns, dirs, ignore=None): - """Load objects from the filesystem into the ``paths`` dictionary - - Also include an attribute on the object, ``relative_path`` which is the - shortened, relative path the package/module - """ - for dir_ in dirs: - dir_root = dir_ - if os.path.exists(os.path.join(dir_, "__init__.py")): - dir_root = os.path.abspath(os.path.join(dir_, os.pardir)) - - for path in self.find_files(patterns=patterns, dirs=[dir_], ignore=ignore): - data = self.read_file(path=path) - if data: - data["relative_path"] = os.path.relpath(path, dir_root) - self.paths[path] = data - - def read_file(self, path, **kwargs): - """Read file input into memory, returning deserialized objects - - :param path: Path of file to read - """ - try: - parsed_data = Parser().parse_file(path) - return parsed_data - except (IOError, TypeError, ImportError): - LOGGER.warning("Error reading file: {0}".format(path)) - return None - - def _resolve_placeholders(self): - """Resolve objects that have been imported from elsewhere.""" - modules = {} - for module in self.paths.values(): - children = {child["name"]: child for child in module["children"]} - modules[module["name"]] = (module, children) - - resolved = set() - for module_name in modules: - visit_path = collections.OrderedDict() - _resolve_module_placeholders(modules, module_name, visit_path, resolved) - - def map(self, options=None): - self._resolve_placeholders() - - super(PythonSphinxMapper, self).map(options) - - parents = {obj.name: obj for obj in self.objects.values()} - for obj in self.objects.values(): - parent_name = obj.name.rsplit(".", 1)[0] - if parent_name in parents and parent_name != obj.name: - parent = parents[parent_name] - attr = "sub{}s".format(obj.type) - getattr(parent, attr).append(obj) - - for obj in self.objects.values(): - obj.submodules.sort() - obj.subpackages.sort() - - def create_class(self, data, options=None, **kwargs): - """Create a class from the passed in data - - :param data: dictionary data of parser output - """ - obj_map = dict( - (cls.type, cls) - for cls in [ - PythonClass, - PythonFunction, - PythonModule, - PythonMethod, - PythonPackage, - PythonAttribute, - PythonData, - PythonException, - ] - ) - try: - cls = obj_map[data["type"]] - except KeyError: - LOGGER.warning("Unknown type: %s" % data["type"]) - else: - obj = cls( - data, - class_content=self.app.config.autoapi_python_class_content, - options=self.app.config.autoapi_options, - jinja_env=self.jinja_env, - url_root=self.url_root, - **kwargs - ) - - lines = sphinx.util.docstrings.prepare_docstring(obj.docstring) - if lines and "autodoc-process-docstring" in self.app.events.events: - self.app.emit( - "autodoc-process-docstring", - cls.type, - obj.name, - None, # object - None, # options - lines, - ) - obj.docstring = "\n".join(lines) - - for child_data in data.get("children", []): - for child_obj in self.create_class( - child_data, options=options, **kwargs - ): - obj.children.append(child_obj) - yield obj - - -class PythonPythonMapper(PythonMapperBase): - - language = "python" - is_callable = False - - def __init__(self, obj, class_content="class", **kwargs): - super(PythonPythonMapper, self).__init__(obj, **kwargs) - - self.name = obj["name"] - self.id = obj.get("full_name", self.name) - - # Optional - self.children = [] - self.args = obj.get("args") - self.docstring = obj["doc"] - - # For later - self.item_map = collections.defaultdict(list) - self._class_content = class_content - - @property - def args(self): - return self._args - - @args.setter - def args(self, value): - self._args = value - - @property - def docstring(self): - return self._docstring - - @docstring.setter - def docstring(self, value): - self._docstring = value - - @property - def is_undoc_member(self): - return not bool(self.docstring) - - @property - def is_private_member(self): - return self.short_name.startswith("_") and not self.short_name.endswith("__") - - @property - def is_special_member(self): - return self.short_name.startswith("__") and self.short_name.endswith("__") - - @property - def display(self): - if self.is_undoc_member and "undoc-members" not in self.options: - return False - if self.is_private_member and "private-members" not in self.options: - return False - if self.is_special_member and "special-members" not in self.options: - return False - return True - - @property - def summary(self): - for line in self.docstring.splitlines(): - line = line.strip() - if line: - return line - - return "" - - def _children_of_type(self, type_): - return list(child for child in self.children if child.type == type_) - - -class PythonFunction(PythonPythonMapper): - type = "function" - is_callable = True - ref_directive = "func" - - -class PythonMethod(PythonPythonMapper): - type = "method" - is_callable = True - ref_directive = "meth" - - def __init__(self, obj, **kwargs): - super(PythonMethod, self).__init__(obj, **kwargs) - - self.method_type = obj["method_type"] - - @property - def display(self): - if self.short_name == "__init__": - return False - - return super(PythonMethod, self).display - - -class PythonData(PythonPythonMapper): - """Global, module level data.""" - - type = "data" - - def __init__(self, obj, **kwargs): - super(PythonData, self).__init__(obj, **kwargs) - - self.value = obj.get("value") - - -class PythonAttribute(PythonData): - """An object/class level attribute.""" - - type = "attribute" - - -class TopLevelPythonPythonMapper(PythonPythonMapper): - ref_directive = "mod" - _RENDER_LOG_LEVEL = "VERBOSE" - - def __init__(self, obj, **kwargs): - super(TopLevelPythonPythonMapper, self).__init__(obj, **kwargs) - - self.top_level_object = "." not in self.name - - self.subpackages = [] - self.submodules = [] - self.all = obj["all"] - - @property - def functions(self): - return self._children_of_type("function") - - @property - def classes(self): - return self._children_of_type("class") - - -class PythonModule(TopLevelPythonPythonMapper): - type = "module" - - -class PythonPackage(TopLevelPythonPythonMapper): - type = "package" - - -class PythonClass(PythonPythonMapper): - type = "class" - - def __init__(self, obj, **kwargs): - super(PythonClass, self).__init__(obj, **kwargs) - - self.bases = obj["bases"] - - @PythonPythonMapper.args.getter - def args(self): - args = self._args - - constructor = self.constructor - if constructor: - args = constructor.args - - if args.startswith("self"): - args = args[4:].lstrip(",").lstrip() - - return args - - @PythonPythonMapper.docstring.getter - def docstring(self): - docstring = super(PythonClass, self).docstring - - if self._class_content in ("both", "init"): - constructor_docstring = self.constructor_docstring - if constructor_docstring: - if self._class_content == "both": - docstring = "{0}\n{1}".format(docstring, constructor_docstring) - else: - docstring = constructor_docstring - - return docstring - - @property - def methods(self): - return self._children_of_type("method") - - @property - def attributes(self): - return self._children_of_type("attribute") - - @property - def classes(self): - return self._children_of_type("class") - - @property - def constructor(self): - for child in self.children: - if child.short_name == "__init__": - return child - - return None - - @property - def constructor_docstring(self): - docstring = "" - - constructor = self.constructor - if constructor and constructor.docstring: - docstring = constructor.docstring - else: - for child in self.children: - if child.short_name == "__new__": - docstring = child.docstring - break - - return docstring - - -class PythonException(PythonClass): - type = "exception" - - -class Parser(object): - def __init__(self): - self._name_stack = [] - self._encoding = None - - def _get_full_name(self, name): - return ".".join(self._name_stack + [name]) - - def _encode(self, to_encode): - if self._encoding: - try: - return _TEXT_TYPE(to_encode, self._encoding) - except TypeError: - # The string was already in the correct format - pass - - return to_encode - - def parse_file(self, file_path): - directory, filename = os.path.split(file_path) - module_parts = [] - if filename != "__init__.py": - module_part = os.path.splitext(filename)[0] - module_parts = [module_part] - module_parts = collections.deque(module_parts) - while os.path.isfile(os.path.join(directory, "__init__.py")): - directory, module_part = os.path.split(directory) - if module_part: - module_parts.appendleft(module_part) - - module_name = ".".join(module_parts) - node = astroid.MANAGER.ast_from_file(file_path, module_name) - return self.parse(node) - - def parse_assign(self, node): - doc = "" - doc_node = node.next_sibling() - if isinstance(doc_node, astroid.nodes.Expr) and isinstance( - doc_node.value, astroid.nodes.Const - ): - doc = doc_node.value.value - - type_ = "data" - if isinstance( - node.scope(), astroid.nodes.ClassDef - ) or astroid_utils.is_constructor(node.scope()): - type_ = "attribute" - - assign_value = astroid_utils.get_assign_value(node) - if not assign_value: - return [] - - target, value = assign_value - data = { - "type": type_, - "name": target, - "full_name": self._get_full_name(target), - "doc": self._encode(doc), - "value": value, - "from_line_no": node.fromlineno, - "to_line_no": node.tolineno, - } - - return [data] - - def parse_classdef(self, node, data=None): - type_ = "class" - if astroid_utils.is_exception(node): - type_ = "exception" - - args = "" - try: - constructor = node.lookup("__init__")[1] - except IndexError: - pass - else: - if isinstance(constructor, astroid.nodes.FunctionDef): - args = constructor.args.as_string() - - basenames = list(astroid_utils.get_full_basenames(node.bases, node.basenames)) - - data = { - "type": type_, - "name": node.name, - "full_name": self._get_full_name(node.name), - "args": args, - "bases": basenames, - "doc": self._encode(node.doc or ""), - "from_line_no": node.fromlineno, - "to_line_no": node.tolineno, - "children": [], - } - - self._name_stack.append(node.name) - for child in node.get_children(): - child_data = self.parse(child) - if child_data: - data["children"].extend(child_data) - self._name_stack.pop() - - return [data] - - def _parse_property(self, node): - data = { - "type": "attribute", - "name": node.name, - "full_name": self._get_full_name(node.name), - "doc": self._encode(node.doc or ""), - "from_line_no": node.fromlineno, - "to_line_no": node.tolineno, - } - - return [data] - - def parse_functiondef(self, node): - if astroid_utils.is_decorated_with_property(node): - return self._parse_property(node) - if astroid_utils.is_decorated_with_property_setter(node): - return [] - - type_ = "function" if node.type == "function" else "method" - - data = { - "type": type_, - "name": node.name, - "full_name": self._get_full_name(node.name), - "args": node.args.as_string(), - "doc": self._encode(node.doc or ""), - "from_line_no": node.fromlineno, - "to_line_no": node.tolineno, - } - - if type_ == "method": - data["method_type"] = node.type - - result = [data] - - if node.name == "__init__": - for child in node.get_children(): - if isinstance(child, astroid.Assign): - child_data = self.parse_assign(child) - result.extend(data for data in child_data if data["doc"]) - - return result - - 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) - - data = { - "type": "placeholder", - "name": original_path if is_wildcard else (alias or name), - "full_name": full_name, - "original_path": original_path, - } - result.append(data) - - return result - - def parse_module(self, node): - path = node.path - if isinstance(node.path, list): - path = node.path[0] if node.path else None - - type_ = "module" - if node.package: - type_ = "package" - - self._name_stack = [node.name] - self._encoding = node.file_encoding - - data = { - "type": type_, - "name": node.name, - "full_name": node.name, - "doc": self._encode(node.doc or ""), - "children": [], - "file_path": path, - "encoding": node.file_encoding, - "all": astroid_utils.get_module_all(node), - } - - top_name = node.name.split(".", 1)[0] - for child in node.get_children(): - if node.package and astroid_utils.is_local_import_from(child, top_name): - child_data = self._parse_local_import_from(child) - else: - child_data = self.parse(child) - - if child_data: - data["children"].extend(child_data) - - return data - - def parse(self, node): - data = {} - - node_type = node.__class__.__name__.lower() - parse_func = getattr(self, "parse_" + node_type, None) - if parse_func: - data = parse_func(node) - else: - for child in node.get_children(): - data = self.parse(child) - if data: - break - - return data diff --git a/autoapi/mappers/python/__init__.py b/autoapi/mappers/python/__init__.py new file mode 100644 index 0000000..2be0cae --- /dev/null +++ b/autoapi/mappers/python/__init__.py @@ -0,0 +1,11 @@ +from .mapper import PythonSphinxMapper +from .objects import ( + PythonClass, + PythonFunction, + PythonModule, + PythonMethod, + PythonPackage, + PythonAttribute, + PythonData, + PythonException, +) diff --git a/autoapi/mappers/astroid_utils.py b/autoapi/mappers/python/astroid_utils.py similarity index 100% rename from autoapi/mappers/astroid_utils.py rename to autoapi/mappers/python/astroid_utils.py diff --git a/autoapi/mappers/python/mapper.py b/autoapi/mappers/python/mapper.py new file mode 100644 index 0000000..8e401a8 --- /dev/null +++ b/autoapi/mappers/python/mapper.py @@ -0,0 +1,308 @@ +import collections +import copy +import os + +import sphinx.util.docstrings +import sphinx.util.logging + +from ..base import SphinxMapperBase +from .parser import Parser +from .objects import ( + PythonClass, + PythonFunction, + PythonModule, + PythonMethod, + PythonPackage, + PythonAttribute, + PythonData, + PythonException, +) + +LOGGER = sphinx.util.logging.getLogger(__name__) + + +def _expand_wildcard_placeholder(original_module, originals_map, placeholder): + """Expand a wildcard placeholder to a sequence of named placeholders. + + :param original_module: The data dictionary of the module + that the placeholder is imported from. + :type original_module: dict + :param originals_map: A map of the names of children under the module + to their data dictionaries. + :type originals_map: dict(str, dict) + :param placeholder: The wildcard placeholder to expand. + :type placeholder: dict + + :returns: The placeholders that the wildcard placeholder represents. + :rtype: list(dict) + """ + originals = originals_map.values() + if original_module["all"] is not None: + originals = [] + for name in original_module["all"]: + if name == "__all__": + continue + + if name not in originals_map: + msg = "Invalid __all__ entry {0} in {1}".format( + name, original_module["name"] + ) + LOGGER.warning(msg) + continue + + originals.append(originals_map[name]) + + placeholders = [] + for original in originals: + new_full_name = placeholder["full_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"], + full_name=new_full_name, + original_path=new_original_path, + ) + placeholders.append(new_placeholder) + + return placeholders + + +def _resolve_module_placeholders(modules, module_name, visit_path, resolved): + """Resolve all placeholder children under a module. + + :param modules: A mapping of module names to their data dictionary. + Placeholders are resolved in place. + :type modules: dict(str, dict) + :param module_name: The name of the module to resolve. + :type module_name: str + :param visit_path: An ordered set of visited module names. + :type visited: collections.OrderedDict + :param resolved: A set of already resolved module names. + :type resolved: set(str) + """ + if module_name in resolved: + return + + visit_path[module_name] = True + + module, children = modules[module_name] + for child in list(children.values()): + if child["type"] != "placeholder": + continue + + if child["original_path"] in modules: + module["children"].remove(child) + children.pop(child["name"]) + continue + + imported_from, original_name = child["original_path"].rsplit(".", 1) + if imported_from in visit_path: + msg = "Cannot resolve cyclic import: {0}, {1}".format( + ", ".join(visit_path), imported_from + ) + LOGGER.warning(msg) + module["children"].remove(child) + children.pop(child["name"]) + continue + + if imported_from not in modules: + msg = "Cannot resolve import of unknown module {0} in {1}".format( + imported_from, module_name + ) + LOGGER.warning(msg) + module["children"].remove(child) + children.pop(child["name"]) + continue + + _resolve_module_placeholders(modules, imported_from, visit_path, resolved) + + if original_name == "*": + original_module, originals_map = modules[imported_from] + + # Replace the wildcard placeholder + # with a list of named placeholders. + new_placeholders = _expand_wildcard_placeholder( + original_module, originals_map, child + ) + child_index = module["children"].index(child) + module["children"][child_index : child_index + 1] = new_placeholders + children.pop(child["name"]) + + for new_placeholder in new_placeholders: + if new_placeholder["name"] not in children: + children[new_placeholder["name"]] = new_placeholder + original = originals_map[new_placeholder["name"]] + _resolve_placeholder(new_placeholder, original) + elif original_name not in modules[imported_from][1]: + msg = "Cannot resolve import of {0} in {1}".format( + child["original_path"], module_name + ) + LOGGER.warning(msg) + module["children"].remove(child) + children.pop(child["name"]) + continue + else: + original = modules[imported_from][1][original_name] + _resolve_placeholder(child, original) + + del visit_path[module_name] + resolved.add(module_name) + + +def _resolve_placeholder(placeholder, original): + """Resolve a placeholder to the given original object. + + :param placeholder: The placeholder to resolve, in place. + :type placeholder: dict + :param original: The object that the placeholder represents. + :type original: dict + """ + new = copy.deepcopy(original) + # The name remains the same. + new["name"] = placeholder["name"] + new["full_name"] = placeholder["full_name"] + # Record where the placeholder originally came from. + new["original_path"] = original["full_name"] + # The source lines for this placeholder do not exist in this file. + # The keys might not exist if original is a resolved placeholder. + new.pop("from_line_no", None) + new.pop("to_line_no", None) + + # Resolve the children + stack = list(new.get("children", ())) + while stack: + child = stack.pop() + # Relocate the child to the new location + assert child["full_name"].startswith(original["full_name"]) + suffix = child["full_name"][len(original["full_name"]) :] + child["full_name"] = new["full_name"] + suffix + # The source lines for this placeholder do not exist in this file. + # The keys might not exist if original is a resolved placeholder. + child.pop("from_line_no", None) + child.pop("to_line_no", None) + # Resolve the remaining children + stack.extend(child.get("children", ())) + + placeholder.clear() + placeholder.update(new) + + +class PythonSphinxMapper(SphinxMapperBase): + + """Auto API domain handler for Python + + Parses directly from Python files. + + :param app: Sphinx application passed in as part of the extension + """ + + def load(self, patterns, dirs, ignore=None): + """Load objects from the filesystem into the ``paths`` dictionary + + Also include an attribute on the object, ``relative_path`` which is the + shortened, relative path the package/module + """ + for dir_ in dirs: + dir_root = dir_ + if os.path.exists(os.path.join(dir_, "__init__.py")): + dir_root = os.path.abspath(os.path.join(dir_, os.pardir)) + + for path in self.find_files(patterns=patterns, dirs=[dir_], ignore=ignore): + data = self.read_file(path=path) + if data: + data["relative_path"] = os.path.relpath(path, dir_root) + self.paths[path] = data + + def read_file(self, path, **kwargs): + """Read file input into memory, returning deserialized objects + + :param path: Path of file to read + """ + try: + parsed_data = Parser().parse_file(path) + return parsed_data + except (IOError, TypeError, ImportError): + LOGGER.warning("Error reading file: {0}".format(path)) + return None + + def _resolve_placeholders(self): + """Resolve objects that have been imported from elsewhere.""" + modules = {} + for module in self.paths.values(): + children = {child["name"]: child for child in module["children"]} + modules[module["name"]] = (module, children) + + resolved = set() + for module_name in modules: + visit_path = collections.OrderedDict() + _resolve_module_placeholders(modules, module_name, visit_path, resolved) + + def map(self, options=None): + self._resolve_placeholders() + + super(PythonSphinxMapper, self).map(options) + + parents = {obj.name: obj for obj in self.objects.values()} + for obj in self.objects.values(): + parent_name = obj.name.rsplit(".", 1)[0] + if parent_name in parents and parent_name != obj.name: + parent = parents[parent_name] + attr = "sub{}s".format(obj.type) + getattr(parent, attr).append(obj) + + for obj in self.objects.values(): + obj.submodules.sort() + obj.subpackages.sort() + + def create_class(self, data, options=None, **kwargs): + """Create a class from the passed in data + + :param data: dictionary data of parser output + """ + obj_map = dict( + (cls.type, cls) + for cls in [ + PythonClass, + PythonFunction, + PythonModule, + PythonMethod, + PythonPackage, + PythonAttribute, + PythonData, + PythonException, + ] + ) + try: + cls = obj_map[data["type"]] + except KeyError: + LOGGER.warning("Unknown type: %s" % data["type"]) + else: + obj = cls( + data, + class_content=self.app.config.autoapi_python_class_content, + options=self.app.config.autoapi_options, + jinja_env=self.jinja_env, + url_root=self.url_root, + **kwargs + ) + + lines = sphinx.util.docstrings.prepare_docstring(obj.docstring) + if lines and "autodoc-process-docstring" in self.app.events.events: + self.app.emit( + "autodoc-process-docstring", + cls.type, + obj.name, + None, # object + None, # options + lines, + ) + obj.docstring = "\n".join(lines) + + for child_data in data.get("children", []): + for child_obj in self.create_class( + child_data, options=options, **kwargs + ): + obj.children.append(child_obj) + yield obj diff --git a/autoapi/mappers/python/objects.py b/autoapi/mappers/python/objects.py new file mode 100644 index 0000000..230162f --- /dev/null +++ b/autoapi/mappers/python/objects.py @@ -0,0 +1,220 @@ +import collections + +from ..base import PythonMapperBase + + +class PythonPythonMapper(PythonMapperBase): + + language = "python" + is_callable = False + + def __init__(self, obj, class_content="class", **kwargs): + super(PythonPythonMapper, self).__init__(obj, **kwargs) + + self.name = obj["name"] + self.id = obj.get("full_name", self.name) + + # Optional + self.children = [] + self.args = obj.get("args") + self.docstring = obj["doc"] + + # For later + self.item_map = collections.defaultdict(list) + self._class_content = class_content + + @property + def args(self): + return self._args + + @args.setter + def args(self, value): + self._args = value + + @property + def docstring(self): + return self._docstring + + @docstring.setter + def docstring(self, value): + self._docstring = value + + @property + def is_undoc_member(self): + return not bool(self.docstring) + + @property + def is_private_member(self): + return self.short_name.startswith("_") and not self.short_name.endswith("__") + + @property + def is_special_member(self): + return self.short_name.startswith("__") and self.short_name.endswith("__") + + @property + def display(self): + if self.is_undoc_member and "undoc-members" not in self.options: + return False + if self.is_private_member and "private-members" not in self.options: + return False + if self.is_special_member and "special-members" not in self.options: + return False + return True + + @property + def summary(self): + for line in self.docstring.splitlines(): + line = line.strip() + if line: + return line + + return "" + + def _children_of_type(self, type_): + return list(child for child in self.children if child.type == type_) + + +class PythonFunction(PythonPythonMapper): + type = "function" + is_callable = True + ref_directive = "func" + + +class PythonMethod(PythonPythonMapper): + type = "method" + is_callable = True + ref_directive = "meth" + + def __init__(self, obj, **kwargs): + super(PythonMethod, self).__init__(obj, **kwargs) + + self.method_type = obj["method_type"] + + @property + def display(self): + if self.short_name == "__init__": + return False + + return super(PythonMethod, self).display + + +class PythonData(PythonPythonMapper): + """Global, module level data.""" + + type = "data" + + def __init__(self, obj, **kwargs): + super(PythonData, self).__init__(obj, **kwargs) + + self.value = obj.get("value") + + +class PythonAttribute(PythonData): + """An object/class level attribute.""" + + type = "attribute" + + +class TopLevelPythonPythonMapper(PythonPythonMapper): + ref_directive = "mod" + _RENDER_LOG_LEVEL = "VERBOSE" + + def __init__(self, obj, **kwargs): + super(TopLevelPythonPythonMapper, self).__init__(obj, **kwargs) + + self.top_level_object = "." not in self.name + + self.subpackages = [] + self.submodules = [] + self.all = obj["all"] + + @property + def functions(self): + return self._children_of_type("function") + + @property + def classes(self): + return self._children_of_type("class") + + +class PythonModule(TopLevelPythonPythonMapper): + type = "module" + + +class PythonPackage(TopLevelPythonPythonMapper): + type = "package" + + +class PythonClass(PythonPythonMapper): + type = "class" + + def __init__(self, obj, **kwargs): + super(PythonClass, self).__init__(obj, **kwargs) + + self.bases = obj["bases"] + + @PythonPythonMapper.args.getter + def args(self): + args = self._args + + constructor = self.constructor + if constructor: + args = constructor.args + + if args.startswith("self"): + args = args[4:].lstrip(",").lstrip() + + return args + + @PythonPythonMapper.docstring.getter + def docstring(self): + docstring = super(PythonClass, self).docstring + + if self._class_content in ("both", "init"): + constructor_docstring = self.constructor_docstring + if constructor_docstring: + if self._class_content == "both": + docstring = "{0}\n{1}".format(docstring, constructor_docstring) + else: + docstring = constructor_docstring + + return docstring + + @property + def methods(self): + return self._children_of_type("method") + + @property + def attributes(self): + return self._children_of_type("attribute") + + @property + def classes(self): + return self._children_of_type("class") + + @property + def constructor(self): + for child in self.children: + if child.short_name == "__init__": + return child + + return None + + @property + def constructor_docstring(self): + docstring = "" + + constructor = self.constructor + if constructor and constructor.docstring: + docstring = constructor.docstring + else: + for child in self.children: + if child.short_name == "__new__": + docstring = child.docstring + break + + return docstring + + +class PythonException(PythonClass): + type = "exception" diff --git a/autoapi/mappers/python/parser.py b/autoapi/mappers/python/parser.py new file mode 100644 index 0000000..0ed11bc --- /dev/null +++ b/autoapi/mappers/python/parser.py @@ -0,0 +1,224 @@ +import collections +import os + +import astroid +from . import astroid_utils + +try: + _TEXT_TYPE = unicode +except NameError: + _TEXT_TYPE = str + + +class Parser(object): + def __init__(self): + self._name_stack = [] + self._encoding = None + + def _get_full_name(self, name): + return ".".join(self._name_stack + [name]) + + def _encode(self, to_encode): + if self._encoding: + try: + return _TEXT_TYPE(to_encode, self._encoding) + except TypeError: + # The string was already in the correct format + pass + + return to_encode + + def parse_file(self, file_path): + directory, filename = os.path.split(file_path) + module_parts = [] + if filename != "__init__.py": + module_part = os.path.splitext(filename)[0] + module_parts = [module_part] + module_parts = collections.deque(module_parts) + while os.path.isfile(os.path.join(directory, "__init__.py")): + directory, module_part = os.path.split(directory) + if module_part: + module_parts.appendleft(module_part) + + module_name = ".".join(module_parts) + node = astroid.MANAGER.ast_from_file(file_path, module_name) + return self.parse(node) + + def parse_assign(self, node): + doc = "" + doc_node = node.next_sibling() + if isinstance(doc_node, astroid.nodes.Expr) and isinstance( + doc_node.value, astroid.nodes.Const + ): + doc = doc_node.value.value + + type_ = "data" + if isinstance( + node.scope(), astroid.nodes.ClassDef + ) or astroid_utils.is_constructor(node.scope()): + type_ = "attribute" + + assign_value = astroid_utils.get_assign_value(node) + if not assign_value: + return [] + + target, value = assign_value + data = { + "type": type_, + "name": target, + "full_name": self._get_full_name(target), + "doc": self._encode(doc), + "value": value, + "from_line_no": node.fromlineno, + "to_line_no": node.tolineno, + } + + return [data] + + def parse_classdef(self, node, data=None): + type_ = "class" + if astroid_utils.is_exception(node): + type_ = "exception" + + args = "" + try: + constructor = node.lookup("__init__")[1] + except IndexError: + pass + else: + if isinstance(constructor, astroid.nodes.FunctionDef): + args = constructor.args.as_string() + + basenames = list(astroid_utils.get_full_basenames(node.bases, node.basenames)) + + data = { + "type": type_, + "name": node.name, + "full_name": self._get_full_name(node.name), + "args": args, + "bases": basenames, + "doc": self._encode(node.doc or ""), + "from_line_no": node.fromlineno, + "to_line_no": node.tolineno, + "children": [], + } + + self._name_stack.append(node.name) + for child in node.get_children(): + child_data = self.parse(child) + if child_data: + data["children"].extend(child_data) + self._name_stack.pop() + + return [data] + + def _parse_property(self, node): + data = { + "type": "attribute", + "name": node.name, + "full_name": self._get_full_name(node.name), + "doc": self._encode(node.doc or ""), + "from_line_no": node.fromlineno, + "to_line_no": node.tolineno, + } + + return [data] + + def parse_functiondef(self, node): + if astroid_utils.is_decorated_with_property(node): + return self._parse_property(node) + if astroid_utils.is_decorated_with_property_setter(node): + return [] + + type_ = "function" if node.type == "function" else "method" + + data = { + "type": type_, + "name": node.name, + "full_name": self._get_full_name(node.name), + "args": node.args.as_string(), + "doc": self._encode(node.doc or ""), + "from_line_no": node.fromlineno, + "to_line_no": node.tolineno, + } + + if type_ == "method": + data["method_type"] = node.type + + result = [data] + + if node.name == "__init__": + for child in node.get_children(): + if isinstance(child, astroid.Assign): + child_data = self.parse_assign(child) + result.extend(data for data in child_data if data["doc"]) + + return result + + 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) + + data = { + "type": "placeholder", + "name": original_path if is_wildcard else (alias or name), + "full_name": full_name, + "original_path": original_path, + } + result.append(data) + + return result + + def parse_module(self, node): + path = node.path + if isinstance(node.path, list): + path = node.path[0] if node.path else None + + type_ = "module" + if node.package: + type_ = "package" + + self._name_stack = [node.name] + self._encoding = node.file_encoding + + data = { + "type": type_, + "name": node.name, + "full_name": node.name, + "doc": self._encode(node.doc or ""), + "children": [], + "file_path": path, + "encoding": node.file_encoding, + "all": astroid_utils.get_module_all(node), + } + + top_name = node.name.split(".", 1)[0] + for child in node.get_children(): + if node.package and astroid_utils.is_local_import_from(child, top_name): + child_data = self._parse_local_import_from(child) + else: + child_data = self.parse(child) + + if child_data: + data["children"].extend(child_data) + + return data + + def parse(self, node): + data = {} + + node_type = node.__class__.__name__.lower() + parse_func = getattr(self, "parse_" + node_type, None) + if parse_func: + data = parse_func(node) + else: + for child in node.get_children(): + data = self.parse(child) + if data: + break + + return data diff --git a/autoapi/templates/dotnet/base_detail.rst b/autoapi/templates/dotnet/base_detail.rst index 1449701..a197d68 100644 --- a/autoapi/templates/dotnet/base_detail.rst +++ b/autoapi/templates/dotnet/base_detail.rst @@ -6,23 +6,24 @@ {% endblock %} {% block summary %} - {%- if obj.summary %} + {% if obj.summary %} {{ obj.summary }} - {%- endif %} + {% endif %} {% endblock %} -{%- if obj.namespace %} +{% if obj.namespace %} Namespace :dn:ns:`{{ obj.namespace }}` -{%- endif %} -{%- if obj.assemblies %} + +{% endif %} +{% if obj.assemblies %} Assemblies - {%- for assembly in obj.assemblies %} + {% for assembly in obj.assemblies %} * {{ assembly }} - {%- endfor %} -{%- endif %} + {% endfor %} +{% endif %} ---- @@ -31,14 +32,14 @@ Assemblies {% block inheritance %} -{%- if obj.inheritance %} +{% if obj.inheritance %} Inheritance Hierarchy --------------------- {% for item in obj.inheritance %} * :dn:{{ item.ref_directive }}:`{{ item.ref_name }}` - {%- endfor %} + {% endfor %} * :dn:{{ obj.ref_directive }}:`{{ obj.ref_name }}` {% endif %} @@ -67,8 +68,8 @@ Syntax .. dn:{{ obj.ref_type }}:: {{ obj.name }} -{%- for item_type in obj.item_map.keys() %} -{%- if item_type in obj.item_map %} +{% for item_type in obj.item_map.keys() %} +{% if item_type in obj.item_map %} {{ item_type.title() }} {{ "-" * item_type|length }} @@ -81,7 +82,7 @@ Syntax {{ obj_item.render()|indent(4) }} {% endfor %} -{%- endif %} -{%- endfor %} +{% endif %} +{% endfor %} {% endblock %} diff --git a/autoapi/templates/dotnet/base_embed.rst b/autoapi/templates/dotnet/base_embed.rst index b2d33ad..62ca04a 100644 --- a/autoapi/templates/dotnet/base_embed.rst +++ b/autoapi/templates/dotnet/base_embed.rst @@ -1,30 +1,29 @@ .. dn:{{ obj.ref_type }}:: {{ obj.name }} {% if obj.summary %} - {{ obj.summary|indent(4) }} {% endif %} - {%- for param in obj.parameters %} + {% for param in obj.parameters %} {% if param.desc %} :param {{ param.name }}: {{ param.desc|indent(8) }} {% endif %} - {%- if param.type %} + {% if param.type %} :type {{ param.name }}: {{ param.type|indent(8) }} - {%- endif %} - {%- endfor %} + {% endif %} + {% endfor %} - {%- if obj.returns.type %} + {% if obj.returns.type %} :rtype: {{ obj.returns.type|indent(8) }} - {%- endif %} - {%- if obj.returns.description %} + {% endif %} + {% if obj.returns.description %} :return: {{ obj.returns.description|indent(8) }} - {%- endif %} + {% endif %} {% if obj.example %} .. code-block:: csharp {{ obj.example|indent(8) }} - {%- endif %} + {% endif %} diff --git a/autoapi/templates/dotnet/base_list.rst b/autoapi/templates/dotnet/base_list.rst index 7f9fd8f..f163527 100644 --- a/autoapi/templates/dotnet/base_list.rst +++ b/autoapi/templates/dotnet/base_list.rst @@ -41,11 +41,11 @@ {% block content %} -{%- macro display_type(item_type) %} +{% macro display_type(item_type) %} .. rubric:: {{ item_type.title() }} -{%- for obj_item in obj.item_map.get(item_type, []) %} +{% for obj_item in obj.item_map.get(item_type, []) %} {% macro render() %}{{ obj_item.summary }}{% endmacro %} {{ obj_item.type }} :dn:{{ obj_item.ref_directive }}:`{{ obj_item.ref_short_name }}` @@ -53,14 +53,14 @@ {{ render()|indent(8) }} -{%- endfor %} -{%- endmacro %} +{% endfor %} +{% endmacro %} .. dn:{{ obj.ref_type }}:: {{ obj.name }} -{%- for item_type in obj.item_map.keys() %} +{% for item_type in obj.item_map.keys() %} {{ display_type(item_type) }} -{%- endfor %} +{% endfor %} {% endblock %} diff --git a/autoapi/templates/go/base_member.rst b/autoapi/templates/go/base_member.rst index d03fef5..ad6e5b6 100644 --- a/autoapi/templates/go/base_member.rst +++ b/autoapi/templates/go/base_member.rst @@ -1,10 +1,10 @@ .. go:{{ obj.ref_type }}:: {{ obj.name }} -{%- if obj.type == 'func' -%} - {%- set argjoin = joiner(', ') -%} - ({%- for param in obj.parameters -%} +{% if obj.type == 'func' %} + {% set argjoin = joiner(', ') %} + ({% for param in obj.parameters %} {{ argjoin() }}{{ param.name }} {{ param.type }} - {%- endfor -%}) -{%- endif %} + {% endfor %}) +{% endif %} {% macro render() %}{{ obj.docstring }}{% endmacro %} {{ render()|indent(4) }} @@ -13,14 +13,14 @@ above #} {% for param in obj.parameters %} :type {{ param.name }}: {{ param.type }} - {%- endfor %} - {%- if obj.returns %} + {% endfor %} + {% if obj.returns %} :rtype: {{ obj.returns.type }} - {%- endif %} + {% endif %} - {% if obj.children -%} - {%- for child in obj.children|sort %} + {% if obj.children %} + {% for child in obj.children|sort %} {% macro render_child() %}{{ child.render() }}{% endmacro %} {{ render_child()|indent(4) }} - {%- endfor %} - {%- endif %} + {% endfor %} + {% endif %} diff --git a/autoapi/templates/go/package.rst b/autoapi/templates/go/package.rst index f0dee41..85cf057 100644 --- a/autoapi/templates/go/package.rst +++ b/autoapi/templates/go/package.rst @@ -4,7 +4,7 @@ {{ "=" * obj.name|length }} {% block toc %} - {%- if obj.children %} + {% if obj.children %} {# TODO Make this work .. toctree:: @@ -12,10 +12,10 @@ {% for item in obj.children|sort %} /autoapi/{{ item.id.split('.')|join('/') }}/index - {%- endfor %} + {% endfor %} #} - {%- endif %} + {% endif %} {% endblock %} {% if obj.docstring %} @@ -23,10 +23,10 @@ {% endif %} {% block content %} - {%- for obj_item in obj.children|sort %} + {% for obj_item in obj.children|sort %} {% macro render() %}{{ obj_item.render() }}{% endmacro %} {{ render()|indent(0) }} - {%- endfor %} + {% endfor %} {% endblock %} diff --git a/autoapi/templates/python/class.rst b/autoapi/templates/python/class.rst index 7c88204..1c082dd 100644 --- a/autoapi/templates/python/class.rst +++ b/autoapi/templates/python/class.rst @@ -1,35 +1,25 @@ -{%- if obj.display %} - +{% if obj.display %} .. py:{{ obj.type }}:: {{ obj.short_name }}{% if obj.args %}({{ obj.args }}){% endif %} - {%- if obj.bases %} - Bases: {%- for base in obj.bases %}:class:`{{ base }}`{%- if not loop.last %}, {% endif %}{% endfor %} + {% if obj.bases %} + Bases: {% for base in obj.bases %}:class:`{{ base }}`{% if not loop.last %}, {% endif %}{% endfor %} + {% endif %} - - {%- if obj.docstring %} - + {% if obj.docstring %} {{ obj.docstring|prepare_docstring|indent(3) }} - {% endif %} - - {%- for klass in obj.classes %} - + {% set visible_classes = obj.classes|selectattr("display")|list %} + {% for klass in visible_classes %} {{ klass.rendered|indent(3) }} - {% endfor %} - - {%- for attribute in obj.attributes %} - + {% set visible_attributes = obj.attributes|selectattr("display")|list %} + {% for attribute in visible_attributes %} {{ attribute.rendered|indent(3) }} - {% endfor %} - - {%- for method in obj.methods %} - + {% set visible_methods = obj.methods|selectattr("display")|list %} + {% for method in visible_methods %} {{ method.rendered|indent(3) }} - - {%- endfor %} - + {% endfor %} {% endif %} diff --git a/autoapi/templates/python/data.rst b/autoapi/templates/python/data.rst index 4051b66..a956b66 100644 --- a/autoapi/templates/python/data.rst +++ b/autoapi/templates/python/data.rst @@ -1,8 +1,7 @@ -{%- if obj.display %} - +{% if obj.display %} .. {{ obj.type }}:: {{ obj.name }} - {% if obj.value is not none %}:annotation: = {{ obj.value }} {% endif %} + {%+ if obj.value is not none %}:annotation: = {{ obj.value }}{% endif %} + {{ obj.docstring|prepare_docstring|indent(3) }} - {% endif %} diff --git a/autoapi/templates/python/function.rst b/autoapi/templates/python/function.rst index 76432b2..b48c473 100644 --- a/autoapi/templates/python/function.rst +++ b/autoapi/templates/python/function.rst @@ -1,9 +1,7 @@ -{%- if obj.display %} - +{% if obj.display %} .. function:: {{ obj.short_name }}({{ obj.args }}) {% if obj.docstring %} {{ obj.docstring|prepare_docstring|indent(3) }} {% endif %} - {% endif %} diff --git a/autoapi/templates/python/module.rst b/autoapi/templates/python/module.rst index bb6cc71..55df3f7 100644 --- a/autoapi/templates/python/module.rst +++ b/autoapi/templates/python/module.rst @@ -1,89 +1,94 @@ -{% if not obj.display %}:orphan:{% endif %} -{% if obj.docstring or obj.subpackages or obj.submodules or obj.children %} +{% if not obj.display %} +:orphan: + +{% endif %} :mod:`{{ obj.name }}` ======={{ "=" * obj.name|length }} .. py:module:: {{ obj.name }} -{%- if obj.docstring %} - +{% if obj.docstring %} .. autoapi-nested-parse:: {{ obj.docstring|prepare_docstring|indent(3) }} {% endif %} -{% block subpackages %}{% if obj.subpackages %} +{% block subpackages %} +{% set visible_subpackages = obj.subpackages|selectattr("display")|list %} +{% if visible_subpackages %} Subpackages ----------- .. toctree:: :titlesonly: :maxdepth: 3 -{% for subpackage in obj.subpackages %} - {%- if subpackage.display %} - {{ subpackage.short_name }}/index.rst - {% endif %} -{%- endfor %} -{% endif %}{% endblock %} -{% block submodules %}{% if obj.submodules %} +{% for subpackage in visible_subpackages %} + {{ subpackage.short_name }}/index.rst +{% endfor %} + + +{% endif %} +{% endblock %} +{% block submodules %} +{% set visible_submodules = obj.submodules|selectattr("display")|list %} +{% if visible_submodules %} Submodules ---------- .. toctree:: :titlesonly: :maxdepth: 1 -{% for submodule in obj.submodules %} - {%- if submodule.display %} + +{% for submodule in visible_submodules %} {{ submodule.short_name }}/index.rst - {% endif %} -{%- endfor %} -{% endif %}{% endblock %} +{% endfor %} -{% block content %}{% if obj.children %} +{% endif %} +{% endblock %} +{% block content %} +{% set visible_children = obj.children|selectattr("display")|list %} +{% if visible_children %} {{ obj.type|title }} Contents {{ "-" * obj.type|length }}--------- -{% if include_summaries %} -{% block classes %}{% if obj.classes %} +{% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %} +{% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %} +{% if include_summaries and (visible_classes or visible_functions) %} +{% block classes %} +{% if visible_classes %} Classes ~~~~~~~ .. autoapisummary:: -{% for klass in obj.classes %} + +{% for klass in visible_classes %} {{ klass.id }} -{%- endfor %} -{% endif %}{% endblock %} +{% endfor %} -{% block methods %}{% if obj.methods %} -Methods -~~~~~~~ -.. autoapisummary:: +{% endif %} +{% endblock %} -{% for method in obj.methods %} - {{ method.id }} -{%- endfor %} -{% endif %}{% endblock %} - -{% block functions %}{% if obj.functions %} +{% block functions %} +{% if visible_functions %} Functions ~~~~~~~~~ + .. autoapisummary:: -{% for function in obj.functions %} +{% for function in visible_functions %} {{ function.id }} -{%- endfor %} -{% endif %}{% endblock %} +{% endfor %} + + {% endif %} - -{%- for obj_item in obj.children %} - +{% endblock %} +{% endif %} +{% for obj_item in visible_children %} {% if obj.all is none or obj_item.short_name in obj.all %} {{ obj_item.rendered|indent(0) }} {% endif %} - {% endfor %} -{% endif %}{% endblock %} - {% endif %} +{% endblock %} diff --git a/tests/python/test_parser.py b/tests/python/test_parser.py index 8f33f1c..a342d8e 100644 --- a/tests/python/test_parser.py +++ b/tests/python/test_parser.py @@ -9,7 +9,7 @@ from textwrap import dedent import astroid import pytest -from autoapi.mappers.python import Parser +from autoapi.mappers.python.parser import Parser if sys.version_info < (3, 0): from StringIO import StringIO diff --git a/tests/test_astroid_utils.py b/tests/test_astroid_utils.py index efdb98a..5ac3123 100644 --- a/tests/test_astroid_utils.py +++ b/tests/test_astroid_utils.py @@ -1,5 +1,5 @@ import astroid -from autoapi.mappers import astroid_utils +from autoapi.mappers.python import astroid_utils import pytest diff --git a/tests/test_integration.py b/tests/test_integration.py index fdabd38..28aadab 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -71,7 +71,7 @@ class DotNetTests(LanguageIntegrationTests): return json.load(open("../fixtures/dotnet.json")) # Mock this because it's slow otherwise - def _dotnet_load(self, patterns, dirs, ignore=[]): + def _dotnet_load(self, patterns, dirs, ignore=()): data = self.read_file(path="inmem") self.paths["inmem"] = data