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
Next Next commit
Add draft of test for recursive asdict.
  • Loading branch information
syastrov committed Jan 28, 2020
commit 0ed29c6b57e0419c3512c29c8ad3a4ffee0eabbd
17 changes: 9 additions & 8 deletions mypy/plugins/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,23 +134,24 @@ def add_method(


def deserialize_and_fixup_type(
data: Union[str, JsonDict], api: SemanticAnalyzerPluginInterface
data: Union[str, JsonDict], api: SemanticAnalyzerPluginInterface
) -> Type:
typ = deserialize_type(data)
typ.accept(TypeFixer(api.modules, allow_missing=False))
return typ


def make_typeddict(api: CheckerPluginInterface, fields: 'OrderedDict[str, Type]',
required_keys: Set[str]) -> TypedDictType:
# Try to resolve the TypedDict fallback type
anonymous_typeddict_type: Optional[Instance] = None
def get_anonymous_typeddict_type(api: CheckerPluginInterface) -> Instance:
for type_fullname in TPDICT_FB_NAMES:
try:
anonymous_typeddict_type = api.named_generic_type(type_fullname, [])
if anonymous_typeddict_type is not None:
break
return anonymous_typeddict_type
except KeyError:
continue
assert anonymous_typeddict_type is not None
return TypedDictType(fields, required_keys=required_keys, fallback=anonymous_typeddict_type)
raise RuntimeError("No TypedDict fallback type found")


def make_anonymous_typeddict(api: CheckerPluginInterface, fields: 'OrderedDict[str, Type]',
required_keys: Set[str]) -> TypedDictType:
return TypedDictType(fields, required_keys=required_keys, fallback=get_anonymous_typeddict_type(api))
4 changes: 2 additions & 2 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
)
from mypy.plugin import ClassDefContext, FunctionContext
from mypy.plugin import SemanticAnalyzerPluginInterface
from mypy.plugins.common import add_method, _get_decorator_bool_argument, make_typeddict
from mypy.plugins.common import add_method, _get_decorator_bool_argument, make_anonymous_typeddict
from mypy.plugins.common import (
deserialize_and_fixup_type,
)
Expand Down Expand Up @@ -411,5 +411,5 @@ def asdict_callback(ctx: FunctionContext) -> Type:
typ = sym_node.type
assert typ is not None
fields[attr.name] = typ
return make_typeddict(ctx.api, fields=fields, required_keys=set(fields.keys()))
return make_anonymous_typeddict(ctx.api, fields=fields, required_keys=set(fields.keys()))
return ctx.default_return_type
44 changes: 44 additions & 0 deletions test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -1046,11 +1046,55 @@ class NotQuiteAPerson:
name: str
other_field: str


reveal_type(Person) # N: Revealed type is 'def (name: builtins.str, age: builtins.int) -> __main__.Person'
reveal_type(asdict(Person('John', 32))) # N: Revealed type is 'TypedDict({'name': builtins.str, 'age': builtins.int})'
Person(**asdict(Person('John', 32))) # Round-trip works
NotQuiteAPerson(**asdict(Person('John', 32))) # E: Extra argument "age" from **args for "NotQuiteAPerson"

reveal_type(asdict(Person('John', 32))) # N: Revealed type is 'TypedDict({'name': builtins.str, 'age': builtins.int})'

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

[case testDataclassesAsdictRecursive]
from dataclasses import dataclass, asdict
from typing import List, Dict, Tuple, NamedTuple

@dataclass
class Person:
name: str
age: int

class NT(NamedTuple):
nt: Dict[str, Tuple[int, Person]]

class PersonList(List[Person]):
pass

@dataclass
class Recurse:
foo: NT
bar: PersonList

# asdict should recurse into dataclasses, dicts, lists, and tuples
instance = Recurse(NT({'key': (1, Person("Joe", 32))}), PersonList([Person("Dave", 32)]))

# Problem 1: There is a problem when subclassing List (and I assume also Dict?)
# The type returned will contain an instance of PersonList, but it will contain dicts instead of Person instances
# How can we say it still is an instance of PersonList when it has values of the wrong type?
# We could copy the existing PersonList type and modify its generic parameter to List...
# but at runtime: isinstance(asdict(instance)['bar'], PersonList) == True

# Problem 2: The same problem occurs for namedtuples: they should be instances of the original namedtuple class,
# but their values which originally were instances of dataclasses should be dicts instead.

# I'm expecting the revealed type to be something like this, but this wouldn't really be correct due to the above...
reveal_type(asdict(instance)) # N: Revealed type is 'TypedDict({'foo': builtins.tuple[Dict[str, Tuple[int, TypedDict({'name': builtins.str, 'age': builtins.int}), fallback=__main__.NT]]], 'bar': __main__.PersonList})'

# TODO: Handle dict_factory param -- what type to give the return value then?

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