retrieving annotations. Given a function, class, or module, it returns
an annotations dictionary in the requested format. This module also provides
functionality for working directly with the :term:`annotate function`
-that is used to evaluate annotations, such as :func:`get_annotate_function`
+that is used to evaluate annotations, such as :func:`get_annotate_from_class_namespace`
and :func:`call_annotate_function`, as well as the
:func:`call_evaluate_function` function for working with
:term:`evaluate functions <evaluate function>`.
.. versionadded:: 3.14
-.. function:: get_annotate_function(obj)
+.. function:: get_annotate_from_class_namespace(namespace)
- Retrieve the :term:`annotate function` for *obj*. Return :const:`!None`
- if *obj* does not have an annotate function. *obj* may be a class, function,
- module, or a namespace dictionary for a class. The last case is useful during
- class creation, e.g. in the ``__new__`` method of a metaclass.
-
- This is usually equivalent to accessing the :attr:`~object.__annotate__`
- attribute of *obj*, but access through this public function is preferred.
+ Retrieve the :term:`annotate function` from a class namespace dictionary *namespace*.
+ Return :const:`!None` if the namespace does not contain an annotate function.
+ This is primarily useful before the class has been fully created (e.g., in a metaclass);
+ after the class exists, the annotate function can be retrieved with ``cls.__annotate__``.
+ See :ref:`below <annotationlib-metaclass>` for an example using this function in a metaclass.
.. versionadded:: 3.14
.. versionadded:: 3.14
+
+Recipes
+-------
+
+.. _annotationlib-metaclass:
+
+Using annotations in a metaclass
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A :ref:`metaclass <metaclasses>` may want to inspect or even modify the annotations
+in a class body during class creation. Doing so requires retrieving annotations
+from the class namespace dictionary. For classes created with
+``from __future__ import annotations``, the annotations will be in the ``__annotations__``
+key of the dictionary. For other classes with annotations,
+:func:`get_annotate_from_class_namespace` can be used to get the
+annotate function, and :func:`call_annotate_function` can be used to call it and
+retrieve the annotations. Using the :attr:`~Format.FORWARDREF` format will usually
+be best, because this allows the annotations to refer to names that cannot yet be
+resolved when the class is created.
+
+To modify the annotations, it is best to create a wrapper annotate function
+that calls the original annotate function, makes any necessary adjustments, and
+returns the result.
+
+Below is an example of a metaclass that filters out all :class:`typing.ClassVar`
+annotations from the class and puts them in a separate attribute:
+
+.. code-block:: python
+
+ import annotationlib
+ import typing
+
+ class ClassVarSeparator(type):
+ def __new__(mcls, name, bases, ns):
+ if "__annotations__" in ns: # from __future__ import annotations
+ annotations = ns["__annotations__"]
+ classvar_keys = {
+ key for key, value in annotations.items()
+ # Use string comparison for simplicity; a more robust solution
+ # could use annotationlib.ForwardRef.evaluate
+ if value.startswith("ClassVar")
+ }
+ classvars = {key: annotations[key] for key in classvar_keys}
+ ns["__annotations__"] = {
+ key: value for key, value in annotations.items()
+ if key not in classvar_keys
+ }
+ wrapped_annotate = None
+ elif annotate := annotationlib.get_annotate_from_class_namespace(ns):
+ annotations = annotationlib.call_annotate_function(
+ annotate, format=annotationlib.Format.FORWARDREF
+ )
+ classvar_keys = {
+ key for key, value in annotations.items()
+ if typing.get_origin(value) is typing.ClassVar
+ }
+ classvars = {key: annotations[key] for key in classvar_keys}
+
+ def wrapped_annotate(format):
+ annos = annotationlib.call_annotate_function(annotate, format, owner=typ)
+ return {key: value for key, value in annos.items() if key not in classvar_keys}
+
+ else: # no annotations
+ classvars = {}
+ wrapped_annotate = None
+ typ = super().__new__(mcls, name, bases, ns)
+
+ if wrapped_annotate is not None:
+ # Wrap the original __annotate__ with a wrapper that removes ClassVars
+ typ.__annotate__ = wrapped_annotate
+ typ.classvars = classvars # Store the ClassVars in a separate attribute
+ return typ
+
:attr:`__annotations__ attributes <object.__annotations__>`.
For best practices on working with :attr:`~object.__annotations__`,
- please see :mod:`annotationlib`.
-
- .. caution::
-
- Accessing the :attr:`!__annotations__` attribute of a class
- object directly may yield incorrect results in the presence of
- metaclasses. In addition, the attribute may not exist for
- some classes. Use :func:`annotationlib.get_annotations` to
- retrieve class annotations safely.
+ please see :mod:`annotationlib`. Where possible, use
+ :func:`annotationlib.get_annotations` instead of accessing this
+ attribute directly.
.. versionchanged:: 3.14
Annotations are now :ref:`lazily evaluated <lazy-evaluation>`.
if the class has no annotations.
See also: :attr:`__annotate__ attributes <object.__annotate__>`.
- .. caution::
-
- Accessing the :attr:`!__annotate__` attribute of a class
- object directly may yield incorrect results in the presence of
- metaclasses. Use :func:`annotationlib.get_annotate_function` to
- retrieve the annotate function safely.
-
.. versionadded:: 3.14
* - .. attribute:: type.__type_params__
"ForwardRef",
"call_annotate_function",
"call_evaluate_function",
- "get_annotate_function",
+ "get_annotate_from_class_namespace",
"get_annotations",
"annotations_to_string",
"type_repr",
raise ValueError(f"Invalid format: {format!r}")
-def get_annotate_function(obj):
- """Get the __annotate__ function for an object.
+def get_annotate_from_class_namespace(obj):
+ """Retrieve the annotate function from a class namespace dictionary.
- obj may be a function, class, or module, or a user-defined type with
- an `__annotate__` attribute.
-
- Returns the __annotate__ function or None.
+ Return None if the namespace does not contain an annotate function.
+ This is useful in metaclass ``__new__`` methods to retrieve the annotate function.
"""
- if isinstance(obj, dict):
- try:
- return obj["__annotate__"]
- except KeyError:
- return obj.get("__annotate_func__", None)
- return getattr(obj, "__annotate__", None)
+ try:
+ return obj["__annotate__"]
+ except KeyError:
+ return obj.get("__annotate_func__", None)
def get_annotations(
May not return a fresh dictionary.
"""
- annotate = get_annotate_function(obj)
+ annotate = getattr(obj, "__annotate__", None)
if annotate is not None:
ann = call_annotate_function(annotate, format, owner=obj)
if not isinstance(ann, dict):
"""Tests for the annotations module."""
+import textwrap
import annotationlib
import builtins
import collections
Format,
ForwardRef,
get_annotations,
- get_annotate_function,
annotations_to_string,
type_repr,
)
b: float
self.assertEqual(get_annotations(Meta), {"a": int})
- self.assertEqual(get_annotate_function(Meta)(Format.VALUE), {"a": int})
+ self.assertEqual(Meta.__annotate__(Format.VALUE), {"a": int})
self.assertEqual(get_annotations(X), {})
- self.assertIs(get_annotate_function(X), None)
+ self.assertIs(X.__annotate__, None)
self.assertEqual(get_annotations(Y), {"b": float})
- self.assertEqual(get_annotate_function(Y)(Format.VALUE), {"b": float})
+ self.assertEqual(Y.__annotate__(Format.VALUE), {"b": float})
def test_unannotated_meta(self):
class Meta(type):
pass
self.assertEqual(get_annotations(Meta), {})
- self.assertIs(get_annotate_function(Meta), None)
+ self.assertIs(Meta.__annotate__, None)
self.assertEqual(get_annotations(Y), {})
- self.assertIs(get_annotate_function(Y), None)
+ self.assertIs(Y.__annotate__, None)
self.assertEqual(get_annotations(X), {"a": str})
- self.assertEqual(get_annotate_function(X)(Format.VALUE), {"a": str})
+ self.assertEqual(X.__annotate__(Format.VALUE), {"a": str})
def test_ordering(self):
# Based on a sample by David Ellis
for c in classes:
with self.subTest(c=c):
self.assertEqual(get_annotations(c), c.expected_annotations)
- annotate_func = get_annotate_function(c)
+ annotate_func = getattr(c, "__annotate__", None)
if c.expected_annotations:
self.assertEqual(
annotate_func(Format.VALUE), c.expected_annotations
self.assertIs(annotate_func, None)
-class TestGetAnnotateFunction(unittest.TestCase):
- def test_static_class(self):
- self.assertIsNone(get_annotate_function(object))
- self.assertIsNone(get_annotate_function(int))
-
- def test_unannotated_class(self):
- class C:
- pass
+class TestGetAnnotateFromClassNamespace(unittest.TestCase):
+ def test_with_metaclass(self):
+ class Meta(type):
+ def __new__(mcls, name, bases, ns):
+ annotate = annotationlib.get_annotate_from_class_namespace(ns)
+ expected = ns["expected_annotate"]
+ with self.subTest(name=name):
+ if expected:
+ self.assertIsNotNone(annotate)
+ else:
+ self.assertIsNone(annotate)
+ return super().__new__(mcls, name, bases, ns)
+
+ class HasAnnotations(metaclass=Meta):
+ expected_annotate = True
+ a: int
- self.assertIsNone(get_annotate_function(C))
+ class NoAnnotations(metaclass=Meta):
+ expected_annotate = False
- D = type("D", (), {})
- self.assertIsNone(get_annotate_function(D))
+ class CustomAnnotate(metaclass=Meta):
+ expected_annotate = True
+ def __annotate__(format):
+ return {}
- def test_annotated_class(self):
- class C:
- a: int
+ code = """
+ from __future__ import annotations
- self.assertEqual(get_annotate_function(C)(Format.VALUE), {"a": int})
+ class HasFutureAnnotations(metaclass=Meta):
+ expected_annotate = False
+ a: int
+ """
+ exec(textwrap.dedent(code), {"Meta": Meta})
class TestTypeRepr(unittest.TestCase):
types = ns["__annotations__"]
field_names = list(types)
annotate = _make_eager_annotate(types)
- elif (original_annotate := _lazy_annotationlib.get_annotate_function(ns)) is not None:
+ elif (original_annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None:
types = _lazy_annotationlib.call_annotate_function(
original_annotate, _lazy_annotationlib.Format.FORWARDREF)
field_names = list(types)
if "__annotations__" in ns:
own_annotate = None
own_annotations = ns["__annotations__"]
- elif (own_annotate := _lazy_annotationlib.get_annotate_function(ns)) is not None:
+ elif (own_annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None:
own_annotations = _lazy_annotationlib.call_annotate_function(
own_annotate, _lazy_annotationlib.Format.FORWARDREF, owner=tp_dict
)
--- /dev/null
+Add :func:`annotationlib.get_annotate_from_class_namespace` as a helper for
+accessing annotations in metaclasses, and remove
+``annotationlib.get_annotate_function``.