`__annotations__` was removed from 'members' in `type.__new__` without alternative in Python 3.14
Personne n'a encore pris cette issue.
- Langage dominant
- Python
- Étoiles
- 77.2k
- Forks
- 35.9k
- Métriques de merge des PR
- Métriques de PR en attente
Description
Bug report
Bug description:
Python 3.14 changes the behavior of annotation in PEP649 - Deferred Evaluation Of Annotations Using Descriptors. This causes my metaclass ExtendedType to fail (part of my collection of Python classes, decorators, functions and metaclasses in pyTooling).
This metaclass infers __slots__ automatically from type annotations and significantly speeds up any Python code by using slots. Furthermore, it enforces good quality code as all members must exist in __slots__. My implementation also capable of handling multiple-inheritance for slotted classes.
Because of this, pyTooling offers currently the fastest tree implementation outperforming other libraries by 2x and more (itertree, anytree, treelib, ...).
By reading:
- https://github.com/python/cpython/issues/139140, and
- PEP649 - Deferred Evaluation Of Annotations Using Descriptors
I can't see how the behavior of Python 3.9..3.13 can be preserved. It's essential to access class annotations while constructing a new class using a metaclass!
The proposed API inpect.get_annotations is defined for existing classes. I need an API accessing annotations before the class exists. There is also a new annotationlib. Again, this is for existing classes and objects.
My Python debugger lists a __annotate_func__ member:
- According to https://github.com/astral-sh/ruff/issues/17859, it shouldn't be used.
The complete code is quite complicated. If needed I could see if I can strip it down to a more minimal example with multiple inheritance, singleton and abstract behavior features.
Here are some essential parts:
class ExtendedType(type):
def __new__(self, className: str, baseClasses: Tuple[type], members: Dict[str, Any], slots: bool = False, mixin: bool = False, singleton: bool = False) -> "ExtendedType":
# Compute slots and mixin-slots from annotated fields as well as class- and object-fields with initial values.
classFields, objectFields = self._computeSlots(className, baseClasses, members, slots, mixin)
# Create a new class
newClass = type.__new__(self, className, baseClasses, members)
# Apply class fields
for fieldName, typeAnnotation in classFields.items():
setattr(newClass, fieldName, typeAnnotation)
return newClass
@classmethod
def _computeSlots(self, className, baseClasses, members, slots, mixin):
# Compute which field are listed in __slots__ and which need to be initialized in an instance or class.
slottedFields = []
objectFields = {}
classFields = {}
if slots or mixin:
# If slots are used, all base classes must use __slots__.
for baseClass in self._iterateBaseClasses(baseClasses):
# Exclude object as a special case
if baseClass is object or baseClass is Generic:
continue
if not hasattr(baseClass, "__slots__"):
ex = BaseClassWithoutSlotsError(f"Base-classes '{baseClass.__name__}' doesn't use '__slots__'.")
ex.add_note(f"All base-classes of a class using '__slots__' must use '__slots__' itself.")
raise ex
# Copy all field names from primary base-class' __slots__, which are later needed for error checking.
inheritedSlottedFields = {}
if len(baseClasses) > 0:
for base in reversed(baseClasses[0].mro()):
# Exclude object as a special case
if base is object or base is Generic:
continue
for annotation in base.__slots__:
inheritedSlottedFields[annotation] = base
# When adding annotated fields to slottedFields, check if name was not used in inheritance hierarchy.
annotations: Dict[str, Any] = members.get("__annotations__", {})
from inspect import get_annotations
anno = members.get("__annotation__")
for fieldName, typeAnnotation in annotations.items():
if fieldName in inheritedSlottedFields:
cls = inheritedSlottedFields[fieldName]
raise AttributeError(f"Slot '{fieldName}' already exists in base-class '{cls.__module__}.{cls.__name__}'.")
# If annotated field is a ClassVar, and it has an initial value
# * copy field and initial value to classFields dictionary
# * remove field from members
if isinstance(typeAnnotation, _GenericAlias) and typeAnnotation.__origin__ is ClassVar and fieldName in members:
classFields[fieldName] = members[fieldName]
del members[fieldName]
# If an annotated field has an initial value
# * copy field and initial value to objectFields dictionary
# * remove field from members
elif fieldName in members:
slottedFields.append(fieldName)
objectFields[fieldName] = members[fieldName]
del members[fieldName]
else:
slottedFields.append(fieldName)
mixinSlots = self._aggregateMixinSlots(className, baseClasses)
else:
# When adding annotated fields to slottedFields, check if name was not used in inheritance hierarchy.
annotations: Dict[str, Any] = members.get("__annotations__", {})
for fieldName, typeAnnotation in annotations.items():
# If annotated field is a ClassVar, and it has an initial value
# * copy field and initial value to classFields dictionary
# * remove field from members
if isinstance(typeAnnotation, _GenericAlias) and typeAnnotation.__origin__ is ClassVar and fieldName in members:
classFields[fieldName] = members[fieldName]
del members[fieldName]
# FIXME: search for fields without annotation
if mixin:
mixinSlots.extend(slottedFields)
members["__slotted__"] = True
members["__slots__"] = tuple()
members["__isMixin__"] = True
members["__mixinSlots__"] = tuple(mixinSlots)
elif slots:
slottedFields.extend(mixinSlots)
members["__slotted__"] = True
members["__slots__"] = tuple(slottedFields)
members["__isMixin__"] = False
members["__mixinSlots__"] = tuple()
else:
members["__slotted__"] = False
# NO __slots__
members["__isMixin__"] = False
members["__mixinSlots__"] = tuple()
return classFields, objectFields
So my question can be summarized as follows:
How to access class annotation in a metaclass creating that class?
CPython versions tested on:
3.14
Operating systems tested on:
Windows
Guide de contribution
Ouvrir le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Piste de recherche
Commencez par PEP 649, l’issue CPython liée 139140 et la documentation de annotationlib ; le rapport ne nomme aucun fichier source CPython ni aucun test. Reproduisez le comportement avec le modèle ExtendedType fourni, puis déterminez si une API d’annotations prise en charge avant la création de la classe ou une résolution documentée est nécessaire.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- python
- Domaine
- compilers
- Type d'issue
- Bug
- Difficulté
- 5/5
- Temps estimé
- Plus d'une semaine
- Activité
- À l'abandon
- Clarté
- À clarifier
- Accessibilité débutants
- 25/100