Skip to content

Commit

Permalink
Make ReadOnly TypedDict items covariant (#17904)
Browse files Browse the repository at this point in the history
Fixes #17901.
  • Loading branch information
JukkaL committed Oct 9, 2024
1 parent 24bfb34 commit 964a7a5
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 9 deletions.
23 changes: 14 additions & 9 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,15 +892,20 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool:
return False
for name, l, r in left.zip(right):
# TODO: should we pass on the full subtype_context here and below?
if self.proper_subtype:
check = is_same_type(l, r)
right_readonly = name in right.readonly_keys
if not right_readonly:
if self.proper_subtype:
check = is_same_type(l, r)
else:
check = is_equivalent(
l,
r,
ignore_type_params=self.subtype_context.ignore_type_params,
options=self.options,
)
else:
check = is_equivalent(
l,
r,
ignore_type_params=self.subtype_context.ignore_type_params,
options=self.options,
)
# Read-only items behave covariantly
check = self._is_subtype(l, r)
if not check:
return False
# Non-required key is not compatible with a required key since
Expand All @@ -917,7 +922,7 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool:
# Readonly fields check:
#
# A = TypedDict('A', {'x': ReadOnly[int]})
# B = TypedDict('A', {'x': int})
# B = TypedDict('B', {'x': int})
# def reset_x(b: B) -> None:
# b['x'] = 0
#
Expand Down
41 changes: 41 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -3988,3 +3988,44 @@ class TP(TypedDict):
k: ReadOnly # E: "ReadOnly[]" must have exactly one type argument
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testTypedDictReadOnlyCovariant]
from typing import ReadOnly, TypedDict, Union

class A(TypedDict):
a: ReadOnly[Union[int, str]]

class A2(TypedDict):
a: ReadOnly[int]

class B(TypedDict):
a: int

class B2(TypedDict):
a: Union[int, str]

class B3(TypedDict):
a: int

def fa(a: A) -> None: ...
def fa2(a: A2) -> None: ...

b: B = {"a": 1}
fa(b)
fa2(b)
b2: B2 = {"a": 1}
fa(b2)
fa2(b2) # E: Argument 1 to "fa2" has incompatible type "B2"; expected "A2"

class C(TypedDict):
a: ReadOnly[Union[int, str]]
b: Union[str, bytes]

class D(TypedDict):
a: int
b: str

d: D = {"a": 1, "b": "x"}
c: C = d # E: Incompatible types in assignment (expression has type "D", variable has type "C")
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

0 comments on commit 964a7a5

Please sign in to comment.