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]

Reply via email to