from typing import AbstractSet, Iterable
from attr import NOTHING, Attribute, Factory
from marshmallow import missing, post_load
from ..base import GeneratedFields, NamedConfigs
from ..exceptions import AnnotationConversionError
from ..scheme import AnnotationSchema, BaseConverter
__all__ = ("AttrsConverter", "AttrsSchema")
__SENTINEL = object()
def _is_attrs(target):
return getattr(target, "__attrs_attrs__", __SENTINEL) is not __SENTINEL
def _get_attr_from_attrs(attrs: Iterable[Attribute], name: str) -> Attribute:
attrs = [a for a in attrs if a.name == name]
return attrs[0]
def _should_include_default(attr):
if not attr.init:
return False
# an attrs factory cannot be copied over to a marshmallow field despite
# marshmallow fields being able to accept a callable there because some
# attrs factories accept the newly created instance as an argument
# which is something marshmallow doesn't support
# see following issue for mypy ignore
# https://github.com/python/mypy/issues/3060
return attr.default != NOTHING and not isinstance( # type: ignore
attr.default, Factory
)
[docs]class AttrsConverter(BaseConverter):
def convert_all(
self,
target: type,
ignore: AbstractSet[str] = frozenset([]), # noqa
configs: NamedConfigs = None,
) -> GeneratedFields:
self._ensure_all_hints_are_attribs(target, ignore)
return super().convert_all(target, ignore, configs)
def _get_field_defaults(self, target):
return {
a.name: a.default
for a in target.__attrs_attrs__
if _should_include_default(a)
}
def _preprocess_typehint(self, typehint, kwargs, field_name, target):
# while this seems contradictory to this converter, we need to
# ignore attrs specific actions when a container type, e.g. a List[T],
# contains a non-attrs manufactured type because this is recursively
# called during schema generation.
# however those types will still require an entry in the registry
# is handled outside this converter
if not _is_attrs(target):
return
attr = _get_attr_from_attrs(target.__attrs_attrs__, field_name)
if attr.default != NOTHING:
# default to optional even if the typehint isn't
# but don't override the user saying otherwise
kwargs.setdefault("required", False)
kwargs.setdefault("missing", missing)
def _postprocess_typehint(self, typehint, kwargs, field_name, target):
# see _preprocess_typehint for details
if not _is_attrs(target):
return
attr = _get_attr_from_attrs(target.__attrs_attrs__, field_name)
if not attr.init:
# force into dump only mode if the field won't be accepted
# into __init__ -- even if the user told us otherwise
kwargs["dump_only"] = True
if attr.metadata:
kwargs.update(attr.metadata)
def _ensure_all_hints_are_attribs(self, target, ignore):
# This would happen if an attrs handled class was subclassed by
# a plain ol' python class and added more non-ignored type hinted
# fields. In theory we could handle this but we won't
hints = {k for k, _ in self._get_type_hints(target, ignore)}
attribs = {a.name for a in target.__attrs_attrs__ if a.name not in ignore}
if hints != attribs:
raise AnnotationConversionError(
f"Mismatched hints and attr.ibs. Got hints: {hints} "
f"but attr.ibs {attribs}"
)