Source code for eradiate.attrs
"""``attrs``-based utility classes and functions."""
from __future__ import annotations
import enum
import re
from textwrap import dedent, indent
import attrs
from .util import numpydoc
class _Auto:
"""
Sentinel class to indicate when a dynamic field value is expected to be
set automatically. ``_Auto`` is a singleton. There is only ever one of it.
Notes
-----
``bool(_Auto)`` evaluates to ``False``.
"""
_singleton = None
def __new__(cls):
if _Auto._singleton is None:
_Auto._singleton = super(_Auto, cls).__new__(cls)
return _Auto._singleton
def __repr__(self):
return "AUTO"
def __bool__(self):
return False
def __len__(self):
return 0
#: Typing alias for :data:`.AUTO`.
AutoType = _Auto
#: Sentinel to indicate when a dynamic field value is expected to be set
#: automatically.
AUTO = _Auto()
class MetadataKey(enum.Enum):
"""
Attribute metadata keys.
These Enum values should be used as metadata attribute keys.
"""
DOC = enum.auto() #: Documentation for this field (str)
TYPE = enum.auto() #: Documented type for this field (str)
INIT_TYPE = enum.auto() #: Documented constructor parameter for this field (str)
DEFAULT = enum.auto() #: Documented default value for this field (str)
class DocFlags(enum.Flag):
"""
Extra flags used to pass information about docs.
"""
NOINIT = 0
# ------------------------------------------------------------------------------
# Attribute docs extension
# ------------------------------------------------------------------------------
@attrs.define
class _FieldDoc:
"""Internal convenience class to store field documentation information."""
doc = attrs.field(default=None)
type = attrs.field(default=None)
init_type = attrs.field(default=None)
default = attrs.field(default=None)
def _eradiate_formatter(cls_doc: str, field_docs: dict[str, _FieldDoc]) -> str:
"""
Appends a section on attributes to a class docstring.
This docstring formatter is appropriate for Eradiate's current docstring
format.
Parameters
----------
cls_doc : str
Class docstring to extend.
field_docs : dict[str, _FieldDoc]
Attributes documentation content.
Returns
-------
str
Updated class docstring.
"""
# Do nothing if field is not documented
if not field_docs:
return cls_doc
docstrings = []
# Create docstring entry for each documented field
for field_name, field_doc in field_docs.items():
type_doc = f": {field_doc.type}" if field_doc.type is not None else ""
default_doc = f" = {field_doc.default}" if field_doc.default is not None else ""
docstrings.append(
f"``{field_name.lstrip('_')}``{type_doc}{default_doc}\n"
f"{indent(field_doc.doc, ' ')}\n"
)
# Assemble entries
if docstrings:
if cls_doc is None:
cls_doc = ""
return "\n".join(
(
dedent(cls_doc.lstrip("\n")).rstrip(),
"",
".. rubric:: Constructor arguments / instance attributes",
"",
"\n".join(docstrings),
"",
)
)
else:
return cls_doc
def _numpy_formatter(cls_doc: str, field_docs: dict[str, _FieldDoc]) -> str:
"""
Append a section on attributes to a class docstring.
This docstring formatter is appropriate for the Numpy docstring format.
Parameters
----------
cls_doc : str
Class docstring to extend.
field_docs : dict[str, _FieldDoc]
Attributes documentation content.
Returns
-------
str
Updated class docstring.
"""
# Do nothing if no field is documented
if not field_docs:
return cls_doc
param_docstrings = []
attr_docstrings = []
# Create docstring entry for each documented field
for field_name, field_doc in field_docs.items():
field_type = field_doc.type
field_init_type = field_doc.init_type
# Generate constructor parameter docstring entry
if field_init_type is not DocFlags.NOINIT:
init_type_doc = (
f" : {field_init_type}" if field_init_type is not None else ""
)
default_doc = (
f", default: {field_doc.default}"
if field_doc.default is not None
else ""
)
param_docstrings.append(
f"{field_name.lstrip('_')}{init_type_doc}{default_doc}\n"
f"{indent(field_doc.doc, ' ')}\n"
)
# Generate attribute docstring entry
type_doc = field_type if field_type is not None else ""
if not field_name.startswith("_"):
field_doc_brief = re.split(r"\. |\.\n", field_doc.doc)[0].strip()
if not field_doc_brief.endswith("."):
field_doc_brief += "."
attr_docstrings.append(
f"{field_name} : {type_doc}\n"
f"{indent(field_doc_brief, ' ')}\n"
)
# Assemble entries
if param_docstrings or attr_docstrings:
if cls_doc is None:
cls_doc = ""
cls_doc = dedent(cls_doc.lstrip("\n")).rstrip()
# Parse docstrings
sections = numpydoc.parse_doc(cls_doc)
# Append generated docstrings to the relevant section
sections["Parameters"] = "\n".join(param_docstrings) + sections.get(
"Parameters", ""
)
sections["Fields"] = "\n".join(attr_docstrings) + sections.get("Fields", "")
# Generate section full text
doc = numpydoc.format_doc(sections)
return doc
else:
return cls_doc
[docs]def parse_docs(cls: type) -> type:
"""
Extract attribute documentation and update class docstring with it.
This decorator will examine each ``attrs`` attribute and check its metadata
for documentation content. It will then update the class's docstring
based on this content.
Parameters
----------
cls : type
Class whose attributes should be processed.
Returns
-------
type
Updated class.
See Also
--------
:func:`documented` : Field documentation definition function.
Notes
-----
* Meant to be used as a class decorator.
* Must be applied **after** :func:`@attr.s <attr.s>` (or any other
attrs decorator).
* Fields must be documented using :func:`documented`.
"""
formatter = _numpy_formatter
docs = {}
for field in cls.__attrs_attrs__:
if MetadataKey.DOC in field.metadata:
# Collect field docstring
docs[field.name] = _FieldDoc(doc=field.metadata[MetadataKey.DOC])
# Collect field type
if MetadataKey.TYPE in field.metadata:
docs[field.name].type = field.metadata[MetadataKey.TYPE]
else:
docs[field.name].type = str(field.type)
# Collect field init type
if MetadataKey.INIT_TYPE in field.metadata:
docs[field.name].init_type = field.metadata[MetadataKey.INIT_TYPE]
elif field.init is False:
docs[field.name].init_type = DocFlags.NOINIT
else:
docs[field.name].init_type = docs[field.name].type
# Collect default value
if MetadataKey.DEFAULT in field.metadata:
docs[field.name].default = field.metadata[MetadataKey.DEFAULT]
# Update docstring
cls.__doc__ = formatter(cls.__doc__, docs)
return cls
[docs]def documented(
attrib: attrs.Attribute,
doc: str | None = None,
type: str | None = None,
init_type: str | None = None,
default: str | None = None,
) -> attrs.Attribute:
"""
Declare an attrs field as documented.
Parameters
----------
attrib : attrs.Attribute
``attrs`` attribute definition to which documentation is to be attached.
doc : str, optional
Docstring for the considered field. If set to ``None``, this function
does nothing.
type : str, optional
Documented type for the considered field.
init_type : str, optional
Documented constructor parameter for the considered field.
default : str, optional
Documented default value for the considered field.
Returns
-------
attrs.Attribute
``attrib``, with metadata updated with documentation contents.
See Also
--------
:func:`attrs.field`, :func:`pinttr.field`, :func:`parse_docs`
"""
if doc is not None:
attrib.metadata[MetadataKey.DOC] = doc
if type is not None:
attrib.metadata[MetadataKey.TYPE] = type
if init_type is not None:
attrib.metadata[MetadataKey.INIT_TYPE] = init_type
if default is not None:
attrib.metadata[MetadataKey.DEFAULT] = default
return attrib
[docs]def get_doc(cls: type, attrib: str, field: str) -> str:
"""
Fetch attribute documentation field. Requires field metadata to be processed
with :func:`documented`.
Parameters
----------
cls : type
Class from which to get the attribute.
attrib : str
Attribute from which to get the doc field.
field : {"doc", "type", "init_type", "default"}
Documentation field to query.
Returns
-------
str
Queried documentation content.
Raises
------
ValueError
If the requested ``field`` is missing from the target attribute's
metadata.
ValueError
If the requested ``field`` is unsupported.
"""
try:
if field == "doc":
return attrs.fields_dict(cls)[attrib].metadata[MetadataKey.DOC]
if field == "type":
return attrs.fields_dict(cls)[attrib].metadata[MetadataKey.TYPE]
if field == "init_type":
return attrs.fields_dict(cls)[attrib].metadata[MetadataKey.INIT_TYPE]
if field == "default":
return attrs.fields_dict(cls)[attrib].metadata[MetadataKey.DEFAULT]
except KeyError:
raise ValueError(
f"{cls.__name__}.{attrib} has no documented field " f"'{field}'"
)
raise ValueError(f"unsupported attribute doc field {field}")