Skip to content

Commit

Permalink
[3.12] gh-125783: Add tests to prevent regressions with the combinati…
Browse files Browse the repository at this point in the history
…on of `ctypes` and metaclasses. (GH-125881) (GH-125988)

cherry picked from commit 1384409
by Jun Komoda

Also: Add test_ctypes/_support.py from 3.13+
This partially backports be89ee5
(#113727)
by AN Long


Co-authored-by: Jun Komoda <[email protected]>
Co-authored-by: AN Long <[email protected]>
Co-authored-by: Erlend E. Aasland <[email protected]>
  • Loading branch information
4 people authored Oct 29, 2024
1 parent bc9ae4a commit bce9df9
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 0 deletions.
24 changes: 24 additions & 0 deletions Lib/test/test_ctypes/_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Some classes and types are not export to _ctypes module directly.

import ctypes
from _ctypes import Structure, Union, _Pointer, Array, _SimpleCData, CFuncPtr


_CData = Structure.__base__
assert _CData.__name__ == "_CData"

class _X(Structure):
_fields_ = [("x", ctypes.c_int)]
CField = type(_X.x)

# metaclasses
PyCStructType = type(Structure)
UnionType = type(Union)
PyCPointerType = type(_Pointer)
PyCArrayType = type(Array)
PyCSimpleType = type(_SimpleCData)
PyCFuncPtrType = type(CFuncPtr)

# type flags
Py_TPFLAGS_DISALLOW_INSTANTIATION = 1 << 7
Py_TPFLAGS_IMMUTABLETYPE = 1 << 8
87 changes: 87 additions & 0 deletions Lib/test/test_ctypes/test_c_simple_type_meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import unittest
import ctypes
from ctypes import POINTER, c_void_p

from ._support import PyCSimpleType


class PyCSimpleTypeAsMetaclassTest(unittest.TestCase):
def tearDown(self):
# to not leak references, we must clean _pointer_type_cache
ctypes._reset_cache()

def test_creating_pointer_in_dunder_new_1(self):
# Test metaclass whose instances are C types; when the type is
# created it automatically creates a pointer type for itself.
# The pointer type is also an instance of the metaclass.
# Such an implementation is used in `IUnknown` of the `comtypes`
# project. See gh-124520.

class ct_meta(type):
def __new__(cls, name, bases, namespace):
self = super().__new__(cls, name, bases, namespace)

# Avoid recursion: don't set up a pointer to
# a pointer (to a pointer...)
if bases == (c_void_p,):
# When creating PtrBase itself, the name
# is not yet available
return self
if issubclass(self, PtrBase):
return self

if bases == (object,):
ptr_bases = (self, PtrBase)
else:
ptr_bases = (self, POINTER(bases[0]))
p = p_meta(f"POINTER({self.__name__})", ptr_bases, {})
ctypes._pointer_type_cache[self] = p
return self

class p_meta(PyCSimpleType, ct_meta):
pass

class PtrBase(c_void_p, metaclass=p_meta):
pass

class CtBase(object, metaclass=ct_meta):
pass

class Sub(CtBase):
pass

class Sub2(Sub):
pass

self.assertIsInstance(POINTER(Sub2), p_meta)
self.assertTrue(issubclass(POINTER(Sub2), Sub2))
self.assertTrue(issubclass(POINTER(Sub2), POINTER(Sub)))
self.assertTrue(issubclass(POINTER(Sub), POINTER(CtBase)))

def test_creating_pointer_in_dunder_new_2(self):
# A simpler variant of the above, used in `CoClass` of the `comtypes`
# project.

class ct_meta(type):
def __new__(cls, name, bases, namespace):
self = super().__new__(cls, name, bases, namespace)
if isinstance(self, p_meta):
return self
p = p_meta(f"POINTER({self.__name__})", (self, c_void_p), {})
ctypes._pointer_type_cache[self] = p
return self

class p_meta(PyCSimpleType, ct_meta):
pass

class Core(object):
pass

class CtBase(Core, metaclass=ct_meta):
pass

class Sub(CtBase):
pass

self.assertIsInstance(POINTER(Sub), p_meta)
self.assertTrue(issubclass(POINTER(Sub), Sub))

0 comments on commit bce9df9

Please sign in to comment.