From 4e2b84a47942709d3454d6dc78906bd00c8f9e56 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 6 Oct 2025 21:28:27 +0100 Subject: [PATCH 1/2] Commit --- Lib/inspect.py | 28 ++++++++++----- Lib/test/test_call.py | 2 +- Lib/test/test_dataclasses/__init__.py | 18 +++++----- Lib/test/test_extcall.py | 16 ++++----- Lib/test/test_positional_only_arg.py | 8 ++--- Python/ceval.c | 49 ++++++++++++++++++++++----- 6 files changed, 83 insertions(+), 38 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index b345623b3fa2db..a04efe1071033d 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1381,7 +1381,7 @@ def convert(name, locals=locals, specs.append(formatvarkw(varkw) + formatvalue(locals[varkw])) return '(' + ', '.join(specs) + ')' -def _missing_arguments(f_name, argnames, pos, values): +def _missing_arguments(f_name, argnames, pos, values, *, has_posonly=False): names = [repr(name) for name in argnames if name not in values] missing = len(names) if missing == 1: @@ -1392,10 +1392,18 @@ def _missing_arguments(f_name, argnames, pos, values): tail = ", {} and {}".format(*names[-2:]) del names[-2:] s = ", ".join(names) + tail - raise TypeError("%s() missing %i required %s argument%s: %s" % - (f_name, missing, - "positional" if pos else "keyword-only", - "" if missing == 1 else "s", s)) + + if pos: + qualifier = "positional" if has_posonly else "" + else: + qualifier = "keyword-only" + if qualifier: + raise TypeError("%s() missing %i required %s argument%s: %s" % + (f_name, missing, qualifier, + "" if missing == 1 else "s", s)) + else: + raise TypeError("%s() missing %i required argument%s: %s" % + (f_name, missing, "" if missing == 1 else "s", s)) def _too_many(f_name, args, kwonly, varargs, defcount, given, values): atleast = len(args) - defcount @@ -1429,7 +1437,9 @@ def getcallargs(func, /, *positional, **named): f_name = func.__name__ arg2value = {} - + num_posonly = 0 + if hasattr(func, '__code__'): + num_posonly = func.__code__.co_posonlyargcount if ismethod(func) and func.__self__ is not None: # implicit 'self' (or 'cls' for classmethods) argument positional = (func.__self__,) + positional @@ -1463,7 +1473,9 @@ def getcallargs(func, /, *positional, **named): req = args[:num_args - num_defaults] for arg in req: if arg not in arg2value: - _missing_arguments(f_name, req, True, arg2value) + missing_posonly = any(i < num_posonly for i, name in enumerate(args) + if name in req and name not in arg2value) + _missing_arguments(f_name, req, True, arg2value, has_posonly=missing_posonly) for i, arg in enumerate(args[num_args - num_defaults:]): if arg not in arg2value: arg2value[arg] = defaults[i] @@ -1475,7 +1487,7 @@ def getcallargs(func, /, *positional, **named): else: missing += 1 if missing: - _missing_arguments(f_name, kwonlyargs, False, arg2value) + _missing_arguments(f_name, kwonlyargs, False, arg2value, has_posonly=False) return arg2value ClosureVars = namedtuple('ClosureVars', 'nonlocals globals builtins unbound') diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 31e58e825be422..cd2957fdb7f18f 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -920,7 +920,7 @@ def check_raises_type_error(self, message): self.assertEqual(str(cm.exception), message) def test_missing_arguments(self): - msg = "A.method_two_args() missing 1 required positional argument: 'y'" + msg = "A.method_two_args() missing 1 required argument: 'y'" with self.check_raises_type_error(msg): A().method_two_args("x") diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 6bf5e5b3e5554b..e830a8b36c90a5 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -534,7 +534,7 @@ class C: with self.assertRaisesRegex(TypeError, r"__init__\(\) missing 1 required " - "positional argument: 'x'"): + "argument: 'x'"): C() def test_field_default(self): @@ -935,7 +935,7 @@ class C: x: int=field(default=MISSING) with self.assertRaisesRegex(TypeError, r'__init__\(\) missing 1 required ' - 'positional argument'): + 'argument'): C() self.assertNotIn('x', C.__dict__) @@ -944,7 +944,7 @@ class D: x: int with self.assertRaisesRegex(TypeError, r'__init__\(\) missing 1 required ' - 'positional argument'): + 'argument'): D() self.assertNotIn('x', D.__dict__) @@ -957,7 +957,7 @@ class C: x: int=field(default_factory=MISSING) with self.assertRaisesRegex(TypeError, r'__init__\(\) missing 1 required ' - 'positional argument'): + 'argument'): C() self.assertNotIn('x', C.__dict__) @@ -966,7 +966,7 @@ class D: x: int=field(default=MISSING, default_factory=MISSING) with self.assertRaisesRegex(TypeError, r'__init__\(\) missing 1 required ' - 'positional argument'): + 'argument'): D() self.assertNotIn('x', D.__dict__) @@ -3260,7 +3260,7 @@ class C: # also have a default value (of type # types.MemberDescriptorType). with self.assertRaisesRegex(TypeError, - r"__init__\(\) missing 1 required positional argument: 'x'"): + r"__init__\(\) missing 1 required argument: 'x'"): C() # We can create an instance, and assign to x. @@ -3784,7 +3784,7 @@ def __init_subclass__(cls, arg): with self.assertRaisesRegex( TypeError, - "missing 1 required positional argument: 'arg'", + "missing 1 required argument: 'arg'", ): @dataclass(slots=True) class WithWrongSuper(WrongSuper, arg=1): @@ -4018,7 +4018,7 @@ def __set__(self, instance: Any, value: int) -> None: class C: i: D = D() - with self.assertRaisesRegex(TypeError, 'missing 1 required positional argument'): + with self.assertRaisesRegex(TypeError, 'missing 1 required argument'): c = C() class TestStringAnnotations(unittest.TestCase): @@ -4227,7 +4227,7 @@ class Base2: C = make_dataclass('C', [('y', int)], bases=(Base1, Base2)) - with self.assertRaisesRegex(TypeError, 'required positional'): + with self.assertRaisesRegex(TypeError, 'required argument'): c = C(2) c = C(1, 2) self.assertIsInstance(c, C) diff --git a/Lib/test/test_extcall.py b/Lib/test/test_extcall.py index d9d85fe79af883..5e2948c453a82b 100644 --- a/Lib/test/test_extcall.py +++ b/Lib/test/test_extcall.py @@ -111,17 +111,17 @@ >>> g() Traceback (most recent call last): ... - TypeError: g() missing 1 required positional argument: 'x' + TypeError: g() missing 1 required argument: 'x' >>> g(*()) Traceback (most recent call last): ... - TypeError: g() missing 1 required positional argument: 'x' + TypeError: g() missing 1 required argument: 'x' >>> g(*(), **{}) Traceback (most recent call last): ... - TypeError: g() missing 1 required positional argument: 'x' + TypeError: g() missing 1 required argument: 'x' >>> g(1) 1 () {} @@ -505,27 +505,27 @@ >>> f() Traceback (most recent call last): ... - TypeError: f() missing 1 required positional argument: 'a' + TypeError: f() missing 1 required argument: 'a' >>> def f(a, b): pass >>> f() Traceback (most recent call last): ... - TypeError: f() missing 2 required positional arguments: 'a' and 'b' + TypeError: f() missing 2 required arguments: 'a' and 'b' >>> def f(a, b, c): pass >>> f() Traceback (most recent call last): ... - TypeError: f() missing 3 required positional arguments: 'a', 'b', and 'c' + TypeError: f() missing 3 required arguments: 'a', 'b', and 'c' >>> def f(a, b, c, d, e): pass >>> f() Traceback (most recent call last): ... - TypeError: f() missing 5 required positional arguments: 'a', 'b', 'c', 'd', and 'e' + TypeError: f() missing 5 required arguments: 'a', 'b', 'c', 'd', and 'e' >>> def f(a, b=4, c=5, d=5): pass >>> f(c=12, b=9) Traceback (most recent call last): ... - TypeError: f() missing 1 required positional argument: 'a' + TypeError: f() missing 1 required argument: 'a' Same with keyword only args: diff --git a/Lib/test/test_positional_only_arg.py b/Lib/test/test_positional_only_arg.py index e412cb1d58d5db..52fcd78537248b 100644 --- a/Lib/test/test_positional_only_arg.py +++ b/Lib/test/test_positional_only_arg.py @@ -142,7 +142,7 @@ def f(a, b, /): def test_positional_only_and_arg_invalid_calls(self): def f(a, b, /, c): pass - with self.assertRaisesRegex(TypeError, r"f\(\) missing 1 required positional argument: 'c'"): + with self.assertRaisesRegex(TypeError, r"f\(\) missing 1 required argument: 'c'"): f(1, 2) with self.assertRaisesRegex(TypeError, r"f\(\) missing 2 required positional arguments: 'b' and 'c'"): f(1) @@ -170,7 +170,7 @@ def f(a, b, /, c, *, d, e): f(1, 2, 3, e=2) with self.assertRaisesRegex(TypeError, r"missing 2 required keyword-only arguments: 'd' and 'e'"): f(1, 2, 3) - with self.assertRaisesRegex(TypeError, r"f\(\) missing 1 required positional argument: 'c'"): + with self.assertRaisesRegex(TypeError, r"f\(\) missing 1 required argument: 'c'"): f(1, 2) with self.assertRaisesRegex(TypeError, r"f\(\) missing 2 required positional arguments: 'b' and 'c'"): f(1) @@ -278,7 +278,7 @@ def g(x2,/,y2): return g self.assertEqual(f(1,2)(3,4), 10) - with self.assertRaisesRegex(TypeError, r"g\(\) missing 1 required positional argument: 'y2'"): + with self.assertRaisesRegex(TypeError, r"g\(\) missing 1 required argument: 'y2'"): f(1,2)(3) with self.assertRaisesRegex(TypeError, r"g\(\) takes 2 positional arguments but 3 were given"): f(1,2)(3,4,5) @@ -296,7 +296,7 @@ def g(x2,/,y2): return g self.assertEqual(f(1,2)(3,4), 10) - with self.assertRaisesRegex(TypeError, r"g\(\) missing 1 required positional argument: 'y2'"): + with self.assertRaisesRegex(TypeError, r"g\(\) missing 1 required argument: 'y2'"): f(1,2)(3) with self.assertRaisesRegex(TypeError, r"g\(\) takes 2 positional arguments but 3 were given"): f(1,2)(3,4,5) diff --git a/Python/ceval.c b/Python/ceval.c index 0ccaacaf3ed5b1..5b3aaad878ca1c 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1322,13 +1322,22 @@ format_missing(PyThreadState *tstate, const char *kind, } if (name_str == NULL) return; - _PyErr_Format(tstate, PyExc_TypeError, - "%U() missing %i required %s argument%s: %U", - qualname, - len, - kind, - len == 1 ? "" : "s", - name_str); + if (kind[0] == '\0') { + _PyErr_Format(tstate, PyExc_TypeError, + "%U() missing %i required argument%s: %U", + qualname, + len, + len == 1 ? "" : "s", + name_str); + } else { + _PyErr_Format(tstate, PyExc_TypeError, + "%U() missing %i required %s argument%s: %U", + qualname, + len, + kind, + len == 1 ? "" : "s", + name_str); + } Py_DECREF(name_str); } @@ -1340,9 +1349,33 @@ missing_arguments(PyThreadState *tstate, PyCodeObject *co, Py_ssize_t i, j = 0; Py_ssize_t start, end; int positional = (defcount != -1); - const char *kind = positional ? "positional" : "keyword-only"; + const char *kind = ""; PyObject *missing_names; + if (positional) { + int has_posonly_missing = 0; + + start = 0; + end = co->co_argcount - defcount; + + for (i = start; i < end; i++) { + if (PyStackRef_IsNull(localsplus[i])) { + if (i < co->co_posonlyargcount) { + has_posonly_missing = 1; + break; + } + } + } + + if (has_posonly_missing) { + kind = "positional"; + } else { + kind = ""; + } + } else { + kind = "keyword-only"; + } + /* Compute the names of the arguments that are missing. */ missing_names = PyList_New(missing); if (missing_names == NULL) From 4cc33d0c4f446378ba57a389523576f9a8015a80 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 6 Oct 2025 21:34:01 +0100 Subject: [PATCH 2/2] Commit --- .../2025-10-06-21-33-55.gh-issue-90795.Oz7t1l.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-06-21-33-55.gh-issue-90795.Oz7t1l.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-06-21-33-55.gh-issue-90795.Oz7t1l.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-06-21-33-55.gh-issue-90795.Oz7t1l.rst new file mode 100644 index 00000000000000..2240d8e965b992 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-06-21-33-55.gh-issue-90795.Oz7t1l.rst @@ -0,0 +1,2 @@ +Fix an error message that incorrectly specified a missing argument as +"positional" in function calls.