From a1b132a4265032c55685abf2cf538245a26b565a Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 29 Jun 2025 03:01:03 +0200 Subject: [PATCH 1/4] Correct missing arguments message in presence of kwargs unpacking --- mypy/checkexpr.py | 32 +++++++++++-- test-data/unit/check-functions.test | 73 +++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 603acbe84f0f..b32d40323e7e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2389,14 +2389,22 @@ def check_argument_count( ) # Check for too many or few values for formals. + missing_contiguous_pos_block = [] + seen_kw = False for i, kind in enumerate(callee.arg_kinds): mapped_args = formal_to_actual[i] + seen_kw = seen_kw or any(actual_kinds[k].is_named(star=True) for k in mapped_args) if kind.is_required() and not mapped_args and not is_unexpected_arg_error: # No actual for a mandatory formal - if kind.is_positional(): - self.msg.too_few_arguments(callee, context, actual_names) - if object_type and callable_name and "." in callable_name: - self.missing_classvar_callable_note(object_type, callable_name, context) + if ( + kind.is_positional() + and not seen_kw + and ( + not missing_contiguous_pos_block + or missing_contiguous_pos_block[-1] == i - 1 + ) + ): + missing_contiguous_pos_block.append(i) else: argname = callee.arg_names[i] or "?" self.msg.missing_named_argument(callee, context, argname) @@ -2432,6 +2440,22 @@ def check_argument_count( if actual_kinds[mapped_args[0]] == nodes.ARG_STAR2 and paramspec_entries > 1: self.msg.fail("ParamSpec.kwargs should only be passed once", context) ok = False + if missing_contiguous_pos_block: + if ArgKind.ARG_STAR2 in actual_kinds: + # To generate a correct message, expand kwargs manually. If a name was + # missing from the call but doesn't belong to continuous positional prefix, + # it was already reported as a missing kwarg. All args before first prefix + # item are guaranteed to have been passed positionally. + names_to_use = [None] * missing_contiguous_pos_block[0] + [ + name + for i, name in enumerate(callee.arg_names) + if name is not None and i not in missing_contiguous_pos_block + ] + else: + names_to_use = actual_names + self.msg.too_few_arguments(callee, context, names_to_use) + if object_type and callable_name and "." in callable_name: + self.missing_classvar_callable_note(object_type, callable_name, context) return ok def check_for_extra_actual_arguments( diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index ceb7af433dce..880a9705e4d9 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -2467,6 +2467,79 @@ f(b=4) # E: Missing positional argument "a" in call to "f" def f(a, b, c, d=None) -> None: pass f(1, d=3) # E: Missing positional arguments "b", "c" in call to "f" +[case testMissingArgumentsErrorFromTypedDict] +from typing import Optional,TypedDict + +class DA(TypedDict): + a: int +class DB(TypedDict): + b: int +class DC(TypedDict): + c: int + +class DAB(DA, DB): pass +class DAC(DA, DC): pass +class DBC(DB, DC): pass +class DABC(DA, DB, DC): pass + +da: DA +db: DB +dc: DC +dab: DAB +dac: DAC +dbc: DBC +dabc: DABC + +def f(a: int, b: int, c: int, d: Optional[int] = None) -> None: pass + +f(**da) # E: Missing named argument "b" for "f" \ + # E: Missing named argument "c" for "f" +f(**db) # E: Missing named argument "c" for "f" \ + # E: Missing positional argument "a" in call to "f" +f(**dc) # E: Missing positional arguments "a", "b" in call to "f" +f(**dab) # E: Missing named argument "c" for "f" +f(**dac) # E: Missing named argument "b" for "f" +f(**dbc) # E: Missing positional argument "a" in call to "f" +f(**dabc) + +def g(a: int, /, b: int, c: int) -> None: pass + +g(**da) # E: Extra argument "a" from **args for "g" +g(0, **da) # E: Extra argument "a" from **args for "g" +g(**db) # E: Missing named argument "c" for "g" \ + # E: Too few arguments for "g" +g(0, **db) # E: Missing named argument "c" for "g" +g(**dc) # E: Too few arguments for "g" +g(0, **dc) # E: Missing positional argument "b" in call to "g" +g(**dab) # E: Extra argument "a" from **args for "g" +g(0, **dab) # E: Extra argument "a" from **args for "g" +g(**dac) # E: Extra argument "a" from **args for "g" +g(0, **dac) # E: Extra argument "a" from **args for "g" +g(**dbc) # E: Too few arguments for "g" +g(0, **dbc) +g(**dabc) # E: Extra argument "a" from **args for "g" + +def h(a: int, b: int, /, c: int) -> None: pass + +h(**da) # E: Extra argument "a" from **args for "h" +h(0, **da) # E: Extra argument "a" from **args for "h" +h(0, 1, **da) # E: Extra argument "a" from **args for "h" +h(**db) # E: Extra argument "b" from **args for "h" +h(0, **db) # E: Extra argument "b" from **args for "h" +h(0, 1, **db) # E: Extra argument "b" from **args for "h" +h(**dc) # E: Too few arguments for "h" +h(0, **dc) # E: Too few arguments for "h" +h(0, 1, **dc) +h(**dab) # E: Extra argument "b" from **args for "h" +h(0, **dab) # E: Extra argument "b" from **args for "h" +h(**dac) # E: Extra argument "a" from **args for "h" +h(0, **dac) # E: Extra argument "a" from **args for "h" +h(**dbc) # E: Extra argument "b" from **args for "h" +h(0, **dbc) # E: Extra argument "b" from **args for "h" +h(**dabc) # E: Extra argument "b" from **args for "h" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + [case testReturnTypeLineNumberWithDecorator] def dec(f): pass From 6e75bfd3b6d8ccecf5c8bcf7611faab48b90cbb2 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 29 Jun 2025 03:21:02 +0200 Subject: [PATCH 2/4] Update other affected tests --- mypy/checkexpr.py | 19 +++++++------------ test-data/unit/check-errorcodes.test | 3 ++- test-data/unit/check-python312.test | 2 +- test-data/unit/check-python38.test | 2 +- test-data/unit/check-typeddict.test | 2 +- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b32d40323e7e..3fda8d52ed75 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2441,18 +2441,13 @@ def check_argument_count( self.msg.fail("ParamSpec.kwargs should only be passed once", context) ok = False if missing_contiguous_pos_block: - if ArgKind.ARG_STAR2 in actual_kinds: - # To generate a correct message, expand kwargs manually. If a name was - # missing from the call but doesn't belong to continuous positional prefix, - # it was already reported as a missing kwarg. All args before first prefix - # item are guaranteed to have been passed positionally. - names_to_use = [None] * missing_contiguous_pos_block[0] + [ - name - for i, name in enumerate(callee.arg_names) - if name is not None and i not in missing_contiguous_pos_block - ] - else: - names_to_use = actual_names + # To generate a correct message, expand kwargs manually. If a name was + # missing from the call but doesn't belong to continuous positional prefix, + # it was already reported as a missing kwarg. All args before first prefix + # item are guaranteed to have been passed positionally. + passed_or_reported_names = callee.arg_names[missing_contiguous_pos_block[-1] + 1 :] + assert None not in passed_or_reported_names + names_to_use = [None] * missing_contiguous_pos_block[0] + passed_or_reported_names self.msg.too_few_arguments(callee, context, names_to_use) if object_type and callable_name and "." in callable_name: self.missing_classvar_callable_note(object_type, callable_name, context) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index d6e3366401dd..9f573b581d00 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -242,7 +242,8 @@ g() # E: Missing named argument "x" for "g" [call-arg] def h(x: int, y: int, z: int) -> None: pass h(y=1, z=1) # E: Missing positional argument "x" in call to "h" [call-arg] -h(y=1) # E: Missing positional arguments "x", "z" in call to "h" [call-arg] +h(y=1) # E: Missing named argument "z" for "h" [call-arg] \ + # E: Missing positional argument "x" in call to "h" [call-arg] [case testErrorCodeArgType] def f(x: int) -> None: pass diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index bfd6334b5077..a0b9f933b656 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -889,7 +889,7 @@ def h(x: int, y: str) -> None: pass g(h, 1, y='x') g(h, 1, x=1) # E: "g" gets multiple values for keyword argument "x" \ - # E: Missing positional argument "y" in call to "g" + # E: Missing named argument "y" for "g" class C[**P, T]: def m(self, *args: P.args, **kwargs: P.kwargs) -> T: ... diff --git a/test-data/unit/check-python38.test b/test-data/unit/check-python38.test index dd3f793fd02b..4f409642a957 100644 --- a/test-data/unit/check-python38.test +++ b/test-data/unit/check-python38.test @@ -154,7 +154,7 @@ f(0, 0, kw=0) f(0, p_or_kw=0, kw=0) f(p=0, p_or_kw=0, kw=0) # E: Unexpected keyword argument "p" for "f" f(0, **d) -f(**d) # E: Missing positional argument "p_or_kw" in call to "f" +f(**d) # E: Too few arguments for "f" [builtins fixtures/dict.pyi] [case testPEP570Signatures1] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index a068a63274ca..2290eae8ba39 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1596,7 +1596,7 @@ f1(**a) f2(**a) # E: Argument "y" to "f2" has incompatible type "str"; expected "int" f3(**a) # E: Argument "x" to "f3" has incompatible type "int"; expected "B" f4(**a) # E: Extra argument "y" from **args for "f4" -f5(**a) # E: Missing positional arguments "y", "z" in call to "f5" +f5(**a) # E: Missing named argument "z" for "f5" f6(**a) # E: Extra argument "y" from **args for "f6" f1(1, **a) # E: "f1" gets multiple values for keyword argument "x" [builtins fixtures/dict.pyi] From e007c3bcdfba7ef4ea8fe85ae80dd542dbdd7e83 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 29 Jun 2025 03:32:10 +0200 Subject: [PATCH 3/4] Account for optional posonly args --- mypy/checkexpr.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3fda8d52ed75..f497b36101f9 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2389,7 +2389,7 @@ def check_argument_count( ) # Check for too many or few values for formals. - missing_contiguous_pos_block = [] + missing_contiguous_pos_block: list[int] = [] seen_kw = False for i, kind in enumerate(callee.arg_kinds): mapped_args = formal_to_actual[i] @@ -2445,8 +2445,12 @@ def check_argument_count( # missing from the call but doesn't belong to continuous positional prefix, # it was already reported as a missing kwarg. All args before first prefix # item are guaranteed to have been passed positionally. - passed_or_reported_names = callee.arg_names[missing_contiguous_pos_block[-1] + 1 :] - assert None not in passed_or_reported_names + passed_or_reported_names = [ + name + for name in callee.arg_names[missing_contiguous_pos_block[-1] + 1 :] + # None may be there if it was an optional posonly param + if name is not None + ] names_to_use = [None] * missing_contiguous_pos_block[0] + passed_or_reported_names self.msg.too_few_arguments(callee, context, names_to_use) if object_type and callable_name and "." in callable_name: From f0a5dd3fe8b63b67c84021373bd09342a95214dd Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 29 Jun 2025 04:14:47 +0200 Subject: [PATCH 4/4] Sync tests again, account for optional kwonly args --- mypy/checkexpr.py | 6 +++++- test-data/unit/check-classes.test | 4 ++-- test-data/unit/check-dataclasses.test | 3 ++- test-data/unit/check-functools.test | 6 +++--- test-data/unit/check-statements.test | 4 ++-- test-data/unit/check-varargs.test | 6 +++--- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f497b36101f9..5534fb7e586a 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2393,7 +2393,11 @@ def check_argument_count( seen_kw = False for i, kind in enumerate(callee.arg_kinds): mapped_args = formal_to_actual[i] - seen_kw = seen_kw or any(actual_kinds[k].is_named(star=True) for k in mapped_args) + seen_kw = ( + seen_kw + or kind == ArgKind.ARG_NAMED_OPT + or any(actual_kinds[k].is_named(star=True) for k in mapped_args) + ) if kind.is_required() and not mapped_args and not is_unexpected_arg_error: # No actual for a mandatory formal if ( diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index bf6c51e86446..981d2d8520ec 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -2008,7 +2008,7 @@ class D: def __set__(self, inst, v, other): pass class A: f = D() -A().f = 'x' # E: Too few arguments for "__set__" +A().f = 'x' # E: Missing positional argument "other" in call to "__set__" [case testDescriptorDunderSetWrongArgTypes] class D: @@ -2036,7 +2036,7 @@ class D: def __get__(self, inst, own, other): pass class A: f = D() -A().f = 'x' # E: Too few arguments for "__get__" +A().f = 'x' # E: Missing positional argument "other" in call to "__get__" [case testDescriptorDunderGetWrongArgTypeForInstance] from typing import Any diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 2ead202bd6af..8c07a09c443d 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -425,10 +425,11 @@ class Application: name: str = 'Unnamed' rating: int = field(kw_only=False) # E: Attributes without a default cannot follow attributes with one +reveal_type(Application) # N: Revealed type is "def (name: builtins.str =, rating: builtins.int) -> __main__.Application" Application(name='name', rating=5) Application('name', 123) Application('name', rating=123) -Application() # E: Missing positional argument "name" in call to "Application" +Application() # E: Too few arguments for "Application" Application('name') # E: Too few arguments for "Application" [builtins fixtures/dataclasses.pyi] diff --git a/test-data/unit/check-functools.test b/test-data/unit/check-functools.test index fa2cacda275d..2901eebb8eac 100644 --- a/test-data/unit/check-functools.test +++ b/test-data/unit/check-functools.test @@ -495,9 +495,9 @@ def main5(**d2: Unpack[D2]) -> None: partial(fn2, **d2)() # E: Extra argument "a2" from **args for "fn2" def main6(a2good: A2Good, a2bad: A2Bad, **d1: Unpack[D1]) -> None: - partial(fn3, **d1)() # E: Missing positional argument "a1" in call to "fn3" + partial(fn3, **d1)() # E: Missing named argument "a2" for "fn3" partial(fn3, **d1)("asdf") # E: Too many positional arguments for "fn3" \ - # E: Too few arguments for "fn3" \ + # E: Missing named argument "a2" for "fn3" \ # E: Argument 1 to "fn3" has incompatible type "str"; expected "int" partial(fn3, **d1)(a2="asdf") partial(fn3, **d1)(**a2good) @@ -530,7 +530,7 @@ reveal_type(first_kw(args=[1])) # N: Revealed type is "builtins.int" # TODO: this is indeed invalid, but the error is incomprehensible. first_kw([1]) # E: Too many positional arguments for "get" \ - # E: Too few arguments for "get" \ + # E: Missing named argument "args" for "get" \ # E: Argument 1 to "get" has incompatible type "list[int]"; expected "int" [builtins fixtures/list.pyi] diff --git a/test-data/unit/check-statements.test b/test-data/unit/check-statements.test index 9ab68b32472d..6e64e299bfa8 100644 --- a/test-data/unit/check-statements.test +++ b/test-data/unit/check-statements.test @@ -453,9 +453,9 @@ if object(): if object(): raise MyErrorWithDefault if object(): - raise MyBaseError # E: Too few arguments for "MyBaseError" + raise MyBaseError # E: Missing positional argument "required" in call to "MyBaseError" if object(): - raise MyError # E: Too few arguments for "MyError" + raise MyError # E: Missing positional arguments "required1", "required2" in call to "MyError" if object(): raise MyKwError # E: Missing named argument "kwonly" for "MyKwError" [builtins fixtures/exception.pyi] diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index 680021a166f2..0f7a4bfb96af 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -266,7 +266,7 @@ f(*(a, b, b)) # E: Argument 1 to "f" has incompatible type "*tuple[A, B, B]"; ex f(*(b, b, c)) # E: Argument 1 to "f" has incompatible type "*tuple[B, B, C]"; expected "A" f(a, *(b, b)) # E: Argument 2 to "f" has incompatible type "*tuple[B, B]"; expected "C" f(b, *(b, c)) # E: Argument 1 to "f" has incompatible type "B"; expected "A" -f(*(a, b)) # E: Missing positional arguments "b", "c" in call to "f" +f(*(a, b)) # E: Missing positional argument "c" in call to "f" f(*(a, b, c, c)) # E: Too many arguments for "f" f(a, *(b, c, c)) # E: Too many arguments for "f" f(*(a, b, c)) @@ -342,7 +342,7 @@ f(b, *(b, b)) # E: Argument 1 to "f" has incompatible type "B"; expected "A" f(b, b, *(b,)) # E: Argument 1 to "f" has incompatible type "B"; expected "A" f(a, a, *(b,)) # E: Argument 2 to "f" has incompatible type "A"; expected "B" f(a, b, *(a,)) # E: Argument 3 to "f" has incompatible type "*tuple[A]"; expected "B" -f(*()) # E: Too few arguments for "f" +f(*()) # E: Missing positional argument "a" in call to "f" f(*(a, b, b)) f(a, *(b, b)) f(a, b, *(b,)) @@ -400,7 +400,7 @@ class A: pass class B: pass a, b = None, None # type: (A, B) -f(*()) # E: Too few arguments for "f" +f(*()) # E: Missing positional argument "a" in call to "f" f(a, *[a]) # E: Argument 2 to "f" has incompatible type "*list[A]"; expected "Optional[B]" \ # E: Argument 2 to "f" has incompatible type "*list[A]"; expected "B" f(a, b, *[a]) # E: Argument 3 to "f" has incompatible type "*list[A]"; expected "B" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy