@@ -40,7 +40,7 @@ The :func:`get_annotations` function is the main entry point for
4040retrieving annotations. Given a function, class, or module, it returns
4141an annotations dictionary in the requested format. This module also provides
4242functionality 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 `
4444and :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+
0 commit comments