|
1 | 1 | """Plugin that provides support for dataclasses."""
|
2 | 2 |
|
3 |
| -from typing import Dict, List, Set, Tuple, Optional |
| 3 | +from collections import OrderedDict |
| 4 | +from typing import Dict, List, Set, Tuple, Optional, FrozenSet, Callable, Union |
| 5 | + |
4 | 6 | from typing_extensions import Final
|
5 | 7 |
|
| 8 | +from mypy.maptype import map_instance_to_supertype |
6 | 9 | from mypy.nodes import (
|
7 |
| - ARG_OPT, ARG_POS, MDEF, Argument, AssignmentStmt, CallExpr, |
8 |
| - Context, Expression, JsonDict, NameExpr, RefExpr, |
9 |
| - SymbolTableNode, TempNode, TypeInfo, Var, TypeVarExpr, PlaceholderNode |
10 |
| -) |
11 |
| -from mypy.plugin import ClassDefContext, SemanticAnalyzerPluginInterface |
12 |
| -from mypy.plugins.common import ( |
13 |
| - add_method, _get_decorator_bool_argument, deserialize_and_fixup_type, |
| 10 | + ARG_OPT, ARG_POS, MDEF, Argument, AssignmentStmt, CallExpr, Context, |
| 11 | + Expression, JsonDict, NameExpr, RefExpr, SymbolTableNode, TempNode, |
| 12 | + TypeInfo, Var, TypeVarExpr, PlaceholderNode |
14 | 13 | )
|
15 |
| -from mypy.types import Type, Instance, NoneType, TypeVarDef, TypeVarType, get_proper_type |
| 14 | +from mypy.plugin import ClassDefContext, FunctionContext, CheckerPluginInterface |
| 15 | +from mypy.plugin import SemanticAnalyzerPluginInterface |
| 16 | +from mypy.plugins.common import (add_method, _get_decorator_bool_argument, |
| 17 | + make_anonymous_typeddict, deserialize_and_fixup_type) |
16 | 18 | from mypy.server.trigger import make_wildcard_trigger
|
| 19 | +from mypy.types import (Instance, NoneType, TypeVarDef, TypeVarType, get_proper_type, Type, |
| 20 | + TupleType, UnionType, AnyType, TypeOfAny) |
17 | 21 |
|
18 | 22 | # The set of decorators that generate dataclasses.
|
19 | 23 | dataclass_makers = {
|
|
24 | 28 | SELF_TVAR_NAME = '_DT' # type: Final
|
25 | 29 |
|
26 | 30 |
|
| 31 | +def is_type_dataclass(info: TypeInfo) -> bool: |
| 32 | + return 'dataclass' in info.metadata |
| 33 | + |
| 34 | + |
27 | 35 | class DataclassAttribute:
|
28 | 36 | def __init__(
|
29 | 37 | self,
|
@@ -68,7 +76,8 @@ def serialize(self) -> JsonDict:
|
68 | 76 |
|
69 | 77 | @classmethod
|
70 | 78 | def deserialize(
|
71 |
| - cls, info: TypeInfo, data: JsonDict, api: SemanticAnalyzerPluginInterface |
| 79 | + cls, info: TypeInfo, data: JsonDict, |
| 80 | + api: Union[SemanticAnalyzerPluginInterface, CheckerPluginInterface] |
72 | 81 | ) -> 'DataclassAttribute':
|
73 | 82 | data = data.copy()
|
74 | 83 | typ = deserialize_and_fixup_type(data.pop('type'), api)
|
@@ -297,7 +306,7 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]:
|
297 | 306 | # we'll have unmodified attrs laying around.
|
298 | 307 | all_attrs = attrs.copy()
|
299 | 308 | for info in cls.info.mro[1:-1]:
|
300 |
| - if 'dataclass' not in info.metadata: |
| 309 | + if not is_type_dataclass(info): |
301 | 310 | continue
|
302 | 311 |
|
303 | 312 | super_attrs = []
|
@@ -386,3 +395,99 @@ def _collect_field_args(expr: Expression) -> Tuple[bool, Dict[str, Expression]]:
|
386 | 395 | args[name] = arg
|
387 | 396 | return True, args
|
388 | 397 | return False, {}
|
| 398 | + |
| 399 | + |
| 400 | +def asdict_callback(ctx: FunctionContext) -> Type: |
| 401 | + positional_arg_types = ctx.arg_types[0] |
| 402 | + |
| 403 | + if positional_arg_types: |
| 404 | + if len(ctx.arg_types) == 2: |
| 405 | + # We can't infer a more precise for calls where dict_factory is set. |
| 406 | + # At least for now, typeshed stubs for asdict don't allow you to pass in `dict` as |
| 407 | + # dict_factory, so we can't special-case that. |
| 408 | + return ctx.default_return_type |
| 409 | + dataclass_instance = positional_arg_types[0] |
| 410 | + dataclass_instance = get_proper_type(dataclass_instance) |
| 411 | + if isinstance(dataclass_instance, Instance): |
| 412 | + info = dataclass_instance.type |
| 413 | + if not is_type_dataclass(info): |
| 414 | + ctx.api.fail('asdict() should be called on dataclass instances', |
| 415 | + dataclass_instance) |
| 416 | + return _asdictify(ctx.api, ctx.context, dataclass_instance) |
| 417 | + return ctx.default_return_type |
| 418 | + |
| 419 | + |
| 420 | +def _transform_type_args(*, typ: Instance, transform: Callable[[Instance], Type]) -> List[Type]: |
| 421 | + """For each type arg used in the Instance, call transform function on it if the arg is an |
| 422 | + Instance.""" |
| 423 | + new_args = [] |
| 424 | + for arg in typ.args: |
| 425 | + proper_arg = get_proper_type(arg) |
| 426 | + if isinstance(proper_arg, Instance): |
| 427 | + new_args.append(transform(proper_arg)) |
| 428 | + else: |
| 429 | + new_args.append(arg) |
| 430 | + return new_args |
| 431 | + |
| 432 | + |
| 433 | +def _asdictify(api: CheckerPluginInterface, context: Context, typ: Type) -> Type: |
| 434 | + """Convert dataclasses into TypedDicts, recursively looking into built-in containers. |
| 435 | +
|
| 436 | + It will look for dataclasses inside of tuples, lists, and dicts and convert them to TypedDicts. |
| 437 | + """ |
| 438 | + |
| 439 | + def _asdictify_inner(typ: Type, seen_dataclasses: FrozenSet[str]) -> Type: |
| 440 | + typ = get_proper_type(typ) |
| 441 | + if isinstance(typ, UnionType): |
| 442 | + return UnionType([_asdictify_inner(item, seen_dataclasses) for item in typ.items]) |
| 443 | + if isinstance(typ, Instance): |
| 444 | + info = typ.type |
| 445 | + if is_type_dataclass(info): |
| 446 | + if info.fullname in seen_dataclasses: |
| 447 | + api.fail( |
| 448 | + "Recursive types are not supported in call to asdict, so falling back to " |
| 449 | + "Dict[str, Any]", |
| 450 | + context) |
| 451 | + # Note: Would be nicer to fallback to default_return_type, but that is Any |
| 452 | + # (due to overloads?) |
| 453 | + return api.named_generic_type('builtins.dict', |
| 454 | + [api.named_generic_type('builtins.str', []), |
| 455 | + AnyType(TypeOfAny.implementation_artifact)]) |
| 456 | + seen_dataclasses |= {info.fullname} |
| 457 | + attrs = info.metadata['dataclass']['attributes'] |
| 458 | + fields = OrderedDict() # type: OrderedDict[str, Type] |
| 459 | + for data in attrs: |
| 460 | + attr = DataclassAttribute.deserialize(info, data, api) |
| 461 | + sym_node = info.names[attr.name] |
| 462 | + attr_type = sym_node.type |
| 463 | + assert attr_type is not None |
| 464 | + fields[attr.name] = _asdictify_inner(attr_type, seen_dataclasses) |
| 465 | + return make_anonymous_typeddict(api, fields=fields, |
| 466 | + required_keys=set(fields.keys())) |
| 467 | + elif info.has_base('builtins.list'): |
| 468 | + supertype_instance = map_instance_to_supertype(typ, api.named_generic_type( |
| 469 | + 'builtins.list', []).type) |
| 470 | + new_args = _transform_type_args( |
| 471 | + typ=supertype_instance, |
| 472 | + transform=lambda arg: _asdictify_inner(arg, seen_dataclasses)) |
| 473 | + return api.named_generic_type('builtins.list', new_args) |
| 474 | + elif info.has_base('builtins.dict'): |
| 475 | + supertype_instance = map_instance_to_supertype(typ, api.named_generic_type( |
| 476 | + 'builtins.dict', []).type) |
| 477 | + new_args = _transform_type_args( |
| 478 | + typ=supertype_instance, |
| 479 | + transform=lambda arg: _asdictify_inner(arg, seen_dataclasses)) |
| 480 | + return api.named_generic_type('builtins.dict', new_args) |
| 481 | + elif isinstance(typ, TupleType): |
| 482 | + if typ.partial_fallback.type.is_named_tuple: |
| 483 | + # For namedtuples, return Any. To properly support transforming namedtuples, |
| 484 | + # we would have to generate a partial_fallback type for the TupleType and add it |
| 485 | + # to the symbol table. It's not currently possibl to do this via the |
| 486 | + # CheckerPluginInterface. Ideally it would use the same code as |
| 487 | + # NamedTupleAnalyzer.build_namedtuple_typeinfo. |
| 488 | + return AnyType(TypeOfAny.implementation_artifact) |
| 489 | + return TupleType([_asdictify_inner(item, seen_dataclasses) for item in typ.items], |
| 490 | + api.named_generic_type('builtins.tuple', []), implicit=typ.implicit) |
| 491 | + return typ |
| 492 | + |
| 493 | + return _asdictify_inner(typ, seen_dataclasses=frozenset()) |
0 commit comments