Skip to content

Commit

Permalink
Consider field value types when disambiguating a union of TypedDicts
Browse files Browse the repository at this point in the history
Fixes python#8533.

Previously, given a union of TypedDicts, e.g. `A|B` in

```py
from typing import TypedDict, Literal, Union

class A(TypedDict):
    tag: Literal['A']
    extra_a: str

class B(TypedDict):
    tag: Literal['B']
    extra_b: str
```

when needing to disambiguate the union, e.g.

```
td: A|B = {
    'tag': 'A',
    'extra_a': 'foo',
}
```

mypy would only consider the *keys* of the dict expression and
TypedDict, e.g. 'tag' and 'extra_a'. But if multiple members of the
union have the same shape, only distinguished by a value type, the
disambiguation fails, e.g.

```py
class A(TypedDict):
    tag: Literal['A']

class B(TypedDict):
    tag: Literal['B']

td: A|B = {  # E: Type of TypedDict is ambiguous, could be any of ("A", "B")
    'tag': 'A',
}
```

To allow this, also consider the types of the dict expression's values
when narrowing the candidates from the union.
  • Loading branch information
bluetech committed Sep 26, 2021
1 parent 3ec0284 commit 63b05e8
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 4 deletions.
20 changes: 16 additions & 4 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,11 +538,23 @@ def match_typeddict_call_with_dict(self, callee: TypedDictType,
kwargs: DictExpr,
context: Context) -> bool:
validated_kwargs = self.validate_typeddict_kwargs(kwargs=kwargs)
if validated_kwargs is not None:
return (callee.required_keys <= set(validated_kwargs.keys())
<= set(callee.items.keys()))
else:
# The dict expression keys are statically known - otherwise can't check
# it against a TypedDict at all.
if validated_kwargs is None:
return False
# The dict expression keys are compatible with this TypedDict.
if not (callee.required_keys <= set(validated_kwargs.keys()) <= set(callee.items.keys())):
return False
# The dict expression value types are compatible with this TypedDict.
for (item_name, item_type) in callee.items.items():
kwarg = validated_kwargs.get(item_name)
if kwarg is None:
continue
kwarg_type = self.accept(kwarg, item_type, always_allow_any=True)
kwarg_type = get_proper_type(kwarg_type)
if not is_subtype(kwarg_type, item_type):
return False
return True

def check_typeddict_call_with_dict(self, callee: TypedDictType,
kwargs: DictExpr,
Expand Down
22 changes: 22 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,28 @@ c: Union[A, B] = {'@type': 'a-type', 'a': 'Test'}
reveal_type(c) # N: Revealed type is "Union[TypedDict('__main__.A', {'@type': Literal['a-type'], 'a': builtins.str}), TypedDict('__main__.B', {'@type': Literal['b-type'], 'b': builtins.int})]"
[builtins fixtures/tuple.pyi]

[case testTypedDictUnionUnambiguousCaseBasedOnType]
from typing import Union, Mapping, Any, cast
from typing_extensions import TypedDict, Literal

A = TypedDict('A', {'@type': Literal['a-type']})
B = TypedDict('B', {'@type': Literal['b-type']})
C1 = TypedDict('C1', {'@type': Literal['c-type'], 'extra': int})
C2 = TypedDict('C2', {'@type': Literal['c-type'], 'extra': str})

x: Union[A, B, C1, C2]
x = {'@type': 'a-type'}
reveal_type(x) # N: Revealed type is "TypedDict('__main__.A', {'@type': Literal['a-type']})"
x = {'@type': 'b-type'}
reveal_type(x) # N: Revealed type is "TypedDict('__main__.B', {'@type': Literal['b-type']})"
x = {'@type': 'c-type', 'extra': 10}
reveal_type(x) # N: Revealed type is "TypedDict('__main__.C1', {'@type': Literal['c-type'], 'extra': builtins.int})"
x = {'@type': 'c-type', 'extra': 'string'}
reveal_type(x) # N: Revealed type is "TypedDict('__main__.C2', {'@type': Literal['c-type'], 'extra': builtins.str})"

x = {'@type': 'c-type', 'extra': 10.5} # E: Incompatible types in assignment (expression has type "Dict[str, object]", variable has type "Union[A, B, C1, C2]")
[builtins fixtures/dict.pyi]

[case testTypedDictUnionAmbiguousCase]
from typing import Union, Mapping, Any, cast
from typing_extensions import TypedDict, Literal
Expand Down

0 comments on commit 63b05e8

Please sign in to comment.