Source code for sphinxcontrib.autodoc_pydantic.directives.directives

"""This module contains **autodoc_pydantic**'s directives.

"""

from typing import Tuple, Union, List

import sphinx
from docutils.nodes import Text
from docutils.parsers.rst.directives import unchanged
from sphinx.addnodes import (
    desc_signature,
    desc_annotation, desc_name, pending_xref
)
from sphinx.domains.python import PyMethod, PyAttribute, PyClasslike, py_sig_re
from sphinxcontrib.autodoc_pydantic.inspection import ModelInspector, \
    ValidatorFieldMap
from sphinxcontrib.autodoc_pydantic.directives.options.composites import (
    DirectiveOptions
)
from sphinxcontrib.autodoc_pydantic.directives.utility import \
    create_field_href, remove_node_by_tagname
from sphinxcontrib.autodoc_pydantic.directives.options.validators import \
    option_default_true, option_list_like

TUPLE_STR = Tuple[str, str]


[docs]class PydanticDirectiveBase: """Base class for pydantic directive providing common functionality. """ config_name: str default_prefix: str def __init__(self, *args): super().__init__(*args) self.pyautodoc = DirectiveOptions(self)
[docs] def get_signature_prefix(self, sig: str) -> Union[str, List[Text]]: """Overwrite original signature prefix with custom pydantic ones. """ config_name = f"{self.config_name}-signature-prefix" prefix = self.pyautodoc.get_value(config_name) value = prefix or self.default_prefix # account for changed signature in sphinx 4.3, see #62 if sphinx.version_info >= (4, 3): from sphinx.addnodes import desc_sig_space return [Text(value), desc_sig_space()] else: return f"{value} "
[docs]class PydanticModel(PydanticDirectiveBase, PyClasslike): """Specialized directive for pydantic models. """ option_spec = PyClasslike.option_spec.copy() option_spec.update({"__doc_disable_except__": option_list_like, "model-signature-prefix": unchanged}) config_name = "model" default_prefix = "class"
[docs]class PydanticSettings(PydanticDirectiveBase, PyClasslike): """Specialized directive for pydantic settings. """ option_spec = PyClasslike.option_spec.copy() option_spec.update({"__doc_disable_except__": option_list_like, "settings-signature-prefix": unchanged}) config_name = "settings" default_prefix = "class"
[docs]class PydanticField(PydanticDirectiveBase, PyAttribute): """Specialized directive for pydantic fields. """ option_spec = PyAttribute.option_spec.copy() option_spec.update({"alias": unchanged, "field-show-alias": option_default_true, "field-swap-name-and-alias": option_default_true, "required": option_default_true, "optional": option_default_true, "__doc_disable_except__": option_list_like, "field-signature-prefix": unchanged}) config_name = "field" default_prefix = "attribute"
[docs] def get_field_name(self, sig: str) -> str: """Get field name from signature. Borrows implementation from `PyObject.handle_signature`. """ return py_sig_re.match(sig).groups()[1]
[docs] def add_required(self, signode: desc_signature): """Add `[Required]` if directive option `required` is set. """ if self.options.get("required"): signode += desc_annotation("", " [Required]")
[docs] def add_optional(self, signode: desc_signature): """Add `[Optional]` if directive option `optional` is set. """ if self.options.get("optional"): signode += desc_annotation("", " [Optional]")
[docs] def add_alias_or_name(self, sig: str, signode: desc_signature): """Add alias or name to signature. Alias is added if `show-alias` is enabled. Name is added if both `show-alias` and `swap-name-and-alias` is enabled. """ if not self.pyautodoc.get_value("field-show-alias"): return elif self.pyautodoc.is_true("field-swap-name-and-alias"): prefix = "name" value = self.get_field_name(sig) else: prefix = "alias" value = self.options.get("alias") signode += desc_annotation("", f" ({prefix} '{value}')")
def _find_desc_name_node(self, sig: str, signode: desc_signature) -> desc_name: """Return `desc_name` node from `signode` that contains the field name. This is used to replace the name with the alias. """ name = self.get_field_name(sig) for node in signode.children: has_correct_text = node.astext() == name is_desc_name = isinstance(node, desc_name) if has_correct_text and is_desc_name: return node
[docs] def swap_name_and_alias(self, sig: str, signode: desc_signature): """Replaces name with alias if `swap-name-and-alias` is enabled. Requires to replace existing `addnodes.desc_name` because name node is added within `handle_signature` and this can't be intercepted or overwritten otherwise. """ if not self.pyautodoc.get_value("field-swap-name-and-alias"): return name_node = self._find_desc_name_node(sig, signode) if not name_node: logger = sphinx.util.logging.getLogger(__name__) logger.warning("Field's `desc_name` node can't be located to " "swap name with alias.", location="autodoc_pydantic") else: text_node = Text(self.options.get("alias")) text_node.parent = name_node name_node.children[0] = text_node
[docs] def handle_signature(self, sig: str, signode: desc_signature) -> TUPLE_STR: """Optionally call add alias method. """ fullname, prefix = super().handle_signature(sig, signode) self.add_required(signode) self.add_optional(signode) if self.options.get("alias") is not None: self.add_alias_or_name(sig, signode) self.swap_name_and_alias(sig, signode) return fullname, prefix
[docs]class PydanticValidator(PydanticDirectiveBase, PyMethod): """Specialized directive for pydantic validators. """ option_spec = PyMethod.option_spec.copy() option_spec.update({"validator-replace-signature": option_default_true, "__doc_disable_except__": option_list_like, "validator-signature-prefix": unchanged, "field-swap-name-and-alias": option_default_true}) config_name = "validator" default_prefix = "classmethod"
[docs] def get_field_href_from_mapping( self, inspector: ModelInspector, mapping: ValidatorFieldMap) -> pending_xref: """Generate the field reference node aka `pending_xref` from given validator-field `mapping` while respecting field name/alias swap possibility. """ name = mapping.field_name if self.pyautodoc.is_true("field-swap-name-and-alias"): name = inspector.fields.get_alias_or_name(mapping.field_name) return create_field_href(name=name, ref=mapping.field_ref, env=self.env)
[docs] def replace_return_node(self, signode: desc_signature): """Replaces the return node with references to validated fields. """ remove_node_by_tagname(signode.children, "desc_parameterlist") # replace nodes class_name = "autodoc_pydantic_validator_arrow" signode += desc_annotation("", " » ", classes=[class_name]) # get imports, names and fields of validator name = signode["fullname"].split(".")[-1] inspector = ModelInspector.from_child_signode(signode) mappings = inspector.references.filter_by_validator_name(name) # add field reference nodes signode += self.get_field_href_from_mapping(inspector=inspector, mapping=mappings[0]) for mapping in mappings[1:]: signode += desc_annotation("", ", ") signode += self.get_field_href_from_mapping(inspector=inspector, mapping=mapping)
[docs] def handle_signature(self, sig: str, signode: desc_signature) -> TUPLE_STR: """Optionally call replace return node method. """ fullname, prefix = super().handle_signature(sig, signode) if self.pyautodoc.get_value("validator-replace-signature"): self.replace_return_node(signode) return fullname, prefix
[docs]class PydanticConfigClass(PydanticDirectiveBase, PyClasslike): """Specialized directive for pydantic config class. """ option_spec = PyClasslike.option_spec.copy() option_spec.update({"__doc_disable_except__": option_list_like, "config-signature-prefix": unchanged}) config_name = "config" default_prefix = "class"