Skip to content

Commit a1b132a

Browse files
committed
Correct missing arguments message in presence of kwargs unpacking
1 parent 5e9d657 commit a1b132a

File tree

2 files changed

+101
-4
lines changed

2 files changed

+101
-4
lines changed

mypy/checkexpr.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2389,14 +2389,22 @@ def check_argument_count(
23892389
)
23902390

23912391
# Check for too many or few values for formals.
2392+
missing_contiguous_pos_block = []
2393+
seen_kw = False
23922394
for i, kind in enumerate(callee.arg_kinds):
23932395
mapped_args = formal_to_actual[i]
2396+
seen_kw = seen_kw or any(actual_kinds[k].is_named(star=True) for k in mapped_args)
23942397
if kind.is_required() and not mapped_args and not is_unexpected_arg_error:
23952398
# No actual for a mandatory formal
2396-
if kind.is_positional():
2397-
self.msg.too_few_arguments(callee, context, actual_names)
2398-
if object_type and callable_name and "." in callable_name:
2399-
self.missing_classvar_callable_note(object_type, callable_name, context)
2399+
if (
2400+
kind.is_positional()
2401+
and not seen_kw
2402+
and (
2403+
not missing_contiguous_pos_block
2404+
or missing_contiguous_pos_block[-1] == i - 1
2405+
)
2406+
):
2407+
missing_contiguous_pos_block.append(i)
24002408
else:
24012409
argname = callee.arg_names[i] or "?"
24022410
self.msg.missing_named_argument(callee, context, argname)
@@ -2432,6 +2440,22 @@ def check_argument_count(
24322440
if actual_kinds[mapped_args[0]] == nodes.ARG_STAR2 and paramspec_entries > 1:
24332441
self.msg.fail("ParamSpec.kwargs should only be passed once", context)
24342442
ok = False
2443+
if missing_contiguous_pos_block:
2444+
if ArgKind.ARG_STAR2 in actual_kinds:
2445+
# To generate a correct message, expand kwargs manually. If a name was
2446+
# missing from the call but doesn't belong to continuous positional prefix,
2447+
# it was already reported as a missing kwarg. All args before first prefix
2448+
# item are guaranteed to have been passed positionally.
2449+
names_to_use = [None] * missing_contiguous_pos_block[0] + [
2450+
name
2451+
for i, name in enumerate(callee.arg_names)
2452+
if name is not None and i not in missing_contiguous_pos_block
2453+
]
2454+
else:
2455+
names_to_use = actual_names
2456+
self.msg.too_few_arguments(callee, context, names_to_use)
2457+
if object_type and callable_name and "." in callable_name:
2458+
self.missing_classvar_callable_note(object_type, callable_name, context)
24352459
return ok
24362460

24372461
def check_for_extra_actual_arguments(

test-data/unit/check-functions.test

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,6 +2467,79 @@ f(b=4) # E: Missing positional argument "a" in call to "f"
24672467
def f(a, b, c, d=None) -> None: pass
24682468
f(1, d=3) # E: Missing positional arguments "b", "c" in call to "f"
24692469

2470+
[case testMissingArgumentsErrorFromTypedDict]
2471+
from typing import Optional,TypedDict
2472+
2473+
class DA(TypedDict):
2474+
a: int
2475+
class DB(TypedDict):
2476+
b: int
2477+
class DC(TypedDict):
2478+
c: int
2479+
2480+
class DAB(DA, DB): pass
2481+
class DAC(DA, DC): pass
2482+
class DBC(DB, DC): pass
2483+
class DABC(DA, DB, DC): pass
2484+
2485+
da: DA
2486+
db: DB
2487+
dc: DC
2488+
dab: DAB
2489+
dac: DAC
2490+
dbc: DBC
2491+
dabc: DABC
2492+
2493+
def f(a: int, b: int, c: int, d: Optional[int] = None) -> None: pass
2494+
2495+
f(**da) # E: Missing named argument "b" for "f" \
2496+
# E: Missing named argument "c" for "f"
2497+
f(**db) # E: Missing named argument "c" for "f" \
2498+
# E: Missing positional argument "a" in call to "f"
2499+
f(**dc) # E: Missing positional arguments "a", "b" in call to "f"
2500+
f(**dab) # E: Missing named argument "c" for "f"
2501+
f(**dac) # E: Missing named argument "b" for "f"
2502+
f(**dbc) # E: Missing positional argument "a" in call to "f"
2503+
f(**dabc)
2504+
2505+
def g(a: int, /, b: int, c: int) -> None: pass
2506+
2507+
g(**da) # E: Extra argument "a" from **args for "g"
2508+
g(0, **da) # E: Extra argument "a" from **args for "g"
2509+
g(**db) # E: Missing named argument "c" for "g" \
2510+
# E: Too few arguments for "g"
2511+
g(0, **db) # E: Missing named argument "c" for "g"
2512+
g(**dc) # E: Too few arguments for "g"
2513+
g(0, **dc) # E: Missing positional argument "b" in call to "g"
2514+
g(**dab) # E: Extra argument "a" from **args for "g"
2515+
g(0, **dab) # E: Extra argument "a" from **args for "g"
2516+
g(**dac) # E: Extra argument "a" from **args for "g"
2517+
g(0, **dac) # E: Extra argument "a" from **args for "g"
2518+
g(**dbc) # E: Too few arguments for "g"
2519+
g(0, **dbc)
2520+
g(**dabc) # E: Extra argument "a" from **args for "g"
2521+
2522+
def h(a: int, b: int, /, c: int) -> None: pass
2523+
2524+
h(**da) # E: Extra argument "a" from **args for "h"
2525+
h(0, **da) # E: Extra argument "a" from **args for "h"
2526+
h(0, 1, **da) # E: Extra argument "a" from **args for "h"
2527+
h(**db) # E: Extra argument "b" from **args for "h"
2528+
h(0, **db) # E: Extra argument "b" from **args for "h"
2529+
h(0, 1, **db) # E: Extra argument "b" from **args for "h"
2530+
h(**dc) # E: Too few arguments for "h"
2531+
h(0, **dc) # E: Too few arguments for "h"
2532+
h(0, 1, **dc)
2533+
h(**dab) # E: Extra argument "b" from **args for "h"
2534+
h(0, **dab) # E: Extra argument "b" from **args for "h"
2535+
h(**dac) # E: Extra argument "a" from **args for "h"
2536+
h(0, **dac) # E: Extra argument "a" from **args for "h"
2537+
h(**dbc) # E: Extra argument "b" from **args for "h"
2538+
h(0, **dbc) # E: Extra argument "b" from **args for "h"
2539+
h(**dabc) # E: Extra argument "b" from **args for "h"
2540+
[builtins fixtures/dict.pyi]
2541+
[typing fixtures/typing-typeddict.pyi]
2542+
24702543
[case testReturnTypeLineNumberWithDecorator]
24712544
def dec(f): pass
24722545

0 commit comments

Comments
 (0)