diff --git a/docs/source/additional_features.rst b/docs/source/additional_features.rst index 19e0d4dcce01..d4fd3ed8f69c 100644 --- a/docs/source/additional_features.rst +++ b/docs/source/additional_features.rst @@ -72,9 +72,13 @@ and :pep:`557`. Caveats/Known Issues ==================== -Some functions in the :py:mod:`dataclasses` module, such as :py:func:`~dataclasses.replace` and :py:func:`~dataclasses.asdict`, +Some functions in the :py:mod:`dataclasses` module, such as :py:func:`~dataclasses.replace`, have imprecise (too permissive) types. This will be fixed in future releases. +Calls to :py:func:`~dataclasses.asdict` will return a ``TypedDict`` based on the original dataclass +definition, transforming it recursively. There are, however, some limitations. In particular, a precise return type +cannot be inferred for recursive dataclasses, and for calls where ``dict_factory`` is set. + Mypy does not yet recognize aliases of :py:func:`dataclasses.dataclass `, and will probably never recognize dynamically computed decorators. The following examples do **not** work: diff --git a/misc/proper_plugin.py b/misc/proper_plugin.py index 249ad983266b..a92409258ae7 100644 --- a/misc/proper_plugin.py +++ b/misc/proper_plugin.py @@ -131,7 +131,7 @@ def proper_types_hook(ctx: FunctionContext) -> Type: def get_proper_type_instance(ctx: FunctionContext) -> Instance: - types = ctx.api.modules['mypy.types'] # type: ignore + types = ctx.api.modules['mypy.types'] proper_type_info = types.names['ProperType'] assert isinstance(proper_type_info.node, TypeInfo) return Instance(proper_type_info.node, []) diff --git a/mypy/checker.py b/mypy/checker.py index 2f99b9b4fece..3aaac938f1e8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5118,6 +5118,21 @@ def named_type(self, name: str) -> Instance: any_type = AnyType(TypeOfAny.from_omitted_generics) return Instance(node, [any_type] * len(node.defn.type_vars)) + def named_type_or_none(self, qualified_name: str, + args: Optional[List[Type]] = None) -> Optional[Instance]: + sym = self.lookup_fully_qualified_or_none(qualified_name) + if not sym: + return None + node = sym.node + if isinstance(node, TypeAlias): + assert isinstance(node.target, Instance) # type: ignore + node = node.target.type + assert isinstance(node, TypeInfo), node + if args is not None: + # TODO: assert len(args) == len(node.defn.type_vars) + return Instance(node, args) + return Instance(node, [AnyType(TypeOfAny.unannotated)] * len(node.defn.type_vars)) + def named_generic_type(self, name: str, args: List[Type]) -> Instance: """Return an instance with the given name and type arguments. @@ -5129,6 +5144,13 @@ def named_generic_type(self, name: str, args: List[Type]) -> Instance: # TODO: assert len(args) == len(info.defn.type_vars) return Instance(info, args) + def add_plugin_dependency(self, trigger: str, target: Optional[str] = None) -> None: + if target is None: + target = self.tscope.current_target() + + cur_module_node = self.modules[self.tscope.current_module_id()] + cur_module_node.plugin_deps.setdefault(trigger, set()).add(target) + def lookup_typeinfo(self, fullname: str) -> TypeInfo: # Assume that the name refers to a class. sym = self.lookup_qualified(fullname) @@ -5200,6 +5222,26 @@ def lookup_qualified(self, name: str) -> SymbolTableNode: msg = "Failed qualified lookup: '{}' (fullname = '{}')." raise KeyError(msg.format(last, name)) + def lookup_fully_qualified_or_none(self, fullname: str) -> Optional[SymbolTableNode]: + """Lookup a fully qualified name that refers to a module-level definition. + + Don't assume that the name is defined. This happens in the global namespace -- + the local module namespace is ignored. This does not dereference indirect + refs. + + Note that this can't be used for names nested in class namespaces. + """ + # TODO: unify/clean-up/simplify lookup methods, see #4157. + # TODO: support nested classes (but consider performance impact, + # we might keep the module level only lookup for thing like 'builtins.int'). + assert '.' in fullname + module, name = fullname.rsplit('.', maxsplit=1) + if module not in self.modules: + return None + filenode = self.modules[module] + result = filenode.names.get(name) + return result + @contextmanager def enter_partial_types(self, *, is_function: bool = False, is_class: bool = False) -> Iterator[None]: diff --git a/mypy/plugin.py b/mypy/plugin.py index dfa446521548..475282a192c7 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -212,6 +212,7 @@ class CheckerPluginInterface: docstrings in checker.py for more details. """ + modules: Dict[str, MypyFile] msg: MessageBuilder options: Options path: str @@ -234,6 +235,19 @@ def named_generic_type(self, name: str, args: List[Type]) -> Instance: """Construct an instance of a builtin type with given type arguments.""" raise NotImplementedError + @abstractmethod + def named_type_or_none(self, qualified_name: str, + args: Optional[List[Type]] = None) -> Optional[Instance]: + raise NotImplementedError + + @abstractmethod + def add_plugin_dependency(self, trigger: str, target: Optional[str] = None) -> None: + """Specify semantic dependencies for generated methods/variables. + + See the same function on SemanticAnalyzerPluginInterface for more details. + """ + raise NotImplementedError + @trait class SemanticAnalyzerPluginInterface: diff --git a/mypy/plugins/common.py b/mypy/plugins/common.py index 95f4618da4a1..7dd624b3e36b 100644 --- a/mypy/plugins/common.py +++ b/mypy/plugins/common.py @@ -1,13 +1,16 @@ -from typing import List, Optional, Union +from collections import OrderedDict +from typing import List, Optional, Union, Set from mypy.nodes import ( ARG_POS, MDEF, Argument, Block, CallExpr, ClassDef, Expression, SYMBOL_FUNCBASE_TYPES, FuncDef, PassStmt, RefExpr, SymbolTableNode, Var, JsonDict, ) -from mypy.plugin import CheckerPluginInterface, ClassDefContext, SemanticAnalyzerPluginInterface +from mypy.plugin import ClassDefContext, SemanticAnalyzerPluginInterface, CheckerPluginInterface from mypy.semanal import set_callable_name +from mypy.semanal_typeddict import get_anonymous_typeddict_type from mypy.types import ( - CallableType, Overloaded, Type, TypeVarType, deserialize_type, get_proper_type, + CallableType, Overloaded, Type, deserialize_type, get_proper_type, + TypedDictType, TypeVarType ) from mypy.typevars import fill_typevars from mypy.util import get_unique_redefinition_name @@ -184,8 +187,17 @@ def add_attribute_to_class( def deserialize_and_fixup_type( - data: Union[str, JsonDict], api: SemanticAnalyzerPluginInterface + data: Union[str, JsonDict], + api: Union[SemanticAnalyzerPluginInterface, CheckerPluginInterface] ) -> Type: typ = deserialize_type(data) typ.accept(TypeFixer(api.modules, allow_missing=False)) return typ + + +def make_anonymous_typeddict(api: CheckerPluginInterface, fields: 'OrderedDict[str, Type]', + required_keys: Set[str]) -> TypedDictType: + fallback = get_anonymous_typeddict_type(api) + assert fallback is not None + return TypedDictType(fields, required_keys=required_keys, + fallback=fallback) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 091c627f5c1b..bf53b41aee1d 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -1,24 +1,30 @@ """Plugin that provides support for dataclasses.""" -from typing import Dict, List, Set, Tuple, Optional +from collections import OrderedDict +from typing import Dict, List, Set, Tuple, Optional, Union + from typing_extensions import Final +from mypy.maptype import map_instance_to_supertype from mypy.nodes import ( ARG_OPT, ARG_NAMED, ARG_NAMED_OPT, ARG_POS, ARG_STAR, ARG_STAR2, MDEF, - Argument, AssignmentStmt, CallExpr, Context, Expression, JsonDict, + Argument, AssignmentStmt, CallExpr, Context, Expression, JsonDict, NameExpr, RefExpr, SymbolTableNode, TempNode, TypeInfo, Var, TypeVarExpr, PlaceholderNode ) -from mypy.plugin import ClassDefContext, SemanticAnalyzerPluginInterface +from mypy.plugin import ClassDefContext, FunctionContext, CheckerPluginInterface +from mypy.plugin import SemanticAnalyzerPluginInterface from mypy.plugins.common import ( - add_method, _get_decorator_bool_argument, deserialize_and_fixup_type, add_attribute_to_class, + add_method, _get_decorator_bool_argument, make_anonymous_typeddict, + deserialize_and_fixup_type, add_attribute_to_class ) from mypy.typeops import map_type_from_supertype +from mypy.type_visitor import TypeTranslator from mypy.types import ( Type, Instance, NoneType, TypeVarType, CallableType, TupleType, LiteralType, - get_proper_type, AnyType, TypeOfAny, + get_proper_type, AnyType, TypeOfAny, TypeAliasType, TypeType ) -from mypy.server.trigger import make_wildcard_trigger +from mypy.server.trigger import make_wildcard_trigger, make_trigger # The set of decorators that generate dataclasses. dataclass_makers: Final = { @@ -34,6 +40,10 @@ SELF_TVAR_NAME: Final = "_DT" +def is_type_dataclass(info: TypeInfo) -> bool: + return 'dataclass' in info.metadata + + class DataclassAttribute: def __init__( self, @@ -90,7 +100,8 @@ def serialize(self) -> JsonDict: @classmethod def deserialize( - cls, info: TypeInfo, data: JsonDict, api: SemanticAnalyzerPluginInterface + cls, info: TypeInfo, data: JsonDict, + api: Union[SemanticAnalyzerPluginInterface, CheckerPluginInterface] ) -> 'DataclassAttribute': data = data.copy() if data.get('kw_only') is None: @@ -390,7 +401,7 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]: # we'll have unmodified attrs laying around. all_attrs = attrs.copy() for info in cls.info.mro[1:-1]: - if 'dataclass' not in info.metadata: + if not is_type_dataclass(info): continue super_attrs = [] @@ -546,3 +557,94 @@ def _collect_field_args(expr: Expression, args[name] = arg return True, args return False, {} + + +def asdict_callback(ctx: FunctionContext) -> Type: + """Check that calls to asdict pass in a dataclass. If possible, return TypedDicts.""" + positional_arg_types = ctx.arg_types[0] + + if positional_arg_types: + dataclass_instance = get_proper_type(positional_arg_types[0]) + if isinstance(dataclass_instance, Instance): + if is_type_dataclass(dataclass_instance.type): + if len(ctx.arg_types) == 1: + # Can only infer a more precise type for calls where dict_factory is not set. + return _asdictify(ctx.api, dataclass_instance) + + return ctx.default_return_type + + +class AsDictVisitor(TypeTranslator): + def __init__(self, api: CheckerPluginInterface) -> None: + self.api = api + self.seen_dataclasses: Set[str] = set() + + def visit_type_alias_type(self, t: TypeAliasType) -> Type: + return t.copy_modified(args=[a.accept(self) for a in t.args]) + + def visit_instance(self, t: Instance) -> Type: + info = t.type + any_type = AnyType(TypeOfAny.implementation_artifact) + if is_type_dataclass(info): + if info.fullname in self.seen_dataclasses: + # Recursive types not supported, so fall back to Dict[str, Any] + # Note: Would be nicer to fallback to default_return_type, but that is Any + # (due to overloads?) + return self.api.named_generic_type( + 'builtins.dict', [self.api.named_generic_type('builtins.str', []), any_type]) + attrs = info.metadata['dataclass']['attributes'] + fields: OrderedDict[str, Type] = OrderedDict() + self.seen_dataclasses.add(info.fullname) + for data in attrs: + attr = DataclassAttribute.deserialize(info, data, self.api) + self.api.add_plugin_dependency(make_trigger(info.fullname + "." + attr.name)) + # TODO: attr.name should be available + sym_node = info.names.get(attr.name, None) + if sym_node is None: + continue + attr_type = sym_node.type + assert attr_type is not None + fields[attr.name] = attr_type.accept(self) + self.seen_dataclasses.remove(info.fullname) + return make_anonymous_typeddict(self.api, fields=fields, + required_keys=set(fields.keys())) + elif info.has_base('builtins.list'): + supertype = map_instance_to_supertype(t, self.api.named_generic_type( + 'builtins.list', [any_type]).type) + return self.api.named_generic_type('builtins.list', + self.translate_types(supertype.args)) + elif info.has_base('builtins.dict'): + supertype = map_instance_to_supertype(t, self.api.named_generic_type( + 'builtins.dict', [any_type, any_type]).type) + return self.api.named_generic_type('builtins.dict', + self.translate_types(supertype.args)) + return t + + def visit_tuple_type(self, t: TupleType) -> Type: + if t.partial_fallback.type.is_named_tuple: + # For namedtuples, return Any. To properly support transforming namedtuples, + # we would have to generate a partial_fallback type for the TupleType and add it + # to the symbol table. It's not currently possible to do this via the + # CheckerPluginInterface. Ideally it would use the same code as + # NamedTupleAnalyzer.build_namedtuple_typeinfo. + return AnyType(TypeOfAny.implementation_artifact) + # Note: Tuple subclasses not supported, hence overriding the fallback + return t.copy_modified(items=self.translate_types(t.items), + fallback=self.api.named_generic_type('builtins.tuple', [])) + + def visit_callable_type(self, t: CallableType) -> Type: + # Leave e.g. Callable[[SomeDataclass], SomeDataclass] alone + return t + + def visit_type_type(self, t: TypeType) -> Type: + # Leave e.g. Type[SomeDataclass] alone + return t + + +def _asdictify(api: CheckerPluginInterface, typ: Type) -> Type: + """Convert dataclasses into TypedDicts, recursively looking into built-in containers. + + It will look for dataclasses inside of tuples, lists, and dicts and convert them to + TypedDicts. + """ + return typ.accept(AsDictVisitor(api)) diff --git a/mypy/plugins/default.py b/mypy/plugins/default.py index c57c5f9a18d9..ab1c2f686942 100644 --- a/mypy/plugins/default.py +++ b/mypy/plugins/default.py @@ -2,7 +2,9 @@ from typing import Callable, Optional, List from mypy import message_registry -from mypy.nodes import Expression, StrExpr, IntExpr, DictExpr, UnaryExpr +from mypy.nodes import ( + Expression, StrExpr, IntExpr, DictExpr, UnaryExpr +) from mypy.plugin import ( Plugin, FunctionContext, MethodContext, MethodSigContext, AttributeContext, ClassDefContext, CheckerPluginInterface, @@ -22,7 +24,7 @@ class DefaultPlugin(Plugin): def get_function_hook(self, fullname: str ) -> Optional[Callable[[FunctionContext], Type]]: - from mypy.plugins import ctypes, singledispatch + from mypy.plugins import ctypes, singledispatch, dataclasses if fullname in ('contextlib.contextmanager', 'contextlib.asynccontextmanager'): return contextmanager_callback @@ -32,6 +34,8 @@ def get_function_hook(self, fullname: str return ctypes.array_constructor_callback elif fullname == 'functools.singledispatch': return singledispatch.create_singledispatch_function_callback + elif fullname == 'dataclasses.asdict': + return dataclasses.asdict_callback return None def get_method_signature_hook(self, fullname: str diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index ffc6a7df3931..f79738822a41 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -1,11 +1,12 @@ """Semantic analysis of TypedDict definitions.""" from mypy.backports import OrderedDict -from typing import Optional, List, Set, Tuple +from typing import Optional, List, Set, Tuple, Union from typing_extensions import Final +from mypy.plugin import CheckerPluginInterface from mypy.types import ( - Type, AnyType, TypeOfAny, TypedDictType, TPDICT_NAMES, RequiredType, + Type, AnyType, TypeOfAny, TypedDictType, TPDICT_NAMES, RequiredType, Instance ) from mypy.nodes import ( CallExpr, TypedDictExpr, Expression, NameExpr, Context, StrExpr, BytesExpr, UnicodeExpr, @@ -362,10 +363,7 @@ def build_typeddict_typeinfo(self, name: str, items: List[str], types: List[Type], required_keys: Set[str], line: int) -> TypeInfo: - # Prefer typing then typing_extensions if available. - fallback = (self.api.named_type_or_none('typing._TypedDict', []) or - self.api.named_type_or_none('typing_extensions._TypedDict', []) or - self.api.named_type_or_none('mypy_extensions._TypedDict', [])) + fallback = get_anonymous_typeddict_type(self.api) assert fallback is not None info = self.api.basic_new_typeinfo(name, fallback, line) info.typeddict_type = TypedDictType(OrderedDict(zip(items, types)), required_keys, @@ -383,3 +381,11 @@ def fail(self, msg: str, ctx: Context, *, code: Optional[ErrorCode] = None) -> N def note(self, msg: str, ctx: Context) -> None: self.api.note(msg, ctx) + + +def get_anonymous_typeddict_type( + api: Union[SemanticAnalyzerInterface, CheckerPluginInterface]) -> Optional[Instance]: + # Prefer typing then typing_extensions if available. + return (api.named_type_or_none('typing._TypedDict', []) or + api.named_type_or_none('typing_extensions._TypedDict', []) or + api.named_type_or_none('mypy_extensions._TypedDict', [])) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index eed329bb59c7..4aab98611478 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1339,6 +1339,353 @@ a.foo = another_foo # E: Incompatible types in assignment (expression has type [typing fixtures/typing-medium.pyi] [builtins fixtures/dataclasses.pyi] +[case testDataclassesAsdict] +# flags: --python-version 3.7 +from dataclasses import dataclass, asdict +from typing import List, Tuple, Dict, Any + +@dataclass +class Person: + name: str + age: int + +class NonDataclass: + pass + +reveal_type(asdict(Person('John', 32))) # N: Revealed type is "TypedDict({'name': builtins.str, 'age': builtins.int})" +# It's OK to call on a non-dataclass, to reduce false-positives. +reveal_type(asdict(Person)) # N: Revealed type is "builtins.dict[builtins.str, Any]" +reveal_type(asdict(NonDataclass())) # N: Revealed type is "builtins.dict[builtins.str, Any]" + +def my_dict_factory(seq: List[Tuple[str, Any]]) -> Dict[str, Any]: + pass + +reveal_type(asdict(NonDataclass(), dict_factory=my_dict_factory)) # N: Revealed type is "builtins.dict*[builtins.str, Any]" + +# Passing in a dict_factory should not return a TypedDict +reveal_type(asdict(Person('John', 32), dict_factory=my_dict_factory)) # N: Revealed type is "builtins.dict*[builtins.str, Any]" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/dict.pyi] + +[case testDataclassesAsdictPython38] +# flags: --python-version 3.8 +from dataclasses import dataclass, asdict + +@dataclass +class Foo: + bar: str + +reveal_type(asdict(Foo('bar'))) # N: Revealed type is "TypedDict({'bar': builtins.str})" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/dict.pyi] + +[case testDataclassesAsdictPython39] +# flags: --python-version 3.9 +from dataclasses import dataclass, asdict + +@dataclass +class Foo: + bar: str + +reveal_type(asdict(Foo('bar'))) # N: Revealed type is "TypedDict({'bar': builtins.str})" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/dict.pyi] + +[case testDataclassesAsdictRecursion] +# flags: --python-version 3.7 +from dataclasses import dataclass, asdict +from typing import Optional + +@dataclass +class C: + a: 'A' + +@dataclass +class B: + c: C + +@dataclass +class A: + b: Optional[B] = None + +# Recursion is not supported, so fall back +result = asdict(A(B(C(A())))) +reveal_type(result) # N: Revealed type is "TypedDict({'b': Union[TypedDict({'c': TypedDict({'a': builtins.dict[builtins.str, Any]})}), None]})" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/tuple.pyi] +[builtins fixtures/dict.pyi] + +[case testDataclassesAsdictUnions] +# flags: --python-version 3.7 +from dataclasses import dataclass, asdict +from typing import Union + +@dataclass +class Card: + last4: int + +@dataclass +class Customer: + card: Union[str, Card] + +reveal_type(asdict(Customer("foo"))) # N: Revealed type is "TypedDict({'card': Union[builtins.str, TypedDict({'last4': builtins.int})]})" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/dict.pyi] + +[case testDataclassesAsdictType] +# flags: --python-version 3.7 +from dataclasses import dataclass, asdict +from typing import Type + +@dataclass +class Card: + last4: int + +@dataclass +class Customer: + card: Type[Card] + +# Type[...] hould be left alone +reveal_type(asdict(Customer(Card))) # N: Revealed type is "TypedDict({'card': Type[__main__.Card]})" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/dict.pyi] + +[case testDataclassesAsdictCallable] +# flags: --python-version 3.7 +from dataclasses import dataclass, asdict +from typing import Callable + +@dataclass +class Card: + last4: int + +@dataclass +class Customer: + card: Callable[[Card], Card] + +def func(_: Card) -> Card: pass + +# Type[...] hould be left alone +reveal_type(asdict(Customer(func))) # N: Revealed type is "TypedDict({'card': def (__main__.Card) -> __main__.Card})" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/dict.pyi] + +[case testDataclassesAsdictList] +# flags: --python-version 3.7 +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] +[builtins fixtures/dict.pyi] + +[case testDataclassesAsdictListSubclass] +# flags: --python-version 3.7 +from dataclasses import dataclass, asdict +from typing import List, Any, TypeVar, Generic + +@dataclass +class Person: + name: str + age: int + + +_T = TypeVar("_T") +class MyList(List[_T]): + pass + +_X = TypeVar("_X") +class MyListWith2TypeVars(List[_T], Generic[_T, _X]): + foo: _X + +_C = TypeVar("_C", Person, int) + +class MyListWithConstraint(List[_C], Generic[_C]): + pass + + +@dataclass +class Course: + list_subclass: MyList[Person] + list_subclass_2_typevars: MyListWith2TypeVars[Person, int] + list_subclass_with_constraint: MyListWithConstraint[Person] + +instance = Course( + list_subclass=MyList([]), + list_subclass_2_typevars=MyListWith2TypeVars[Person, int]([Person("John", 23)]), + list_subclass_with_constraint=MyListWithConstraint([Person("Tim", 29)]) +) +result = asdict(instance) + +# 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] +[builtins fixtures/dict.pyi] + +[case testDataclassesAsdictDict] +# flags: --python-version 3.7 +from dataclasses import dataclass, asdict +from typing import Dict + +@dataclass +class Person: + name: str + age: int + +@dataclass +class Course: + participants_by_name: Dict[str, Person] + +instance = Course(participants_by_name={"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 testDataclassesAsdictDictSubclass] +# flags: --python-version 3.7 +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] +# flags: --python-version 3.7 +from dataclasses import dataclass, asdict +from typing import Tuple + +@dataclass +class Person: + name: str + age: int + +@dataclass +class Course: + partners: Tuple[Person, Person] + +instance = Course(partners=(Person("Joe", 32), Person("John", 23))) +result = asdict(instance) +reveal_type(result['partners']) # N: Revealed type is "Tuple[TypedDict({'name': builtins.str, 'age': builtins.int}), TypedDict({'name': builtins.str, 'age': builtins.int})]" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/tuple.pyi] +[builtins fixtures/dict.pyi] + +[case testDataclassesAsdictTupleSubclass] +# flags: --python-version 3.7 +from dataclasses import dataclass, asdict +from typing import Tuple, Generic, TypeVar + +@dataclass +class Person: + name: str + age: int + +class MyTuple(Tuple[Person, str]): + pass + +@dataclass +class Course: + tuple_subclass: MyTuple + +instance = Course(tuple_subclass=MyTuple()) +result = asdict(instance) + +# For now, subclasses of Tuple are transformed to the Tuple base class +# This is because the subclass, if it itself contains dataclass fields, may be transformed in such a way that it +# is no longer compatible with the original Tuple class it is extending. +reveal_type(result['tuple_subclass']) # N: Revealed type is "Tuple[TypedDict({'name': builtins.str, 'age': builtins.int}), builtins.str]" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/tuple.pyi] +[builtins fixtures/dict.pyi] + +[case testDataclassesAsdictNamedTuple] +# flags: --python-version 3.7 +from dataclasses import dataclass, asdict +from typing import NamedTuple + +@dataclass +class Person: + name: str + age: int + + +class Staff(NamedTuple): + teacher: Person + assistant: Person + + def staff_method(self): + pass + +@dataclass +class Course: + staff: Staff + +instance = Course(staff=Staff(teacher=Person("Joe", 32), assistant=Person("John", 23))) +result = asdict(instance) +# Due to implementation limitations, namedtuples are transformed to Any +reveal_type(result['staff']) # N: Revealed type is "Any" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/tuple.pyi] +[builtins fixtures/dict.pyi] + [case testDataclassFieldDoesNotFailOnKwargsUnpacking] # flags: --python-version 3.7 # https://github.com/python/mypy/issues/10879 @@ -1457,7 +1804,6 @@ class Other: x: int [builtins fixtures/dataclasses.pyi] - [case testSlotsDefinitionWithTwoPasses1] # flags: --python-version 3.10 # https://github.com/python/mypy/issues/11821 diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index bdf75b2dc58c..8d6a649a12f9 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -1777,7 +1777,7 @@ P = Callable[[mypy_extensions.VarArg(int)], int] # ok Q = Callable[[Arg(int, type=int)], int] # E: Invalid type alias: expression is not a valid type # E: Value of type "int" is not indexable # E: "Arg" gets multiple values for keyword argument "type" R = Callable[[Arg(int, 'x', name='y')], int] # E: Invalid type alias: expression is not a valid type # E: Value of type "int" is not indexable # E: "Arg" gets multiple values for keyword argument "name" -[builtins fixtures/dict.pyi] +[builtins fixtures/primitives.pyi] [case testCallableParsing] from typing import Callable diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test index fd593a975ca0..257e8ea9441c 100644 --- a/test-data/unit/deps.test +++ b/test-data/unit/deps.test @@ -1447,3 +1447,36 @@ class B(A): -> m -> m -> m + + +[case testDataclassesAsdictDeps] +# flags: --python-version 3.8 +from dataclasses import asdict +from a import my_var +x = asdict(my_var) +x['attr'] + "foo" + +[file a.py] +from dataclasses import dataclass +from b import AttributeInOtherModule + +@dataclass +class MyDataclass: + attr: AttributeInOtherModule + +my_var: MyDataclass + +[file b.py] +AttributeInOtherModule = str + +[typing fixtures/typing-typeddict.pyi] +[builtins fixtures/fine_grained.pyi] +[builtins fixtures/primitives.pyi] + +[out] + -> m + -> m + -> m + -> m + -> m + -> m \ No newline at end of file diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index ad67ff19dfd2..ef11d8adc9b3 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -910,6 +910,35 @@ class A: main:3: error: Missing positional argument "c" in call to "C" == +[case testDataclassesAsdictFineGrained] +# flags: --python-version 3.8 +[file a.py] +from dataclasses import dataclass +from b import AttributeInOtherModule + +@dataclass +class MyDataclass: + attr: AttributeInOtherModule + +my_var: MyDataclass + +[file b.py] +AttributeInOtherModule = str +[file c.py] +from dataclasses import asdict +from a import my_var +asdict(my_var)['attr'] + "foo" + +[file b.py.2] +AttributeInOtherModule = int + +[typing fixtures/typing-typeddict.pyi] +[builtins fixtures/fine_grained.pyi] +[builtins fixtures/primitives.pyi] +[out] +== +c.py:3: error: Unsupported operand types for + ("int" and "str") + [case testAttrsUpdate1] [file a.py] import attr diff --git a/test-data/unit/fixtures/dict.pyi b/test-data/unit/fixtures/dict.pyi index f8a5e3481d13..d20df2572035 100644 --- a/test-data/unit/fixtures/dict.pyi +++ b/test-data/unit/fixtures/dict.pyi @@ -43,6 +43,10 @@ class unicode: pass # needed for py2 docstrings class bytes: pass class list(Sequence[T]): # needed by some test cases + @overload + def __init__(self) -> None: pass + @overload + def __init__(self, x: Iterable[T]) -> None: pass def __getitem__(self, x: int) -> T: pass def __iter__(self) -> Iterator[T]: pass def __mul__(self, x: int) -> list[T]: pass diff --git a/test-data/unit/lib-stub/dataclasses.pyi b/test-data/unit/lib-stub/dataclasses.pyi index bd33b459266c..17891bdef17a 100644 --- a/test-data/unit/lib-stub/dataclasses.pyi +++ b/test-data/unit/lib-stub/dataclasses.pyi @@ -1,4 +1,4 @@ -from typing import Any, Callable, Generic, Mapping, Optional, TypeVar, overload, Type +from typing import Any, Callable, Generic, Mapping, Optional, TypeVar, overload, Type, Dict, List, Tuple _T = TypeVar('_T') @@ -7,6 +7,11 @@ class InitVar(Generic[_T]): class KW_ONLY: ... +@overload +def asdict(obj: Any) -> Dict[str, Any]: ... +@overload +def asdict(obj: Any, *, dict_factory: Callable[[List[Tuple[str, Any]]], _T]) -> _T: ... + @overload def dataclass(_cls: Type[_T]) -> Type[_T]: ...