You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sphinx-autoapi/autoapi/_objects.py

536 lines
16 KiB
Python

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