-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Description
Currently, attributes of ctypes.Structure
, such as ctypes.wintypes.POINT
, are annotated with names and types that specified in _fields_
.
typeshed/stdlib/ctypes/wintypes.pyi
Lines 126 to 128 in df08fce
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.