@@ -40,7 +40,7 @@ The :func:`get_annotations` function is the main entry point for
40
40
retrieving annotations. Given a function, class, or module, it returns
41
41
an annotations dictionary in the requested format. This module also provides
42
42
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 `
44
44
and :func: `call_annotate_function `, as well as the
45
45
:func: `call_evaluate_function ` function for working with
46
46
:term: `evaluate functions <evaluate function> `.
@@ -300,15 +300,13 @@ Functions
300
300
301
301
.. versionadded :: 3.14
302
302
303
- .. function :: get_annotate_function(obj )
303
+ .. function :: get_annotate_from_class_namespace(namespace )
304
304
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.
312
310
313
311
.. versionadded :: 3.14
314
312
@@ -407,3 +405,76 @@ Functions
407
405
408
406
.. versionadded :: 3.14
409
407
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
+
0 commit comments