"""This module defines the documented objects classes. - the generic [`Object`][pytkdocs.objects.Object] class - the [`Module`][pytkdocs.objects.Module] class - the [`Class`][pytkdocs.objects.Class] class - the [`Method`][pytkdocs.objects.Method] class - the [`Function`][pytkdocs.objects.Function] class - the [`Attribute`][pytkdocs.objects.Attribute] class Note that properties are considered attributes, because they are used like such. It also defines a convenient [`Source`][pytkdocs.objects.Source] class to represent source code. """ import importlib import inspect import os import sys from abc import ABCMeta from functools import lru_cache from pathlib import Path from typing import Any, Optional, Union from pytkdocs.parsers.docstrings.base import Parser, Section from pytkdocs.properties import NAME_CLASS_PRIVATE, NAME_PRIVATE, NAME_SPECIAL, ApplicableNameProperty class Source: """Helper class to represent source code. It is simply used to wrap the result of [`inspect.getsourceslines`](https://docs.python.org/3/library/inspect.html#inspect.getsourcelines). """ def __init__(self, lines: Union[str, list[str]], line_start: int) -> None: """Initialize the object. Arguments: lines: A list of strings. The strings should have trailing newlines. line_start: The line number of where the code starts in the file. """ code = "".join(lines) if isinstance(lines, list) else lines self.code = code """The code, as a single string.""" self.line_start = line_start """The first line number.""" class Object(metaclass=ABCMeta): # noqa: B024 """A base class to store information about a Python object. Each instance additionally stores references to its children, grouped by category. """ possible_name_properties: list[ApplicableNameProperty] = [] # noqa: RUF012 """ The properties that we can apply to the object based on its name. The applicable properties vary from one subclass of `Object` to another. """ def __init__( self, name: str, path: str, file_path: str, docstring: Optional[str] = "", properties: Optional[list[str]] = None, source: Optional[Source] = None, ) -> None: """Initialize the object. Arguments: name: The object's name. path: The object's dotted-path. file_path: The file path of the object's direct parent module. docstring: The object's docstring. properties: The object's properties. source: The object's source code. """ self.name = name """The object's name.""" self.path = path """The object's dotted-path.""" self.file_path = file_path """The file path of the object's direct parent module.""" self.docstring = docstring """The object's docstring.""" self.docstring_sections: list[Section] = [] """The object's docstring parsed into sections.""" self.docstring_errors: list[str] = [] """The errors detected while parsing the docstring.""" self.properties = properties or [] """The object's properties.""" self.parent: Optional[Object] = None """The object's parent (another instance of a subclass of `Object`).""" self.source = source """The object's source code.""" self._path_map = {self.path: self} self._parsed = False self.attributes: list[Attribute] = [] """The list of all the object's attributes.""" self.methods: list[Method] = [] """The list of all the object's methods.""" self.functions: list[Function] = [] """The list of all the object's functions.""" self.modules: list[Module] = [] """The list of all the object's submodules.""" self.classes: list[Class] = [] """The list of all the object's classes.""" self.children: list[Object] = [] """The list of all the object's children.""" def __str__(self) -> str: return self.path @property def category(self) -> str: """Return the object's category. Returns: The object's category (module, class, function, method or attribute). """ return self.__class__.__name__.lower() @property def root(self) -> "Object": """Return the object's root. Returns: The object's root (top-most parent). """ obj = self while obj.parent: obj = obj.parent return obj @property def relative_file_path(self) -> str: """Return the relative file path of the object. It is the relative path to the object's module, starting at the path of the top-most package it is contained in. For example: - package is `a` - package absolute path is `/abs/path/to/a` - module is `a.b.c` - object is `c` or anything defined in `c` - relative file path is `a/b/c.py` If the relative file path cannot be determined, the value returned is `""` (empty string). Returns: The path relative to the object's package. """ parts = self.path.split(".") namespaces = [".".join(parts[:length]) for length in range(1, len(parts) + 1)] # Iterate through all sub namespaces including the last in case it is a module for namespace in namespaces: try: importlib.import_module(namespace) top_package = sys.modules[namespace] except (ModuleNotFoundError, ImportError, KeyError): # ImportError: Triggered if the namespace is not importable # ModuleNotFoundError: Triggered if the namespace is not a module # KeyError: Triggered if the imported package isn't referenced under the same fully qualified name # Namespace packages are importable, so this should work for them return "" try: top_package_path = Path(inspect.getabsfile(top_package)).parent return str(Path(self.file_path).relative_to(top_package_path.parent)) except TypeError: # Triggered if getabsfile() can't be found in the case of a Namespace package pass except ValueError: # Triggered if Path().relative_to can't find an appropriate path return "" return "" @property def name_to_check(self) -> str: """Return the attribute to check against name-properties regular expressions (private, class-private, special). Returns: The attribute to check (its name). """ return self.name @property def name_properties(self) -> list[str]: """Return the object's name properties. Returns: The object's name properties (private, class-private, special). """ properties = [] for prop, predicate in self.possible_name_properties: if predicate(self.name_to_check): properties.append(prop) return properties @property def parent_path(self) -> str: """Return the parent's path, computed from the current path. The parent object path is not used: this property is used to see if an object is really related to another one, to add it as a child to the other. When we do that, the child doesn't even have a parent. Returns: The dotted path of the parent object. """ return self.path.rsplit(".", 1)[0] def add_child(self, obj: "Object") -> None: """Add an object as a child of this object. If the child computed `parent_path` is not equal to this object's path, abort. Append the child to the `children` list, and to the right category list. Arguments: obj: An instance of documented object. """ if obj.parent_path != self.path: return self.children.append(obj) if isinstance(obj, Module): self.modules.append(obj) elif isinstance(obj, Class): self.classes.append(obj) elif isinstance(obj, Function): self.functions.append(obj) elif isinstance(obj, Method): self.methods.append(obj) elif isinstance(obj, Attribute): # Dataclass attributes with default values will already be present in `self.attributes` as they are # resolved differently by the python interpreter. As they have a concrete value, they are already present # in the "original" class. They should be overridden with the new "dataclass" attribute coming in here # (having the "dataclass_field" property set) new_attribute_name = obj.name for attribute in self.attributes: if attribute.name == new_attribute_name: self.attributes.remove(attribute) self.attributes.append(obj) obj.parent = self self._path_map[obj.path] = obj def add_children(self, children: list["Object"]) -> None: """Add a list of objects as children of this object. Arguments: children: The list of children to add. """ for child in children: self.add_child(child) def parse_docstring(self, parser: Parser, **context: Any) -> None: """Parse the docstring of this object. Arguments: parser: A parser to parse the docstrings. **context: Additional context to use when parsing. """ if self.docstring and not self._parsed: sections, errors = parser.parse(self.docstring, {"obj": self, **context}) self.docstring_sections = sections self.docstring_errors = errors self._parsed = True def parse_all_docstrings(self, parser: Parser) -> None: """Recursively parse the docstring of this object and its children. Arguments: parser: A parser to parse the docstrings. """ self.parse_docstring(parser) for child in self.children: child.parse_all_docstrings(parser) @lru_cache # noqa: B019 def has_contents(self) -> bool: """Tells if the object has "contents". An object has contents when: - it is the root of the object tree - it has a docstring - at least one of its children (whatever the depth) has contents The value is cached, so this method should be called last, when the tree doesn't change anymore. Returns: Whether this object has contents or not. """ has_docstring = bool(self.docstring) is_root = not self.parent children_have_contents = any(child.has_contents() for child in self.children) return has_docstring or is_root or children_have_contents class Module(Object): """A class to store information about a module.""" possible_name_properties: list[ApplicableNameProperty] = [NAME_SPECIAL, NAME_PRIVATE] # noqa: RUF012 @property def file_name(self) -> str: """Return the base name of the module file, without the extension. Returns: The module file's base name. """ return os.path.splitext(os.path.basename(self.file_path))[0] @property def name_to_check(self) -> str: # noqa: D102 return self.file_name class Class(Object): """A class to store information about a class.""" possible_name_properties: list[ApplicableNameProperty] = [NAME_PRIVATE] # noqa: RUF012 def __init__(self, *args: Any, bases: Optional[list[str]] = None, **kwargs: Any): """Initialize the object. Arguments: *args: Arguments passed to the parent class Initialize the object. bases: The base classes (dotted paths). **kwargs: Arguments passed to the parent class Initialize the object. """ super().__init__(*args, **kwargs) self.bases = bases or ["object"] class Function(Object): """A class to store information about a function. It accepts an additional `signature` argument at instantiation. """ possible_name_properties: list[ApplicableNameProperty] = [NAME_PRIVATE] # noqa: RUF012 def __init__(self, *args: Any, signature: Optional[inspect.Signature] = None, **kwargs: Any): """Initialize the object. Arguments: *args: Arguments passed to the parent class Initialize the object. signature: The function signature. **kwargs: Arguments passed to the parent class Initialize the object. """ super().__init__(*args, **kwargs) self.signature = signature class Method(Object): """A class to store information about a method. It accepts an additional `signature` argument at instantiation. """ possible_name_properties: list[ApplicableNameProperty] = [NAME_SPECIAL, NAME_PRIVATE] # noqa: RUF012 def __init__(self, *args: Any, signature: Optional[inspect.Signature] = None, **kwargs: Any): """Initialize the object. Arguments: *args: Arguments passed to the parent class Initialize the object. signature: The function signature. **kwargs: Arguments passed to the parent class Initialize the object. """ super().__init__(*args, **kwargs) self.signature = signature class Attribute(Object): """A class to store information about an attribute. It accepts an additional `attr_type` argument at instantiation. """ possible_name_properties: list[ApplicableNameProperty] = [NAME_SPECIAL, NAME_CLASS_PRIVATE, NAME_PRIVATE] # noqa: RUF012 def __init__(self, *args: Any, attr_type: Optional[Any] = None, **kwargs: Any): """Initialize the object. Arguments: *args: Arguments passed to the parent class Initialize the object. attr_type: The attribute type. **kwargs: Arguments passed to the parent class Initialize the object. """ super().__init__(*args, **kwargs) self.type = attr_type