Skip to content

Commit f72d032

Browse files
eendebakptclaude
andcommitted
gh-136681: Drop _PyArg_Parser for positional-only defining-class methods
A method that takes a defining class and whose other parameters are all positional-only (e.g. decimal.Decimal.from_float) was routed through Argument Clinic's generic keyword path: it emitted a static _PyArg_Parser, an (empty) keyword tuple and an _PyArg_UnpackKeywords() call, even though it accepts no keyword arguments. Such methods now go through parse_pos_only(): the wrapper still uses the mandatory METH_METHOD|METH_FASTCALL|METH_KEYWORDS convention (the only one that delivers the defining class), but it rejects keywords with _PyArg_NoKwnames() and reads positional arguments directly, with no parser, keyword tuple or argsbuf. This converts 135 methods across 19 clinic headers, removing the static _PyArg_Parser, the keyword arrays/tuples and the per-call _PyArg_UnpackKeywords() + parser_init() overhead for each. _PyArg_NoKwnames() is now exported (PyAPI_FUNC) so generated code in shared extension modules can use it, like _PyArg_NoKeywords(). Error messages for these methods now match ordinary positional-only functions ("expected ... arguments" / "takes no keyword arguments" instead of the _PyArg_UnpackKeywords phrasing); affected stdlib tests are updated accordingly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent fb46c67 commit f72d032

26 files changed

Lines changed: 644 additions & 2399 deletions

Include/internal/pycore_modsupport.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ extern "C" {
1010
#endif
1111

1212

13-
extern int _PyArg_NoKwnames(const char *funcname, PyObject *kwnames);
13+
// Export for 'array' shared extension
14+
PyAPI_FUNC(int) _PyArg_NoKwnames(const char *funcname, PyObject *kwnames);
1415
#define _PyArg_NoKwnames(funcname, kwnames) \
1516
((kwnames) == NULL || _PyArg_NoKwnames((funcname), (kwnames)))
1617

Lib/test/clinic.test.c

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6002,23 +6002,13 @@ static PyObject *
60026002
Test__pyarg_parsestackandkeywords(PyObject *self, PyTypeObject *cls, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
60036003
{
60046004
PyObject *return_value = NULL;
6005-
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
6006-
# define KWTUPLE (PyObject *)&_Py_SINGLETON(tuple_empty)
6007-
#else
6008-
# define KWTUPLE NULL
6009-
#endif
6010-
6011-
static const char * const _keywords[] = {"", NULL};
6012-
static _PyArg_Parser _parser = {
6013-
.keywords = _keywords,
6014-
.format = "s#:_pyarg_parsestackandkeywords",
6015-
.kwtuple = KWTUPLE,
6016-
};
6017-
#undef KWTUPLE
60186005
const char *key;
60196006
Py_ssize_t key_length;
60206007

6021-
if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser,
6008+
if (!_PyArg_NoKwnames("_pyarg_parsestackandkeywords", kwnames)) {
6009+
goto exit;
6010+
}
6011+
if (!_PyArg_ParseStack(args, nargs, "s#:_pyarg_parsestackandkeywords",
60226012
&key, &key_length)) {
60236013
goto exit;
60246014
}
@@ -6032,7 +6022,7 @@ static PyObject *
60326022
Test__pyarg_parsestackandkeywords_impl(TestObj *self, PyTypeObject *cls,
60336023
const char *key,
60346024
Py_ssize_t key_length)
6035-
/*[clinic end generated code: output=7060c213d7b8200e input=fc72ef4b4cfafabc]*/
6025+
/*[clinic end generated code: output=21bbb9a8d0100d72 input=fc72ef4b4cfafabc]*/
60366026

60376027

60386028
/*[clinic input]

Lib/test/test_clinic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4898,7 +4898,7 @@ def test_defclass_posonly_varpos(self):
48984898
cls = ac_tester.TestClass
48994899
obj = cls()
49004900
fn = obj.defclass_posonly_varpos
4901-
errmsg = 'takes at least 2 positional arguments'
4901+
errmsg = 'expected at least 2 arguments'
49024902
self.assertRaisesRegex(TypeError, errmsg, fn)
49034903
self.assertRaisesRegex(TypeError, errmsg, fn, 1)
49044904
self.assertEqual(fn(1, 2), (cls, 1, 2, ()))

Lib/test/test_sqlite3/test_hooks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ def test_trace_bad_handler(self):
450450

451451
def test_set_trace_callback_keyword_args(self):
452452
with self.assertRaisesRegex(TypeError,
453-
'takes exactly 1 positional argument'):
453+
'takes no keyword arguments'):
454454
self.con.set_trace_callback(trace_callback=lambda: None)
455455

456456
# When a handler has an invalid signature, the exception raised is

Lib/test/test_sqlite3/test_userfunctions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ def test_aggr_text(self):
720720

721721
def test_agg_keyword_args(self):
722722
with self.assertRaisesRegex(TypeError,
723-
'takes exactly 3 positional arguments'):
723+
'takes no keyword arguments'):
724724
self.con.create_aggregate("test", 1, aggregate_class=AggrText)
725725

726726

@@ -767,7 +767,7 @@ def test_clear_authorizer(self):
767767

768768
def test_authorizer_keyword_args(self):
769769
with self.assertRaisesRegex(TypeError,
770-
'takes exactly 1 positional argument'):
770+
'takes no keyword arguments'):
771771
self.con.set_authorizer(authorizer_callback=lambda: None)
772772

773773

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Argument Clinic now parses arguments of positional-only methods with a
2+
*defining class* directly, without a :c:type:`!_PyArg_Parser`.

0 commit comments

Comments
 (0)