From 453280a49a4c108fc29421422579eaf76bcf4901 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Fri, 28 Aug 2026 00:55:41 +0300 Subject: [PATCH 1/2] gh-156500: Reject non-positional parameters in wrap_dll_function() argtypes was built from every annotated parameter, so an annotated keyword-only, *args or **kwargs parameter contributed an extra positional entry. Such a parameter has no positional counterpart to describe, so it now raises ValueError at decoration time. --- Doc/library/ctypes.rst | 7 +++++-- Lib/ctypes/util.py | 8 ++++++++ Lib/test/test_ctypes/test_funcptr.py | 29 ++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index acd6c2f97348e1..b0493d6525be47 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -715,8 +715,11 @@ Specifying function pointers using type annotations and do not have to match the underlying C implementation. If the decorated function does not have a return type annotation, a - :exc:`ValueError` is raised. If the name of the function does not exist - in *dll*, an :exc:`AttributeError` is raised. + :exc:`ValueError` is raised. A :exc:`ValueError` is also raised if it has a + keyword-only, ``*args`` or ``**kwargs`` parameter, since + :attr:`~ctypes._CFuncPtr.argtypes` describes positional arguments only. If + the name of the function does not exist in *dll*, an :exc:`AttributeError` + is raised. For example:: diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index 141d3fbe938247..2b01506f3d4ffa 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -4,6 +4,7 @@ from dataclasses import dataclass lazy import functools +lazy import inspect lazy import shutil lazy import subprocess @@ -509,6 +510,13 @@ def decorator(func): except KeyError as error: raise ValueError(f"{name!r} missing return type annotation") from error + for param in inspect.signature(func).parameters.values(): + if param.kind not in (param.POSITIONAL_ONLY, + param.POSITIONAL_OR_KEYWORD): + raise ValueError(f"{name!r} has non-positional parameter " + f"{param.name!r}; argtypes describes " + f"positional arguments only") + ptr.restype = restype ptr.argtypes = tuple(annotations.values()) functools.update_wrapper(ptr, func, updated=()) diff --git a/Lib/test/test_ctypes/test_funcptr.py b/Lib/test/test_ctypes/test_funcptr.py index 28ff34f9048454..9c54fb97703d90 100644 --- a/Lib/test/test_ctypes/test_funcptr.py +++ b/Lib/test/test_ctypes/test_funcptr.py @@ -153,6 +153,35 @@ def noexist(): def PyObject_GetAttrString(op: ctypes.py_object, attr: ctypes.c_char_p): pass + def test_wrap_dll_function_non_positional(self): + # argtypes describes positional arguments only, so a parameter that + # cannot be passed positionally is rejected. + regex = "'PyObject_GetAttr' has non-positional parameter" + + with self.assertRaisesRegex(ValueError, regex): + @wrap_dll_function(ctypes.pythonapi) + def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object, + *args: ctypes.c_int) -> ctypes.py_object: + pass + + with self.assertRaisesRegex(ValueError, regex): + @wrap_dll_function(ctypes.pythonapi) + def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object, + **kwargs: ctypes.c_int) -> ctypes.py_object: + pass + + with self.assertRaisesRegex(ValueError, regex): + @wrap_dll_function(ctypes.pythonapi) + def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object, + *, kwonly: ctypes.c_int) -> ctypes.py_object: + pass + + with self.assertRaisesRegex(ValueError, regex): + @wrap_dll_function(ctypes.pythonapi) + def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object, + *, kwonly) -> ctypes.py_object: + pass + def test_wrap_dll_function_str_ann(self): from test.test_ctypes import wrap_str_ann version = wrap_str_ann.Py_GetVersion() From fd94ad9d5851e6bea9be28e7b68e139dbf9d801f Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Sat, 29 Aug 2026 00:53:16 +0300 Subject: [PATCH 2/2] Address review: Oxford comma in docs, test positional-only params --- Doc/library/ctypes.rst | 2 +- Lib/test/test_ctypes/test_funcptr.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index b0493d6525be47..d76c449626fab4 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -716,7 +716,7 @@ Specifying function pointers using type annotations If the decorated function does not have a return type annotation, a :exc:`ValueError` is raised. A :exc:`ValueError` is also raised if it has a - keyword-only, ``*args`` or ``**kwargs`` parameter, since + keyword-only, ``*args``, or ``**kwargs`` parameter, since :attr:`~ctypes._CFuncPtr.argtypes` describes positional arguments only. If the name of the function does not exist in *dll*, an :exc:`AttributeError` is raised. diff --git a/Lib/test/test_ctypes/test_funcptr.py b/Lib/test/test_ctypes/test_funcptr.py index 9c54fb97703d90..b4c27809b5333d 100644 --- a/Lib/test/test_ctypes/test_funcptr.py +++ b/Lib/test/test_ctypes/test_funcptr.py @@ -182,6 +182,18 @@ def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object, *, kwonly) -> ctypes.py_object: pass + # Positional-only parameters have a positional counterpart, so they + # are accepted. + @wrap_dll_function(ctypes.pythonapi) + def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object, + /) -> ctypes.py_object: + pass + + class Foo: + a = "abc" + + self.assertEqual(PyObject_GetAttr(Foo, "a"), "abc") + def test_wrap_dll_function_str_ann(self): from test.test_ctypes import wrap_str_ann version = wrap_str_ann.Py_GetVersion()