8000 DRAFT: Returning TypedDict for dataclasses.asdict by syastrov · Pull Request #8339 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

DRAFT: Returning TypedDict for dataclasses.asdict #8339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
Prev Previous commit
asdict: For now, handle subtypes of list/dict by converting them to i…
…nstances of the supertype (list/dict).

Currently, I cannot figure out how to throw variance/constraint errors
when they are violated as a result of transforming the type.
  • Loading branch information
syastrov committed Feb 17, 2020
commit 139694539aaa6c179b1ffe528cacbea01ff66e68
32 changes: 11 additions & 21 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,20 +417,10 @@ def asdict_callback(ctx: FunctionContext) -> Type:
return ctx.default_return_type


def _transform_type_args_used_in_supertype(*, typ: Instance, supertype: TypeInfo, transform: Callable[[Instance], Type]) -> \
def _transform_type_args(*, typ: Instance, transform: Callable[[Instance], Type]) -> \
List[Type]:
"""For each type arg used in typ, call transform function if the arg is used in the given supertype."""
supertype_instance = map_instance_to_supertype(typ, supertype)
supertype_args = set([arg for arg in supertype_instance.args if isinstance(arg, Instance)])
new_args = []
# Transform existing type args if they are part of the supertype's type args
for arg in typ.args:
if isinstance(arg, Instance) and arg in supertype_args:
new_arg = transform(arg)
else:
new_arg = arg
new_args.append(new_arg)
return new_args
"""For each type arg used in the Instance, call transform function on it if the arg is an Instance."""
return [transform(arg) if isinstance(arg, Instance) else arg for arg in typ.args]


def _type_asdict(api: CheckerPluginInterface, context: Context, typ: Type) -> Type:
Expand Down Expand Up @@ -464,19 +454,19 @@ def _type_asdict_inner(typ: Type, seen_dataclasses: FrozenSet[str]) -> Type:
fields[attr.name] = _type_asdict_inner(typ, seen_dataclasses)
return make_anonymous_typeddict(api, fields=fields, required_keys=set(fields.keys()))
elif info.has_base('builtins.list'):
new_args = _transform_type_args_used_in_supertype(
typ=typ,
supertype=api.named_generic_type('builtins.list', []).type,
supertype_instance = map_instance_to_supertype(typ, api.named_generic_type('builtins.list', []).type)
new_args = _transform_type_args(
typ=supertype_instance,
transform=lambda arg: _type_asdict_inner(arg, seen_dataclasses)
)
return Instance(typ.type, new_args)
return api.named_generic_type('builtins.list', new_args)
elif info.has_base('builtins.dict'):
new_args = _transform_type_args_used_in_supertype(
typ=typ,
supertype=api.named_generic_type('builtins.dict', []).type,
supertype_instance = map_instance_to_supertype(typ, api.named_generic_type('builtins.dict', []).type)
new_args = _transform_type_args(
typ=supertype_instance,
transform=lambda arg: _type_asdict_inner(arg, seen_dataclasses)
)
return Instance(typ.type, new_args)
return api.named_generic_type('builtins.dict', new_args)
elif isinstance(typ, TupleType):
# TODO: Support subclasses/namedtuples properly
return TupleType([_type_asdict_inner(item, seen_dataclasses) for item in typ.items],
Expand Down
79 changes: 64 additions & 15 deletions test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,36 @@ reveal_type(asdict(Person('John', 32), dict_factory=my_dict_factory)) # N: Reve

[case testDataclassesAsdictList]
from dataclasses import dataclass, asdict
from typing import List, Any

@dataclass
class Person:
name: str
age: int


@dataclass
class Course:
participants: List[Person]
any_list: List[Any]
list_no_generic: list

instance = Course(
participants=[Person("Joe", 32)],
any_list=[],
list_no_generic=[],
)
result = asdict(instance)
reveal_type(result['participants']) # N: Revealed type is 'builtins.list[TypedDict({'name': builtins.str, 'age': builtins.int})]'
reveal_type(result['any_list']) # N: Revealed type is 'builtins.list[Any]'
reveal_type(result['list_no_generic']) # N: Revealed type is 'builtins.list[Any]'

[typing fixtures/typing-full.pyi]
[builtins fixtures/list.pyi]


[case testDataclassesAsdictListSubclass]
from dataclasses import dataclass, asdict
from typing import List, Any, TypeVar, Generic

@dataclass
Expand All @@ -1151,34 +1181,26 @@ _C = TypeVar("_C", Person, int)
class MyListWithConstraint(List[_C], Generic[_C]):
pass


@dataclass
class Course:
participants: List[Person]
any_list: List[Any]
list_no_generic: list
list_subclass: MyList[Person]
list_subclass_2_typevars: MyListWith2TypeVars[Person, int]
list_subclass_with_constraint: MyListWithConstraint[Person]

instance = Course(
participants=[Person("Joe", 32)],
any_list=[],
list_no_generic=[],
list_subclass=MyList([]),
list_subclass_2_typevars=MyListWith2TypeVars[Person, int]([Person("John", 23)]),
list_subclass_with_constraint=MyListWithConstraint([Person("Tim", 29)])
)
result = asdict(instance)
reveal_type(result['participants']) # N: Revealed type is 'builtins.list[TypedDict({'name': builtins.str, 'age': builtins.int})]'
reveal_type(result['any_list']) # N: Revealed type is 'builtins.list[Any]'
reveal_type(result['list_no_generic']) # N: Revealed type is 'builtins.list[Any]'
reveal_type(result['list_subclass']) # N: Revealed type is '__main__.MyList[TypedDict({'name': builtins.str, 'age': builtins.int})]'
reveal_type(result['list_subclass_2_typevars']) # N: Revealed type is '__main__.MyListWith2TypeVars[TypedDict({'name': builtins.str, 'age': builtins.int}), builtins.int]'

# TODO: actually, error should be expected since constraints are violated.
# So how to handle this? Should we just say it is not actually a MyListWithConstraint, but Any?
# Or should it genuinely throw a type checking error
reveal_type(result['list_subclass_with_constraint']) # N: Revealed type is '__main__.MyListWithConstraint[TypedDict({'name': builtins.str, 'age': builtins.int})]'
# Supertypes (list) are returned, since there could be a constraint on the TypeVar
# used on the subclass such that when the type argument to the subclass is substituted with a TypedDict,
# it may not type-check.
reveal_type(result['list_subclass']) # N: Revealed type is 'builtins.list[TypedDict({'name': builtins.str, 'age': builtins.int})]'
reveal_type(result['list_subclass_2_typevars']) # N: Revealed type is 'builtins.list[TypedDict({'name': builtins.str, 'age': builtins.int})]'
reveal_type(result['list_subclass_with_constraint']) # N: Revealed type is 'builtins.list[TypedDict({'name': builtins.str, 'age': builtins.int})]'

[typing fixtures/typing-full.pyi]
931A [builtins fixtures/list.pyi]
Expand All @@ -1204,6 +1226,33 @@ reveal_type(result['participants_by_name']) # N: Revealed type is 'builtins.dict
[typing fixtures/typing-full.pyi]
[builtins fixtures/dict.pyi]

[case testDataclassesAsdictDictSubclass]
from dataclasses import dataclass, asdict
from typing import Dict, Generic, TypeVar

@dataclass
class Person:
name: str
age: int

_KT = TypeVar("_KT")
_VT = TypeVar("_VT")
_Other = TypeVar("_Other")
class MyDict(Dict[_KT, _VT], Generic[_Other, _KT, _VT]):
pass

@dataclass
class Course:
participants_by_name: MyDict[int, str, Person]

instance = Course(participants_by_name=MyDict[int, str, Person]([("Joe", Person("Joe", 32))]))
result = asdict(instance)
reveal_type(result['participants_by_name']) # N: Revealed type is 'builtins.dict[builtins.str*, TypedDict({'name': builtins.str, 'age': builtins.int})]'

[typing fixtures/typing-full.pyi]
[builtins fixtures/dict.pyi]


[case testDataclassesAsdictTuple]
from dataclasses import dataclass, asdict
from typing import Tuple
Expand Down
0