https://github.com/python/cpython/commit/2c142a14edd70941731d695b48e4c0e2ac5f6f43
commit: 2c142a14edd70941731d695b48e4c0e2ac5f6f43
branch: main
author: Donghee Na <[email protected]>
committer: ZeroIntensity <[email protected]>
date: 2026-10-21T12:06:29-04:00
summary:
gh-114258: Use Argument Clinic for `__len__` (GH-160012)
files:
M Lib/test/test_clinic.py
M Modules/_dbmmodule.c
M Modules/_gdbmmodule.c
M Modules/clinic/_dbmmodule.c.h
M Modules/clinic/_gdbmmodule.c.h
M Modules/clinic/mmapmodule.c.h
M Modules/mmapmodule.c
M Tools/clinic/libclinic/dsl_parser.py
M Tools/clinic/libclinic/slots.py
diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py
index 8d2aced0b9caaf..e3dd4a2be9e410 100644
--- a/Lib/test/test_clinic.py
+++ b/Lib/test/test_clinic.py
@@ -23,8 +23,7 @@
import libclinic
from libclinic import ClinicError, unspecified, NULL, fail
from libclinic.converters import (
- int_converter, object_converter, str_converter, self_converter,
- Py_ssize_t_converter)
+ int_converter, object_converter, str_converter, self_converter)
from libclinic.function import (
Module, Class, Function, FunctionKind, Parameter,
permute_optional_groups, permute_right_option_groups,
@@ -35,7 +34,7 @@
from libclinic.return_converters import return_converters,
int_return_converter
from libclinic.block_parser import Block, BlockParser
from libclinic.codegen import BlockPrinter, Destination
- from libclinic.dsl_parser import DSLParser
+ from libclinic.dsl_parser import DSLParser, unsupported_special_methods
from libclinic.slots import SlotSpec, SLOTS
from libclinic.cli import parse_file, Clinic
@@ -858,11 +857,10 @@ class Foo "FooObject *" "&Foo_Type"
def test_slot_with_parameters_and_return_type(self):
# Slots which take arguments or return something else than an
- # object are not supported yet; check the machinery with fake ones.
+ # object are not supported yet; check the machinery with them.
slots = {
- '__contains__': SlotSpec('__contains__', 'int', 'int',
- {(object_converter,): 'sq_contains'}),
- '__bool__': SlotSpec('__bool__', 'int', 'int', {(): 'nb_bool'}),
+ '__contains__': SlotSpec(return_converter='int'),
+ '__bool__': SlotSpec(return_converter='int'),
}
block = """
/*[clinic input]
@@ -3353,42 +3351,38 @@ class Foo "FooObject *" ""
Foo.__next__
""", signatures_in_block=3, function_index=2)
self.assertIs(function.kind, FunctionKind.SLOT)
- self.assertEqual(function.slot.name, "__next__")
+ self.assertIs(function.slot, SLOTS["__next__"])
self.assertEqual(function.c_basename, "Foo_next")
self.assertEqual(function.return_converter.type, "PyObject *")
self.assertEqual(list(function.parameters), ["self"])
def test_slot_parameters(self):
- slot = SlotSpec('__contains__', 'int', 'int', {
- (Py_ssize_t_converter,): 'sq_fake',
- (object_converter,): 'sq_contains'})
+ # __getitem__ is not supported yet; its parameter gives the slot
+ # (Py_ssize_t for sq_item, object for mp_subscript) and is not parsed.
block = """
module foo
class Foo "FooObject *" ""
- Foo.__contains__
- value: {converter}
+ Foo.__getitem__
+ key: {converter}
/
"""
errors = [
- ("Foo.__contains__",
- "'__contains__' must declare (Py_ssize_t) for sq_fake or "
- "(object) for sq_contains; got ()"),
- ("Foo.__contains__\n value: int\n /",
- "'__contains__' must declare"),
- ("Foo.__contains__\n value: object",
- "The parameters of '__contains__' must be positional-only"),
- ("Foo.__contains__\n value: object = None\n /",
- "The parameters of '__contains__' cannot have a default value"),
+ ("Foo.__getitem__\n key: object",
+ "The parameters of '__getitem__' must be positional-only"),
+ ("Foo.__getitem__\n key: object = None\n /",
+ "The parameters of '__getitem__' cannot have a default value"),
]
- with mock.patch.dict(SLOTS, {'__contains__': slot}):
+ supported = unsupported_special_methods - {'__getitem__'}
+ with (mock.patch.dict(SLOTS, {'__getitem__':
SlotSpec(return_converter='object')}),
+ mock.patch.object(libclinic.dsl_parser,
+ 'unsupported_special_methods', supported)):
for converter in "Py_ssize_t", "object":
with self.subTest(converter=converter):
function = self.parse_function(
block.format(converter=converter),
signatures_in_block=3, function_index=2)
self.assertIs(function.kind, FunctionKind.SLOT)
- self.assertEqual(function.return_converter.type, "int")
- self.assertEqual(list(function.parameters), ["self",
"value"])
+ self.assertEqual(list(function.parameters), ["self",
"key"])
for body, err in errors:
with self.subTest(block=body):
body = 'class Foo "FooObject *" "&Foo_Type"\n' + body
@@ -3406,8 +3400,6 @@ def test_slot_errors(self):
"@coexist cannot be applied to '__next__'"),
("@vectorcall\nFoo.__next__",
"@vectorcall can only be used with __init__ and __new__"),
- ("Foo.__next__\n a: int",
- "'__next__' methods cannot define parameters"),
("Foo.__next__ -> int",
"'Foo.__next__' cannot define a return type"),
("Foo.__next__\n\nReturn the next item.",
@@ -3660,10 +3652,10 @@ def test_illegal_c_identifier(self):
self.expect_failure(block, err, lineno=2)
def test_cannot_convert_special_method(self):
- err = "'__len__' is a special method and cannot be converted"
+ err = "'__iter__' is a special method and cannot be converted"
block = """
class T "" ""
- T.__len__
+ T.__iter__
"""
self.expect_failure(block, err, lineno=1)
diff --git a/Modules/_dbmmodule.c b/Modules/_dbmmodule.c
index 39fa804d1dcef4..3186ce92749b1b 100644
--- a/Modules/_dbmmodule.c
+++ b/Modules/_dbmmodule.c
@@ -116,10 +116,16 @@ dbm_dealloc(PyObject *self)
Py_DECREF(tp);
}
+/*[clinic input]
+@critical_section
+_dbm.dbm.__len__ as dbm_length
+[clinic start generated code]*/
+
static Py_ssize_t
-dbm_length_lock_held(PyObject *self)
+dbm_length_impl(dbmobject *self)
+/*[clinic end generated code: output=d7ea9510562e5f86 input=f912d65e9b06fef5]*/
{
- dbmobject *dp = dbmobject_CAST(self);
+ dbmobject *dp = self;
_dbm_state *state = PyType_GetModuleState(Py_TYPE(dp));
assert(state != NULL);
if (dp->di_dbm == NULL) {
@@ -139,16 +145,6 @@ dbm_length_lock_held(PyObject *self)
return dp->di_size;
}
-static Py_ssize_t
-dbm_length(PyObject *self)
-{
- Py_ssize_t result;
- Py_BEGIN_CRITICAL_SECTION(self);
- result = dbm_length_lock_held(self);
- Py_END_CRITICAL_SECTION();
- return result;
-}
-
static int
dbm_bool_lock_held(PyObject *self)
{
diff --git a/Modules/_gdbmmodule.c b/Modules/_gdbmmodule.c
index bdc2d96d8324b0..0a15012e6eb01a 100644
--- a/Modules/_gdbmmodule.c
+++ b/Modules/_gdbmmodule.c
@@ -136,10 +136,16 @@ gdbm_dealloc(PyObject *op)
Py_DECREF(tp);
}
+/*[clinic input]
+@critical_section
+_gdbm.gdbm.__len__ as gdbm_length
+[clinic start generated code]*/
+
static Py_ssize_t
-gdbm_length_lock_held(PyObject *op)
+gdbm_length_impl(gdbmobject *self)
+/*[clinic end generated code: output=3ccb956974ed73b5 input=57878f60d264ed98]*/
{
- gdbmobject *dp = _gdbmobject_CAST(op);
+ gdbmobject *dp = self;
_gdbm_state *state = PyType_GetModuleState(Py_TYPE(dp));
if (dp->di_dbm == NULL) {
PyErr_SetString(state->gdbm_error, "GDBM object has already been
closed");
@@ -183,16 +189,6 @@ gdbm_length_lock_held(PyObject *op)
return dp->di_size;
}
-static Py_ssize_t
-gdbm_length(PyObject *op)
-{
- Py_ssize_t result;
- Py_BEGIN_CRITICAL_SECTION(op);
- result = gdbm_length_lock_held(op);
- Py_END_CRITICAL_SECTION();
- return result;
-}
-
static int
gdbm_bool_lock_held(PyObject *op)
{
diff --git a/Modules/clinic/_dbmmodule.c.h b/Modules/clinic/_dbmmodule.c.h
index 14657b5b688221..4e1a9eaa741e4e 100644
--- a/Modules/clinic/_dbmmodule.c.h
+++ b/Modules/clinic/_dbmmodule.c.h
@@ -6,6 +6,21 @@ preserve
#include "pycore_modsupport.h" // _PyArg_CheckPositional()
#include "pycore_unicodeobject.h" // _PyUnicode_AsUTF8NoNUL()
+static Py_ssize_t
+dbm_length_impl(dbmobject *self);
+
+static Py_ssize_t
+dbm_length(PyObject *self)
+{
+ Py_ssize_t return_value = -1;
+
+ Py_BEGIN_CRITICAL_SECTION(self);
+ return_value = dbm_length_impl((dbmobject *)self);
+ Py_END_CRITICAL_SECTION();
+
+ return return_value;
+}
+
PyDoc_STRVAR(_dbm_dbm_close__doc__,
"close($self, /)\n"
"--\n"
@@ -249,4 +264,4 @@ dbmopen(PyObject *module, PyObject *const *args, Py_ssize_t
nargs)
exit:
return return_value;
}
-/*[clinic end generated code: output=f9d904af4ea43d1d input=a9049054013a1b77]*/
+/*[clinic end generated code: output=7ae22ba4a7aeffa2 input=a9049054013a1b77]*/
diff --git a/Modules/clinic/_gdbmmodule.c.h b/Modules/clinic/_gdbmmodule.c.h
index a6991041ffae04..55e2100751f090 100644
--- a/Modules/clinic/_gdbmmodule.c.h
+++ b/Modules/clinic/_gdbmmodule.c.h
@@ -6,6 +6,21 @@ preserve
#include "pycore_modsupport.h" // _PyArg_CheckPositional()
#include "pycore_unicodeobject.h" // _PyUnicode_AsUTF8NoNUL()
+static Py_ssize_t
+gdbm_length_impl(gdbmobject *self);
+
+static Py_ssize_t
+gdbm_length(PyObject *self)
+{
+ Py_ssize_t return_value = -1;
+
+ Py_BEGIN_CRITICAL_SECTION(self);
+ return_value = gdbm_length_impl((gdbmobject *)self);
+ Py_END_CRITICAL_SECTION();
+
+ return return_value;
+}
+
PyDoc_STRVAR(_gdbm_gdbm_get__doc__,
"get($self, key, default=None, /)\n"
"--\n"
@@ -401,4 +416,4 @@ dbmopen(PyObject *module, PyObject *const *args, Py_ssize_t
nargs)
exit:
return return_value;
}
-/*[clinic end generated code: output=ea86a820d233fcad input=a9049054013a1b77]*/
+/*[clinic end generated code: output=36e1ed1cc3523aeb input=a9049054013a1b77]*/
diff --git a/Modules/clinic/mmapmodule.c.h b/Modules/clinic/mmapmodule.c.h
index 1343eda42e84ec..9874ab2b3de0a9 100644
--- a/Modules/clinic/mmapmodule.c.h
+++ b/Modules/clinic/mmapmodule.c.h
@@ -862,6 +862,21 @@ mmap_mmap_madvise(PyObject *self, PyObject *const *args,
Py_ssize_t nargs)
#endif /* defined(HAVE_MADVISE) */
+static Py_ssize_t
+mmap_length_impl(mmap_object *self);
+
+static Py_ssize_t
+mmap_length(PyObject *self)
+{
+ Py_ssize_t return_value = -1;
+
+ Py_BEGIN_CRITICAL_SECTION(self);
+ return_value = mmap_length_impl((mmap_object *)self);
+ Py_END_CRITICAL_SECTION();
+
+ return return_value;
+}
+
#ifndef MMAP_MMAP_RESIZE_METHODDEF
#define MMAP_MMAP_RESIZE_METHODDEF
#endif /* !defined(MMAP_MMAP_RESIZE_METHODDEF) */
@@ -877,4 +892,4 @@ mmap_mmap_madvise(PyObject *self, PyObject *const *args,
Py_ssize_t nargs)
#ifndef MMAP_MMAP_MADVISE_METHODDEF
#define MMAP_MMAP_MADVISE_METHODDEF
#endif /* !defined(MMAP_MMAP_MADVISE_METHODDEF) */
-/*[clinic end generated code: output=d2ae0d1d754f51cc input=a9049054013a1b77]*/
+/*[clinic end generated code: output=e52bf0f914c90a96 input=a9049054013a1b77]*/
diff --git a/Modules/mmapmodule.c b/Modules/mmapmodule.c
index 766e85a1bdba98..124245e4bec105 100644
--- a/Modules/mmapmodule.c
+++ b/Modules/mmapmodule.c
@@ -1487,24 +1487,19 @@ mmap_buffer_releasebuf(PyObject *op, Py_buffer
*Py_UNUSED(view))
Py_END_CRITICAL_SECTION();
}
+/*[clinic input]
+@critical_section
+mmap.mmap.__len__ as mmap_length
+[clinic start generated code]*/
+
static Py_ssize_t
-mmap_length_lock_held(PyObject *op)
+mmap_length_impl(mmap_object *self)
+/*[clinic end generated code: output=a63e62615cdd9960 input=b7bbf695dc9b2b66]*/
{
- mmap_object *self = mmap_object_CAST(op);
CHECK_VALID(-1);
return self->size;
}
-static Py_ssize_t
-mmap_length(PyObject *op)
-{
- Py_ssize_t result;
- Py_BEGIN_CRITICAL_SECTION(op);
- result = mmap_length_lock_held(op);
- Py_END_CRITICAL_SECTION();
- return result;
-}
-
static PyObject *
mmap_item_lock_held(PyObject *op, Py_ssize_t i)
{
diff --git a/Tools/clinic/libclinic/dsl_parser.py
b/Tools/clinic/libclinic/dsl_parser.py
index 60730b8828d843..4a38cb6b314fff 100644
--- a/Tools/clinic/libclinic/dsl_parser.py
+++ b/Tools/clinic/libclinic/dsl_parser.py
@@ -72,7 +72,6 @@
__itruediv__
__ixor__
__le__
-__len__
__lshift__
__lt__
__matmul__
@@ -695,7 +694,7 @@ def resolve_return_converter(
return int_return_converter()
if self.kind is SLOT:
assert self.slot is not None
- return return_converters[self.slot.return_converter]()
+ return self.slot.make_return_converter()
return CReturnConverter()
def parse_cloned_function(self, names: FunctionNames, existing: str) ->
None:
@@ -920,11 +919,6 @@ def state_parameters_start(self, line: str) -> None:
assert self.function is not None
if self.function.kind is GETTER:
fail("@getter methods cannot define parameters")
- if self.function.kind is SLOT:
- assert self.function.slot is not None
- if not self.function.slot.takes_parameters:
- fail(f"{self.function.name!r} methods cannot define
parameters")
-
self.parameter_continuation = ''
return self.next(self.state_parameter, line)
@@ -1677,19 +1671,12 @@ def check_vectorcall_parameters(self, lineno: int) ->
None:
line_number=lineno)
def check_slot_parameters(self, func: Function) -> None:
- """Check that the parameters of a slot method select a C slot."""
- assert func.slot is not None
- params = list(func.parameters.values())[1:]
- for p in params:
+ """Check that the parameters of a slot method fit a C signature."""
+ for p in list(func.parameters.values())[1:]:
if not p.is_positional_only():
fail(f"The parameters of {func.name!r} must be
positional-only")
if p.default is not unspecified:
fail(f"The parameters of {func.name!r} cannot have a default
value")
- converters = [p.converter for p in params]
- if func.slot.c_slot(converters) is None:
- got = ', '.join(c.dsl_name() for c in converters)
- fail(f"{func.name!r} must declare "
- f"{func.slot.describe_signatures()}; got ({got})")
def do_post_block_processing_cleanup(self, lineno: int) -> None:
"""
diff --git a/Tools/clinic/libclinic/slots.py b/Tools/clinic/libclinic/slots.py
index ee38b06c2ce4a8..95137800fd868c 100644
--- a/Tools/clinic/libclinic/slots.py
+++ b/Tools/clinic/libclinic/slots.py
@@ -5,55 +5,39 @@
the implementation function. The wrapper is the place where
``@critical_section`` acquires the lock.
-The parameters declared in the DSL only select the C signature; they must be
-positional-only, without a default value. When a special method maps to
-several slots (``__getitem__`` is ``sq_item`` or ``mp_subscript``),
-the converters of its parameters determine which one it implements.
+The parameters declared in the DSL only give the C signature of the slot;
+they must be positional-only, without a default value.
"""
from __future__ import annotations
import dataclasses as dc
from typing import Final
-from libclinic.converter import CConverter
+from libclinic.return_converters import CReturnConverter, return_converters
[email protected](frozen=True, slots=True)
[email protected](frozen=True, slots=True, kw_only=True)
class SlotSpec:
- # The special method name, e.g. '__next__'.
- name: str
- # The C return type of the slot function.
- return_type: str
- # The name of the return converter which declares `return_type`.
+ # The name of the return converter of the implementation function,
+ # which declares the C return type of the slot function.
return_converter: str
- # Maps the converter classes of the parameters (excluding self) to
- # the C slot.
- signatures: dict[tuple[type[CConverter], ...], str]
+
+ @property
+ def return_type(self) -> str:
+ """The C return type of the slot function."""
+ return self.make_return_converter().type
@property
def error_value(self) -> str:
"""The value returned by the slot function on error."""
return 'NULL' if self.return_type.endswith('*') else '-1'
- @property
- def takes_parameters(self) -> bool:
- return any(self.signatures)
-
- def c_slot(self, converters: list[CConverter]) -> str | None:
- """Return the C slot for the parameter converters, or None."""
- classes = tuple(type(c) for c in converters)
- return self.signatures.get(classes)
-
- def describe_signatures(self) -> str:
- return " or ".join(
- f"({', '.join(c.dsl_name() for c in sig) or 'no parameters'})"
- f" for {slot}"
- for sig, slot in self.signatures.items())
+ def make_return_converter(self) -> CReturnConverter:
+ return return_converters[self.return_converter]()
# Maps the name of a special method to its slot. A name listed here must
# not be in dsl_parser.unsupported_special_methods.
SLOTS: Final[dict[str, SlotSpec]] = {
- '__next__': SlotSpec('__next__', 'PyObject *', 'object', {
- (): 'tp_iternext',
- }),
+ '__next__': SlotSpec(return_converter='object'),
+ '__len__': SlotSpec(return_converter='Py_ssize_t'),
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]