-
-
Notifications
You must be signed in to change notification settings - Fork 30.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Breaking backward compatibility between ctypes
and metaclasses in Python 3.13.
#124520
Comments
Thanks for reporting this! If I knew about this earlier I'd try to make your life easier, but at this point it looks like a docs change is the best way to go. I'm afraid that by using For the diff you linked: Unfortunately I don't have a Windows box to test on. What error are you getting if you put all of the logic in If the if/else is needed, I recommend putting the logic in common methods like if sys.version_info >= (3, 13):
def __new__(cls, ...):
return cls._new(...)
def __init__(self, ...):
self._init(...)
else:
def __new__(cls, ...):
self = cls._new(cls, ...)
self.__init__(...)
return self or even: if sys.version_info >= (3, 13):
__new__ = _new
__init__ = _init
else:
def __new__(cls, ...):
self = cls._new(cls, ...)
self._init(...)
return self |
Hm, I don't understand this point. Do you have an example? |
In https://github.com/enthought/comtypes/compare/c631f97..6f036d4
|
--- a/comtypes/_post_coinit/unknwn.py
+++ b/comtypes/_post_coinit/unknwn.py
@@ -75,7 +75,9 @@ class _cominterface_meta(type):
new_cls._methods_ = methods
if dispmethods is not None:
new_cls._disp_methods_ = dispmethods
+ return new_cls
+ def __init__(self, name, bases, namespace):
# If we sublass a COM interface, for example:
#
# class IDispatch(IUnknown):
@@ -85,23 +87,22 @@ class _cominterface_meta(type):
# subclass of POINTER(IUnknown) because of the way ctypes
# typechecks work.
if bases == (object,):
- _ptr_bases = (new_cls, _compointer_base)
+ _ptr_bases = (self, _compointer_base)
else:
- _ptr_bases = (new_cls, POINTER(bases[0]))
+ _ptr_bases = (self, POINTER(bases[0]))
# The interface 'new_cls' is used as a mixin.
p = type(_compointer_base)(
- "POINTER(%s)" % new_cls.__name__,
+ "POINTER(%s)" % self.__name__,
_ptr_bases,
- {"__com_interface__": new_cls, "_needs_com_addref_": None},
+ {"__com_interface__": self, "_needs_com_addref_": None},
)
from ctypes import _pointer_type_cache
- _pointer_type_cache[new_cls] = p
-
- if new_cls._case_insensitive_:
+ _pointer_type_cache[self] = p
+ if self._case_insensitive_:
@patcher.Patch(p)
class CaseInsensitive(object):
# case insensitive attributes for COM methods and properties
@@ -155,8 +156,6 @@ class _cominterface_meta(type):
CopyComPointer(value, self)
- return new_cls
- The following error occurs in Python 3.11 when making the changes mentioned above.
|
Thanks! |
Thank you.
Fortunately, libraries like If it is still possible to modify things so that performing all initialization in |
…__ change (pythonGH-124546) (cherry picked from commit 3387f76) Co-authored-by: Petr Viktorin <[email protected]>
where it runs successfully on Python 3.13, but a `NameError` occurs on Python 3.11. It corresponds to an updated version of the reproducer reported at python/cpython#124520 (comment).
To reproduce the error that occurs in Python 3.11 by modifying the current |
where it runs successfully on Python 3.13, but a `NameError` occurs on Python 3.11. It corresponds to an updated version of the reproducer reported at python/cpython#124520 (comment).
where it runs successfully on Python 3.13, but a `NameError` occurs on Python 3.11. It corresponds to an updated version of the reproducer reported at python/cpython#124520 (comment).
Hi, It seems the main complexity it the pre-processing is that while an instance of a metaclass is being created, its _initializing_coclass_meta = False
class _coclass_meta(type):
def __init__(self, name, bases, namespace):
...
global _initializing_coclass_meta
if _initializing_coclass_meta:
return
try:
_initializing_coclass_meta = True
PTR = _coclass_pointer_meta(
"POINTER(%s)" % self.__name__,
(self, c_void_p),
{
"__ctypes_from_outparam__": _wrap_coclass,
"from_param": classmethod(_coclass_from_param),
},
)
finally:
_initializing_coclass_meta = False
... …and similarly in In # Run code from __setattr__ on _methods_ & _disp_methods_
methods = namespace.get("_methods_", None)
if methods is not None:
self._make_methods(methods)
self._make_specials()
dispmethods = namespace.get("_disp_methods_", None)
if dispmethods is not None:
self._make_dispmethods(dispmethods)
self._make_specials() You can remove the - def __new__(cls, name, bases, namespace):
- return type.__new__(cls, name, bases, namespace) I haven't yet been able to untangle why class _cominterface_meta(type):
def __init(self, name, bases, namespace):
# (put all the init code here)
if sys.version_info < (3, 13):
def __new__(cls, name, bases, namespace):
self = type.__new__(cls, name, bases, namespace)
self.__init(name, bases, namespace)
return self
else:
__init__ = __init |
Hi, Thank you for your investigation.
it definitely worked in Python 3.13 and earlier versions. But,
As you mentioned, using Until we come up with a more elegant way to signal the stopping of recursion, I believe it might be better to add a comment like I would be happy to hear any further thoughts you may have. |
I investigated what values are assigned to These changes have been committed and pushed to enthought/comtypes@4c779617^...312e268 (https://github.com/junkmd/comtypes/tree/py313_early_returns). This change makes explicit what the metaclass would have done implicitly until now. The changes using version bridges also has been committed to enthought/comtypes@4c779617^...6501fd1 (https://github.com/junkmd/comtypes/tree/py313_version_bridges). Any opinions would be appreciated. |
That sounds reasonable. (That said, I hope the details of the initialization won't need to change for another decade and so the fragility will be just theoretical from now on. Again, I'd appreciate if you test with 3.14 alphas/betas as they come out.) One more possibility to consider would be catching the try:
is_compointer = issubclass(self, _compointer_base)
except NameError:
# On some versions of Python, `_compointer_base` is not
# yet available in the accessible namespace at this
# point in its initialization.
# In this case, `self` will become `_compointer_base`
# later, so `issubclass(self, _compointer_base)`
# will be True.
is_compointer = True
# Double-check that self is actually _compointer_base.
assert bases == (c_void_p,)
if is_compointer:
# `self` is `POINTER(interface)` type.
# Prevent registering a pointer to a pointer (to a pointer...),
# which would lead to infinite recursion.
# Depending on a version or revision of Python, this may be essential.
return self (Not tested; I didn't boot Windows today.) |
I'll close the issue as there's nothing more to do in CPython code/docs. |
Thank you for your feedback.
It's very reassuring to hear something like this from the maintainer of the library.
When Python 3.14 alphas/betas is released, I plan to create a repository has a GitHub Actions workflow that will run tests of
I have confirmed that the code you proposed above also works correctly. |
Thank you very much again. I am also considering submitting another issue/PR to |
Thank you! |
I have created a PoC repository to verify whether In the above repository, I have confirmed that the combination of |
Please see this: #125783 |
... and see also #125881 |
Documentation
Background
I am one of the maintainers of
comtypes
.comtypes
is based onctypes
and uses metaclasses to implementIUnknown
.It was reported to the
comtypes
community that an error occurs when attempting to use conventional metaclasses with Python 3.13.A similar error was encountered in
pyglet
, which also uses metaclasses to implement COM interfaces, when running on Python 3.13.By referring to pyglet/pyglet#1196 and pyglet/pyglet#1199, I made several modifications to the code through trial and error, and now
comtypes
works in both Python 3.13 and earlier versions without problems:__new__
in places where the metaclass is instantiated (i.e., where the class is dynamically defined). This also works in versions prior to Python 3.13.type.__new__
, any remaining initialization would be handled in__init__
instead of in__new__
. Since this results in an error in versions prior to Python 3.13, a bridge usingsys.version_info
is necessary.I think these changes are likely related to #114314 and #117142 and were introduced by the PRs linked to those issues.
Since this change to
ctypes
breaks compatibility, I think it should be mentioned in the What’s New In Python 3.13 and/or in thectypes
documentation.There are likely other projects besides
comtypes
andpyglet
that rely on the combination ofctypes
and metaclasses, and I want to prevent confusion for those maintainers when they try to support Python 3.13.(Additionally, I would like to ask with the
ctypes
maintainers to confirm whether the changes for the metaclasses incomtypes
(andpyglet
) are appropriate.)Linked PRs
The text was updated successfully, but these errors were encountered: