"""This module contains **autodoc_pydantic**'s autodocumenters."""
from __future__ import annotations
import json
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable
import sphinx
from pydantic import BaseModel
from pydantic_settings import BaseSettings
from sphinx.ext.autodoc import (
AttributeDocumenter,
ClassDocumenter,
Documenter,
MethodDocumenter,
)
from sphinx.util.docstrings import prepare_docstring
from sphinx.util.inspect import object_description
from sphinx.util.typing import OptionSpec, get_type_hints
try:
from sphinx.util.typing import stringify_annotation
except ImportError:
# fall back to older name for older versions of Sphinx
from sphinx.util.typing import ( # type: ignore[no-redef]
stringify as stringify_annotation,
)
from sphinxcontrib.autodoc_pydantic.directives.options.composites import AutoDocOptions
from sphinxcontrib.autodoc_pydantic.directives.options.definition import (
OPTIONS_FIELD,
OPTIONS_MERGED,
OPTIONS_MODEL,
OPTIONS_SETTINGS,
OPTIONS_VALIDATOR,
)
from sphinxcontrib.autodoc_pydantic.directives.options.enums import (
OptionsFieldDocPolicy,
OptionsJsonErrorStrategy,
OptionsSummaryListOrder,
)
from sphinxcontrib.autodoc_pydantic.directives.templates import to_collapsable
from sphinxcontrib.autodoc_pydantic.directives.utility import (
intercept_type_annotations_py_gt_39,
)
from sphinxcontrib.autodoc_pydantic.inspection import (
ASTERISK_FIELD_NAME,
ModelInspector,
ValidatorFieldMap,
)
if TYPE_CHECKING:
from docutils.statemachine import StringList
from pydantic import BaseModel
[docs]
class PydanticAutoDoc:
"""Composite to provide single namespace to access all **autodoc_pydantic**
relevant documenter directive functionalities.
"""
def __init__(self, documenter: Documenter, is_child: bool) -> None: # noqa: FBT001
self._documenter = documenter
self._is_child = is_child
self._inspect: ModelInspector | None = None
self._options = AutoDocOptions(self._documenter)
self._model: type[BaseModel] | None = None
@property
def model(self) -> type[BaseModel]:
"""Lazily load pydantic model after initialization. For more, please
read `inspect` doc string.
"""
if self._model is not None:
return self._model
if self._is_child:
self._model = self._documenter.parent
else:
self._model = self._documenter.object
return self._model
@property
def options(self) -> AutoDocOptions:
"""Provides access to :obj:`PydanticDocumenterOptions` to handle
global and local configuration settings.
"""
return self._options
@property
def inspect(self) -> ModelInspector:
"""Provides :obj:`ModelInspector` to access all inspection methods.
You may wonder why this ``inspect`` is a property instead of a simple
attribute being defined in the ``__init__`` method of this class. The
reason is the following: auto-documenters do not have their ``object``
attribute being correctly set after instantiation which typically holds
a reference to the corresponding pydantic model and objects to be
documented. Instead, ``object`` is ``None`` after plain instantiation
(executing ``__init__``). However, this composite class is added during
instantiation of the autodocumenter for consistency reasons. Therefore,
``ModelInspector`` can't be created at instantiation time of this class
because the ``object`` is still ``None``. Hence, it is lazily created
once the inspection methods are first required. At this point in time,
it is guaranteed by the auto-documenter base class that ``object`` is
then already correctly provided and the ``ModelInspector`` works as
expected.
"""
if self._inspect:
return self._inspect
obj = self._documenter.parent if self._is_child else self._documenter.object
self._inspect = ModelInspector(obj)
return self._inspect
[docs]
def get_field_name_or_alias(self, field_name: str) -> str:
"""If `field-swap-name-and-alias` is enabled, provide alias (if
present) for given field.
"""
if self.options.is_true('field-swap-name-and-alias'):
return self.inspect.fields.get_alias_or_name(field_name)
return field_name
[docs]
def get_non_inherited_members(self) -> set[str]:
"""Return all member names of autodocumented object which are
prefiltered to exclude inherited members.
"""
object_members = self._documenter.get_object_members(want_all=True)[1]
return {x.__name__ for x in object_members} # type: ignore[union-attr]
def get_base_class_names(self) -> list[str]:
return [x.__name__ for x in self.model.__mro__]
[docs]
def resolve_inherited_validator_reference(self, ref: str) -> str:
"""Provide correct validator reference in case validator is inherited
and explicitly shown in docs via directive option
`inherited-members`.
More concretely, inherited validators are not shown from parent class
unless directive option `inherited-members` is used. The validator
references may either point to the parent class or the child class.
This logic is implemented here.
"""
ref_parts = ref.split('.')
class_name = ref_parts[-2]
# early exit if ref class name equals model name -> no inheritance
if class_name == self.model.__name__:
return ref
validator_name = ref_parts[-1]
base_class_names = self.get_base_class_names()
is_base_class = class_name in base_class_names
is_inherited = self.options.exists('inherited-members')
is_member = validator_name in self.inspect.validators.names
if is_member and is_base_class and is_inherited:
ref_parts[-2] = self.model.__name__
return '.'.join(ref_parts)
return ref
[docs]
class PydanticModelDocumenter(ClassDocumenter):
"""Represents specialized Documenter subclass for pydantic models."""
objtype = 'pydantic_model'
directivetype = 'pydantic_model'
priority = 10 + ClassDocumenter.priority
option_spec = ClassDocumenter.option_spec.copy()
option_spec.update({**OPTIONS_MODEL, **OPTIONS_MERGED})
pyautodoc_pass_to_directive = ('model-signature-prefix',)
pyautodoc_set_default_option = (
'member-order',
'undoc-members',
)
pyautodoc_prefix = 'model'
[docs]
@classmethod
def can_document_member(
cls,
member: Any, # noqa: ANN401
membername: str,
isattr: bool, # noqa: FBT001
parent: Any, # noqa: ANN401
) -> bool:
"""Filter only pydantic models."""
is_val = super().can_document_member(member, membername, isattr, parent)
is_model = ModelInspector.static.is_pydantic_model(member)
return is_val and is_model
def __init__(self, *args) -> None: # noqa: ANN002
super().__init__(*args)
exclude_members = self.options.setdefault('exclude-members', set())
exclude_members.add('model_fields')
exclude_members.add('model_config')
exclude_members.add('model_computed_fields')
self.pydantic = PydanticAutoDoc(self, is_child=False)
[docs]
def document_members(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
"""Modify member options before starting to document members."""
self.pydantic.options.set_members_all()
if self.options.get('undoc-members') is False:
self.options.pop('undoc-members')
if self.pydantic.options.is_false('show-validator-members', prefix=True):
self.hide_validator_members()
if self.pydantic.options.is_true('hide-reused-validator', prefix=True):
self.hide_reused_validators()
if self.pydantic.options.exists('inherited-members'):
self.hide_inherited_members()
super().document_members(*args, **kwargs)
[docs]
def hide_inherited_members(self) -> None:
"""If inherited-members is set, make sure that these are excluded from
the class documenter, too"""
exclude_members = self.options['exclude-members']
squash_set = self.options['inherited-members']
for cl in self.pydantic.model.__mro__:
if cl.__name__ in squash_set:
for item in dir(cl):
exclude_members.add(item)
[docs]
def hide_validator_members(self) -> None:
"""Add validator names to `exclude_members`."""
validators = self.pydantic.inspect.validators.names
exclude_members = self.options['exclude-members']
exclude_members.update(validators)
[docs]
def hide_reused_validators(self) -> None:
"""Add reused validators to `exclude_members` option."""
validators = self.pydantic.inspect.validators
reused_validators = validators.get_reused_validators_names()
exclude_members = self.options['exclude-members']
exclude_members.update(reused_validators)
[docs]
def add_content(
self,
more_content: StringList | None,
**kwargs, # noqa: ANN003
) -> None:
"""Delegate additional content creation."""
super().add_content(more_content, **kwargs)
# do not provide any additional info if documented as attribute
if self.doc_as_attr:
return
if self.pydantic.options.is_true('erdantic-figure', prefix=True):
self.add_erdantic_figure()
if self.pydantic.options.is_true('show-json', prefix=True):
self.add_collapsable_schema()
if self.pydantic.options.is_true('show-config-summary', prefix=True):
self.add_config_summary()
if self.pydantic.options.is_true('show-field-summary', prefix=True):
self.add_field_summary()
if self.pydantic.options.is_true('show-validator-summary', prefix=True):
self.add_validators_summary()
[docs]
def add_collapsable_schema(self) -> None:
"""Adds collapse code block containing JSON schema."""
non_serializable = self.pydantic.inspect.fields.non_json_serializable
# handle non serializable fields
strategy = self.pydantic.options.get_value('show-json-error-strategy')
if non_serializable:
error_msg = (
f"JSON schema can't be generated for '{self.fullname}' "
f"because the following pydantic fields can't be serialized "
f'properly: {non_serializable}.'
)
if strategy == OptionsJsonErrorStrategy.WARN:
logger = sphinx.util.logging.getLogger(__name__)
logger.warning(error_msg, location='autodoc_pydantic')
elif strategy == OptionsJsonErrorStrategy.RAISE:
raise sphinx.errors.ExtensionError(error_msg)
elif strategy != OptionsJsonErrorStrategy.COERCE:
error_msg = (
f"Invalid option provided for 'show-json-error-strategy'. "
f'Allowed values are f{OptionsJsonErrorStrategy.values()}'
)
raise sphinx.errors.ExtensionError(error_msg)
schema = self.pydantic.inspect.schema.sanitized
schema_rest = self._convert_json_schema_to_rest(schema)
source_name = self.get_sourcename()
for line in schema_rest:
self.add_line(line, source_name)
[docs]
def add_config_summary(self) -> None:
"""Adds summary section describing the model configuration."""
if not self.pydantic.inspect.config.is_configured:
return
cfg_items = self.pydantic.inspect.config.items
source_name = self.get_sourcename()
self.add_line(':Config:', source_name)
for name, value in cfg_items.items():
line = f' - **{name}**: *{type(value).__name__} = {value}*'
self.add_line(line, source_name)
self.add_line('', source_name)
def _get_idx_mappings(self, members: Iterable[str]) -> dict[str, int]:
"""Get index positions for given members while respecting
`OptionsSummaryListOrder`.
"""
sorted_members = self._sort_summary_list(members)
return {name: idx for idx, name in enumerate(sorted_members)}
def _get_reference_sort_func(self, references: list[ValidatorFieldMap]) -> Callable:
"""Helper function to create sorting function for instances of
`ValidatorFieldMap` which first sorts by validator name and second by
field name while respecting `OptionsSummaryListOrder`.
This is used for validator summary section.
"""
all_fields = [ref.field_name for ref in references]
all_validators = [ref.validator_name for ref in references]
idx_validators = self._get_idx_mappings(all_validators)
idx_fields = self._get_idx_mappings(all_fields)
def sort_func(reference: ValidatorFieldMap) -> tuple[int, int]:
return (
idx_validators.get(reference.validator_name, -1),
idx_fields.get(reference.field_name, -1),
)
return sort_func
def _get_validator_summary_references(self) -> list[ValidatorFieldMap]:
"""Filter and sort validator-field mappings for validator summary
section.
"""
base_class_validators = self._get_base_model_validators()
inherited_validators = self._get_inherited_validators()
references = base_class_validators + inherited_validators
sort_func = self._get_reference_sort_func(references)
return sorted(references, key=sort_func)
def _build_validator_summary_rest_line(self, reference: ValidatorFieldMap) -> str:
"""Generates reST line for validator-field mapping with references for
validator summary section.
"""
name = self.pydantic.get_field_name_or_alias(reference.field_name)
validator_ref = self.pydantic.resolve_inherited_validator_reference(
reference.validator_ref,
)
return (
f' - '
f':py:obj:`{reference.validator_name} <{validator_ref}>`'
f' ยป '
f':py:obj:`{name} <{reference.field_ref}>`'
)
[docs]
def add_validators_summary(self) -> None:
"""Adds summary section describing all validators with corresponding
fields.
"""
if not self.pydantic.inspect.validators:
return
sorted_references = self._get_validator_summary_references()
source_name = self.get_sourcename()
self.add_line(':Validators:', source_name)
for ref in sorted_references:
line = self._build_validator_summary_rest_line(ref)
self.add_line(line, source_name)
self.add_line('', source_name)
def _get_base_model_validators(self) -> list[ValidatorFieldMap]:
"""Return the validators on the model being documented"""
result = []
base_model_fields = set(self._get_base_model_fields())
base_object = self.object_name
references = self.pydantic.inspect.references.mappings
# The validator is considered part of the base_object if
# the field that is being validated is on the object being
# documented, if the method that is doing the validating
# is on that object (even if that method is validating
# an inherited field)
for ref in references:
if ref.field_name in base_model_fields:
result.append(ref)
else:
validator_class = ref.validator_ref.split('.')[-2]
if validator_class == base_object:
result.append(ref)
return result
def _get_inherited_validators(self) -> list[ValidatorFieldMap]:
"""Return the validators on inherited fields to be documented,
if any"""
if not self.pydantic.options.exists('inherited-members'):
return []
squash_set = self.options['inherited-members']
references = self.pydantic.inspect.references.mappings
base_object = self.object_name
already_documented = self._get_base_model_validators()
result = []
for ref in references:
if ref in already_documented:
continue
validator_class = ref.validator_ref.split('.')[-2]
foreign_validator = validator_class != base_object
not_ignored = validator_class not in squash_set
if foreign_validator and not_ignored:
result.append(ref)
return result
[docs]
def add_field_summary(self) -> None:
"""Adds summary section describing all fields."""
if not self.pydantic.inspect.fields:
return
base_class_fields = self._get_base_model_fields()
inherited_fields = self._get_inherited_fields()
valid_fields = base_class_fields + inherited_fields
sorted_fields = self._sort_summary_list(valid_fields)
source_name = self.get_sourcename()
self.add_line(':Fields:', source_name)
for field_name in sorted_fields:
line = self._get_field_summary_line(field_name)
self.add_line(line, source_name)
self.add_line('', source_name)
def _get_base_model_fields(self) -> list[str]:
"""Returns all field names that are valid members of pydantic model."""
fields = self.pydantic.inspect.fields.names
valid_members = self.pydantic.get_non_inherited_members()
return [field for field in fields if field in valid_members]
def _get_inherited_fields(self) -> list[str]:
"""Return the inherited fields if inheritance is enabled"""
if not self.pydantic.options.exists('inherited-members'):
return []
fields = self.pydantic.inspect.fields.names
base_class_fields = self.pydantic.get_non_inherited_members()
return [field for field in fields if field not in base_class_fields]
def _get_tagorder(self, name: str) -> int | None:
"""Get tagorder for given `name`."""
if self.analyzer is None:
return None
if name in self.analyzer.tagorder:
return self.analyzer.tagorder.get(name)
for base in self.pydantic.get_base_class_names():
name_with_class = f'{base}.{name}'
if name_with_class in self.analyzer.tagorder:
return self.analyzer.tagorder.get(name_with_class)
if name == ASTERISK_FIELD_NAME:
return -1
return None
def _sort_summary_list(self, names: Iterable[str]) -> list[str]:
"""Sort member names according to given sort order
`OptionsSummaryListOrder`.
"""
sort_order = self.pydantic.options.get_value(
name='summary-list-order', prefix=True, force_availability=True
)
if sort_order == OptionsSummaryListOrder.ALPHABETICAL:
def sort_func(name: str) -> str:
return name
elif sort_order == OptionsSummaryListOrder.BYSOURCE:
def sort_func(name: str) -> int: # type: ignore[misc]
tagorder = self._get_tagorder(name)
# catch cases where field is not found in tagorder
if tagorder is None:
msg = f'Field {name} in {self.object_name} not found in tagorder'
raise ValueError(msg)
return tagorder
try:
return sorted(names, key=sort_func)
except TypeError as e:
msg = (
f'Uncaught exception while sorting fields for model '
f'{self.name} with sort order {sort_order}.'
)
raise ValueError(msg).with_traceback(e.__traceback__) from e
def _get_field_summary_line(self, field_name: str) -> str:
"""Get reST for field summary for given `member_name`."""
ref_func = self.pydantic.inspect.references.create_model_reference
name = self.pydantic.get_field_name_or_alias(field_name)
ref = ref_func(field_name)
typ = self._stringify_type(field_name)
return f' - :py:obj:`{name} ({typ}) <{ref}>`'
def _stringify_type(self, field_name: str) -> str:
"""Get proper string representation of type for given `member_nane`
relying on sphinx functionality.
"""
type_aliases = self.config.autodoc_type_aliases
annotations = get_type_hints(self.object, None, type_aliases)
return stringify_annotation(annotations.get(field_name, ''))
@staticmethod
def _convert_json_schema_to_rest(schema: dict) -> list[str]:
"""Convert model's schema dict into reST."""
schema_str = json.dumps(schema, default=str, indent=3)
lines = [f' {line}' for line in schema_str.split('\n')]
lines = ['.. code-block:: json', '', *lines]
return to_collapsable(
lines,
'Show JSON schema',
'autodoc_pydantic_collapsable_json',
)
[docs]
class PydanticSettingsDocumenter(PydanticModelDocumenter):
"""Represents specialized Documenter subclass for pydantic settings."""
objtype = 'pydantic_settings'
directivetype = 'pydantic_settings'
priority = 10 + ClassDocumenter.priority
option_spec = ClassDocumenter.option_spec.copy()
option_spec.update({**OPTIONS_SETTINGS, **OPTIONS_MERGED})
pyautodoc_pass_to_directive = ('settings-signature-prefix',)
pyautodoc_set_default_option = (
'member-order',
'undoc-members',
)
pyautodoc_prefix = 'settings'
def __init__(self, *args) -> None: # noqa: ANN002
super().__init__(*args)
self.options['exclude-members'].add('settings_customise_sources')
[docs]
@classmethod
def can_document_member(
cls,
member: Any, # noqa: ANN401
membername: str,
isattr: bool, # noqa: FBT001
parent: Any, # noqa: ANN401
) -> bool:
"""Filter only pydantic models."""
is_val = super().can_document_member(member, membername, isattr, parent)
if is_val:
return issubclass(member, BaseSettings)
return False
[docs]
class PydanticFieldDocumenter(AttributeDocumenter):
"""Represents specialized Documenter subclass for pydantic fields."""
objtype = 'pydantic_field'
directivetype = 'pydantic_field'
priority = 10 + AttributeDocumenter.priority
option_spec: ClassVar[OptionSpec] = dict(AttributeDocumenter.option_spec)
option_spec.update(OPTIONS_FIELD)
member_order = 0
pyautodoc_pass_to_directive = (
'field-signature-prefix',
'field-show-alias',
'field-swap-name-and-alias',
)
def __init__(self, *args) -> None: # noqa: ANN002
super().__init__(*args)
self.pydantic = PydanticAutoDoc(self, is_child=True)
[docs]
@classmethod
def can_document_member(
cls,
member: Any, # noqa: ANN401
membername: str,
isattr: bool, # noqa: FBT001
parent: Any, # noqa: ANN401
) -> bool:
"""Filter only pydantic fields."""
is_valid = super().can_document_member(member, membername, isattr, parent)
is_field = ModelInspector.static.is_pydantic_field(
parent=parent.object,
field_name=membername,
)
return is_valid and is_field and isattr
@property
def pydantic_field_name(self) -> str:
"""Provide the pydantic field name which refers to the member name of
the parent pydantic model.
"""
return self.objpath[-1]
@property
def needs_required_marker(self) -> bool:
"""Indicate if field should be marked as required."""
field_name = self.pydantic_field_name
is_required = self.pydantic.inspect.fields.is_required(field_name)
show_required = self.pydantic.options.is_true('field-show-required')
return is_required and show_required
@property
def needs_optional_marker(self) -> bool:
"""Indicate if field should be marked as optional."""
field_name = self.pydantic_field_name
check_func = self.pydantic.inspect.fields.has_default_factory
has_default_factory = check_func(field_name)
show_optional = self.pydantic.options.is_true('field-show-optional')
return has_default_factory and show_optional
[docs]
def get_default_value(self) -> str:
"""Gets the default value of pydantic field as reST."""
field_name = self.pydantic_field_name
default = self.pydantic.inspect.fields.get(field_name).default
value = object_description(default)
return f' :value: {value}'
[docs]
def add_default_value_or_marker(self) -> None:
"""Adds default value or a marker for field being required or optional."""
sourcename = self.get_sourcename()
show_default = self.pydantic.options.is_true('field-show-default')
if self.needs_required_marker:
self.add_line(' :required:', sourcename)
elif self.needs_optional_marker:
self.add_line(' :optional:', sourcename)
elif show_default:
self.add_line(self.get_default_value(), sourcename)
[docs]
def add_alias(self) -> None:
"""Adds alias directive option."""
field_name = self.pydantic_field_name
field = self.pydantic.inspect.fields.get(field_name)
alias_given = field.alias and field.alias != field_name
show_alias = self.pydantic.options.is_true('field-show-alias')
swap = self.pydantic.options.is_true('field-swap-name-and-alias')
alias_required = show_alias or swap
if alias_given and alias_required:
sourcename = self.get_sourcename()
self.add_line(f' :alias: {field.alias}', sourcename)
@property
def needs_doc_string(self) -> bool:
"""Indicate if docstring from attribute should be added to field."""
doc_policy = self.pydantic.options.get_value('field-doc-policy')
return doc_policy != OptionsFieldDocPolicy.DESCRIPTION
@property
def needs_description(self) -> bool:
"""Indicate if pydantic field description should be added to field."""
doc_policy = self.pydantic.options.get_value('field-doc-policy')
is_enabled = doc_policy in (
OptionsFieldDocPolicy.BOTH,
OptionsFieldDocPolicy.DESCRIPTION,
)
description = self._get_field_description()
has_description = bool(description)
identical_doc = description == self._get_pydantic_sanitized_doc_string()
is_duplicated = identical_doc and self.needs_doc_string
return is_enabled and has_description and not is_duplicated
[docs]
def add_content(
self,
more_content: StringList | None,
**kwargs, # noqa: ANN003
) -> None:
"""Delegate additional content creation."""
if self.needs_doc_string:
super().add_content(more_content, **kwargs)
if self.needs_description:
self.add_description()
if self.pydantic.options.is_true('field-show-constraints'):
self.add_constraints()
if self.pydantic.options.is_true('field-list-validators'):
self.add_validators()
[docs]
def add_constraints(self) -> None:
"""Adds section showing all defined constraints."""
field_name = self.pydantic_field_name
constraints = self.pydantic.inspect.fields.get_constraints(field_name)
if constraints:
source_name = self.get_sourcename()
self.add_line(':Constraints:', source_name)
for key, value in constraints.items():
line = f' - **{key}** = {value}'
self.add_line(line, source_name)
self.add_line('', source_name)
def _get_field_description(self) -> str:
"""Get field description from schema if present."""
field_name = self.pydantic_field_name
func = self.pydantic.inspect.fields.get_property_from_field_info
return func(field_name, 'description')
def _get_pydantic_sanitized_doc_string(self) -> str:
"""Helper to get sanitized docstring for pydantic field that
uses same formatting as pydantic's method to extract the doc
string for automated field description provisioning.
"""
docstrings = self.get_doc()
if not docstrings:
return ''
docstring = docstrings[0] # first element is always the docstring
without_last = docstring[:-1] # last element is always empty
substitute_linebreaks = ['\n\n' if x == '' else x for x in without_last]
return ''.join(substitute_linebreaks)
[docs]
def add_description(self) -> None:
"""Adds description from schema if present."""
description = self._get_field_description()
tabsize = self.directive.state.document.settings.tab_width
lines = prepare_docstring(description, tabsize=tabsize)
source_name = self.get_sourcename()
for line in lines:
self.add_line(line, source_name)
[docs]
def add_validators(self) -> None:
"""Add section with all validators that process this field."""
field_name = self.pydantic_field_name
func = self.pydantic.inspect.references.filter_by_field_name
references = func(field_name)
sorted_references = sorted(references, key=lambda x: x.validator_name)
if not references:
return
source_name = self.get_sourcename()
self.add_line(':Validated by:', source_name)
for reference in sorted_references:
resolver = self.pydantic.resolve_inherited_validator_reference
ref = resolver(reference.validator_ref)
line = f' - :py:obj:`{reference.validator_name} <{ref}>`'
self.add_line(line, source_name)
self.add_line('', source_name)
[docs]
def add_line(self, line: str, source: str, *lineno: int) -> None:
"""Intercept added rst lines to handle edge cases such as correct
string representation for annotated types in python < 3.9.
"""
line = intercept_type_annotations_py_gt_39(line)
super().add_line(line, source, *lineno)
[docs]
class PydanticValidatorDocumenter(MethodDocumenter):
"""Represents specialized Documenter subclass for pydantic validators."""
objtype = 'pydantic_validator'
directivetype = 'pydantic_validator'
member_order = 50
priority = 10 + MethodDocumenter.priority
option_spec = MethodDocumenter.option_spec.copy()
option_spec.update(OPTIONS_VALIDATOR)
pyautodoc_pass_to_directive = (
'validator-signature-prefix',
'validator-replace-signature',
'field-swap-name-and-alias',
)
def __init__(self, *args: Any) -> None: # noqa: ANN401
super().__init__(*args)
self.pydantic = PydanticAutoDoc(self, is_child=True)
[docs]
@classmethod
def can_document_member(
cls,
member: Any, # noqa: ANN401
membername: str,
isattr: bool, # noqa: FBT001
parent: Any, # noqa: ANN401
) -> bool:
"""Filter only pydantic validators."""
is_val = super().can_document_member(member, membername, isattr, parent)
is_validator = ModelInspector.static.is_validator_by_name(
membername, parent.object
)
return is_val and is_validator
[docs]
def add_content(
self,
more_content: StringList | None,
**kwargs, # noqa: ANN003
) -> None:
"""Optionally show validator content."""
super().add_content(more_content, **kwargs)
if self.pydantic.options.is_true('validator-list-fields'):
self.add_field_list()
def _build_field_list_rest_line(self, reference: ValidatorFieldMap) -> str:
"""Generates reST line for field reference for field list section."""
name = self.pydantic.get_field_name_or_alias(reference.field_name)
return f' - :py:obj:' f'`{name} ' f'<{reference.field_ref}>`'
[docs]
def add_field_list(self) -> None:
"""Adds a field list with all fields that are validated by this
validator.
"""
func = self.pydantic.inspect.references.filter_by_validator_name
references = func(self.object_name)
if not references:
return
source_name = self.get_sourcename()
self.add_line(':Validates:', source_name)
for reference in references:
line = self._build_field_list_rest_line(reference)
self.add_line(line, source_name)
self.add_line('', source_name)