Skip to content

Add tests to prevent regressions with the combination of ctypes and metaclasses. #125783

Closed
@junkmd

Description

@junkmd

Feature or enhancement

Proposal:

There were breaking changes to ctypes in Python 3.13.
Projects like comtypes and pyglet, which implement functionality by combining ctypes and metaclasses, no longer work unless their codebases are updated.
We discussed what changes such projects need to make to their codebases in gh-124520.

I believe the easiest and most effective way to prevent regressions from happening again is to add tests on the cpython side.

I wrote a simple test like the one below.

# Lib/test/test_ctypes/test_c_simple_type_meta.py?
import unittest
import ctypes
from ctypes import POINTER, c_void_p

from ._support import PyCSimpleType, _CData, _SimpleCData


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

    def test_early_return_in_dunder_new_1(self):
        # this implementation is used in `IUnknown` of comtypes.

        class _ct_meta(type):
            def __new__(cls, name, bases, namespace):
                self = super().__new__(cls, name, bases, namespace)
                if bases == (c_void_p,):
                    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


    def test_early_return_in_dunder_new_2(self):
        # this implementation is used in `CoClass` of comtypes.

        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 _Base(object):
            pass

        class _Sub(_Base, metaclass=_ct_meta):
            pass

        class _Sub2(_Sub):
            pass

If no errors occur when this test is executed, we can assume that no regressions have occurred with the combination of ctypes and metaclasses.
However, I feel that this may not be enough. What should we specify as the target for the assert?

Has this already been discussed elsewhere?

No response given

Links to previous discussion of this feature:

#124520

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions