Skip to content

Commit 7dec46b

Browse files
Merge branch 'main' into extra-names
2 parents 0a75e5f + 7cb86c5 commit 7dec46b

File tree

6 files changed

+135
-64
lines changed

6 files changed

+135
-64
lines changed

Doc/library/annotationlib.rst

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ The :func:`get_annotations` function is the main entry point for
4040
retrieving annotations. Given a function, class, or module, it returns
4141
an annotations dictionary in the requested format. This module also provides
4242
functionality for working directly with the :term:`annotate function`
43-
that is used to evaluate annotations, such as :func:`get_annotate_function`
43+
that is used to evaluate annotations, such as :func:`get_annotate_from_class_namespace`
4444
and :func:`call_annotate_function`, as well as the
4545
:func:`call_evaluate_function` function for working with
4646
:term:`evaluate functions <evaluate function>`.
@@ -300,15 +300,13 @@ Functions
300300

301301
.. versionadded:: 3.14
302302

303-
.. function:: get_annotate_function(obj)
303+
.. function:: get_annotate_from_class_namespace(namespace)
304304

305-
Retrieve the :term:`annotate function` for *obj*. Return :const:`!None`
306-
if *obj* does not have an annotate function. *obj* may be a class, function,
307-
module, or a namespace dictionary for a class. The last case is useful during
308-
class creation, e.g. in the ``__new__`` method of a metaclass.
309-
310-
This is usually equivalent to accessing the :attr:`~object.__annotate__`
311-
attribute of *obj*, but access through this public function is preferred.
305+
Retrieve the :term:`annotate function` from a class namespace dictionary *namespace*.
306+
Return :const:`!None` if the namespace does not contain an annotate function.
307+
This is primarily useful before the class has been fully created (e.g., in a metaclass);
308+
after the class exists, the annotate function can be retrieved with ``cls.__annotate__``.
309+
See :ref:`below <annotationlib-metaclass>` for an example using this function in a metaclass.
312310

313311
.. versionadded:: 3.14
314312

@@ -407,3 +405,76 @@ Functions
407405

408406
.. versionadded:: 3.14
409407

408+
409+
Recipes
410+
-------
411+
412+
.. _annotationlib-metaclass:
413+
414+
Using annotations in a metaclass
415+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
416+
417+
A :ref:`metaclass <metaclasses>` may want to inspect or even modify the annotations
418+
in a class body during class creation. Doing so requires retrieving annotations
419+
from the class namespace dictionary. For classes created with
420+
``from __future__ import annotations``, the annotations will be in the ``__annotations__``
421+
key of the dictionary. For other classes with annotations,
422+
:func:`get_annotate_from_class_namespace` can be used to get the
423+
annotate function, and :func:`call_annotate_function` can be used to call it and
424+
retrieve the annotations. Using the :attr:`~Format.FORWARDREF` format will usually
425+
be best, because this allows the annotations to refer to names that cannot yet be
426+
resolved when the class is created.
427+
428+
To modify the annotations, it is best to create a wrapper annotate function
429+
that calls the original annotate function, makes any necessary adjustments, and
430+
returns the result.
431+
432+
Below is an example of a metaclass that filters out all :class:`typing.ClassVar`
433+
annotations from the class and puts them in a separate attribute:
434+
435+
.. code-block:: python
436+
437+
import annotationlib
438+
import typing
439+
440+
class ClassVarSeparator(type):
441+
def __new__(mcls, name, bases, ns):
442+
if "__annotations__" in ns: # from __future__ import annotations
443+
annotations = ns["__annotations__"]
444+
classvar_keys = {
445+
key for key, value in annotations.items()
446+
# Use string comparison for simplicity; a more robust solution
447+
# could use annotationlib.ForwardRef.evaluate
448+
if value.startswith("ClassVar")
449+
}
450+
classvars = {key: annotations[key] for key in classvar_keys}
451+
ns["__annotations__"] = {
452+
key: value for key, value in annotations.items()
453+
if key not in classvar_keys
454+
}
455+
wrapped_annotate = None
456+
elif annotate := annotationlib.get_annotate_from_class_namespace(ns):
457+
annotations = annotationlib.call_annotate_function(
458+
annotate, format=annotationlib.Format.FORWARDREF
459+
)
460+
classvar_keys = {
461+
key for key, value in annotations.items()
462+
if typing.get_origin(value) is typing.ClassVar
463+
}
464+
classvars = {key: annotations[key] for key in classvar_keys}
465+
466+
def wrapped_annotate(format):
467+
annos = annotationlib.call_annotate_function(annotate, format, owner=typ)
468+
return {key: value for key, value in annos.items() if key not in classvar_keys}
469+
470+
else: # no annotations
471+
classvars = {}
472+
wrapped_annotate = None
473+
typ = super().__new__(mcls, name, bases, ns)
474+
475+
if wrapped_annotate is not None:
476+
# Wrap the original __annotate__ with a wrapper that removes ClassVars
477+
typ.__annotate__ = wrapped_annotate
478+
typ.classvars = classvars # Store the ClassVars in a separate attribute
479+
return typ
480+

Doc/reference/datamodel.rst

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,15 +1228,9 @@ Special attributes
12281228
:attr:`__annotations__ attributes <object.__annotations__>`.
12291229

12301230
For best practices on working with :attr:`~object.__annotations__`,
1231-
please see :mod:`annotationlib`.
1232-
1233-
.. caution::
1234-
1235-
Accessing the :attr:`!__annotations__` attribute of a class
1236-
object directly may yield incorrect results in the presence of
1237-
metaclasses. In addition, the attribute may not exist for
1238-
some classes. Use :func:`annotationlib.get_annotations` to
1239-
retrieve class annotations safely.
1231+
please see :mod:`annotationlib`. Where possible, use
1232+
:func:`annotationlib.get_annotations` instead of accessing this
1233+
attribute directly.
12401234

12411235
.. versionchanged:: 3.14
12421236
Annotations are now :ref:`lazily evaluated <lazy-evaluation>`.
@@ -1247,13 +1241,6 @@ Special attributes
12471241
if the class has no annotations.
12481242
See also: :attr:`__annotate__ attributes <object.__annotate__>`.
12491243

1250-
.. caution::
1251-
1252-
Accessing the :attr:`!__annotate__` attribute of a class
1253-
object directly may yield incorrect results in the presence of
1254-
metaclasses. Use :func:`annotationlib.get_annotate_function` to
1255-
retrieve the annotate function safely.
1256-
12571244
.. versionadded:: 3.14
12581245

12591246
* - .. attribute:: type.__type_params__

Lib/annotationlib.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"ForwardRef",
1313
"call_annotate_function",
1414
"call_evaluate_function",
15-
"get_annotate_function",
15+
"get_annotate_from_class_namespace",
1616
"get_annotations",
1717
"annotations_to_string",
1818
"type_repr",
@@ -708,20 +708,16 @@ def _stringify_single(anno):
708708
return repr(anno)
709709

710710

711-
def get_annotate_function(obj):
712-
"""Get the __annotate__ function for an object.
711+
def get_annotate_from_class_namespace(obj):
712+
"""Retrieve the annotate function from a class namespace dictionary.
713713
714-
obj may be a function, class, or module, or a user-defined type with
715-
an `__annotate__` attribute.
716-
717-
Returns the __annotate__ function or None.
714+
Return None if the namespace does not contain an annotate function.
715+
This is useful in metaclass ``__new__`` methods to retrieve the annotate function.
718716
"""
719-
if isinstance(obj, dict):
720-
try:
721-
return obj["__annotate__"]
722-
except KeyError:
723-
return obj.get("__annotate_func__", None)
724-
return getattr(obj, "__annotate__", None)
717+
try:
718+
return obj["__annotate__"]
719+
except KeyError:
720+
return obj.get("__annotate_func__", None)
725721

726722

727723
def get_annotations(
@@ -921,7 +917,7 @@ def _get_and_call_annotate(obj, format):
921917
922918
May not return a fresh dictionary.
923919
"""
924-
annotate = get_annotate_function(obj)
920+
annotate = getattr(obj, "__annotate__", None)
925921
if annotate is not None:
926922
ann = call_annotate_function(annotate, format, owner=obj)
927923
if not isinstance(ann, dict):

Lib/test/test_annotationlib.py

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for the annotations module."""
22

3+
import textwrap
34
import annotationlib
45
import builtins
56
import collections
@@ -12,7 +13,6 @@
1213
Format,
1314
ForwardRef,
1415
get_annotations,
15-
get_annotate_function,
1616
annotations_to_string,
1717
type_repr,
1818
)
@@ -1155,13 +1155,13 @@ class Y(metaclass=Meta):
11551155
b: float
11561156

11571157
self.assertEqual(get_annotations(Meta), {"a": int})
1158-
self.assertEqual(get_annotate_function(Meta)(Format.VALUE), {"a": int})
1158+
self.assertEqual(Meta.__annotate__(Format.VALUE), {"a": int})
11591159

11601160
self.assertEqual(get_annotations(X), {})
1161-
self.assertIs(get_annotate_function(X), None)
1161+
self.assertIs(X.__annotate__, None)
11621162

11631163
self.assertEqual(get_annotations(Y), {"b": float})
1164-
self.assertEqual(get_annotate_function(Y)(Format.VALUE), {"b": float})
1164+
self.assertEqual(Y.__annotate__(Format.VALUE), {"b": float})
11651165

11661166
def test_unannotated_meta(self):
11671167
class Meta(type):
@@ -1174,13 +1174,13 @@ class Y(X):
11741174
pass
11751175

11761176
self.assertEqual(get_annotations(Meta), {})
1177-
self.assertIs(get_annotate_function(Meta), None)
1177+
self.assertIs(Meta.__annotate__, None)
11781178

11791179
self.assertEqual(get_annotations(Y), {})
1180-
self.assertIs(get_annotate_function(Y), None)
1180+
self.assertIs(Y.__annotate__, None)
11811181

11821182
self.assertEqual(get_annotations(X), {"a": str})
1183-
self.assertEqual(get_annotate_function(X)(Format.VALUE), {"a": str})
1183+
self.assertEqual(X.__annotate__(Format.VALUE), {"a": str})
11841184

11851185
def test_ordering(self):
11861186
# Based on a sample by David Ellis
@@ -1218,7 +1218,7 @@ class D(metaclass=Meta):
12181218
for c in classes:
12191219
with self.subTest(c=c):
12201220
self.assertEqual(get_annotations(c), c.expected_annotations)
1221-
annotate_func = get_annotate_function(c)
1221+
annotate_func = getattr(c, "__annotate__", None)
12221222
if c.expected_annotations:
12231223
self.assertEqual(
12241224
annotate_func(Format.VALUE), c.expected_annotations
@@ -1227,25 +1227,39 @@ class D(metaclass=Meta):
12271227
self.assertIs(annotate_func, None)
12281228

12291229

1230-
class TestGetAnnotateFunction(unittest.TestCase):
1231-
def test_static_class(self):
1232-
self.assertIsNone(get_annotate_function(object))
1233-
self.assertIsNone(get_annotate_function(int))
1234-
1235-
def test_unannotated_class(self):
1236-
class C:
1237-
pass
1230+
class TestGetAnnotateFromClassNamespace(unittest.TestCase):
1231+
def test_with_metaclass(self):
1232+
class Meta(type):
1233+
def __new__(mcls, name, bases, ns):
1234+
annotate = annotationlib.get_annotate_from_class_namespace(ns)
1235+
expected = ns["expected_annotate"]
1236+
with self.subTest(name=name):
1237+
if expected:
1238+
self.assertIsNotNone(annotate)
1239+
else:
1240+
self.assertIsNone(annotate)
1241+
return super().__new__(mcls, name, bases, ns)
1242+
1243+
class HasAnnotations(metaclass=Meta):
1244+
expected_annotate = True
1245+
a: int
12381246

1239-
self.assertIsNone(get_annotate_function(C))
1247+
class NoAnnotations(metaclass=Meta):
1248+
expected_annotate = False
12401249

1241-
D = type("D", (), {})
1242-
self.assertIsNone(get_annotate_function(D))
1250+
class CustomAnnotate(metaclass=Meta):
1251+
expected_annotate = True
1252+
def __annotate__(format):
1253+
return {}
12431254

1244-
def test_annotated_class(self):
1245-
class C:
1246-
a: int
1255+
code = """
1256+
from __future__ import annotations
12471257
1248-
self.assertEqual(get_annotate_function(C)(Format.VALUE), {"a": int})
1258+
class HasFutureAnnotations(metaclass=Meta):
1259+
expected_annotate = False
1260+
a: int
1261+
"""
1262+
exec(textwrap.dedent(code), {"Meta": Meta})
12491263

12501264

12511265
class TestTypeRepr(unittest.TestCase):

Lib/typing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2906,7 +2906,7 @@ def __new__(cls, typename, bases, ns):
29062906
types = ns["__annotations__"]
29072907
field_names = list(types)
29082908
annotate = _make_eager_annotate(types)
2909-
elif (original_annotate := _lazy_annotationlib.get_annotate_function(ns)) is not None:
2909+
elif (original_annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None:
29102910
types = _lazy_annotationlib.call_annotate_function(
29112911
original_annotate, _lazy_annotationlib.Format.FORWARDREF)
29122912
field_names = list(types)
@@ -3092,7 +3092,7 @@ def __new__(cls, name, bases, ns, total=True):
30923092
if "__annotations__" in ns:
30933093
own_annotate = None
30943094
own_annotations = ns["__annotations__"]
3095-
elif (own_annotate := _lazy_annotationlib.get_annotate_function(ns)) is not None:
3095+
elif (own_annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None:
30963096
own_annotations = _lazy_annotationlib.call_annotate_function(
30973097
own_annotate, _lazy_annotationlib.Format.FORWARDREF, owner=tp_dict
30983098
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add :func:`annotationlib.get_annotate_from_class_namespace` as a helper for
2+
accessing annotations in metaclasses, and remove
3+
``annotationlib.get_annotate_function``.

0 commit comments

Comments
 (0)