Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,7 @@ def visit_instance(self, template: Instance) -> list[Constraint]:
if isinstance(actual, Overloaded) and actual.fallback is not None:
actual = actual.fallback
if isinstance(actual, TypedDictType):
actual = actual.as_anonymous().fallback
actual = actual.create_anonymous_fallback()
if isinstance(actual, LiteralType):
actual = actual.fallback
if isinstance(actual, Instance):
Expand Down
2 changes: 1 addition & 1 deletion mypy/meet.py
Original file line number Diff line number Diff line change
Expand Up @@ -1263,7 +1263,7 @@ def typed_dict_mapping_overlap(
key_type, value_type = get_proper_types(other.args)

# TODO: is there a cleaner way to get str_type here?
fallback = typed.as_anonymous().fallback
fallback = typed.create_anonymous_fallback()
str_type = fallback.type.bases[0].args[0] # typing._TypedDict inherits Mapping[str, object]

# Special case: a TypedDict with no required keys overlaps with an empty dict.
Expand Down
5 changes: 3 additions & 2 deletions mypy/plugins/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,8 +479,9 @@ def typed_dict_update_signature_callback(ctx: MethodSigContext) -> CallableType:
arg_type = get_proper_type(signature.arg_types[0])
if not isinstance(arg_type, TypedDictType):
return signature
arg_type = arg_type.as_anonymous()
arg_type = arg_type.copy_modified(required_keys=set())
arg_type = ctx.type.copy_modified(
fallback=arg_type.create_anonymous_fallback(), required_keys=set()
)
if ctx.args and ctx.args[0]:
if signature.name in _TP_DICT_MUTATING_METHODS:
# If we want to mutate this object in place, we need to set this flag,
Expand Down
10 changes: 3 additions & 7 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3080,11 +3080,11 @@ def is_final(self) -> bool:
def is_anonymous(self) -> bool:
return self.fallback.type.fullname in TPDICT_FB_NAMES

def as_anonymous(self) -> TypedDictType:
def create_anonymous_fallback(self) -> Instance:
if self.is_anonymous():
return self
return self.fallback
assert self.fallback.type.typeddict_type is not None
return self.fallback.type.typeddict_type.as_anonymous()
return self.fallback.type.typeddict_type.create_anonymous_fallback()

def copy_modified(
self,
Expand All @@ -3110,10 +3110,6 @@ def copy_modified(
required_keys &= set(item_names)
return TypedDictType(items, required_keys, readonly_keys, fallback, self.line, self.column)

def create_anonymous_fallback(self) -> Instance:
anonymous = self.as_anonymous()
return anonymous.fallback

def names_are_wider_than(self, other: TypedDictType) -> bool:
return len(other.items.keys() - self.items.keys()) == 0

Expand Down
33 changes: 33 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -3011,6 +3011,39 @@ reveal_type(s3) # N: Revealed type is "TypedDict('__main__.Sub3', {'x': Any, 'y
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testGenericTypedDictUpdate]
from typing import TypedDict, Generic, TypeVar

T = TypeVar("T")

class Group(TypedDict, Generic[T]):
a: T

value: Group[int] = {"a": 1}

def func(value2: Group[int]) -> None:
value.update(value2)
value.update({"a": 2})
value.update({"a": "string"}) # E: Incompatible types (expression has type "str", TypedDict item "a" has type "int")

S = TypeVar("S")

class MultiField(TypedDict, Generic[T, S]):
x: T
y: S

mf1: MultiField[int, str] = {"x": 1, "y": "a"}
mf2: MultiField[int, str] = {"x": 2, "y": "b"}
mf1.update(mf2)

mf3: MultiField[str, str] = {"x": "test", "y": "c"}
mf1.update(mf3) # E: Argument 1 to "update" of "TypedDict" has incompatible type "MultiField[str, str]"; expected "TypedDict({'x': int, 'y': str})"

mf1.update({"x": 3})
mf1.update({"y": "d"})
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testTypedDictAttributeOnClassObject]
from typing import TypedDict

Expand Down