mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +00:00
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.
This commit is contained in:
parent
5a57248b22
commit
7cb86c5def
6 changed files with 136 additions and 65 deletions
|
@ -40,7 +40,7 @@ The :func:`get_annotations` function is the main entry point for
|
|||
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>`.
|
||||
|
@ -300,15 +300,13 @@ Functions
|
|||
|
||||
.. 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
|
||||
|
||||
|
@ -407,3 +405,76 @@ Functions
|
|||
|
||||
.. 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
|
||||
|
||||
|
|
|
@ -1228,15 +1228,9 @@ Special attributes
|
|||
: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>`.
|
||||
|
@ -1247,13 +1241,6 @@ Special attributes
|
|||
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__
|
||||
|
|
|
@ -12,7 +12,7 @@ __all__ = [
|
|||
"ForwardRef",
|
||||
"call_annotate_function",
|
||||
"call_evaluate_function",
|
||||
"get_annotate_function",
|
||||
"get_annotate_from_class_namespace",
|
||||
"get_annotations",
|
||||
"annotations_to_string",
|
||||
"type_repr",
|
||||
|
@ -619,20 +619,16 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
|
|||
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(
|
||||
|
@ -832,7 +828,7 @@ def _get_and_call_annotate(obj, format):
|
|||
|
||||
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):
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Tests for the annotations module."""
|
||||
|
||||
import textwrap
|
||||
import annotationlib
|
||||
import builtins
|
||||
import collections
|
||||
|
@ -12,7 +13,6 @@ from annotationlib import (
|
|||
Format,
|
||||
ForwardRef,
|
||||
get_annotations,
|
||||
get_annotate_function,
|
||||
annotations_to_string,
|
||||
type_repr,
|
||||
)
|
||||
|
@ -933,13 +933,13 @@ class MetaclassTests(unittest.TestCase):
|
|||
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):
|
||||
|
@ -952,13 +952,13 @@ class MetaclassTests(unittest.TestCase):
|
|||
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
|
||||
|
@ -996,7 +996,7 @@ class MetaclassTests(unittest.TestCase):
|
|||
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
|
||||
|
@ -1005,25 +1005,39 @@ class MetaclassTests(unittest.TestCase):
|
|||
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))
|
||||
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)
|
||||
|
||||
def test_unannotated_class(self):
|
||||
class C:
|
||||
pass
|
||||
|
||||
self.assertIsNone(get_annotate_function(C))
|
||||
|
||||
D = type("D", (), {})
|
||||
self.assertIsNone(get_annotate_function(D))
|
||||
|
||||
def test_annotated_class(self):
|
||||
class C:
|
||||
class HasAnnotations(metaclass=Meta):
|
||||
expected_annotate = True
|
||||
a: int
|
||||
|
||||
self.assertEqual(get_annotate_function(C)(Format.VALUE), {"a": int})
|
||||
class NoAnnotations(metaclass=Meta):
|
||||
expected_annotate = False
|
||||
|
||||
class CustomAnnotate(metaclass=Meta):
|
||||
expected_annotate = True
|
||||
def __annotate__(format):
|
||||
return {}
|
||||
|
||||
code = """
|
||||
from __future__ import annotations
|
||||
|
||||
class HasFutureAnnotations(metaclass=Meta):
|
||||
expected_annotate = False
|
||||
a: int
|
||||
"""
|
||||
exec(textwrap.dedent(code), {"Meta": Meta})
|
||||
|
||||
|
||||
class TestTypeRepr(unittest.TestCase):
|
||||
|
|
|
@ -2906,7 +2906,7 @@ class NamedTupleMeta(type):
|
|||
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)
|
||||
|
@ -3092,7 +3092,7 @@ class _TypedDictMeta(type):
|
|||
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
|
||||
)
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
Add :func:`annotationlib.get_annotate_from_class_namespace` as a helper for
|
||||
accessing annotations in metaclasses, and remove
|
||||
``annotationlib.get_annotate_function``.
|
Loading…
Add table
Add a link
Reference in a new issue