From 964a7a5b9b1d93a5aa5c3cf87b2a283f015b217b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 9 Oct 2024 14:48:09 +0100 Subject: [PATCH] Make ReadOnly TypedDict items covariant (#17904) Fixes #17901. --- mypy/subtypes.py | 23 +++++++++------- test-data/unit/check-typeddict.test | 41 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 787d5cb89b0a..a63db93fd9cb 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -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 @@ -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 # diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index e1797421636e..affa472bb640 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -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]