Skip to content

Commit 058f8fd

Browse files
authored
Do not allow TypedDict classes with extra keywords (#16438)
1 parent 1cc62a2 commit 058f8fd

File tree

3 files changed

+47
-4
lines changed

3 files changed

+47
-4
lines changed

mypy/messages.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -991,10 +991,17 @@ def maybe_note_about_special_args(self, callee: CallableType, context: Context)
991991
context,
992992
)
993993

994+
def unexpected_keyword_argument_for_function(
995+
self, for_func: str, name: str, context: Context, *, matches: list[str] | None = None
996+
) -> None:
997+
msg = f'Unexpected keyword argument "{name}"' + for_func
998+
if matches:
999+
msg += f"; did you mean {pretty_seq(matches, 'or')}?"
1000+
self.fail(msg, context, code=codes.CALL_ARG)
1001+
9941002
def unexpected_keyword_argument(
9951003
self, callee: CallableType, name: str, arg_type: Type, context: Context
9961004
) -> None:
997-
msg = f'Unexpected keyword argument "{name}"' + for_function(callee)
9981005
# Suggest intended keyword, look for type match else fallback on any match.
9991006
matching_type_args = []
10001007
not_matching_type_args = []
@@ -1008,9 +1015,9 @@ def unexpected_keyword_argument(
10081015
matches = best_matches(name, matching_type_args, n=3)
10091016
if not matches:
10101017
matches = best_matches(name, not_matching_type_args, n=3)
1011-
if matches:
1012-
msg += f"; did you mean {pretty_seq(matches, 'or')}?"
1013-
self.fail(msg, context, code=codes.CALL_ARG)
1018+
self.unexpected_keyword_argument_for_function(
1019+
for_function(callee), name, context, matches=matches
1020+
)
10141021
module = find_defining_module(self.modules, callee)
10151022
if module:
10161023
assert callee.definition is not None

mypy/semanal_typeddict.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,12 @@ def analyze_typeddict_classdef_fields(
323323
total: bool | None = True
324324
if "total" in defn.keywords:
325325
total = require_bool_literal_argument(self.api, defn.keywords["total"], "total", True)
326+
if defn.keywords and defn.keywords.keys() != {"total"}:
327+
for_function = ' for "__init_subclass__" of "TypedDict"'
328+
for key in defn.keywords.keys():
329+
if key == "total":
330+
continue
331+
self.msg.unexpected_keyword_argument_for_function(for_function, key, defn)
326332
required_keys = {
327333
field
328334
for (field, t) in zip(fields, types)

test-data/unit/check-typeddict.test

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3408,3 +3408,33 @@ B = TypedDict("B", { # E: Type of a TypedDict key becomes "Any" due to an unfol
34083408
})
34093409
[builtins fixtures/dict.pyi]
34103410
[typing fixtures/typing-typeddict.pyi]
3411+
3412+
[case testTypedDictWithClassLevelKeywords]
3413+
from typing import TypedDict, Generic, TypeVar
3414+
3415+
T = TypeVar('T')
3416+
3417+
class Meta(type): ...
3418+
3419+
class WithMetaKeyword(TypedDict, metaclass=Meta): # E: Unexpected keyword argument "metaclass" for "__init_subclass__" of "TypedDict"
3420+
...
3421+
3422+
class GenericWithMetaKeyword(TypedDict, Generic[T], metaclass=Meta): # E: Unexpected keyword argument "metaclass" for "__init_subclass__" of "TypedDict"
3423+
...
3424+
3425+
# We still don't allow this, because the implementation is much easier
3426+
# and it does not make any practical sense to do it:
3427+
class WithTypeMeta(TypedDict, metaclass=type): # E: Unexpected keyword argument "metaclass" for "__init_subclass__" of "TypedDict"
3428+
...
3429+
3430+
class OtherKeywords(TypedDict, a=1, b=2, c=3, total=True): # E: Unexpected keyword argument "a" for "__init_subclass__" of "TypedDict" \
3431+
# E: Unexpected keyword argument "b" for "__init_subclass__" of "TypedDict" \
3432+
# E: Unexpected keyword argument "c" for "__init_subclass__" of "TypedDict"
3433+
...
3434+
3435+
class TotalInTheMiddle(TypedDict, a=1, total=True, b=2, c=3): # E: Unexpected keyword argument "a" for "__init_subclass__" of "TypedDict" \
3436+
# E: Unexpected keyword argument "b" for "__init_subclass__" of "TypedDict" \
3437+
# E: Unexpected keyword argument "c" for "__init_subclass__" of "TypedDict"
3438+
...
3439+
[builtins fixtures/dict.pyi]
3440+
[typing fixtures/typing-typeddict.pyi]

0 commit comments

Comments
 (0)