python / python/cpython

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

オープン
#139,186 コメント 3 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

3.14 docs topic-typing
主要言語
Python
スター
77.2k
フォーク
35.9k
PR マージ指標
PR 指標を取得中

説明

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

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

PEP 649、リンクされた CPython issue 139140、および annotationlib のドキュメントから始めてください。レポートでは CPython のソースファイルもテストも指定されていません。提供されている ExtendedType パターンで動作を再現し、その後、クラス作成前に使用できるサポート対象のアノテーション API または文書化された解決方法が必要かどうかを判断してください。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
compilers
issue の種類
バグ
難易度
5/5
見積もり時間
1週間以上
活発さ
停滞
明瞭さ
説明が足りない
初心者へのやさしさ
25/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。