mirror of
https://github.com/python/cpython.git
synced 2025-08-04 17:08:35 +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
|
retrieving annotations. Given a function, class, or module, it returns
|
||||||
an annotations dictionary in the requested format. This module also provides
|
an annotations dictionary in the requested format. This module also provides
|
||||||
functionality for working directly with the :term:`annotate function`
|
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
|
and :func:`call_annotate_function`, as well as the
|
||||||
:func:`call_evaluate_function` function for working with
|
:func:`call_evaluate_function` function for working with
|
||||||
:term:`evaluate functions <evaluate function>`.
|
:term:`evaluate functions <evaluate function>`.
|
||||||
|
@ -300,15 +300,13 @@ Functions
|
||||||
|
|
||||||
.. versionadded:: 3.14
|
.. 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`
|
Retrieve the :term:`annotate function` from a class namespace dictionary *namespace*.
|
||||||
if *obj* does not have an annotate function. *obj* may be a class, function,
|
Return :const:`!None` if the namespace does not contain an annotate function.
|
||||||
module, or a namespace dictionary for a class. The last case is useful during
|
This is primarily useful before the class has been fully created (e.g., in a metaclass);
|
||||||
class creation, e.g. in the ``__new__`` method of 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.
|
||||||
This is usually equivalent to accessing the :attr:`~object.__annotate__`
|
|
||||||
attribute of *obj*, but access through this public function is preferred.
|
|
||||||
|
|
||||||
.. versionadded:: 3.14
|
.. versionadded:: 3.14
|
||||||
|
|
||||||
|
@ -407,3 +405,76 @@ Functions
|
||||||
|
|
||||||
.. versionadded:: 3.14
|
.. 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__>`.
|
:attr:`__annotations__ attributes <object.__annotations__>`.
|
||||||
|
|
||||||
For best practices on working with :attr:`~object.__annotations__`,
|
For best practices on working with :attr:`~object.__annotations__`,
|
||||||
please see :mod:`annotationlib`.
|
please see :mod:`annotationlib`. Where possible, use
|
||||||
|
:func:`annotationlib.get_annotations` instead of accessing this
|
||||||
.. caution::
|
attribute directly.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.14
|
.. versionchanged:: 3.14
|
||||||
Annotations are now :ref:`lazily evaluated <lazy-evaluation>`.
|
Annotations are now :ref:`lazily evaluated <lazy-evaluation>`.
|
||||||
|
@ -1247,13 +1241,6 @@ Special attributes
|
||||||
if the class has no annotations.
|
if the class has no annotations.
|
||||||
See also: :attr:`__annotate__ attributes <object.__annotate__>`.
|
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
|
.. versionadded:: 3.14
|
||||||
|
|
||||||
* - .. attribute:: type.__type_params__
|
* - .. attribute:: type.__type_params__
|
||||||
|
|
|
@ -12,7 +12,7 @@ __all__ = [
|
||||||
"ForwardRef",
|
"ForwardRef",
|
||||||
"call_annotate_function",
|
"call_annotate_function",
|
||||||
"call_evaluate_function",
|
"call_evaluate_function",
|
||||||
"get_annotate_function",
|
"get_annotate_from_class_namespace",
|
||||||
"get_annotations",
|
"get_annotations",
|
||||||
"annotations_to_string",
|
"annotations_to_string",
|
||||||
"type_repr",
|
"type_repr",
|
||||||
|
@ -619,20 +619,16 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
|
||||||
raise ValueError(f"Invalid format: {format!r}")
|
raise ValueError(f"Invalid format: {format!r}")
|
||||||
|
|
||||||
|
|
||||||
def get_annotate_function(obj):
|
def get_annotate_from_class_namespace(obj):
|
||||||
"""Get the __annotate__ function for an object.
|
"""Retrieve the annotate function from a class namespace dictionary.
|
||||||
|
|
||||||
obj may be a function, class, or module, or a user-defined type with
|
Return None if the namespace does not contain an annotate function.
|
||||||
an `__annotate__` attribute.
|
This is useful in metaclass ``__new__`` methods to retrieve the annotate function.
|
||||||
|
|
||||||
Returns the __annotate__ function or None.
|
|
||||||
"""
|
"""
|
||||||
if isinstance(obj, dict):
|
try:
|
||||||
try:
|
return obj["__annotate__"]
|
||||||
return obj["__annotate__"]
|
except KeyError:
|
||||||
except KeyError:
|
return obj.get("__annotate_func__", None)
|
||||||
return obj.get("__annotate_func__", None)
|
|
||||||
return getattr(obj, "__annotate__", None)
|
|
||||||
|
|
||||||
|
|
||||||
def get_annotations(
|
def get_annotations(
|
||||||
|
@ -832,7 +828,7 @@ def _get_and_call_annotate(obj, format):
|
||||||
|
|
||||||
May not return a fresh dictionary.
|
May not return a fresh dictionary.
|
||||||
"""
|
"""
|
||||||
annotate = get_annotate_function(obj)
|
annotate = getattr(obj, "__annotate__", None)
|
||||||
if annotate is not None:
|
if annotate is not None:
|
||||||
ann = call_annotate_function(annotate, format, owner=obj)
|
ann = call_annotate_function(annotate, format, owner=obj)
|
||||||
if not isinstance(ann, dict):
|
if not isinstance(ann, dict):
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""Tests for the annotations module."""
|
"""Tests for the annotations module."""
|
||||||
|
|
||||||
|
import textwrap
|
||||||
import annotationlib
|
import annotationlib
|
||||||
import builtins
|
import builtins
|
||||||
import collections
|
import collections
|
||||||
|
@ -12,7 +13,6 @@ from annotationlib import (
|
||||||
Format,
|
Format,
|
||||||
ForwardRef,
|
ForwardRef,
|
||||||
get_annotations,
|
get_annotations,
|
||||||
get_annotate_function,
|
|
||||||
annotations_to_string,
|
annotations_to_string,
|
||||||
type_repr,
|
type_repr,
|
||||||
)
|
)
|
||||||
|
@ -933,13 +933,13 @@ class MetaclassTests(unittest.TestCase):
|
||||||
b: float
|
b: float
|
||||||
|
|
||||||
self.assertEqual(get_annotations(Meta), {"a": int})
|
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.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_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):
|
def test_unannotated_meta(self):
|
||||||
class Meta(type):
|
class Meta(type):
|
||||||
|
@ -952,13 +952,13 @@ class MetaclassTests(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.assertEqual(get_annotations(Meta), {})
|
self.assertEqual(get_annotations(Meta), {})
|
||||||
self.assertIs(get_annotate_function(Meta), None)
|
self.assertIs(Meta.__annotate__, None)
|
||||||
|
|
||||||
self.assertEqual(get_annotations(Y), {})
|
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_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):
|
def test_ordering(self):
|
||||||
# Based on a sample by David Ellis
|
# Based on a sample by David Ellis
|
||||||
|
@ -996,7 +996,7 @@ class MetaclassTests(unittest.TestCase):
|
||||||
for c in classes:
|
for c in classes:
|
||||||
with self.subTest(c=c):
|
with self.subTest(c=c):
|
||||||
self.assertEqual(get_annotations(c), c.expected_annotations)
|
self.assertEqual(get_annotations(c), c.expected_annotations)
|
||||||
annotate_func = get_annotate_function(c)
|
annotate_func = getattr(c, "__annotate__", None)
|
||||||
if c.expected_annotations:
|
if c.expected_annotations:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
annotate_func(Format.VALUE), c.expected_annotations
|
annotate_func(Format.VALUE), c.expected_annotations
|
||||||
|
@ -1005,25 +1005,39 @@ class MetaclassTests(unittest.TestCase):
|
||||||
self.assertIs(annotate_func, None)
|
self.assertIs(annotate_func, None)
|
||||||
|
|
||||||
|
|
||||||
class TestGetAnnotateFunction(unittest.TestCase):
|
class TestGetAnnotateFromClassNamespace(unittest.TestCase):
|
||||||
def test_static_class(self):
|
def test_with_metaclass(self):
|
||||||
self.assertIsNone(get_annotate_function(object))
|
class Meta(type):
|
||||||
self.assertIsNone(get_annotate_function(int))
|
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 HasAnnotations(metaclass=Meta):
|
||||||
class C:
|
expected_annotate = True
|
||||||
pass
|
|
||||||
|
|
||||||
self.assertIsNone(get_annotate_function(C))
|
|
||||||
|
|
||||||
D = type("D", (), {})
|
|
||||||
self.assertIsNone(get_annotate_function(D))
|
|
||||||
|
|
||||||
def test_annotated_class(self):
|
|
||||||
class C:
|
|
||||||
a: int
|
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):
|
class TestTypeRepr(unittest.TestCase):
|
||||||
|
|
|
@ -2906,7 +2906,7 @@ class NamedTupleMeta(type):
|
||||||
types = ns["__annotations__"]
|
types = ns["__annotations__"]
|
||||||
field_names = list(types)
|
field_names = list(types)
|
||||||
annotate = _make_eager_annotate(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(
|
types = _lazy_annotationlib.call_annotate_function(
|
||||||
original_annotate, _lazy_annotationlib.Format.FORWARDREF)
|
original_annotate, _lazy_annotationlib.Format.FORWARDREF)
|
||||||
field_names = list(types)
|
field_names = list(types)
|
||||||
|
@ -3092,7 +3092,7 @@ class _TypedDictMeta(type):
|
||||||
if "__annotations__" in ns:
|
if "__annotations__" in ns:
|
||||||
own_annotate = None
|
own_annotate = None
|
||||||
own_annotations = ns["__annotations__"]
|
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_annotations = _lazy_annotationlib.call_annotate_function(
|
||||||
own_annotate, _lazy_annotationlib.Format.FORWARDREF, owner=tp_dict
|
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