Source code for cattr.disambiguators

"""Utilities for union (sum type) disambiguation."""
from collections import OrderedDict
from functools import reduce
from operator import or_
from typing import (  # noqa: F401, imported for Mypy.
    Callable,
    Dict,
    Mapping,
    Optional,
    Type,
)

from attr import NOTHING, fields

from cattr._compat import get_origin


[docs]def create_uniq_field_dis_func(*classes: Type) -> Callable: """Given attr classes, generate a disambiguation function. The function is based on unique fields.""" if len(classes) < 2: raise ValueError("At least two classes required.") cls_and_attrs = [ (cl, set(at.name for at in fields(get_origin(cl) or cl))) for cl in classes ] if len([attrs for _, attrs in cls_and_attrs if len(attrs) == 0]) > 1: raise ValueError("At least two classes have no attributes.") # TODO: Deal with a single class having no required attrs. # For each class, attempt to generate a single unique required field. uniq_attrs_dict = OrderedDict() # type: Dict[str, Type] cls_and_attrs.sort(key=lambda c_a: -len(c_a[1])) fallback = None # If none match, try this. for i, (cl, cl_reqs) in enumerate(cls_and_attrs): other_classes = cls_and_attrs[i + 1 :] if other_classes: other_reqs = reduce(or_, (c_a[1] for c_a in other_classes)) uniq = cl_reqs - other_reqs if not uniq: m = "{} has no usable unique attributes.".format(cl) raise ValueError(m) # We need a unique attribute with no default. cl_fields = fields(get_origin(cl) or cl) for attr_name in uniq: if getattr(cl_fields, attr_name).default is NOTHING: break else: raise ValueError(f"{cl} has no usable non-default attributes.") uniq_attrs_dict[attr_name] = cl else: fallback = cl def dis_func(data): # type: (Mapping) -> Optional[Type] if not isinstance(data, Mapping): raise ValueError("Only input mappings are supported.") for k, v in uniq_attrs_dict.items(): if k in data: return v return fallback return dis_func