python / python/cpython

`__annotations__` was removed from 'members' in `type.__new__` without alternative in Python 3.14

Ouverte
#139,186 3 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

3.14 docs topic-typing
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:

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:

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

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. 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

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.