diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index acd6c2f97348e19..d76c449626fab4e 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 141d3fbe9382472..2b01506f3d4ffab 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 28ff34f9048454d..b4c27809b5333d5 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()