Skip to content

Commit 7cb86c5

Browse files
gh-132426: Add get_annotate_from_class_namespace replacing get_annotate_function (#132490)
As noted on the issue, making get_annotate_function() support both types and mappings is problematic because one object may be both. So let's add a new one that works with any mapping. This leaves get_annotate_function() not very useful, so remove it.
1 parent 5a57248 commit 7cb86c5

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",
@@ -619,20 +619,16 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
619619
raise ValueError(f"Invalid format: {format!r}")
620620

621621

622-
def get_annotate_function(obj):
623-
"""Get the __annotate__ function for an object.
622+
def get_annotate_from_class_namespace(obj):
623+
"""Retrieve the annotate function from a class namespace dictionary.
624624
625-
obj may be a function, class, or module, or a user-defined type with
626-
an `__annotate__` attribute.
627-
628-
Returns the __annotate__ function or None.
625+
Return None if the namespace does not contain an annotate function.
626+
This is useful in metaclass ``__new__`` methods to retrieve the annotate function.
629627
"""
630-
if isinstance(obj, dict):
631-
try:
632-
return obj["__annotate__"]
633-
except KeyError:
634-
return obj.get("__annotate_func__", None)
635-
return getattr(obj, "__annotate__", None)
628+
try:
629+
return obj["__annotate__"]
630+
except KeyError:
631+
return obj.get("__annotate_func__", None)
636632

637633

638634
def get_annotations(
@@ -832,7 +828,7 @@ def _get_and_call_annotate(obj, format):
832828
833829
May not return a fresh dictionary.
834830
"""
835-
annotate = get_annotate_function(obj)
831+
annotate = getattr(obj, "__annotate__", None)
836832
if annotate is not None:
837833
ann = call_annotate_function(annotate, format, owner=obj)
838834
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
)
@@ -933,13 +933,13 @@ class Y(metaclass=Meta):
933933
b: float
934934

935935
self.assertEqual(get_annotations(Meta), {"a": int})
936-
self.assertEqual(get_annotate_function(Meta)(Format.VALUE), {"a": int})
936+
self.assertEqual(Meta.__annotate__(Format.VALUE), {"a": int})
937937

938938
self.assertEqual(get_annotations(X), {})
939-
self.assertIs(get_annotate_function(X), None)
939+
self.assertIs(X.__annotate__, None)
940940

941941
self.assertEqual(get_annotations(Y), {"b": float})
942-
self.assertEqual(get_annotate_function(Y)(Format.VALUE), {"b": float})
942+
self.assertEqual(Y.__annotate__(Format.VALUE), {"b": float})
943943

944944
def test_unannotated_meta(self):
945945
class Meta(type):
@@ -952,13 +952,13 @@ class Y(X):
952952
pass
953953

954954
self.assertEqual(get_annotations(Meta), {})
955-
self.assertIs(get_annotate_function(Meta), None)
955+
self.assertIs(Meta.__annotate__, None)
956956

957957
self.assertEqual(get_annotations(Y), {})
958-
self.assertIs(get_annotate_function(Y), None)
958+
self.assertIs(Y.__annotate__, None)
959959

960960
self.assertEqual(get_annotations(X), {"a": str})
961-
self.assertEqual(get_annotate_function(X)(Format.VALUE), {"a": str})
961+
self.assertEqual(X.__annotate__(Format.VALUE), {"a": str})
962962

963963
def test_ordering(self):
964964
# Based on a sample by David Ellis
@@ -996,7 +996,7 @@ class D(metaclass=Meta):
996996
for c in classes:
997997
with self.subTest(c=c):
998998
self.assertEqual(get_annotations(c), c.expected_annotations)
999-
annotate_func = get_annotate_function(c)
999+
annotate_func = getattr(c, "__annotate__", None)
10001000
if c.expected_annotations:
10011001
self.assertEqual(
10021002
annotate_func(Format.VALUE), c.expected_annotations
@@ -1005,25 +1005,39 @@ class D(metaclass=Meta):
10051005
self.assertIs(annotate_func, None)
10061006

10071007

1008-
class TestGetAnnotateFunction(unittest.TestCase):
1009-
def test_static_class(self):
1010-
self.assertIsNone(get_annotate_function(object))
1011-
self.assertIsNone(get_annotate_function(int))
1012-
1013-
def test_unannotated_class(self):
1014-
class C:
1015-
pass
1008+
class TestGetAnnotateFromClassNamespace(unittest.TestCase):
1009+
def test_with_metaclass(self):
1010+
class Meta(type):
1011+
def __new__(mcls, name, bases, ns):
1012+
annotate = annotationlib.get_annotate_from_class_namespace(ns)
1013+
expected = ns["expected_annotate"]
1014+
with self.subTest(name=name):
1015+
if expected:
1016+
self.assertIsNotNone(annotate)
1017+
else:
1018+
self.assertIsNone(annotate)
1019+
return super().__new__(mcls, name, bases, ns)
1020+
1021+
class HasAnnotations(metaclass=Meta):
1022+
expected_annotate = True
1023+
a: int
10161024

1017-
self.assertIsNone(get_annotate_function(C))
1025+
class NoAnnotations(metaclass=Meta):
1026+
expected_annotate = False
10181027

1019-
D = type("D", (), {})
1020-
self.assertIsNone(get_annotate_function(D))
1028+
class CustomAnnotate(metaclass=Meta):
1029+
expected_annotate = True
1030+
def __annotate__(format):
1031+
return {}
10211032

1022-
def test_annotated_class(self):
1023-
class C:
1024-
a: int
1033+
code = """
1034+
from __future__ import annotations
10251035
1026-
self.assertEqual(get_annotate_function(C)(Format.VALUE), {"a": int})
1036+
class HasFutureAnnotations(metaclass=Meta):
1037+
expected_annotate = False
1038+
a: int
1039+
"""
1040+
exec(textwrap.dedent(code), {"Meta": Meta})
10271041

10281042

10291043
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy