Skip to content

Using ctypes.CField for annotations of fields within ctypes.Structure-like #10567

@junkmd

Description

@junkmd

Currently, attributes of ctypes.Structure, such as ctypes.wintypes.POINT, are annotated with names and types that specified in _fields_.

class POINT(Structure):
x: LONG
y: LONG

The problem with this approach is that at runtime, the attributes return types like int or bytes, while type checkers interpret them as returning c_int or c_char.

Below is the simplest example of a structure created for explanation purposes.
At runtime, the types of these fields are ctypes.CField. In the example below, x is a data descriptor that returns int as the getter and accepts both c_int and int as the setter.

>>> import ctypes
>>>      
>>> class Foo(ctypes.Structure):
...     pass
... 
>>> Foo._fields_ = [('x', ctypes.c_int)]
>>> Foo.x
<Field type=c_long, ofs=0, size=4>
>>> type(Foo.x) 
<class '_ctypes.CField'>
>>> foo = Foo()
>>> foo
<__main__.Foo object at 0x0000023CDB80B8C0>
>>> foo.x
0
>>> foo.x = 3
>>> foo.x
3
>>> foo.x = ctypes.c_int(2)
>>> foo.x
2

Having stubs with types that deviate from the runtime is not an ideal situation.
Therefore, I propose modifying CField as shown below for use in annotating fields.

_T = TypeVar("_T")
_CT = TypeVar("_CT", bound=_CData)


class CField(Generic[_T, _CT]):
    offset: int
    size: int
    @overload
    def __get__(self, instance: None, owner: type[Any]) -> Self: ...
    @overload
    def __get__(self, instance: Any, owner: type[Any] | None) -> _T: ...
    def __set__(self, instance: Any, value: _T | _CT) -> None: ...


class Foo(Structure):
    x: ClassVar[CField[int, c_int]]


# Foo._fields_ = [('x', c_int)]  # required in runtime


a = Foo.x  # CField[int, c_int]
foo = Foo()
b = foo.x  # int
foo.x = 3  # OK
foo.x = c_int(2)  # OK
foo.x = 3.14  # NG
foo.x = c_double(3.14)  # NG

another idea

I thought it elegant to specify only subclasses of _SimpleCData[_T] as type parameters, given the potential for inferring _T, as shown below.

class Bar(Structure):
    x: ClassVar[CField[c_int]]  # returns `int`, can take `int` or `c_int`

However, current static type system cannot that.

Moreover, considering that there exist not only subclasses of _SimpleCData defined within ctypes, but also third-party developers who define _SimpleCData subclasses within their own projects.

There is the redundancy, but I believe that utilizing the two type parameters would enhance flexibility.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions