Skip to content

Commit a1643cf

Browse files
committed
Fix TypedDict closed argument version check
1 parent d17b99e commit a1643cf

3 files changed

Lines changed: 109 additions & 10 deletions

File tree

‎mypy/semanal_typeddict.py‎

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from mypy.semanal_shared import (
4141
SemanticAnalyzerInterface,
4242
has_placeholder,
43+
parse_bool,
4344
require_bool_literal_argument,
4445
)
4546
from mypy.state import state
@@ -111,10 +112,18 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
111112
if isinstance(defn.analyzed, TypedDictExpr):
112113
existing_info = defn.analyzed.info
113114

115+
base_fullname: str | None = None
116+
if (
117+
len(defn.base_type_exprs) == 1
118+
and isinstance(defn.base_type_exprs[0], RefExpr)
119+
and defn.base_type_exprs[0].fullname in TPDICT_NAMES
120+
):
121+
base_fullname = defn.base_type_exprs[0].fullname
122+
114123
is_closed: bool | None = None
115124
if "closed" in defn.keywords:
116-
is_closed = require_bool_literal_argument(
117-
self.api, defn.keywords["closed"], "closed", False
125+
is_closed = self.parse_typeddict_closed_argument(
126+
defn.keywords["closed"], base_fullname
118127
)
119128

120129
if (
@@ -599,7 +608,7 @@ def check_typeddict(
599608
fullname = callee.fullname
600609
if fullname not in TPDICT_NAMES:
601610
return False, None, []
602-
res = self.parse_typeddict_args(call)
611+
res = self.parse_typeddict_args(call, fullname)
603612
if res is None:
604613
# This is a valid typed dict, but some type is not ready.
605614
# The caller should defer this until next iteration.
@@ -662,7 +671,7 @@ def check_typeddict(
662671
return True, info, tvar_defs
663672

664673
def parse_typeddict_args(
665-
self, call: CallExpr
674+
self, call: CallExpr, fullname: str
666675
) -> tuple[str, list[str], list[Type], bool, bool, list[TypeVarLikeType], bool] | None:
667676
"""Parse typed dict call expression.
668677
@@ -702,7 +711,10 @@ def parse_typeddict_args(
702711
closed: bool = False
703712
for arg_name, arg in zip(call.arg_names[2:], call.args[2:]):
704713
assert arg_name
705-
value = require_bool_literal_argument(self.api, arg, arg_name)
714+
if arg_name == "closed":
715+
value = self.parse_typeddict_closed_argument(arg, fullname)
716+
else:
717+
value = require_bool_literal_argument(self.api, arg, arg_name)
706718
if value is None:
707719
return "", [], [], True, False, [], False
708720
if arg_name == "closed":
@@ -719,6 +731,22 @@ def parse_typeddict_args(
719731
assert total is not None
720732
return args[0].value, items, types, total, closed, tvar_defs, ok
721733

734+
def parse_typeddict_closed_argument(
735+
self, arg: Expression, fullname: str | None
736+
) -> bool | None:
737+
literal_value = parse_bool(arg)
738+
value = require_bool_literal_argument(self.api, arg, "closed", False)
739+
if (
740+
literal_value is not None
741+
and fullname == "typing.TypedDict"
742+
and self.options.python_version < (3, 15)
743+
):
744+
self.fail(
745+
'"closed" argument to TypedDict is only available in Python 3.15 and later', arg
746+
)
747+
return None
748+
return value
749+
722750
def parse_typeddict_fields_with_types(
723751
self, dict_items: list[tuple[Expression | None, Expression]]
724752
) -> tuple[list[str], list[Type], bool] | None:

‎test-data/unit/check-serialize.test‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,7 @@ main:2: note: Revealed type is "TypedDict('m.D', {'x'?: builtins.int, 'y'?: buil
10891089
main:2: note: Revealed type is "TypedDict('m.D', {'x'?: builtins.int, 'y'?: builtins.str})"
10901090

10911091
[case testSerializeClosedTotalTypedDict]
1092+
# flags: --python-version 3.15
10921093
from m import d
10931094
reveal_type(d)
10941095
[file m.py]
@@ -1098,9 +1099,9 @@ d: D
10981099
[builtins fixtures/dict.pyi]
10991100
[typing fixtures/typing-typeddict.pyi]
11001101
[out1]
1101-
main:2: note: Revealed type is "TypedDict('m.D', {'x': builtins.int, 'y': builtins.str}, closed=True)"
1102+
main:3: note: Revealed type is "TypedDict('m.D', {'x': builtins.int, 'y': builtins.str}, closed=True)"
11021103
[out2]
1103-
main:2: note: Revealed type is "TypedDict('m.D', {'x': builtins.int, 'y': builtins.str}, closed=True)"
1104+
main:3: note: Revealed type is "TypedDict('m.D', {'x': builtins.int, 'y': builtins.str}, closed=True)"
11041105

11051106
--
11061107
-- Modules

‎test-data/unit/check-typeddict.test‎

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5203,6 +5203,7 @@ reveal_type(j(c, b)) # N: Revealed type is "TypedDict({'x'?=: builtins.int})"
52035203
# See https://peps.python.org/pep-0728/
52045204

52055205
[case testTypedDictWithClosedFalse]
5206+
# flags: --python-version 3.15
52065207
from typing import TypedDict
52075208
D = TypedDict('D', {'x': int, 'y': str}, closed=False)
52085209
d: D
@@ -5216,6 +5217,7 @@ reveal_type(e) # N: Revealed type is "TypedDict('__main__.E', {'x': builtins.in
52165217
[typing fixtures/typing-typeddict.pyi]
52175218

52185219
[case testTypedDictWithClosedTrue]
5220+
# flags: --python-version 3.15
52195221
from typing import TypedDict
52205222
D = TypedDict('D', {'x': int, 'y': str}, closed=True)
52215223
d: D
@@ -5228,6 +5230,51 @@ reveal_type(e) # N: Revealed type is "TypedDict('__main__.E', {'x': builtins.in
52285230
[builtins fixtures/dict.pyi]
52295231
[typing fixtures/typing-typeddict.pyi]
52305232

5233+
[case testTypedDictClosedArgumentBeforePython315]
5234+
# flags: --python-version 3.14
5235+
from typing import TypedDict
5236+
5237+
class ClassClosed(TypedDict, closed=True): # E: "closed" argument to TypedDict is only available in Python 3.15 and later
5238+
x: int
5239+
class ClassOpen(TypedDict, closed=False): # E: "closed" argument to TypedDict is only available in Python 3.15 and later
5240+
x: int
5241+
FunctionalClosed = TypedDict('FunctionalClosed', {'x': int}, closed=True) # E: "closed" argument to TypedDict is only available in Python 3.15 and later
5242+
FunctionalOpen = TypedDict('FunctionalOpen', {'x': int}, closed=False) # E: "closed" argument to TypedDict is only available in Python 3.15 and later
5243+
class InvalidClosed(TypedDict, closed=0): # E: "closed" argument must be a True or False literal
5244+
x: int
5245+
class WithoutClosed(TypedDict):
5246+
x: int
5247+
[builtins fixtures/dict.pyi]
5248+
[typing fixtures/typing-typeddict.pyi]
5249+
5250+
[case testTypingExtensionsTypedDictClosedArgumentBeforePython315]
5251+
# flags: --python-version 3.14
5252+
from typing_extensions import TypedDict
5253+
5254+
class ClassClosed(TypedDict, closed=True):
5255+
x: int
5256+
class ClassOpen(TypedDict, closed=False):
5257+
x: int
5258+
FunctionalClosed = TypedDict('FunctionalClosed', {'x': int}, closed=True)
5259+
FunctionalOpen = TypedDict('FunctionalOpen', {'x': int}, closed=False)
5260+
[builtins fixtures/dict.pyi]
5261+
[typing fixtures/typing-typeddict.pyi]
5262+
[file typing_extensions.pyi]
5263+
TypedDict = 0
5264+
5265+
[case testTypedDictClosedArgumentInPython315]
5266+
# flags: --python-version 3.15
5267+
from typing import TypedDict
5268+
5269+
class ClassClosed(TypedDict, closed=True):
5270+
x: int
5271+
class ClassOpen(TypedDict, closed=False):
5272+
x: int
5273+
FunctionalClosed = TypedDict('FunctionalClosed', {'x': int}, closed=True)
5274+
FunctionalOpen = TypedDict('FunctionalOpen', {'x': int}, closed=False)
5275+
[builtins fixtures/dict.pyi]
5276+
[typing fixtures/typing-typeddict.pyi]
5277+
52315278
[case testTypedDictWithInvalidClosedArgument]
52325279
from typing import TypedDict
52335280
A = TypedDict('A', {'x': int}, closed=0) # E: "closed" argument must be a True or False literal
@@ -5240,6 +5287,7 @@ class D(TypedDict, closed=bool): # E: "closed" argument must be a True or False
52405287
[typing fixtures/typing-typeddict.pyi]
52415288

52425289
[case testTypedDictWithClosedAndTotal]
5290+
# flags: --python-version 3.15
52435291
from typing import TypedDict
52445292
A = TypedDict('A', {'x': int, 'y': str}, total=False, closed=True)
52455293
B = TypedDict('B', {'x': int, 'y': str}, closed=True, total=False)
@@ -5261,6 +5309,7 @@ B = TypedDict('B', {'x': int}, total=True, total=False) # E: Repeated keyword a
52615309
[typing fixtures/typing-typeddict.pyi]
52625310

52635311
[case testTypedDictSubclassingClosed]
5312+
# flags: --python-version 3.15
52645313
from typing import TypedDict
52655314
class D(TypedDict, closed=True):
52665315
x: int
@@ -5308,6 +5357,7 @@ reveal_type(d8) # N: Revealed type is "TypedDict('__main__.D8', {'x': builtins.
53085357
[typing fixtures/typing-typeddict.pyi]
53095358

53105359
[case testTypedDictSubclassingOpenAndClosed]
5360+
# flags: --python-version 3.15
53115361
from typing import TypedDict
53125362
class B1(TypedDict, closed=True):
53135363
x: int
@@ -5332,6 +5382,7 @@ reveal_type(d4) # N: Revealed type is "TypedDict('__main__.D4', {'x': builtins.
53325382
[typing fixtures/typing-typeddict.pyi]
53335383

53345384
[case testCanSubclassClosedTypedDictWithForwardDeclarations]
5385+
# flags: --python-version 3.15
53355386
from typing import TypedDict, final
53365387

53375388
class D1(TypedDict, closed=True):
@@ -5349,6 +5400,7 @@ reveal_type(d2) # N: Revealed type is "TypedDict('__main__.D2', {'forward_decla
53495400
[typing fixtures/typing-full.pyi]
53505401

53515402
[case testTypedDictSubtypingClosedMustRemainClosed]
5403+
# flags: --python-version 3.15
53525404
from typing import TypedDict
53535405
A = TypedDict('A', {'x': int}, closed=True)
53545406
B = TypedDict('B', {'x': int}, closed=True)
@@ -5368,6 +5420,7 @@ g(b)
53685420
[typing fixtures/typing-typeddict.pyi]
53695421

53705422
[case testTypedDictSubtypingCannotAddKeysToAClosedSupertype]
5423+
# flags: --python-version 3.15
53715424
from typing import TypedDict
53725425
A = TypedDict('A', {'x': int}, closed=True)
53735426
B = TypedDict('B', {'x': int, 'y': int}, closed=True)
@@ -5383,6 +5436,7 @@ g(d)
53835436
[typing fixtures/typing-typeddict.pyi]
53845437

53855438
[case testTypedDictSubtypingCanDropOptionalReadonlyKeysInAClosedSubtype]
5439+
# flags: --python-version 3.15
53865440
from typing import ReadOnly, TypedDict
53875441
# Optional, readonly, closed: permitted
53885442
A = TypedDict('A', {'x': int, 'y': ReadOnly[int]}, total=False, closed=True)
@@ -5416,6 +5470,7 @@ f_g(h) # E: Argument 1 to "f_g" has incompatible type "H"; expected "G"
54165470
[typing fixtures/typing-typeddict.pyi]
54175471

54185472
[case testTypedDictSubtypingClosedTypeVarBound]
5473+
# flags: --python-version 3.15
54195474
from typing import TypedDict, TypeVar
54205475
A = TypedDict('A', {'x': int}, closed=True)
54215476
B = TypedDict('B', {'x': int})
@@ -5433,6 +5488,7 @@ fB(b)
54335488
[typing fixtures/typing-typeddict.pyi]
54345489

54355490
[case testTypedDictSubtypingClosedExplicitNever]
5491+
# flags: --python-version 3.15
54365492
from typing import NotRequired, ReadOnly, TypedDict
54375493
from typing_extensions import Never
54385494
A = TypedDict('A', {}, closed=True)
@@ -5457,6 +5513,7 @@ f_b(c) # E: Argument 1 to "f_b" has incompatible type "C"; expected "B"
54575513
[typing fixtures/typing-typeddict.pyi]
54585514

54595515
[case testJoinOfClosedTypedDict]
5516+
# flags: --python-version 3.15
54605517
from typing import Any, TypedDict, TypeVar
54615518
A = TypedDict('A', {'x': int}, closed=True)
54625519
B = TypedDict('B', {'y': int})
@@ -5484,6 +5541,7 @@ reveal_type(j(e, b)) # N: Revealed type is "TypedDict({'y': Any})"
54845541
[typing fixtures/typing-typeddict.pyi]
54855542

54865543
[case testMeetOfClosedTypedDictsWithMatchingRequiredKeysIsNotAnonymous]
5544+
# flags: --python-version 3.15
54875545
from typing import TypedDict, TypeVar, Callable
54885546
X = TypedDict('X', {'x': int, 'y': int}, closed=True)
54895547
Y = TypedDict('Y', {'x': int, 'y': int}, closed=True)
@@ -5502,6 +5560,7 @@ reveal_type(meet(fZX)) # N: Revealed type is "TypedDict('__main__.X', {'x': bui
55025560
[typing fixtures/typing-typeddict.pyi]
55035561

55045562
[case testMeetOfClosedTypedDictsWithDifferentKeys]
5563+
# flags: --python-version 3.15
55055564
from typing import TypedDict, TypeVar, Callable, ReadOnly
55065565
T = TypeVar('T')
55075566
def meet(x: Callable[[T, T], None]) -> T: pass
@@ -5538,6 +5597,7 @@ if int():
55385597
[typing fixtures/typing-typeddict.pyi]
55395598

55405599
[case testMeetOfTypedDictsExplicitAndImplicitClosedMissing]
5600+
# flags: --python-version 3.15
55415601
from typing import Any, NotRequired, ReadOnly, TypedDict, TypeVar, Callable
55425602
from typing_extensions import Never
55435603
A = TypedDict('A', {'x': ReadOnly[NotRequired[Never]]}, closed=True)
@@ -5550,6 +5610,7 @@ reveal_type(f(g)) # N: Revealed type is "TypedDict({}, closed=True)"
55505610
[typing fixtures/typing-typeddict.pyi]
55515611

55525612
[case testMeetOfTypedDictClosedMissingAndAny]
5613+
# flags: --python-version 3.15
55535614
from typing import Any, TypedDict, TypeVar, Callable, ReadOnly
55545615
T = TypeVar('T')
55555616
def meet(x: Callable[[T, T], None]) -> T: pass
@@ -5570,6 +5631,7 @@ reveal_type(meet(fCA)) # N: Revealed type is "TypedDict({}, closed=True)"
55705631
[typing fixtures/typing-typeddict.pyi]
55715632

55725633
[case testMeetOfTypedDictsClosedNotRequiredNeverAndAny]
5634+
# flags: --python-version 3.15
55735635
from typing import Any, NotRequired, TypedDict, TypeVar, Callable
55745636
from typing_extensions import Never
55755637
A = TypedDict('A', {'x': NotRequired[Any], 'y': NotRequired[Never]}, closed=True)
@@ -5582,6 +5644,7 @@ reveal_type(f(g)) # N: Revealed type is "TypedDict({}, closed=True)"
55825644
[typing fixtures/typing-typeddict.pyi]
55835645

55845646
[case testMeetOfOpenAndClosedTypedDictsExtraKeyInClosed]
5647+
# flags: --python-version 3.15
55855648
from typing import TypedDict, TypeVar, Callable, ReadOnly
55865649
T = TypeVar('T')
55875650
def meet(x: Callable[[T, T], None]) -> T: pass
@@ -5595,6 +5658,7 @@ reveal_type(meet(fAyBAx)) # N: Revealed type is "TypedDict({'a'=: builtins.int,
55955658
[typing fixtures/typing-typeddict.pyi]
55965659

55975660
[case testMeetOfOpenAndClosedTypedDictsExtraKeyInOpen]
5661+
# flags: --python-version 3.15
55985662
from typing import TypedDict, TypeVar, Callable, ReadOnly
55995663
T = TypeVar('T')
56005664
def meet(x: Callable[[T, T], None]) -> T: pass
@@ -5629,6 +5693,7 @@ reveal_type(meet(fD4C)) # N: Revealed type is "TypedDict({'a'=: builtins.int},
56295693
[typing fixtures/typing-typeddict.pyi]
56305694

56315695
[case testTypedDictGetMethodClosed]
5696+
# flags: --python-version 3.15
56325697
from typing import TypedDict, Literal
56335698
class Unrelated: pass
56345699
D = TypedDict('D', {'x': int, 'y': str}, closed=True)
@@ -5666,6 +5731,7 @@ reveal_type(d.get(x_or_y_or_z, u)) # N: Revealed type is "builtins.int | builtin
56665731
[typing fixtures/typing-typeddict.pyi]
56675732

56685733
[case testOperatorContainsNarrowsTypedDicts_unionWithList_closed]
5734+
# flags: --python-version 3.15
56695735
from __future__ import annotations
56705736
from typing import assert_type, TypedDict, Union
56715737

@@ -5685,6 +5751,7 @@ else:
56855751
[typing fixtures/typing-full.pyi]
56865752

56875753
[case testOperatorContainsNarrowsTypedDicts_total_closed]
5754+
# flags: --python-version 3.15
56885755
from __future__ import annotations
56895756
from typing import assert_type, Literal, TypedDict, TypeVar, Union
56905757

@@ -5729,7 +5796,7 @@ def f(arg: TD) -> None:
57295796
[typing fixtures/typing-full.pyi]
57305797

57315798
[case testOperatorContainsNarrowsTypedDicts_closed]
5732-
# flags: --warn-unreachable
5799+
# flags: --python-version 3.15 --warn-unreachable
57335800
from __future__ import annotations
57345801
from typing import assert_type, TypedDict, Union
57355802

@@ -5768,6 +5835,7 @@ else:
57685835
[typing fixtures/typing-full.pyi]
57695836

57705837
[case testOperatorContainsNarrowsTypedDicts_partialThroughTotalFalse_closed]
5838+
# flags: --python-version 3.15
57715839
from __future__ import annotations
57725840
from typing import assert_type, Literal, TypedDict, Union
57735841

@@ -5798,6 +5866,7 @@ else:
57985866
[typing fixtures/typing-full.pyi]
57995867

58005868
[case testOperatorContainsNarrowsTypedDicts_partialThroughNotRequired_closed]
5869+
# flags: --python-version 3.15
58015870
from __future__ import annotations
58025871
from typing import assert_type, TypedDict, Union
58035872
from typing_extensions import Required, NotRequired
@@ -5825,6 +5894,7 @@ else:
58255894
[typing fixtures/typing-full.pyi]
58265895

58275896
[case testOperatorContainsNarrowsTypeVarWithClosedTypedDictBound]
5897+
# flags: --python-version 3.15
58285898
from typing import TypedDict, TypeVar
58295899
from typing_extensions import Never, assert_type
58305900

@@ -5840,7 +5910,7 @@ def func(t: T):
58405910
[typing fixtures/typing-typeddict.pyi]
58415911

58425912
[case testTypedDictUnpackFromClosedMissingKey]
5843-
# flags: --extra-checks
5913+
# flags: --python-version 3.15 --extra-checks
58445914
from typing import TypedDict
58455915
from typing_extensions import Never, NotRequired
58465916
D1 = TypedDict("D1", {"a": int, "b": str}, closed=True)
@@ -5853,7 +5923,7 @@ d3: D3 = {**d1}
58535923
[typing fixtures/typing-typeddict.pyi]
58545924

58555925
[case testTypedDictUnpackIntoClosed]
5856-
# flags: --extra-checks
5926+
# flags: --python-version 3.15 --extra-checks
58575927
from typing import Any, Mapping, TypedDict, Union
58585928
from typing_extensions import Never, NotRequired
58595929
D1 = TypedDict("D1", {"a": int, "b": str}, closed=True)

0 commit comments

Comments
 (0)