Skip to content

Commit

Permalink
Move _SimpleCData and Array from ctypes/__init__.pyi to `_ctype…
Browse files Browse the repository at this point in the history
…s.pyi` (#10118)
  • Loading branch information
junkmd authored Apr 30, 2023
1 parent 2c34496 commit ae0c9f9
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 81 deletions.
87 changes: 85 additions & 2 deletions stdlib/_ctypes.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import sys
from ctypes import _CArgObject, _PointerLike
from typing_extensions import TypeAlias
from _typeshed import ReadableBuffer, WriteableBuffer
from abc import abstractmethod
from collections.abc import Iterable, Iterator, Mapping
from ctypes import CDLL, _CArgObject, _PointerLike
from typing import Any, Generic, TypeVar, overload
from typing_extensions import Self, TypeAlias

if sys.version_info >= (3, 9):
from types import GenericAlias

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

FUNCFLAG_CDECL: int
FUNCFLAG_PYTHONAPI: int
Expand All @@ -27,3 +37,76 @@ if sys.platform == "win32":

FUNCFLAG_HRESULT: int
FUNCFLAG_STDCALL: int

class _CDataMeta(type):
# By default mypy complains about the following two methods, because strictly speaking cls
# might not be a Type[_CT]. However this can never actually happen, because the only class that
# uses _CDataMeta as its metaclass is _CData. So it's safe to ignore the errors here.
def __mul__(cls: type[_CT], other: int) -> type[Array[_CT]]: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
def __rmul__(cls: type[_CT], other: int) -> type[Array[_CT]]: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]

class _CData(metaclass=_CDataMeta):
_b_base_: int
_b_needsfree_: bool
_objects: Mapping[Any, int] | None
@classmethod
def from_buffer(cls, source: WriteableBuffer, offset: int = ...) -> Self: ...
@classmethod
def from_buffer_copy(cls, source: ReadableBuffer, offset: int = ...) -> Self: ...
@classmethod
def from_address(cls, address: int) -> Self: ...
@classmethod
def from_param(cls, obj: Any) -> Self | _CArgObject: ...
@classmethod
def in_dll(cls, library: CDLL, name: str) -> Self: ...

class _SimpleCData(Generic[_T], _CData):
value: _T
# The TypeVar can be unsolved here,
# but we can't use overloads without creating many, many mypy false-positive errors
def __init__(self, value: _T = ...) -> None: ... # pyright: ignore[reportInvalidTypeVarUse]

class Array(Generic[_CT], _CData):
@property
@abstractmethod
def _length_(self) -> int: ...
@_length_.setter
def _length_(self, value: int) -> None: ...
@property
@abstractmethod
def _type_(self) -> type[_CT]: ...
@_type_.setter
def _type_(self, value: type[_CT]) -> None: ...
# Note: only available if _CT == c_char
@property
def raw(self) -> bytes: ...
@raw.setter
def raw(self, value: ReadableBuffer) -> None: ...
value: Any # Note: bytes if _CT == c_char, str if _CT == c_wchar, unavailable otherwise
# TODO These methods cannot be annotated correctly at the moment.
# All of these "Any"s stand for the array's element type, but it's not possible to use _CT
# here, because of a special feature of ctypes.
# By default, when accessing an element of an Array[_CT], the returned object has type _CT.
# However, when _CT is a "simple type" like c_int, ctypes automatically "unboxes" the object
# and converts it to the corresponding Python primitive. For example, when accessing an element
# of an Array[c_int], a Python int object is returned, not a c_int.
# This behavior does *not* apply to subclasses of "simple types".
# If MyInt is a subclass of c_int, then accessing an element of an Array[MyInt] returns
# a MyInt, not an int.
# This special behavior is not easy to model in a stub, so for now all places where
# the array element type would belong are annotated with Any instead.
def __init__(self, *args: Any) -> None: ...
@overload
def __getitem__(self, __key: int) -> Any: ...
@overload
def __getitem__(self, __key: slice) -> list[Any]: ...
@overload
def __setitem__(self, __key: int, __value: Any) -> None: ...
@overload
def __setitem__(self, __key: slice, __value: Iterable[Any]) -> None: ...
def __iter__(self) -> Iterator[Any]: ...
# Can't inherit from Sized because the metaclass conflict between
# Sized and _CData prevents using _CDataMeta.
def __len__(self) -> int: ...
if sys.version_info >= (3, 9):
def __class_getitem__(cls, item: Any) -> GenericAlias: ...
88 changes: 10 additions & 78 deletions stdlib/ctypes/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import sys
from _ctypes import RTLD_GLOBAL as RTLD_GLOBAL, RTLD_LOCAL as RTLD_LOCAL
from _typeshed import ReadableBuffer, WriteableBuffer
from abc import abstractmethod
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
from _ctypes import (
RTLD_GLOBAL as RTLD_GLOBAL,
RTLD_LOCAL as RTLD_LOCAL,
Array as Array,
_CData as _CData,
_CDataMeta as _CDataMeta,
_SimpleCData as _SimpleCData,
)
from collections.abc import Callable, Sequence
from typing import Any, ClassVar, Generic, TypeVar, overload
from typing_extensions import Self, TypeAlias
from typing_extensions import TypeAlias

if sys.version_info >= (3, 9):
from types import GenericAlias
Expand Down Expand Up @@ -65,28 +70,6 @@ if sys.platform == "win32":
pydll: LibraryLoader[PyDLL]
pythonapi: PyDLL

class _CDataMeta(type):
# By default mypy complains about the following two methods, because strictly speaking cls
# might not be a Type[_CT]. However this can never actually happen, because the only class that
# uses _CDataMeta as its metaclass is _CData. So it's safe to ignore the errors here.
def __mul__(cls: type[_CT], other: int) -> type[Array[_CT]]: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
def __rmul__(cls: type[_CT], other: int) -> type[Array[_CT]]: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]

class _CData(metaclass=_CDataMeta):
_b_base_: int
_b_needsfree_: bool
_objects: Mapping[Any, int] | None
@classmethod
def from_buffer(cls, source: WriteableBuffer, offset: int = ...) -> Self: ...
@classmethod
def from_buffer_copy(cls, source: ReadableBuffer, offset: int = ...) -> Self: ...
@classmethod
def from_address(cls, address: int) -> Self: ...
@classmethod
def from_param(cls, obj: Any) -> Self | _CArgObject: ...
@classmethod
def in_dll(cls, library: CDLL, name: str) -> Self: ...

class _CanCastTo(_CData): ...
class _PointerLike(_CanCastTo): ...

Expand Down Expand Up @@ -190,12 +173,6 @@ if sys.platform == "win32":

def wstring_at(address: _CVoidConstPLike, size: int = -1) -> str: ...

class _SimpleCData(Generic[_T], _CData):
value: _T
# The TypeVar can be unsolved here,
# but we can't use overloads without creating many, many mypy false-positive errors
def __init__(self, value: _T = ...) -> None: ... # pyright: ignore[reportInvalidTypeVarUse]

class c_byte(_SimpleCData[int]): ...

class c_char(_SimpleCData[bytes]):
Expand Down Expand Up @@ -259,48 +236,3 @@ class Union(_StructUnionBase): ...
class Structure(_StructUnionBase): ...
class BigEndianStructure(Structure): ...
class LittleEndianStructure(Structure): ...

class Array(Generic[_CT], _CData):
@property
@abstractmethod
def _length_(self) -> int: ...
@_length_.setter
def _length_(self, value: int) -> None: ...
@property
@abstractmethod
def _type_(self) -> type[_CT]: ...
@_type_.setter
def _type_(self, value: type[_CT]) -> None: ...
# Note: only available if _CT == c_char
@property
def raw(self) -> bytes: ...
@raw.setter
def raw(self, value: ReadableBuffer) -> None: ...
value: Any # Note: bytes if _CT == c_char, str if _CT == c_wchar, unavailable otherwise
# TODO These methods cannot be annotated correctly at the moment.
# All of these "Any"s stand for the array's element type, but it's not possible to use _CT
# here, because of a special feature of ctypes.
# By default, when accessing an element of an Array[_CT], the returned object has type _CT.
# However, when _CT is a "simple type" like c_int, ctypes automatically "unboxes" the object
# and converts it to the corresponding Python primitive. For example, when accessing an element
# of an Array[c_int], a Python int object is returned, not a c_int.
# This behavior does *not* apply to subclasses of "simple types".
# If MyInt is a subclass of c_int, then accessing an element of an Array[MyInt] returns
# a MyInt, not an int.
# This special behavior is not easy to model in a stub, so for now all places where
# the array element type would belong are annotated with Any instead.
def __init__(self, *args: Any) -> None: ...
@overload
def __getitem__(self, __key: int) -> Any: ...
@overload
def __getitem__(self, __key: slice) -> list[Any]: ...
@overload
def __setitem__(self, __key: int, __value: Any) -> None: ...
@overload
def __setitem__(self, __key: slice, __value: Iterable[Any]) -> None: ...
def __iter__(self) -> Iterator[Any]: ...
# Can't inherit from Sized because the metaclass conflict between
# Sized and _CData prevents using _CDataMeta.
def __len__(self) -> int: ...
if sys.version_info >= (3, 9):
def __class_getitem__(cls, item: Any) -> GenericAlias: ...
5 changes: 4 additions & 1 deletion tests/stubtest_allowlists/py3_common.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ csv.Dialect.skipinitialspace
csv.DictReader.__init__ # runtime sig has *args but will error if more than 5 positional args are supplied
csv.DictWriter.__init__ # runtime sig has *args but will error if more than 5 positional args are supplied
ctypes.Array._type_ # _type_ and _length_ are abstract, https://github.com/python/typeshed/pull/6361
_ctypes.Array._type_
ctypes.Array._length_
_ctypes.Array._length_
ctypes.CDLL._FuncPtr # None at class level but initialized in __init__ to this value
ctypes.memmove # CFunctionType
ctypes.memset # CFunctionType
Expand Down Expand Up @@ -332,7 +334,6 @@ turtle.ScrolledCanvas.onResize
wave.Wave_read.initfp
wave.Wave_write.initfp

_ctypes.Array
_ctypes.CFuncPtr
_ctypes.POINTER
_ctypes.PyObj_FromPtr
Expand All @@ -357,6 +358,7 @@ _ctypes.sizeof
# ==========

ctypes.Array.raw # exists but stubtest can't see it; only available if _CT == c_char
_ctypes.Array.raw

_collections_abc.AsyncGenerator.asend # async at runtime, deliberately not in the stub, see #7491. Pos-only differences also.
_collections_abc.AsyncGenerator.__anext__ # async at runtime, deliberately not in the stub, see #7491
Expand Down Expand Up @@ -605,6 +607,7 @@ wsgiref.handlers.BaseHandler.status

# Iterable classes that don't define __iter__ at runtime (usually iterable via __getitem__)
# These would ideally be special-cased by type checkers; see https://github.com/python/mypy/issues/2220
_ctypes.Array.__iter__
ctypes.Array.__iter__
mmap.mmap.__iter__
mmap.mmap.__contains__
Expand Down

0 comments on commit ae0c9f9

Please sign in to comment.