gh-156500: Reject non-positional parameters in `ctypes.util.wrap_dll_function` (GH-156501)
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.
diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst
index acd6c2f..d76c449 100644
--- a/Doc/library/ctypes.rst
+++ b/Doc/library/ctypes.rst
@@ -715,8 +715,11 @@
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 141d3fb..2b01506 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 28ff34f..b4c2780 100644
--- a/Lib/test/test_ctypes/test_funcptr.py
+++ b/Lib/test/test_ctypes/test_funcptr.py
@@ -153,6 +153,47 @@ 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
+
+ # 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()