Skip to content

Commit ce14043

Browse files
Use more precise context for TypedDict plugin errors (#18293)
Fixes #12271 Uses an applicable argument expression as the error context instead of the overall CallExpr. **Given:** ```python # flags: --pretty --show-column-number from typing import TypedDict class A(TypedDict): x: int a: A x.setdefault("y", 123) x.setdefault("x", "bad") # Non-TypedDict case for reference b: dict[str, int] b.setdefault("x", "bad") ``` **Before:** ``` main.py:8:1: error: TypedDict "A" has no key "y" [typeddict-item] a.setdefault("y", 123) ^~~~~~~~~~~~~~~~~~~~~~ main.py:9:1: error: Argument 2 to "setdefault" of "TypedDict" has incompatible type "str"; expected "int" [typeddict-item] a.setdefault("x", "bad") ^~~~~~~~~~~~~~~~~~~~~~~~ main.py:13:19: error: Argument 2 to "setdefault" of "MutableMapping" has incompatible type "str"; expected "int" [arg-type] b.setdefault("x", "bad") ^~~~~ Found 3 errors in 1 file (checked 1 source file) ``` **After:** ``` main.py:8:14: error: TypedDict "A" has no key "y" [typeddict-item] a.setdefault("y", 123) ^~~ main.py:9:19: error: Argument 2 to "setdefault" of "TypedDict" has incompatible type "str"; expected "int" [typeddict-item] a.setdefault("x", "bad") ^~~~~ main.py:13:19: error: Argument 2 to "setdefault" of "MutableMapping" has incompatible type "str"; expected "int" [arg-type] b.setdefault("x", "bad") ^~~~~ Found 3 errors in 1 file (checked 1 source file) ```
1 parent 973618a commit ce14043

File tree

4 files changed

+40
-22
lines changed

4 files changed

+40
-22
lines changed

mypy/plugins/default.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -304,25 +304,26 @@ def typed_dict_pop_callback(ctx: MethodContext) -> Type:
304304
and len(ctx.arg_types) >= 1
305305
and len(ctx.arg_types[0]) == 1
306306
):
307-
keys = try_getting_str_literals(ctx.args[0][0], ctx.arg_types[0][0])
307+
key_expr = ctx.args[0][0]
308+
keys = try_getting_str_literals(key_expr, ctx.arg_types[0][0])
308309
if keys is None:
309310
ctx.api.fail(
310311
message_registry.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL,
311-
ctx.context,
312+
key_expr,
312313
code=codes.LITERAL_REQ,
313314
)
314315
return AnyType(TypeOfAny.from_error)
315316

316317
value_types = []
317318
for key in keys:
318319
if key in ctx.type.required_keys:
319-
ctx.api.msg.typeddict_key_cannot_be_deleted(ctx.type, key, ctx.context)
320+
ctx.api.msg.typeddict_key_cannot_be_deleted(ctx.type, key, key_expr)
320321

321322
value_type = ctx.type.items.get(key)
322323
if value_type:
323324
value_types.append(value_type)
324325
else:
325-
ctx.api.msg.typeddict_key_not_found(ctx.type, key, ctx.context)
326+
ctx.api.msg.typeddict_key_not_found(ctx.type, key, key_expr)
326327
return AnyType(TypeOfAny.from_error)
327328

328329
if len(ctx.args[1]) == 0:
@@ -363,27 +364,29 @@ def typed_dict_setdefault_callback(ctx: MethodContext) -> Type:
363364
and len(ctx.arg_types[0]) == 1
364365
and len(ctx.arg_types[1]) == 1
365366
):
366-
keys = try_getting_str_literals(ctx.args[0][0], ctx.arg_types[0][0])
367+
key_expr = ctx.args[0][0]
368+
keys = try_getting_str_literals(key_expr, ctx.arg_types[0][0])
367369
if keys is None:
368370
ctx.api.fail(
369371
message_registry.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL,
370-
ctx.context,
372+
key_expr,
371373
code=codes.LITERAL_REQ,
372374
)
373375
return AnyType(TypeOfAny.from_error)
374376

375377
assigned_readonly_keys = ctx.type.readonly_keys & set(keys)
376378
if assigned_readonly_keys:
377-
ctx.api.msg.readonly_keys_mutated(assigned_readonly_keys, context=ctx.context)
379+
ctx.api.msg.readonly_keys_mutated(assigned_readonly_keys, context=key_expr)
378380

379381
default_type = ctx.arg_types[1][0]
382+
default_expr = ctx.args[1][0]
380383

381384
value_types = []
382385
for key in keys:
383386
value_type = ctx.type.items.get(key)
384387

385388
if value_type is None:
386-
ctx.api.msg.typeddict_key_not_found(ctx.type, key, ctx.context)
389+
ctx.api.msg.typeddict_key_not_found(ctx.type, key, key_expr)
387390
return AnyType(TypeOfAny.from_error)
388391

389392
# The signature_callback above can't always infer the right signature
@@ -392,7 +395,7 @@ def typed_dict_setdefault_callback(ctx: MethodContext) -> Type:
392395
# default can be assigned to all key-value pairs we're updating.
393396
if not is_subtype(default_type, value_type):
394397
ctx.api.msg.typeddict_setdefault_arguments_inconsistent(
395-
default_type, value_type, ctx.context
398+
default_type, value_type, default_expr
396399
)
397400
return AnyType(TypeOfAny.from_error)
398401

@@ -409,20 +412,21 @@ def typed_dict_delitem_callback(ctx: MethodContext) -> Type:
409412
and len(ctx.arg_types) == 1
410413
and len(ctx.arg_types[0]) == 1
411414
):
412-
keys = try_getting_str_literals(ctx.args[0][0], ctx.arg_types[0][0])
415+
key_expr = ctx.args[0][0]
416+
keys = try_getting_str_literals(key_expr, ctx.arg_types[0][0])
413417
if keys is None:
414418
ctx.api.fail(
415419
message_registry.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL,
416-
ctx.context,
420+
key_expr,
417421
code=codes.LITERAL_REQ,
418422
)
419423
return AnyType(TypeOfAny.from_error)
420424

421425
for key in keys:
422426
if key in ctx.type.required_keys or key in ctx.type.readonly_keys:
423-
ctx.api.msg.typeddict_key_cannot_be_deleted(ctx.type, key, ctx.context)
427+
ctx.api.msg.typeddict_key_cannot_be_deleted(ctx.type, key, key_expr)
424428
elif key not in ctx.type.items:
425-
ctx.api.msg.typeddict_key_not_found(ctx.type, key, ctx.context)
429+
ctx.api.msg.typeddict_key_not_found(ctx.type, key, key_expr)
426430
return ctx.default_return_type
427431

428432

test-data/unit/check-columns.test

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,19 @@ class D(TypedDict):
227227
x: int
228228
t: D = {'x':
229229
'y'} # E:5: Incompatible types (expression has type "str", TypedDict item "x" has type "int")
230+
s: str
230231

231232
if int():
232-
del t['y'] # E:5: TypedDict "D" has no key "y"
233+
del t[s] # E:11: Expected TypedDict key to be string literal
234+
del t["x"] # E:11: Key "x" of TypedDict "D" cannot be deleted
235+
del t["y"] # E:11: TypedDict "D" has no key "y"
236+
237+
t.pop(s) # E:7: Expected TypedDict key to be string literal
238+
t.pop("y") # E:7: TypedDict "D" has no key "y"
239+
240+
t.setdefault(s, 123) # E:14: Expected TypedDict key to be string literal
241+
t.setdefault("x", "a") # E:19: Argument 2 to "setdefault" of "TypedDict" has incompatible type "str"; expected "int"
242+
t.setdefault("y", 123) # E:14: TypedDict "D" has no key "y"
233243
[builtins fixtures/dict.pyi]
234244
[typing fixtures/typing-typeddict.pyi]
235245

test-data/unit/check-literal.test

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1909,8 +1909,9 @@ reveal_type(d.get(a_key, u)) # N: Revealed type is "Union[builtins.int, __main_
19091909
reveal_type(d.get(b_key, u)) # N: Revealed type is "Union[builtins.str, __main__.Unrelated]"
19101910
reveal_type(d.get(c_key, u)) # N: Revealed type is "builtins.object"
19111911

1912-
reveal_type(d.pop(a_key)) # E: Key "a" of TypedDict "Outer" cannot be deleted \
1913-
# N: Revealed type is "builtins.int"
1912+
reveal_type(d.pop(a_key)) # N: Revealed type is "builtins.int" \
1913+
# E: Key "a" of TypedDict "Outer" cannot be deleted
1914+
19141915
reveal_type(d.pop(b_key)) # N: Revealed type is "builtins.str"
19151916
d.pop(c_key) # E: TypedDict "Outer" has no key "c"
19161917

test-data/unit/check-typeddict.test

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1747,8 +1747,9 @@ td: Union[TDA, TDB]
17471747

17481748
reveal_type(td.pop('a')) # N: Revealed type is "builtins.int"
17491749
reveal_type(td.pop('b')) # N: Revealed type is "Union[builtins.str, builtins.int]"
1750-
reveal_type(td.pop('c')) # E: TypedDict "TDA" has no key "c" \
1751-
# N: Revealed type is "Union[Any, builtins.int]"
1750+
reveal_type(td.pop('c')) # N: Revealed type is "Union[Any, builtins.int]" \
1751+
# E: TypedDict "TDA" has no key "c"
1752+
17521753
[builtins fixtures/dict.pyi]
17531754
[typing fixtures/typing-typeddict.pyi]
17541755

@@ -2614,8 +2615,9 @@ def func(foo: Union[Foo1, Foo2]):
26142615

26152616
del foo["missing"] # E: TypedDict "Foo1" has no key "missing" \
26162617
# E: TypedDict "Foo2" has no key "missing"
2617-
del foo[1] # E: Expected TypedDict key to be string literal \
2618-
# E: Argument 1 to "__delitem__" has incompatible type "int"; expected "str"
2618+
del foo[1] # E: Argument 1 to "__delitem__" has incompatible type "int"; expected "str" \
2619+
# E: Expected TypedDict key to be string literal
2620+
26192621
[builtins fixtures/dict.pyi]
26202622
[typing fixtures/typing-typeddict.pyi]
26212623

@@ -3726,8 +3728,9 @@ class TP(TypedDict):
37263728
mutable: bool
37273729

37283730
x: TP
3729-
reveal_type(x.pop("key")) # E: Key "key" of TypedDict "TP" cannot be deleted \
3730-
# N: Revealed type is "builtins.str"
3731+
reveal_type(x.pop("key")) # N: Revealed type is "builtins.str" \
3732+
# E: Key "key" of TypedDict "TP" cannot be deleted
3733+
37313734

37323735
x.update({"key": "abc", "other": 1, "mutable": True}) # E: ReadOnly TypedDict keys ("key", "other") TypedDict are mutated
37333736
x.setdefault("key", "abc") # E: ReadOnly TypedDict key "key" TypedDict is mutated

0 commit comments

Comments
 (0)