diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 0a6b065f..088bade0 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -3,7 +3,19 @@ from dataclasses import Field from enum import Enum from functools import lru_cache -from typing import Any, Callable, Dict, Optional, Tuple, Type, TypeVar, Union +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + NoReturn, + Optional, + Tuple, + Type, + TypeVar, + Union, +) from attr import Attribute from attr import has as attrs_has @@ -19,6 +31,7 @@ OriginAbstractSet, OriginMutableSet, Sequence, + Set, fields, get_newtype_base, get_origin, @@ -43,6 +56,11 @@ from .dispatch import MultiStrategyDispatch from .gen import ( AttributeOverride, + DictStructureFn, + HeteroTupleUnstructureFn, + IterableUnstructureFn, + MappingStructureFn, + MappingUnstructureFn, make_dict_structure_fn, make_dict_unstructure_fn, make_hetero_tuple_unstructure_fn, @@ -63,26 +81,26 @@ class UnstructureStrategy(Enum): AS_TUPLE = "astuple" -def _subclass(typ): +def _subclass(typ: Type) -> Callable[[Type], bool]: """a shortcut""" return lambda cls: issubclass(cls, typ) -def is_attrs_union(typ): +def is_attrs_union(typ: Type) -> bool: return is_union_type(typ) and all(has(get_origin(e) or e) for e in typ.__args__) -def is_attrs_union_or_none(typ): +def is_attrs_union_or_none(typ: Type) -> bool: return is_union_type(typ) and all( e is NoneType or has(get_origin(e) or e) for e in typ.__args__ ) -def is_optional(typ): +def is_optional(typ: Type) -> bool: return is_union_type(typ) and NoneType in typ.__args__ and len(typ.__args__) == 2 -def is_literal_containing_enums(typ): +def is_literal_containing_enums(typ: Type) -> bool: return is_literal(typ) and any(isinstance(val, Enum) for val in typ.__args__) @@ -212,8 +230,8 @@ def register_unstructure_hook(self, cls: Any, func: Callable[[Any], Any]) -> Non self._unstructure_func.register_cls_list([(cls, func)]) def register_unstructure_hook_func( - self, check_func: Callable[[Any], bool], func: Callable[[T], Any] - ): + self, check_func: Callable[[Any], bool], func: Callable[[Any], Any] + ) -> None: """Register a class-to-primitive converter function for a class, using a function to check if it's a match. """ @@ -235,7 +253,9 @@ def register_unstructure_hook_factory( """ self._unstructure_func.register_func_list([(predicate, factory, True)]) - def register_structure_hook(self, cl: Any, func: Callable[[Any, Type[T]], T]): + def register_structure_hook( + self, cl: Any, func: Callable[[Any, Type[T]], T] + ) -> None: """Register a primitive-to-class converter function for a type. The converter function should take two arguments: @@ -255,7 +275,7 @@ def register_structure_hook(self, cl: Any, func: Callable[[Any, Type[T]], T]): def register_structure_hook_func( self, check_func: Callable[[Type[T]], bool], func: Callable[[Any, Type[T]], T] - ): + ) -> None: """Register a class-to-primitive converter function for a class, using a function to check if it's a match. """ @@ -283,7 +303,7 @@ def structure(self, obj: Any, cl: Type[T]) -> T: return self._structure_func.dispatch(cl)(obj, cl) # Classes to Python primitives. - def unstructure_attrs_asdict(self, obj) -> Dict[str, Any]: + def unstructure_attrs_asdict(self, obj: Any) -> Dict[str, Any]: """Our version of `attrs.asdict`, so we can call back to us.""" attrs = fields(obj.__class__) dispatch = self._unstructure_func.dispatch @@ -294,7 +314,7 @@ def unstructure_attrs_asdict(self, obj) -> Dict[str, Any]: rv[name] = dispatch(a.type or v.__class__)(v) return rv - def unstructure_attrs_astuple(self, obj) -> Tuple[Any, ...]: + def unstructure_attrs_astuple(self, obj: Any) -> Tuple[Any, ...]: """Our version of `attrs.astuple`, so we can call back to us.""" attrs = fields(obj.__class__) dispatch = self._unstructure_func.dispatch @@ -305,22 +325,22 @@ def unstructure_attrs_astuple(self, obj) -> Tuple[Any, ...]: res.append(dispatch(a.type or v.__class__)(v)) return tuple(res) - def _unstructure_enum(self, obj): + def _unstructure_enum(self, obj: Enum) -> Any: """Convert an enum to its value.""" return obj.value @staticmethod - def _unstructure_identity(obj): + def _unstructure_identity(obj: T) -> T: """Just pass it through.""" return obj - def _unstructure_seq(self, seq): + def _unstructure_seq(self, seq: Sequence[T]) -> Sequence[T]: """Convert a sequence to primitive equivalents.""" # We can reuse the sequence class, so tuples stay tuples. dispatch = self._unstructure_func.dispatch return seq.__class__(dispatch(e.__class__)(e) for e in seq) - def _unstructure_mapping(self, mapping): + def _unstructure_mapping(self, mapping: Mapping[T, V]) -> Mapping[T, V]: """Convert a mapping of attr classes to primitive equivalents.""" # We can reuse the mapping class, so dicts stay dicts and OrderedDicts @@ -331,7 +351,10 @@ def _unstructure_mapping(self, mapping): for k, v in mapping.items() ) - def _unstructure_union(self, obj): + # note: Use UnionType when 3.11 is released as + # the behaviour of @final is changed. This would + # affect how we can support UnionType in ._compat.py + def _unstructure_union(self, obj: Any) -> Any: """ Unstructure an object as a union. @@ -342,19 +365,21 @@ def _unstructure_union(self, obj): # Python primitives to classes. @staticmethod - def _structure_error(_, cl): + def _structure_error(_, cl: Type) -> NoReturn: """At the bottom of the condition stack, we explode if we can't handle it.""" msg = "Unsupported type: {0!r}. Register a structure hook for " "it.".format(cl) raise StructureHandlerNotFoundError(msg, type_=cl) - def _gen_structure_generic(self, cl): + def _gen_structure_generic(self, cl: Type[T]) -> DictStructureFn[T]: """Create and return a hook for structuring generics.""" fn = make_dict_structure_fn( cl, self, _cattrs_prefer_attrib_converters=self._prefer_attrib_converters ) return fn - def _gen_attrs_union_structure(self, cl): + def _gen_attrs_union_structure( + self, cl: Any + ) -> Callable[[Any, Type[T]], Optional[Type[T]]]: """Generate a structuring function for a union of attrs classes (and maybe None).""" dis_fn = self._get_dis_func(cl) has_none = NoneType in cl.__args__ @@ -374,7 +399,7 @@ def structure_attrs_union(obj, _): return structure_attrs_union @staticmethod - def _structure_call(obj, cl): + def _structure_call(obj: Any, cl: Type[T]) -> Any: """Just call ``cl`` with the given ``obj``. This is just an optimization on the ``_structure_default`` case, when @@ -411,7 +436,7 @@ def structure_attrs_fromtuple(self, obj: Tuple[Any, ...], cl: Type[T]) -> T: converted = self._structure_attribute(a, value) conv_obj.append(converted) - return cl(*conv_obj) # type: ignore + return cl(*conv_obj) def _structure_attribute(self, a: Union[Attribute, Field], value: Any) -> Any: """Handle an individual attrs attribute.""" @@ -440,7 +465,7 @@ def structure_attrs_fromdict(self, obj: Mapping[str, Any], cl: Type[T]) -> T: # For public use. conv_obj = {} # Start with a fresh dict, to ignore extra keys. - for a in fields(cl): # type: ignore + for a in fields(cl): name = a.name try: @@ -453,9 +478,9 @@ def structure_attrs_fromdict(self, obj: Mapping[str, Any], cl: Type[T]) -> T: conv_obj[name] = self._structure_attribute(a, val) - return cl(**conv_obj) # type: ignore + return cl(**conv_obj) - def _structure_list(self, obj, cl): + def _structure_list(self, obj: Iterable[T], cl: Any) -> List[T]: """Convert an iterable to a potentially generic list.""" if is_bare(cl) or cl.__args__[0] is Any: res = [e for e in obj] @@ -482,7 +507,9 @@ def _structure_list(self, obj, cl): res = [handler(e, elem_type) for e in obj] return res - def _structure_set(self, obj, cl, structure_to=set): + def _structure_set( + self, obj: Iterable[T], cl: Any, structure_to: type = set + ) -> Set[T]: """Convert an iterable into a potentially generic set.""" if is_bare(cl) or cl.__args__[0] is Any: return structure_to(obj) @@ -507,11 +534,13 @@ def _structure_set(self, obj, cl, structure_to=set): else: return structure_to([handler(e, elem_type) for e in obj]) - def _structure_frozenset(self, obj, cl): + def _structure_frozenset( + self, obj: Iterable[T], cl: Any + ) -> FrozenSetSubscriptable[T]: """Convert an iterable into a potentially generic frozenset.""" - return self._structure_set(obj, cl, structure_to=frozenset) + return self._structure_set(obj, cl, structure_to=frozenset) # type: ignore (incompatible type between frozenset and set) - def _structure_dict(self, obj, cl): + def _structure_dict(self, obj: Mapping[T, V], cl: Any) -> Dict[T, V]: """Convert a mapping into a potentially generic dict.""" if is_bare(cl) or cl.__args__ == (Any, Any): return dict(obj) @@ -543,7 +572,7 @@ def _structure_union(self, obj, union): handler = self._union_struct_registry[union] return handler(obj, union) - def _structure_tuple(self, obj, tup: Type[T]) -> T: + def _structure_tuple(self, obj: Any, tup: Type[T]) -> T: """Deal with structuring into a tuple.""" if tup in (Tuple, tuple): tup_params = None @@ -853,7 +882,9 @@ def gen_structure_attrs_fromdict( # only direct dispatch so that subclasses get separately generated return h - def gen_unstructure_iterable(self, cl: Any, unstructure_to=None): + def gen_unstructure_iterable( + self, cl: Any, unstructure_to: Any = None + ) -> IterableUnstructureFn: unstructure_to = self._unstruct_collection_overrides.get( get_origin(cl) or cl, unstructure_to or list ) @@ -861,7 +892,9 @@ def gen_unstructure_iterable(self, cl: Any, unstructure_to=None): self._unstructure_func.register_cls_list([(cl, h)], direct=True) return h - def gen_unstructure_hetero_tuple(self, cl: Any, unstructure_to=None): + def gen_unstructure_hetero_tuple( + self, cl: Any, unstructure_to: Any = None + ) -> HeteroTupleUnstructureFn: unstructure_to = self._unstruct_collection_overrides.get( get_origin(cl) or cl, unstructure_to or list ) @@ -869,7 +902,12 @@ def gen_unstructure_hetero_tuple(self, cl: Any, unstructure_to=None): self._unstructure_func.register_cls_list([(cl, h)], direct=True) return h - def gen_unstructure_mapping(self, cl: Any, unstructure_to=None, key_handler=None): + def gen_unstructure_mapping( + self, + cl: Any, + unstructure_to: Any = None, + key_handler: Optional[Callable[[Any, Optional[Any]], Any]] = None, + ) -> MappingUnstructureFn: unstructure_to = self._unstruct_collection_overrides.get( get_origin(cl) or cl, unstructure_to or dict ) @@ -879,7 +917,7 @@ def gen_unstructure_mapping(self, cl: Any, unstructure_to=None, key_handler=None self._unstructure_func.register_cls_list([(cl, h)], direct=True) return h - def gen_structure_counter(self, cl: Any): + def gen_structure_counter(self, cl: Any) -> MappingStructureFn[T]: h = make_mapping_structure_fn( cl, self, @@ -890,7 +928,7 @@ def gen_structure_counter(self, cl: Any): self._structure_func.register_cls_list([(cl, h)], direct=True) return h - def gen_structure_mapping(self, cl: Any): + def gen_structure_mapping(self, cl: Any) -> MappingStructureFn[T]: h = make_mapping_structure_fn( cl, self, detailed_validation=self.detailed_validation ) diff --git a/src/cattrs/disambiguators.py b/src/cattrs/disambiguators.py index d3735fd9..2b36fc54 100644 --- a/src/cattrs/disambiguators.py +++ b/src/cattrs/disambiguators.py @@ -2,14 +2,16 @@ from collections import OrderedDict from functools import reduce from operator import or_ -from typing import Callable, Dict, Mapping, Optional, Type +from typing import Any, Callable, Dict, Mapping, Optional, Type from attr import NOTHING, fields from cattrs._compat import get_origin -def create_uniq_field_dis_func(*classes: Type) -> Callable: +def create_uniq_field_dis_func( + *classes: Type[Any], +) -> Callable[[Mapping[Any, Any]], Optional[Type[Any]]]: """Given attr classes, generate a disambiguation function. The function is based on unique fields.""" @@ -22,7 +24,7 @@ def create_uniq_field_dis_func(*classes: Type) -> Callable: 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] + uniq_attrs_dict: Dict[str, Type] = OrderedDict() cls_and_attrs.sort(key=lambda c_a: -len(c_a[1])) fallback = None # If none match, try this. @@ -46,8 +48,7 @@ def create_uniq_field_dis_func(*classes: Type) -> Callable: else: fallback = cl - def dis_func(data): - # type: (Mapping) -> Optional[Type] + def dis_func(data: Mapping[Any, Any]) -> Optional[Type]: if not isinstance(data, Mapping): raise ValueError("Only input mappings are supported.") for k, v in uniq_attrs_dict.items(): diff --git a/src/cattrs/gen.py b/src/cattrs/gen.py index 5a3ed22f..597a672a 100644 --- a/src/cattrs/gen.py +++ b/src/cattrs/gen.py @@ -3,7 +3,18 @@ import uuid from dataclasses import is_dataclass from threading import local -from typing import TYPE_CHECKING, Any, Callable, Dict, Mapping, Optional, Type, TypeVar +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + Mapping, + Optional, + Tuple, + Type, + TypeVar, +) import attr from attr import NOTHING, frozen, resolve_types @@ -218,6 +229,9 @@ def _generate_mapping(cl: Type, old_mapping: Dict[str, type]) -> Dict[str, type] return mapping +DictStructureFn = Callable[[Mapping[str, Any], Any], T] + + def make_dict_structure_fn( cl: Type[T], converter: "BaseConverter", @@ -226,7 +240,7 @@ def make_dict_structure_fn( _cattrs_prefer_attrib_converters: bool = False, _cattrs_detailed_validation: bool = True, **kwargs: AttributeOverride, -) -> Callable[[Mapping[str, Any], Any], T]: +) -> DictStructureFn[T]: """Generate a specialized dict structuring function for an attrs class.""" mapping = {} @@ -504,7 +518,12 @@ def make_dict_structure_fn( return globs[fn_name] -def make_iterable_unstructure_fn(cl: Any, converter, unstructure_to=None): +IterableUnstructureFn = Callable[[Iterable[Any]], Any] + + +def make_iterable_unstructure_fn( + cl: Any, converter: "BaseConverter", unstructure_to: Any = None +) -> IterableUnstructureFn: """Generate a specialized unstructure function for an iterable.""" handler = converter.unstructure @@ -535,7 +554,12 @@ def make_iterable_unstructure_fn(cl: Any, converter, unstructure_to=None): return fn -def make_hetero_tuple_unstructure_fn(cl: Any, converter, unstructure_to=None): +HeteroTupleUnstructureFn = Callable[[Tuple[Any, ...]], Any] + + +def make_hetero_tuple_unstructure_fn( + cl: Any, converter: "BaseConverter", unstructure_to: Any = None +) -> HeteroTupleUnstructureFn: """Generate a specialized unstructure function for a heterogenous tuple.""" fn_name = "unstructure_tuple" @@ -576,9 +600,15 @@ def make_hetero_tuple_unstructure_fn(cl: Any, converter, unstructure_to=None): return fn +MappingUnstructureFn = Callable[[Mapping[Any, Any]], Any] + + def make_mapping_unstructure_fn( - cl: Any, converter, unstructure_to=None, key_handler=None -): + cl: Any, + converter: "BaseConverter", + unstructure_to: Any = None, + key_handler: Optional[Callable[[Any, Optional[Any]], Any]] = None, +) -> MappingUnstructureFn: """Generate a specialized unstructure function for a mapping.""" kh = key_handler or converter.unstructure val_handler = converter.unstructure @@ -627,18 +657,21 @@ def make_mapping_unstructure_fn( return fn +MappingStructureFn = Callable[[Mapping[Any, Any], Any], T] + + def make_mapping_structure_fn( cl: Type[T], converter: "BaseConverter", - structure_to=dict, + structure_to: Type = dict, key_type=NOTHING, val_type=NOTHING, detailed_validation: bool = True, -) -> Callable[[Mapping, Any], T]: +) -> MappingStructureFn[T]: """Generate a specialized unstructure function for a mapping.""" fn_name = "structure_mapping" - globs = {"__cattr_mapping_cl": structure_to} + globs: Dict[str, Type] = {"__cattr_mapping_cl": structure_to} lines = [] lines.append(f"def {fn_name}(mapping, _):") @@ -734,7 +767,7 @@ def make_mapping_structure_fn( return fn -def _generate_unique_filename(cls, func_name, reserve=True): +def _generate_unique_filename(cls: Any, func_name: str, reserve: bool = True) -> str: """ Create a "filename" suitable for a function being generated. """