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
def generate_mapping(cl: Type, old_mapping): mapping = {} for p, t in zip(get_origin(cl).__parameters__, get_args(cl)): if isinstance(t, TypeVar): continue mapping[p.__name__] = t if not mapping: return old_mapping cls = attr.make_class( "GenericMapping", {x: attr.attrib() for x in mapping.keys()}, frozen=True, ) return cls(**mapping)
def make_dict_structure_fn(cl: Type, converter, **kwargs): """Generate a specialized dict structuring function for an attrs class.""" mapping = None if is_generic(cl): base = get_origin(cl) mapping = generate_mapping(cl, mapping) cl = base for base in getattr(cl, "__orig_bases__", ()): if is_generic(base) and not str(base).startswith("typing.Generic"): mapping = generate_mapping(base, mapping) break if isinstance(cl, TypeVar): cl = getattr(mapping, cl.__name__, cl) cl_name = cl.__name__ fn_name = "structure_" + cl_name # We have generic paramters and need to generate a unique name for the function for p in getattr(cl, "__parameters__", ()): # This is nasty, I am not sure how best to handle `typing.List[str]` or `TClass[int, int]` as a parameter type here name_base = getattr(mapping, p.__name__) name = getattr(name_base, "__name__", str(name_base)) name = re.sub(r"[\[\.\] ,]", "_", name) fn_name += f"_{name}" globs = {"__c_s": converter.structure, "__cl": cl, "__m": mapping} lines = [] post_lines = [] attrs = cl.__attrs_attrs__ if any(isinstance(a.type, str) for a in attrs): # PEP 563 annotations - need to be resolved. resolve_types(cl) lines.append(f"def {fn_name}(o, *_):") lines.append(" res = {") for a in attrs: an = a.name override = kwargs.pop(an, _neutral) type = a.type if isinstance(type, TypeVar): type = getattr(mapping, type.__name__, type) ian = an if an[0] != "_" else an[1:] kn = an if override.rename is None else override.rename globs[f"__c_t_{an}"] = type if a.default is NOTHING: lines.append(f" '{ian}': __c_s(o['{kn}'], __c_t_{an}),") else: post_lines.append(f" if '{kn}' in o:") post_lines.append( f" res['{ian}'] = __c_s(o['{kn}'], __c_t_{an})" ) lines.append(" }") total_lines = lines + post_lines + [" return __cl(**res)"] eval(compile("\n".join(total_lines), "", "exec"), globs) return globs[fn_name]