Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Doc/library/ctypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand Down
8 changes: 8 additions & 0 deletions Lib/ctypes/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from dataclasses import dataclass

lazy import functools
lazy import inspect
lazy import shutil
lazy import subprocess

Expand Down Expand Up @@ -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=())
Expand Down
41 changes: 41 additions & 0 deletions Lib/test/test_ctypes/test_funcptr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +156 to +158

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also test that positional-only arguments (def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object, /)) do not raise an exception?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, done.

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()
Expand Down
Loading