Skip to content

Abstract class inheritance regression in Python 3.13 #127967

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

Closed
txgk opened this issue Dec 15, 2024 · 4 comments
Closed

Abstract class inheritance regression in Python 3.13 #127967

txgk opened this issue Dec 15, 2024 · 4 comments
Labels
3.13 bugs and security fixes topic-ctypes type-bug An unexpected behavior, bug, or error

Comments

@txgk
Copy link

txgk commented Dec 15, 2024

Bug report

Bug description:

Good day to you, dear Python maintainers! I have a little issue with the recent Python release, 3.13. Here's a snippet:

import ctypes

class EnumMetaclass(type(ctypes.c_uint)):
  def __new__(metaclass, name, bases, cls_dict):
    cls = type(ctypes.c_uint).__new__(metaclass, name, bases, cls_dict)
    if name == 'Enum':
      return cls
    for i, value in enumerate(cls_dict['_values_']):
      setattr(cls, value, cls.from_param(i))
    return cls

class Enum(ctypes.c_uint, metaclass=EnumMetaclass):
  @classmethod
  def from_param(cls, param):
    print("Calling method 'from_param' of", cls)
    return cls(param)
  def __repr__(self):
    return self._values_[self.value]

class NodeType(Enum):
  _values_ = ['DOCUMENT', 'ELEMENT', 'TEXT']

def main():
  print(NodeType(1))

if __name__ == "__main__":
  main()

When run with Python 3.12.7, I get the following output:

Calling method 'from_param' of <class '__main__.NodeType'>
Calling method 'from_param' of <class '__main__.NodeType'>
Calling method 'from_param' of <class '__main__.NodeType'>
ELEMENT

When run with Python 3.13.1, I get the following output:

Calling method 'from_param' of <class '__main__.NodeType'>
Traceback (most recent call last):
  File "/home/grisha/test.py", line 20, in <module>
    class NodeType(Enum):
      _values_ = ['DOCUMENT', 'ELEMENT', 'TEXT']
  File "/home/grisha/test.py", line 9, in __new__
    setattr(cls, value, cls.from_param(i))
                        ~~~~~~~~~~~~~~^^^
  File "/home/grisha/test.py", line 16, in from_param
    return cls(param)
TypeError: abstract class

As you can see, new TypeError detection triggers where it presumably mustn't have, because it's perfectly correct to call from_param from NodeType object.

I checked that this problem persists in both 3.13.0 and 3.13.1.

CPython versions tested on:

3.13

Operating systems tested on:

Linux

@txgk txgk added the type-bug An unexpected behavior, bug, or error label Dec 15, 2024
@ZeroIntensity
Copy link
Member

Is this related to #124520?

@picnixz picnixz added 3.13 bugs and security fixes pending The issue will be closed if no feedback is provided labels Dec 15, 2024
@txgk
Copy link
Author

txgk commented Dec 15, 2024

Is this related to #124520?

Hello, @ZeroIntensity! Seems like it is related. I've read through the discussion at #124520 and modified the snippet according to the new requirements of metaclasses initialization, i.e. moved custom logic to the __init__ method and made sure my metaclass is called explicitly (return cls(param)), as per this piece of documentation:

  • As a consequence of necessary internal refactoring, initialization of
    internal metaclasses now happens in __init__ rather
    than in __new__. This affects projects that subclass these internal
    metaclasses to provide custom initialization.
    Generally:

    • Custom logic that was done in __new__ after calling super().__new__
      should be moved to __init__.
    • To create a class, call the metaclass, not only the metaclass's
      __new__ method.

    See :gh:124520 for discussion and links to changes in some affected
    projects.

Here's a modified snippet:

import ctypes

class EnumMetaclass(type(ctypes.c_uint)):
  def __new__(metaclass, name, bases, cls_dict):
    cls = super().__new__(metaclass, name, bases, cls_dict)
    return cls
  def __init__(self, name, bases, cls_dict):
    if name != 'Enum':
      for i, value in enumerate(cls_dict['_values_']):
        setattr(self, value, self.from_param(i))

class Enum(ctypes.c_uint, metaclass=EnumMetaclass):
  @classmethod
  def from_param(cls, param):
    print("Calling method 'from_param' of", cls)
    return cls(param)
  def __repr__(self):
    return self._values_[self.value]

class NodeType(Enum):
  _values_ = ['DOCUMENT', 'ELEMENT', 'TEXT']

def main():
  print(NodeType(1))

if __name__ == "__main__":
  main()

Unfortunately, this doesn't fix the problem when it's run with Python 3.13.1:

Calling method 'from_param' of <class '__main__.NodeType'>
Traceback (most recent call last):
  File "/home/grisha/test.py", line 20, in <module>
    class NodeType(Enum):
      _values_ = ['DOCUMENT', 'ELEMENT', 'TEXT']
  File "/home/grisha/test.py", line 10, in __init__
    setattr(self, value, self.from_param(i))
                         ~~~~~~~~~~~~~~~^^^
  File "/home/grisha/test.py", line 16, in from_param
    return cls(param)
TypeError: abstract class

@ZeroIntensity
Copy link
Member

You need to call __init__ of ctypes.c_uint in EnumMetaclass.__init__. As the documentation says:

Custom logic that was done in new after calling super().new
should be moved to init.

ctypes.c_uint isn't ready until it's __init__ has been called.

@txgk
Copy link
Author

txgk commented Dec 15, 2024

You need to call __init__ of ctypes.c_uint in EnumMetaclass.__init__. As the documentation says:

Custom logic that was done in new after calling super().new
should be moved to init.

ctypes.c_uint isn't ready until it's __init__ has been called.

You're right. Adding super().__init__(name, bases, cls_dict) in the beginning of the EnumMetaclass's __init__ did help. Now code works the same for both 3.12.7 and 3.13.1.

In case someone stumbles upon this discussion in the future, here's a patch against original snippet which has fixed the erroneous behavior:

--- a/test.py   2024-12-15 21:59:57.248825568 +0300
+++ b/test.py   2024-12-15 21:59:34.655857763 +0300
@@ -2,12 +2,13 @@
 
 class EnumMetaclass(type(ctypes.c_uint)):
   def __new__(metaclass, name, bases, cls_dict):
-    cls = type(ctypes.c_uint).__new__(metaclass, name, bases, cls_dict)
-    if name == 'Enum':
-      return cls
-    for i, value in enumerate(cls_dict['_values_']):
-      setattr(cls, value, cls.from_param(i))
+    cls = super().__new__(metaclass, name, bases, cls_dict)
     return cls
+  def __init__(self, name, bases, cls_dict):
+    super().__init__(name, bases, cls_dict)
+    if name != 'Enum':
+      for i, value in enumerate(cls_dict['_values_']):
+        setattr(self, value, self.from_param(i))
 
 class Enum(ctypes.c_uint, metaclass=EnumMetaclass):
   @classmethod

@ZeroIntensity, thank you very much for your help and your fast replies!

Best regards, Grigory

@txgk txgk closed this as completed Dec 15, 2024
@picnixz picnixz removed the pending The issue will be closed if no feedback is provided label Dec 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.13 bugs and security fixes topic-ctypes type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

4 participants