Skip to content

Commit 1c4fa4f

Browse files
eendebakptclaude
andauthored
gh-157407: Argument Clinic: parse positional-only defining_class methods without _PyArg_Parser (#157483)
* 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> * gh-157407: Rename NEWS entry to the correct issue number Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ca5277c commit 1c4fa4f

26 files changed

Lines changed: 643 additions & 2398 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
@@ -6032,23 +6032,13 @@ static PyObject *
60326032
Test__pyarg_parsestackandkeywords(PyObject *self, PyTypeObject *cls, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
60336033
{
60346034
PyObject *return_value = NULL;
6035-
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
6036-
# define KWTUPLE (PyObject *)&_Py_SINGLETON(tuple_empty)
6037-
#else
6038-
# define KWTUPLE NULL
6039-
#endif
6040-
6041-
static const char * const _keywords[] = {"", NULL};
6042-
static _PyArg_Parser _parser = {
6043-
.keywords = _keywords,
6044-
.format = "s#:_pyarg_parsestackandkeywords",
6045-
.kwtuple = KWTUPLE,
6046-
};
6047-
#undef KWTUPLE
60486035
const char *key;
60496036
Py_ssize_t key_length;
60506037

6051-
if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser,
6038+
if (!_PyArg_NoKwnames("_pyarg_parsestackandkeywords", kwnames)) {
6039+
goto exit;
6040+
}
6041+
if (!_PyArg_ParseStack(args, nargs, "s#:_pyarg_parsestackandkeywords",
60526042
&key, &key_length)) {
60536043
goto exit;
60546044
}
@@ -6062,7 +6052,7 @@ static PyObject *
60626052
Test__pyarg_parsestackandkeywords_impl(TestObj *self, PyTypeObject *cls,
60636053
const char *key,
60646054
Py_ssize_t key_length)
6065-
/*[clinic end generated code: output=7060c213d7b8200e input=fc72ef4b4cfafabc]*/
6055+
/*[clinic end generated code: output=21bbb9a8d0100d72 input=fc72ef4b4cfafabc]*/
60666056

60676057

60686058
/*[clinic input]

‎Lib/test/test_clinic.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5114,7 +5114,7 @@ def test_defclass_posonly_varpos(self):
51145114
cls = ac_tester.TestClass
51155115
obj = cls()
51165116
fn = obj.defclass_posonly_varpos
5117-
errmsg = 'takes at least 2 positional arguments'
5117+
errmsg = 'expected at least 2 arguments'
51185118
self.assertRaisesRegex(TypeError, errmsg, fn)
51195119
self.assertRaisesRegex(TypeError, errmsg, fn, 1)
51205120
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)