Skip to content

Commit

Permalink
chore: update annotations (#302)
Browse files Browse the repository at this point in the history
* chore: improve annotations

Signed-off-by: Aaron Pham <[email protected]>

* type: supports converters and gen type hint

A few notes:
- Use UnionType when cattrs decides to support 3.11 since in 3.11 @Final
  has a different implementation, which inherently affects UnionType
  support in _compat.py
  For cattrs to support it we might need to update typing_extensions
  version
- Making generated function a generic type alias now. This should make
  it easiert to type hint any future works that involves the given make
  and gen function.

revert: changes from upstream

Signed-off-by: Aaron Pham <[email protected]>

Signed-off-by: Aaron Pham <[email protected]>
Co-authored-by: Tin Tvrtković <[email protected]>
  • Loading branch information
aarnphm and Tinche authored Sep 18, 2022
1 parent c3faf86 commit 5b3fb4d
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 50 deletions.
108 changes: 73 additions & 35 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,6 +31,7 @@
OriginAbstractSet,
OriginMutableSet,
Sequence,
Set,
fields,
get_newtype_base,
get_origin,
Expand All @@ -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,
Expand All @@ -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__)


Expand Down Expand Up @@ -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.
"""
Expand All @@ -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:
Expand All @@ -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.
"""
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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__
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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:
Expand All @@ -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]
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -853,23 +882,32 @@ 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
)
h = make_iterable_unstructure_fn(cl, self, unstructure_to=unstructure_to)
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
)
h = make_hetero_tuple_unstructure_fn(cl, self, unstructure_to=unstructure_to)
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
)
Expand All @@ -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,
Expand All @@ -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
)
Expand Down
11 changes: 6 additions & 5 deletions src/cattrs/disambiguators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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.
Expand All @@ -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():
Expand Down
Loading

0 comments on commit 5b3fb4d

Please sign in to comment.