mirror of
https://github.com/python/cpython.git
synced 2025-10-17 12:18:23 +00:00
gh-119180: Add annotationlib
module to support PEP 649 (#119891)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
64e221d7ad
commit
7b7b90d1ce
15 changed files with 1815 additions and 510 deletions
|
@ -1366,11 +1366,15 @@ Using the non-data descriptor protocol, a pure Python version of
|
||||||
def __call__(self, *args, **kwds):
|
def __call__(self, *args, **kwds):
|
||||||
return self.f(*args, **kwds)
|
return self.f(*args, **kwds)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __annotations__(self):
|
||||||
|
return self.f.__annotations__
|
||||||
|
|
||||||
The :func:`functools.update_wrapper` call adds a ``__wrapped__`` attribute
|
The :func:`functools.update_wrapper` call adds a ``__wrapped__`` attribute
|
||||||
that refers to the underlying function. Also it carries forward
|
that refers to the underlying function. Also it carries forward
|
||||||
the attributes necessary to make the wrapper look like the wrapped
|
the attributes necessary to make the wrapper look like the wrapped
|
||||||
function: :attr:`~function.__name__`, :attr:`~function.__qualname__`,
|
function, including :attr:`~function.__name__`, :attr:`~function.__qualname__`,
|
||||||
:attr:`~function.__doc__`, and :attr:`~function.__annotations__`.
|
and :attr:`~function.__doc__`.
|
||||||
|
|
||||||
.. testcode::
|
.. testcode::
|
||||||
:hide:
|
:hide:
|
||||||
|
|
655
Lib/annotationlib.py
Normal file
655
Lib/annotationlib.py
Normal file
|
@ -0,0 +1,655 @@
|
||||||
|
"""Helpers for introspecting and wrapping annotations."""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import enum
|
||||||
|
import functools
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
__all__ = ["Format", "ForwardRef", "call_annotate_function", "get_annotations"]
|
||||||
|
|
||||||
|
|
||||||
|
class Format(enum.IntEnum):
|
||||||
|
VALUE = 1
|
||||||
|
FORWARDREF = 2
|
||||||
|
SOURCE = 3
|
||||||
|
|
||||||
|
|
||||||
|
_Union = None
|
||||||
|
_sentinel = object()
|
||||||
|
|
||||||
|
# Slots shared by ForwardRef and _Stringifier. The __forward__ names must be
|
||||||
|
# preserved for compatibility with the old typing.ForwardRef class. The remaining
|
||||||
|
# names are private.
|
||||||
|
_SLOTS = (
|
||||||
|
"__forward_evaluated__",
|
||||||
|
"__forward_value__",
|
||||||
|
"__forward_is_argument__",
|
||||||
|
"__forward_is_class__",
|
||||||
|
"__forward_module__",
|
||||||
|
"__weakref__",
|
||||||
|
"__arg__",
|
||||||
|
"__ast_node__",
|
||||||
|
"__code__",
|
||||||
|
"__globals__",
|
||||||
|
"__owner__",
|
||||||
|
"__cell__",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ForwardRef:
|
||||||
|
"""Wrapper that holds a forward reference."""
|
||||||
|
|
||||||
|
__slots__ = _SLOTS
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
arg,
|
||||||
|
*,
|
||||||
|
module=None,
|
||||||
|
owner=None,
|
||||||
|
is_argument=True,
|
||||||
|
is_class=False,
|
||||||
|
_globals=None,
|
||||||
|
_cell=None,
|
||||||
|
):
|
||||||
|
if not isinstance(arg, str):
|
||||||
|
raise TypeError(f"Forward reference must be a string -- got {arg!r}")
|
||||||
|
|
||||||
|
self.__arg__ = arg
|
||||||
|
self.__forward_evaluated__ = False
|
||||||
|
self.__forward_value__ = None
|
||||||
|
self.__forward_is_argument__ = is_argument
|
||||||
|
self.__forward_is_class__ = is_class
|
||||||
|
self.__forward_module__ = module
|
||||||
|
self.__code__ = None
|
||||||
|
self.__ast_node__ = None
|
||||||
|
self.__globals__ = _globals
|
||||||
|
self.__cell__ = _cell
|
||||||
|
self.__owner__ = owner
|
||||||
|
|
||||||
|
def __init_subclass__(cls, /, *args, **kwds):
|
||||||
|
raise TypeError("Cannot subclass ForwardRef")
|
||||||
|
|
||||||
|
def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
|
||||||
|
"""Evaluate the forward reference and return the value.
|
||||||
|
|
||||||
|
If the forward reference is not evaluatable, raise an exception.
|
||||||
|
"""
|
||||||
|
if self.__forward_evaluated__:
|
||||||
|
return self.__forward_value__
|
||||||
|
if self.__cell__ is not None:
|
||||||
|
try:
|
||||||
|
value = self.__cell__.cell_contents
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.__forward_evaluated__ = True
|
||||||
|
self.__forward_value__ = value
|
||||||
|
return value
|
||||||
|
if owner is None:
|
||||||
|
owner = self.__owner__
|
||||||
|
if type_params is None and owner is None:
|
||||||
|
raise TypeError("Either 'type_params' or 'owner' must be provided")
|
||||||
|
|
||||||
|
if self.__forward_module__ is not None:
|
||||||
|
globals = getattr(
|
||||||
|
sys.modules.get(self.__forward_module__, None), "__dict__", globals
|
||||||
|
)
|
||||||
|
if globals is None:
|
||||||
|
globals = self.__globals__
|
||||||
|
if globals is None:
|
||||||
|
if isinstance(owner, type):
|
||||||
|
module_name = getattr(owner, "__module__", None)
|
||||||
|
if module_name:
|
||||||
|
module = sys.modules.get(module_name, None)
|
||||||
|
if module:
|
||||||
|
globals = getattr(module, "__dict__", None)
|
||||||
|
elif isinstance(owner, types.ModuleType):
|
||||||
|
globals = getattr(owner, "__dict__", None)
|
||||||
|
elif callable(owner):
|
||||||
|
globals = getattr(owner, "__globals__", None)
|
||||||
|
|
||||||
|
if locals is None:
|
||||||
|
locals = {}
|
||||||
|
if isinstance(self.__owner__, type):
|
||||||
|
locals.update(vars(self.__owner__))
|
||||||
|
|
||||||
|
if type_params is None and self.__owner__ is not None:
|
||||||
|
# "Inject" type parameters into the local namespace
|
||||||
|
# (unless they are shadowed by assignments *in* the local namespace),
|
||||||
|
# as a way of emulating annotation scopes when calling `eval()`
|
||||||
|
type_params = getattr(self.__owner__, "__type_params__", None)
|
||||||
|
|
||||||
|
# type parameters require some special handling,
|
||||||
|
# as they exist in their own scope
|
||||||
|
# but `eval()` does not have a dedicated parameter for that scope.
|
||||||
|
# For classes, names in type parameter scopes should override
|
||||||
|
# names in the global scope (which here are called `localns`!),
|
||||||
|
# but should in turn be overridden by names in the class scope
|
||||||
|
# (which here are called `globalns`!)
|
||||||
|
if type_params is not None:
|
||||||
|
globals, locals = dict(globals), dict(locals)
|
||||||
|
for param in type_params:
|
||||||
|
param_name = param.__name__
|
||||||
|
if not self.__forward_is_class__ or param_name not in globals:
|
||||||
|
globals[param_name] = param
|
||||||
|
locals.pop(param_name, None)
|
||||||
|
|
||||||
|
code = self.__forward_code__
|
||||||
|
value = eval(code, globals=globals, locals=locals)
|
||||||
|
self.__forward_evaluated__ = True
|
||||||
|
self.__forward_value__ = value
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard):
|
||||||
|
import typing
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
if type_params is _sentinel:
|
||||||
|
typing._deprecation_warning_for_no_type_params_passed(
|
||||||
|
"typing.ForwardRef._evaluate"
|
||||||
|
)
|
||||||
|
type_params = ()
|
||||||
|
warnings._deprecated(
|
||||||
|
"ForwardRef._evaluate",
|
||||||
|
"{name} is a private API and is retained for compatibility, but will be removed"
|
||||||
|
" in Python 3.16. Use ForwardRef.evaluate() or typing.evaluate_forward_ref() instead.",
|
||||||
|
remove=(3, 16),
|
||||||
|
)
|
||||||
|
return typing.evaluate_forward_ref(
|
||||||
|
self,
|
||||||
|
globals=globalns,
|
||||||
|
locals=localns,
|
||||||
|
type_params=type_params,
|
||||||
|
_recursive_guard=recursive_guard,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __forward_arg__(self):
|
||||||
|
if self.__arg__ is not None:
|
||||||
|
return self.__arg__
|
||||||
|
if self.__ast_node__ is not None:
|
||||||
|
self.__arg__ = ast.unparse(self.__ast_node__)
|
||||||
|
return self.__arg__
|
||||||
|
raise AssertionError(
|
||||||
|
"Attempted to access '__forward_arg__' on an uninitialized ForwardRef"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __forward_code__(self):
|
||||||
|
if self.__code__ is not None:
|
||||||
|
return self.__code__
|
||||||
|
arg = self.__forward_arg__
|
||||||
|
# If we do `def f(*args: *Ts)`, then we'll have `arg = '*Ts'`.
|
||||||
|
# Unfortunately, this isn't a valid expression on its own, so we
|
||||||
|
# do the unpacking manually.
|
||||||
|
if arg.startswith("*"):
|
||||||
|
arg_to_compile = f"({arg},)[0]" # E.g. (*Ts,)[0] or (*tuple[int, int],)[0]
|
||||||
|
else:
|
||||||
|
arg_to_compile = arg
|
||||||
|
try:
|
||||||
|
self.__code__ = compile(arg_to_compile, "<string>", "eval")
|
||||||
|
except SyntaxError:
|
||||||
|
raise SyntaxError(f"Forward reference must be an expression -- got {arg!r}")
|
||||||
|
return self.__code__
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, ForwardRef):
|
||||||
|
return NotImplemented
|
||||||
|
if self.__forward_evaluated__ and other.__forward_evaluated__:
|
||||||
|
return (
|
||||||
|
self.__forward_arg__ == other.__forward_arg__
|
||||||
|
and self.__forward_value__ == other.__forward_value__
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
self.__forward_arg__ == other.__forward_arg__
|
||||||
|
and self.__forward_module__ == other.__forward_module__
|
||||||
|
)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.__forward_arg__, self.__forward_module__))
|
||||||
|
|
||||||
|
def __or__(self, other):
|
||||||
|
global _Union
|
||||||
|
if _Union is None:
|
||||||
|
from typing import Union as _Union
|
||||||
|
return _Union[self, other]
|
||||||
|
|
||||||
|
def __ror__(self, other):
|
||||||
|
global _Union
|
||||||
|
if _Union is None:
|
||||||
|
from typing import Union as _Union
|
||||||
|
return _Union[other, self]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.__forward_module__ is None:
|
||||||
|
module_repr = ""
|
||||||
|
else:
|
||||||
|
module_repr = f", module={self.__forward_module__!r}"
|
||||||
|
return f"ForwardRef({self.__forward_arg__!r}{module_repr})"
|
||||||
|
|
||||||
|
|
||||||
|
class _Stringifier:
|
||||||
|
# Must match the slots on ForwardRef, so we can turn an instance of one into an
|
||||||
|
# instance of the other in place.
|
||||||
|
__slots__ = _SLOTS
|
||||||
|
|
||||||
|
def __init__(self, node, globals=None, owner=None, is_class=False, cell=None):
|
||||||
|
assert isinstance(node, ast.AST)
|
||||||
|
self.__arg__ = None
|
||||||
|
self.__forward_evaluated__ = False
|
||||||
|
self.__forward_value__ = None
|
||||||
|
self.__forward_is_argument__ = False
|
||||||
|
self.__forward_is_class__ = is_class
|
||||||
|
self.__forward_module__ = None
|
||||||
|
self.__code__ = None
|
||||||
|
self.__ast_node__ = node
|
||||||
|
self.__globals__ = globals
|
||||||
|
self.__cell__ = cell
|
||||||
|
self.__owner__ = owner
|
||||||
|
|
||||||
|
def __convert(self, other):
|
||||||
|
if isinstance(other, _Stringifier):
|
||||||
|
return other.__ast_node__
|
||||||
|
elif isinstance(other, slice):
|
||||||
|
return ast.Slice(
|
||||||
|
lower=self.__convert(other.start) if other.start is not None else None,
|
||||||
|
upper=self.__convert(other.stop) if other.stop is not None else None,
|
||||||
|
step=self.__convert(other.step) if other.step is not None else None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ast.Constant(value=other)
|
||||||
|
|
||||||
|
def __make_new(self, node):
|
||||||
|
return _Stringifier(
|
||||||
|
node, self.__globals__, self.__owner__, self.__forward_is_class__
|
||||||
|
)
|
||||||
|
|
||||||
|
# Must implement this since we set __eq__. We hash by identity so that
|
||||||
|
# stringifiers in dict keys are kept separate.
|
||||||
|
def __hash__(self):
|
||||||
|
return id(self)
|
||||||
|
|
||||||
|
def __getitem__(self, other):
|
||||||
|
# Special case, to avoid stringifying references to class-scoped variables
|
||||||
|
# as '__classdict__["x"]'.
|
||||||
|
if (
|
||||||
|
isinstance(self.__ast_node__, ast.Name)
|
||||||
|
and self.__ast_node__.id == "__classdict__"
|
||||||
|
):
|
||||||
|
raise KeyError
|
||||||
|
if isinstance(other, tuple):
|
||||||
|
elts = [self.__convert(elt) for elt in other]
|
||||||
|
other = ast.Tuple(elts)
|
||||||
|
else:
|
||||||
|
other = self.__convert(other)
|
||||||
|
assert isinstance(other, ast.AST), repr(other)
|
||||||
|
return self.__make_new(ast.Subscript(self.__ast_node__, other))
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return self.__make_new(ast.Attribute(self.__ast_node__, attr))
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return self.__make_new(
|
||||||
|
ast.Call(
|
||||||
|
self.__ast_node__,
|
||||||
|
[self.__convert(arg) for arg in args],
|
||||||
|
[
|
||||||
|
ast.keyword(key, self.__convert(value))
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield self.__make_new(ast.Starred(self.__ast_node__))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ast.unparse(self.__ast_node__)
|
||||||
|
|
||||||
|
def __format__(self, format_spec):
|
||||||
|
raise TypeError("Cannot stringify annotation containing string formatting")
|
||||||
|
|
||||||
|
def _make_binop(op: ast.AST):
|
||||||
|
def binop(self, other):
|
||||||
|
return self.__make_new(
|
||||||
|
ast.BinOp(self.__ast_node__, op, self.__convert(other))
|
||||||
|
)
|
||||||
|
|
||||||
|
return binop
|
||||||
|
|
||||||
|
__add__ = _make_binop(ast.Add())
|
||||||
|
__sub__ = _make_binop(ast.Sub())
|
||||||
|
__mul__ = _make_binop(ast.Mult())
|
||||||
|
__matmul__ = _make_binop(ast.MatMult())
|
||||||
|
__truediv__ = _make_binop(ast.Div())
|
||||||
|
__mod__ = _make_binop(ast.Mod())
|
||||||
|
__lshift__ = _make_binop(ast.LShift())
|
||||||
|
__rshift__ = _make_binop(ast.RShift())
|
||||||
|
__or__ = _make_binop(ast.BitOr())
|
||||||
|
__xor__ = _make_binop(ast.BitXor())
|
||||||
|
__and__ = _make_binop(ast.BitAnd())
|
||||||
|
__floordiv__ = _make_binop(ast.FloorDiv())
|
||||||
|
__pow__ = _make_binop(ast.Pow())
|
||||||
|
|
||||||
|
del _make_binop
|
||||||
|
|
||||||
|
def _make_rbinop(op: ast.AST):
|
||||||
|
def rbinop(self, other):
|
||||||
|
return self.__make_new(
|
||||||
|
ast.BinOp(self.__convert(other), op, self.__ast_node__)
|
||||||
|
)
|
||||||
|
|
||||||
|
return rbinop
|
||||||
|
|
||||||
|
__radd__ = _make_rbinop(ast.Add())
|
||||||
|
__rsub__ = _make_rbinop(ast.Sub())
|
||||||
|
__rmul__ = _make_rbinop(ast.Mult())
|
||||||
|
__rmatmul__ = _make_rbinop(ast.MatMult())
|
||||||
|
__rtruediv__ = _make_rbinop(ast.Div())
|
||||||
|
__rmod__ = _make_rbinop(ast.Mod())
|
||||||
|
__rlshift__ = _make_rbinop(ast.LShift())
|
||||||
|
__rrshift__ = _make_rbinop(ast.RShift())
|
||||||
|
__ror__ = _make_rbinop(ast.BitOr())
|
||||||
|
__rxor__ = _make_rbinop(ast.BitXor())
|
||||||
|
__rand__ = _make_rbinop(ast.BitAnd())
|
||||||
|
__rfloordiv__ = _make_rbinop(ast.FloorDiv())
|
||||||
|
__rpow__ = _make_rbinop(ast.Pow())
|
||||||
|
|
||||||
|
del _make_rbinop
|
||||||
|
|
||||||
|
def _make_compare(op):
|
||||||
|
def compare(self, other):
|
||||||
|
return self.__make_new(
|
||||||
|
ast.Compare(
|
||||||
|
left=self.__ast_node__,
|
||||||
|
ops=[op],
|
||||||
|
comparators=[self.__convert(other)],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return compare
|
||||||
|
|
||||||
|
__lt__ = _make_compare(ast.Lt())
|
||||||
|
__le__ = _make_compare(ast.LtE())
|
||||||
|
__eq__ = _make_compare(ast.Eq())
|
||||||
|
__ne__ = _make_compare(ast.NotEq())
|
||||||
|
__gt__ = _make_compare(ast.Gt())
|
||||||
|
__ge__ = _make_compare(ast.GtE())
|
||||||
|
|
||||||
|
del _make_compare
|
||||||
|
|
||||||
|
def _make_unary_op(op):
|
||||||
|
def unary_op(self):
|
||||||
|
return self.__make_new(ast.UnaryOp(op, self.__ast_node__))
|
||||||
|
|
||||||
|
return unary_op
|
||||||
|
|
||||||
|
__invert__ = _make_unary_op(ast.Invert())
|
||||||
|
__pos__ = _make_unary_op(ast.UAdd())
|
||||||
|
__neg__ = _make_unary_op(ast.USub())
|
||||||
|
|
||||||
|
del _make_unary_op
|
||||||
|
|
||||||
|
|
||||||
|
class _StringifierDict(dict):
|
||||||
|
def __init__(self, namespace, globals=None, owner=None, is_class=False):
|
||||||
|
super().__init__(namespace)
|
||||||
|
self.namespace = namespace
|
||||||
|
self.globals = globals
|
||||||
|
self.owner = owner
|
||||||
|
self.is_class = is_class
|
||||||
|
self.stringifiers = []
|
||||||
|
|
||||||
|
def __missing__(self, key):
|
||||||
|
fwdref = _Stringifier(
|
||||||
|
ast.Name(id=key),
|
||||||
|
globals=self.globals,
|
||||||
|
owner=self.owner,
|
||||||
|
is_class=self.is_class,
|
||||||
|
)
|
||||||
|
self.stringifiers.append(fwdref)
|
||||||
|
return fwdref
|
||||||
|
|
||||||
|
|
||||||
|
def call_annotate_function(annotate, format, owner=None):
|
||||||
|
"""Call an __annotate__ function. __annotate__ functions are normally
|
||||||
|
generated by the compiler to defer the evaluation of annotations. They
|
||||||
|
can be called with any of the format arguments in the Format enum, but
|
||||||
|
compiler-generated __annotate__ functions only support the VALUE format.
|
||||||
|
This function provides additional functionality to call __annotate__
|
||||||
|
functions with the FORWARDREF and SOURCE formats.
|
||||||
|
|
||||||
|
*annotate* must be an __annotate__ function, which takes a single argument
|
||||||
|
and returns a dict of annotations.
|
||||||
|
|
||||||
|
*format* must be a member of the Format enum or one of the corresponding
|
||||||
|
integer values.
|
||||||
|
|
||||||
|
*owner* can be the object that owns the annotations (i.e., the module,
|
||||||
|
class, or function that the __annotate__ function derives from). With the
|
||||||
|
FORWARDREF format, it is used to provide better evaluation capabilities
|
||||||
|
on the generated ForwardRef objects.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return annotate(format)
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
if format == Format.SOURCE:
|
||||||
|
# SOURCE is implemented by calling the annotate function in a special
|
||||||
|
# environment where every name lookup results in an instance of _Stringifier.
|
||||||
|
# _Stringifier supports every dunder operation and returns a new _Stringifier.
|
||||||
|
# At the end, we get a dictionary that mostly contains _Stringifier objects (or
|
||||||
|
# possibly constants if the annotate function uses them directly). We then
|
||||||
|
# convert each of those into a string to get an approximation of the
|
||||||
|
# original source.
|
||||||
|
globals = _StringifierDict({})
|
||||||
|
if annotate.__closure__:
|
||||||
|
freevars = annotate.__code__.co_freevars
|
||||||
|
new_closure = []
|
||||||
|
for i, cell in enumerate(annotate.__closure__):
|
||||||
|
if i < len(freevars):
|
||||||
|
name = freevars[i]
|
||||||
|
else:
|
||||||
|
name = "__cell__"
|
||||||
|
fwdref = _Stringifier(ast.Name(id=name))
|
||||||
|
new_closure.append(types.CellType(fwdref))
|
||||||
|
closure = tuple(new_closure)
|
||||||
|
else:
|
||||||
|
closure = None
|
||||||
|
func = types.FunctionType(annotate.__code__, globals, closure=closure)
|
||||||
|
annos = func(Format.VALUE)
|
||||||
|
return {
|
||||||
|
key: val if isinstance(val, str) else repr(val)
|
||||||
|
for key, val in annos.items()
|
||||||
|
}
|
||||||
|
elif format == Format.FORWARDREF:
|
||||||
|
# FORWARDREF is implemented similarly to SOURCE, but there are two changes,
|
||||||
|
# at the beginning and the end of the process.
|
||||||
|
# First, while SOURCE uses an empty dictionary as the namespace, so that all
|
||||||
|
# name lookups result in _Stringifier objects, FORWARDREF uses the globals
|
||||||
|
# and builtins, so that defined names map to their real values.
|
||||||
|
# Second, instead of returning strings, we want to return either real values
|
||||||
|
# or ForwardRef objects. To do this, we keep track of all _Stringifier objects
|
||||||
|
# created while the annotation is being evaluated, and at the end we convert
|
||||||
|
# them all to ForwardRef objects by assigning to __class__. To make this
|
||||||
|
# technique work, we have to ensure that the _Stringifier and ForwardRef
|
||||||
|
# classes share the same attributes.
|
||||||
|
# We use this technique because while the annotations are being evaluated,
|
||||||
|
# we want to support all operations that the language allows, including even
|
||||||
|
# __getattr__ and __eq__, and return new _Stringifier objects so we can accurately
|
||||||
|
# reconstruct the source. But in the dictionary that we eventually return, we
|
||||||
|
# want to return objects with more user-friendly behavior, such as an __eq__
|
||||||
|
# that returns a bool and an defined set of attributes.
|
||||||
|
namespace = {**annotate.__builtins__, **annotate.__globals__}
|
||||||
|
is_class = isinstance(owner, type)
|
||||||
|
globals = _StringifierDict(namespace, annotate.__globals__, owner, is_class)
|
||||||
|
if annotate.__closure__:
|
||||||
|
freevars = annotate.__code__.co_freevars
|
||||||
|
new_closure = []
|
||||||
|
for i, cell in enumerate(annotate.__closure__):
|
||||||
|
try:
|
||||||
|
cell.cell_contents
|
||||||
|
except ValueError:
|
||||||
|
if i < len(freevars):
|
||||||
|
name = freevars[i]
|
||||||
|
else:
|
||||||
|
name = "__cell__"
|
||||||
|
fwdref = _Stringifier(
|
||||||
|
ast.Name(id=name),
|
||||||
|
cell=cell,
|
||||||
|
owner=owner,
|
||||||
|
globals=annotate.__globals__,
|
||||||
|
is_class=is_class,
|
||||||
|
)
|
||||||
|
globals.stringifiers.append(fwdref)
|
||||||
|
new_closure.append(types.CellType(fwdref))
|
||||||
|
else:
|
||||||
|
new_closure.append(cell)
|
||||||
|
closure = tuple(new_closure)
|
||||||
|
else:
|
||||||
|
closure = None
|
||||||
|
func = types.FunctionType(annotate.__code__, globals, closure=closure)
|
||||||
|
result = func(Format.VALUE)
|
||||||
|
for obj in globals.stringifiers:
|
||||||
|
obj.__class__ = ForwardRef
|
||||||
|
return result
|
||||||
|
elif format == Format.VALUE:
|
||||||
|
# Should be impossible because __annotate__ functions must not raise
|
||||||
|
# NotImplementedError for this format.
|
||||||
|
raise RuntimeError("annotate function does not support VALUE format")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid format: {format!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_annotations(
|
||||||
|
obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE
|
||||||
|
):
|
||||||
|
"""Compute the annotations dict for an object.
|
||||||
|
|
||||||
|
obj may be a callable, class, or module.
|
||||||
|
Passing in an object of any other type raises TypeError.
|
||||||
|
|
||||||
|
Returns a dict. get_annotations() returns a new dict every time
|
||||||
|
it's called; calling it twice on the same object will return two
|
||||||
|
different but equivalent dicts.
|
||||||
|
|
||||||
|
This function handles several details for you:
|
||||||
|
|
||||||
|
* If eval_str is true, values of type str will
|
||||||
|
be un-stringized using eval(). This is intended
|
||||||
|
for use with stringized annotations
|
||||||
|
("from __future__ import annotations").
|
||||||
|
* If obj doesn't have an annotations dict, returns an
|
||||||
|
empty dict. (Functions and methods always have an
|
||||||
|
annotations dict; classes, modules, and other types of
|
||||||
|
callables may not.)
|
||||||
|
* Ignores inherited annotations on classes. If a class
|
||||||
|
doesn't have its own annotations dict, returns an empty dict.
|
||||||
|
* All accesses to object members and dict values are done
|
||||||
|
using getattr() and dict.get() for safety.
|
||||||
|
* Always, always, always returns a freshly-created dict.
|
||||||
|
|
||||||
|
eval_str controls whether or not values of type str are replaced
|
||||||
|
with the result of calling eval() on those values:
|
||||||
|
|
||||||
|
* If eval_str is true, eval() is called on values of type str.
|
||||||
|
* If eval_str is false (the default), values of type str are unchanged.
|
||||||
|
|
||||||
|
globals and locals are passed in to eval(); see the documentation
|
||||||
|
for eval() for more information. If either globals or locals is
|
||||||
|
None, this function may replace that value with a context-specific
|
||||||
|
default, contingent on type(obj):
|
||||||
|
|
||||||
|
* If obj is a module, globals defaults to obj.__dict__.
|
||||||
|
* If obj is a class, globals defaults to
|
||||||
|
sys.modules[obj.__module__].__dict__ and locals
|
||||||
|
defaults to the obj class namespace.
|
||||||
|
* If obj is a callable, globals defaults to obj.__globals__,
|
||||||
|
although if obj is a wrapped function (using
|
||||||
|
functools.update_wrapper()) it is first unwrapped.
|
||||||
|
"""
|
||||||
|
if eval_str and format != Format.VALUE:
|
||||||
|
raise ValueError("eval_str=True is only supported with format=Format.VALUE")
|
||||||
|
|
||||||
|
# For VALUE format, we look at __annotations__ directly.
|
||||||
|
if format != Format.VALUE:
|
||||||
|
annotate = getattr(obj, "__annotate__", None)
|
||||||
|
if annotate is not None:
|
||||||
|
ann = call_annotate_function(annotate, format, owner=obj)
|
||||||
|
if not isinstance(ann, dict):
|
||||||
|
raise ValueError(f"{obj!r}.__annotate__ returned a non-dict")
|
||||||
|
return dict(ann)
|
||||||
|
|
||||||
|
ann = getattr(obj, "__annotations__", None)
|
||||||
|
if ann is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if not isinstance(ann, dict):
|
||||||
|
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
|
||||||
|
|
||||||
|
if not ann:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if not eval_str:
|
||||||
|
return dict(ann)
|
||||||
|
|
||||||
|
if isinstance(obj, type):
|
||||||
|
# class
|
||||||
|
obj_globals = None
|
||||||
|
module_name = getattr(obj, "__module__", None)
|
||||||
|
if module_name:
|
||||||
|
module = sys.modules.get(module_name, None)
|
||||||
|
if module:
|
||||||
|
obj_globals = getattr(module, "__dict__", None)
|
||||||
|
obj_locals = dict(vars(obj))
|
||||||
|
unwrap = obj
|
||||||
|
elif isinstance(obj, types.ModuleType):
|
||||||
|
# module
|
||||||
|
obj_globals = getattr(obj, "__dict__")
|
||||||
|
obj_locals = None
|
||||||
|
unwrap = None
|
||||||
|
elif callable(obj):
|
||||||
|
# this includes types.Function, types.BuiltinFunctionType,
|
||||||
|
# types.BuiltinMethodType, functools.partial, functools.singledispatch,
|
||||||
|
# "class funclike" from Lib/test/test_inspect... on and on it goes.
|
||||||
|
obj_globals = getattr(obj, "__globals__", None)
|
||||||
|
obj_locals = None
|
||||||
|
unwrap = obj
|
||||||
|
elif ann is not None:
|
||||||
|
obj_globals = obj_locals = unwrap = None
|
||||||
|
else:
|
||||||
|
raise TypeError(f"{obj!r} is not a module, class, or callable.")
|
||||||
|
|
||||||
|
if unwrap is not None:
|
||||||
|
while True:
|
||||||
|
if hasattr(unwrap, "__wrapped__"):
|
||||||
|
unwrap = unwrap.__wrapped__
|
||||||
|
continue
|
||||||
|
if isinstance(unwrap, functools.partial):
|
||||||
|
unwrap = unwrap.func
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
if hasattr(unwrap, "__globals__"):
|
||||||
|
obj_globals = unwrap.__globals__
|
||||||
|
|
||||||
|
if globals is None:
|
||||||
|
globals = obj_globals
|
||||||
|
if locals is None:
|
||||||
|
locals = obj_locals
|
||||||
|
|
||||||
|
# "Inject" type parameters into the local namespace
|
||||||
|
# (unless they are shadowed by assignments *in* the local namespace),
|
||||||
|
# as a way of emulating annotation scopes when calling `eval()`
|
||||||
|
if type_params := getattr(obj, "__type_params__", ()):
|
||||||
|
if locals is None:
|
||||||
|
locals = {}
|
||||||
|
locals = {param.__name__: param for param in type_params} | locals
|
||||||
|
|
||||||
|
return_value = {
|
||||||
|
key: value if not isinstance(value, str) else eval(value, globals, locals)
|
||||||
|
for key, value in ann.items()
|
||||||
|
}
|
||||||
|
return return_value
|
|
@ -5,6 +5,7 @@ import types
|
||||||
import inspect
|
import inspect
|
||||||
import keyword
|
import keyword
|
||||||
import itertools
|
import itertools
|
||||||
|
import annotationlib
|
||||||
import abc
|
import abc
|
||||||
from reprlib import recursive_repr
|
from reprlib import recursive_repr
|
||||||
|
|
||||||
|
@ -981,7 +982,8 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
|
||||||
# actual default value. Pseudo-fields ClassVars and InitVars are
|
# actual default value. Pseudo-fields ClassVars and InitVars are
|
||||||
# included, despite the fact that they're not real fields. That's
|
# included, despite the fact that they're not real fields. That's
|
||||||
# dealt with later.
|
# dealt with later.
|
||||||
cls_annotations = inspect.get_annotations(cls)
|
cls_annotations = annotationlib.get_annotations(
|
||||||
|
cls, format=annotationlib.Format.FORWARDREF)
|
||||||
|
|
||||||
# Now find fields in our class. While doing so, validate some
|
# Now find fields in our class. While doing so, validate some
|
||||||
# things, and set the default values (as class attributes) where
|
# things, and set the default values (as class attributes) where
|
||||||
|
|
|
@ -32,7 +32,7 @@ GenericAlias = type(list[int])
|
||||||
# wrapper functions that can handle naive introspection
|
# wrapper functions that can handle naive introspection
|
||||||
|
|
||||||
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
|
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
|
||||||
'__annotations__', '__type_params__')
|
'__annotate__', '__type_params__')
|
||||||
WRAPPER_UPDATES = ('__dict__',)
|
WRAPPER_UPDATES = ('__dict__',)
|
||||||
def update_wrapper(wrapper,
|
def update_wrapper(wrapper,
|
||||||
wrapped,
|
wrapped,
|
||||||
|
@ -882,8 +882,8 @@ def singledispatch(func):
|
||||||
f"Invalid first argument to `register()`. "
|
f"Invalid first argument to `register()`. "
|
||||||
f"{cls!r} is not a class or union type."
|
f"{cls!r} is not a class or union type."
|
||||||
)
|
)
|
||||||
ann = getattr(cls, '__annotations__', {})
|
ann = getattr(cls, '__annotate__', None)
|
||||||
if not ann:
|
if ann is None:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"Invalid first argument to `register()`: {cls!r}. "
|
f"Invalid first argument to `register()`: {cls!r}. "
|
||||||
f"Use either `@register(some_class)` or plain `@register` "
|
f"Use either `@register(some_class)` or plain `@register` "
|
||||||
|
@ -893,13 +893,19 @@ def singledispatch(func):
|
||||||
|
|
||||||
# only import typing if annotation parsing is necessary
|
# only import typing if annotation parsing is necessary
|
||||||
from typing import get_type_hints
|
from typing import get_type_hints
|
||||||
argname, cls = next(iter(get_type_hints(func).items()))
|
from annotationlib import Format, ForwardRef
|
||||||
|
argname, cls = next(iter(get_type_hints(func, format=Format.FORWARDREF).items()))
|
||||||
if not _is_valid_dispatch_type(cls):
|
if not _is_valid_dispatch_type(cls):
|
||||||
if _is_union_type(cls):
|
if _is_union_type(cls):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"Invalid annotation for {argname!r}. "
|
f"Invalid annotation for {argname!r}. "
|
||||||
f"{cls!r} not all arguments are classes."
|
f"{cls!r} not all arguments are classes."
|
||||||
)
|
)
|
||||||
|
elif isinstance(cls, ForwardRef):
|
||||||
|
raise TypeError(
|
||||||
|
f"Invalid annotation for {argname!r}. "
|
||||||
|
f"{cls!r} is an unresolved forward reference."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"Invalid annotation for {argname!r}. "
|
f"Invalid annotation for {argname!r}. "
|
||||||
|
|
116
Lib/inspect.py
116
Lib/inspect.py
|
@ -142,6 +142,7 @@ __all__ = [
|
||||||
|
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
from annotationlib import get_annotations
|
||||||
import ast
|
import ast
|
||||||
import dis
|
import dis
|
||||||
import collections.abc
|
import collections.abc
|
||||||
|
@ -173,121 +174,6 @@ del k, v, mod_dict
|
||||||
TPFLAGS_IS_ABSTRACT = 1 << 20
|
TPFLAGS_IS_ABSTRACT = 1 << 20
|
||||||
|
|
||||||
|
|
||||||
def get_annotations(obj, *, globals=None, locals=None, eval_str=False):
|
|
||||||
"""Compute the annotations dict for an object.
|
|
||||||
|
|
||||||
obj may be a callable, class, or module.
|
|
||||||
Passing in an object of any other type raises TypeError.
|
|
||||||
|
|
||||||
Returns a dict. get_annotations() returns a new dict every time
|
|
||||||
it's called; calling it twice on the same object will return two
|
|
||||||
different but equivalent dicts.
|
|
||||||
|
|
||||||
This function handles several details for you:
|
|
||||||
|
|
||||||
* If eval_str is true, values of type str will
|
|
||||||
be un-stringized using eval(). This is intended
|
|
||||||
for use with stringized annotations
|
|
||||||
("from __future__ import annotations").
|
|
||||||
* If obj doesn't have an annotations dict, returns an
|
|
||||||
empty dict. (Functions and methods always have an
|
|
||||||
annotations dict; classes, modules, and other types of
|
|
||||||
callables may not.)
|
|
||||||
* Ignores inherited annotations on classes. If a class
|
|
||||||
doesn't have its own annotations dict, returns an empty dict.
|
|
||||||
* All accesses to object members and dict values are done
|
|
||||||
using getattr() and dict.get() for safety.
|
|
||||||
* Always, always, always returns a freshly-created dict.
|
|
||||||
|
|
||||||
eval_str controls whether or not values of type str are replaced
|
|
||||||
with the result of calling eval() on those values:
|
|
||||||
|
|
||||||
* If eval_str is true, eval() is called on values of type str.
|
|
||||||
* If eval_str is false (the default), values of type str are unchanged.
|
|
||||||
|
|
||||||
globals and locals are passed in to eval(); see the documentation
|
|
||||||
for eval() for more information. If either globals or locals is
|
|
||||||
None, this function may replace that value with a context-specific
|
|
||||||
default, contingent on type(obj):
|
|
||||||
|
|
||||||
* If obj is a module, globals defaults to obj.__dict__.
|
|
||||||
* If obj is a class, globals defaults to
|
|
||||||
sys.modules[obj.__module__].__dict__ and locals
|
|
||||||
defaults to the obj class namespace.
|
|
||||||
* If obj is a callable, globals defaults to obj.__globals__,
|
|
||||||
although if obj is a wrapped function (using
|
|
||||||
functools.update_wrapper()) it is first unwrapped.
|
|
||||||
"""
|
|
||||||
if isinstance(obj, type):
|
|
||||||
# class
|
|
||||||
ann = obj.__annotations__
|
|
||||||
|
|
||||||
obj_globals = None
|
|
||||||
module_name = getattr(obj, '__module__', None)
|
|
||||||
if module_name:
|
|
||||||
module = sys.modules.get(module_name, None)
|
|
||||||
if module:
|
|
||||||
obj_globals = getattr(module, '__dict__', None)
|
|
||||||
obj_locals = dict(vars(obj))
|
|
||||||
unwrap = obj
|
|
||||||
elif isinstance(obj, types.ModuleType):
|
|
||||||
# module
|
|
||||||
ann = getattr(obj, '__annotations__', None)
|
|
||||||
obj_globals = getattr(obj, '__dict__')
|
|
||||||
obj_locals = None
|
|
||||||
unwrap = None
|
|
||||||
elif callable(obj):
|
|
||||||
# this includes types.Function, types.BuiltinFunctionType,
|
|
||||||
# types.BuiltinMethodType, functools.partial, functools.singledispatch,
|
|
||||||
# "class funclike" from Lib/test/test_inspect... on and on it goes.
|
|
||||||
ann = getattr(obj, '__annotations__', None)
|
|
||||||
obj_globals = getattr(obj, '__globals__', None)
|
|
||||||
obj_locals = None
|
|
||||||
unwrap = obj
|
|
||||||
else:
|
|
||||||
raise TypeError(f"{obj!r} is not a module, class, or callable.")
|
|
||||||
|
|
||||||
if ann is None:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
if not isinstance(ann, dict):
|
|
||||||
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
|
|
||||||
|
|
||||||
if not ann:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
if not eval_str:
|
|
||||||
return dict(ann)
|
|
||||||
|
|
||||||
if unwrap is not None:
|
|
||||||
while True:
|
|
||||||
if hasattr(unwrap, '__wrapped__'):
|
|
||||||
unwrap = unwrap.__wrapped__
|
|
||||||
continue
|
|
||||||
if isinstance(unwrap, functools.partial):
|
|
||||||
unwrap = unwrap.func
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
if hasattr(unwrap, "__globals__"):
|
|
||||||
obj_globals = unwrap.__globals__
|
|
||||||
|
|
||||||
if globals is None:
|
|
||||||
globals = obj_globals
|
|
||||||
if locals is None:
|
|
||||||
locals = obj_locals or {}
|
|
||||||
|
|
||||||
# "Inject" type parameters into the local namespace
|
|
||||||
# (unless they are shadowed by assignments *in* the local namespace),
|
|
||||||
# as a way of emulating annotation scopes when calling `eval()`
|
|
||||||
if type_params := getattr(obj, "__type_params__", ()):
|
|
||||||
locals = {param.__name__: param for param in type_params} | locals
|
|
||||||
|
|
||||||
return_value = {key:
|
|
||||||
value if not isinstance(value, str) else eval(value, globals, locals)
|
|
||||||
for key, value in ann.items() }
|
|
||||||
return return_value
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------- type-checking
|
# ----------------------------------------------------------- type-checking
|
||||||
def ismodule(object):
|
def ismodule(object):
|
||||||
"""Return true if the object is a module."""
|
"""Return true if the object is a module."""
|
||||||
|
|
771
Lib/test/test_annotationlib.py
Normal file
771
Lib/test/test_annotationlib.py
Normal file
|
@ -0,0 +1,771 @@
|
||||||
|
"""Tests for the annotations module."""
|
||||||
|
|
||||||
|
import annotationlib
|
||||||
|
import functools
|
||||||
|
import pickle
|
||||||
|
import unittest
|
||||||
|
from typing import Unpack
|
||||||
|
|
||||||
|
from test.test_inspect import inspect_stock_annotations
|
||||||
|
from test.test_inspect import inspect_stringized_annotations
|
||||||
|
from test.test_inspect import inspect_stringized_annotations_2
|
||||||
|
from test.test_inspect import inspect_stringized_annotations_pep695
|
||||||
|
|
||||||
|
|
||||||
|
def times_three(fn):
|
||||||
|
@functools.wraps(fn)
|
||||||
|
def wrapper(a, b):
|
||||||
|
return fn(a * 3, b * 3)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormat(unittest.TestCase):
|
||||||
|
def test_enum(self):
|
||||||
|
self.assertEqual(annotationlib.Format.VALUE.value, 1)
|
||||||
|
self.assertEqual(annotationlib.Format.VALUE, 1)
|
||||||
|
|
||||||
|
self.assertEqual(annotationlib.Format.FORWARDREF.value, 2)
|
||||||
|
self.assertEqual(annotationlib.Format.FORWARDREF, 2)
|
||||||
|
|
||||||
|
self.assertEqual(annotationlib.Format.SOURCE.value, 3)
|
||||||
|
self.assertEqual(annotationlib.Format.SOURCE, 3)
|
||||||
|
|
||||||
|
|
||||||
|
class TestForwardRefFormat(unittest.TestCase):
|
||||||
|
def test_closure(self):
|
||||||
|
def inner(arg: x):
|
||||||
|
pass
|
||||||
|
|
||||||
|
anno = annotationlib.get_annotations(
|
||||||
|
inner, format=annotationlib.Format.FORWARDREF
|
||||||
|
)
|
||||||
|
fwdref = anno["arg"]
|
||||||
|
self.assertIsInstance(fwdref, annotationlib.ForwardRef)
|
||||||
|
self.assertEqual(fwdref.__forward_arg__, "x")
|
||||||
|
with self.assertRaises(NameError):
|
||||||
|
fwdref.evaluate()
|
||||||
|
|
||||||
|
x = 1
|
||||||
|
self.assertEqual(fwdref.evaluate(), x)
|
||||||
|
|
||||||
|
anno = annotationlib.get_annotations(
|
||||||
|
inner, format=annotationlib.Format.FORWARDREF
|
||||||
|
)
|
||||||
|
self.assertEqual(anno["arg"], x)
|
||||||
|
|
||||||
|
def test_function(self):
|
||||||
|
def f(x: int, y: doesntexist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
anno = annotationlib.get_annotations(f, format=annotationlib.Format.FORWARDREF)
|
||||||
|
self.assertIs(anno["x"], int)
|
||||||
|
fwdref = anno["y"]
|
||||||
|
self.assertIsInstance(fwdref, annotationlib.ForwardRef)
|
||||||
|
self.assertEqual(fwdref.__forward_arg__, "doesntexist")
|
||||||
|
with self.assertRaises(NameError):
|
||||||
|
fwdref.evaluate()
|
||||||
|
self.assertEqual(fwdref.evaluate(globals={"doesntexist": 1}), 1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSourceFormat(unittest.TestCase):
|
||||||
|
def test_closure(self):
|
||||||
|
x = 0
|
||||||
|
|
||||||
|
def inner(arg: x):
|
||||||
|
pass
|
||||||
|
|
||||||
|
anno = annotationlib.get_annotations(inner, format=annotationlib.Format.SOURCE)
|
||||||
|
self.assertEqual(anno, {"arg": "x"})
|
||||||
|
|
||||||
|
def test_function(self):
|
||||||
|
def f(x: int, y: doesntexist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
anno = annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE)
|
||||||
|
self.assertEqual(anno, {"x": "int", "y": "doesntexist"})
|
||||||
|
|
||||||
|
def test_expressions(self):
|
||||||
|
def f(
|
||||||
|
add: a + b,
|
||||||
|
sub: a - b,
|
||||||
|
mul: a * b,
|
||||||
|
matmul: a @ b,
|
||||||
|
truediv: a / b,
|
||||||
|
mod: a % b,
|
||||||
|
lshift: a << b,
|
||||||
|
rshift: a >> b,
|
||||||
|
or_: a | b,
|
||||||
|
xor: a ^ b,
|
||||||
|
and_: a & b,
|
||||||
|
floordiv: a // b,
|
||||||
|
pow_: a**b,
|
||||||
|
lt: a < b,
|
||||||
|
le: a <= b,
|
||||||
|
eq: a == b,
|
||||||
|
ne: a != b,
|
||||||
|
gt: a > b,
|
||||||
|
ge: a >= b,
|
||||||
|
invert: ~a,
|
||||||
|
neg: -a,
|
||||||
|
pos: +a,
|
||||||
|
getitem: a[b],
|
||||||
|
getattr: a.b,
|
||||||
|
call: a(b, *c, d=e), # **kwargs are not supported
|
||||||
|
*args: *a,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
anno = annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE)
|
||||||
|
self.assertEqual(
|
||||||
|
anno,
|
||||||
|
{
|
||||||
|
"add": "a + b",
|
||||||
|
"sub": "a - b",
|
||||||
|
"mul": "a * b",
|
||||||
|
"matmul": "a @ b",
|
||||||
|
"truediv": "a / b",
|
||||||
|
"mod": "a % b",
|
||||||
|
"lshift": "a << b",
|
||||||
|
"rshift": "a >> b",
|
||||||
|
"or_": "a | b",
|
||||||
|
"xor": "a ^ b",
|
||||||
|
"and_": "a & b",
|
||||||
|
"floordiv": "a // b",
|
||||||
|
"pow_": "a ** b",
|
||||||
|
"lt": "a < b",
|
||||||
|
"le": "a <= b",
|
||||||
|
"eq": "a == b",
|
||||||
|
"ne": "a != b",
|
||||||
|
"gt": "a > b",
|
||||||
|
"ge": "a >= b",
|
||||||
|
"invert": "~a",
|
||||||
|
"neg": "-a",
|
||||||
|
"pos": "+a",
|
||||||
|
"getitem": "a[b]",
|
||||||
|
"getattr": "a.b",
|
||||||
|
"call": "a(b, *c, d=e)",
|
||||||
|
"args": "*a",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_reverse_ops(self):
|
||||||
|
def f(
|
||||||
|
radd: 1 + a,
|
||||||
|
rsub: 1 - a,
|
||||||
|
rmul: 1 * a,
|
||||||
|
rmatmul: 1 @ a,
|
||||||
|
rtruediv: 1 / a,
|
||||||
|
rmod: 1 % a,
|
||||||
|
rlshift: 1 << a,
|
||||||
|
rrshift: 1 >> a,
|
||||||
|
ror: 1 | a,
|
||||||
|
rxor: 1 ^ a,
|
||||||
|
rand: 1 & a,
|
||||||
|
rfloordiv: 1 // a,
|
||||||
|
rpow: 1**a,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
anno = annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE)
|
||||||
|
self.assertEqual(
|
||||||
|
anno,
|
||||||
|
{
|
||||||
|
"radd": "1 + a",
|
||||||
|
"rsub": "1 - a",
|
||||||
|
"rmul": "1 * a",
|
||||||
|
"rmatmul": "1 @ a",
|
||||||
|
"rtruediv": "1 / a",
|
||||||
|
"rmod": "1 % a",
|
||||||
|
"rlshift": "1 << a",
|
||||||
|
"rrshift": "1 >> a",
|
||||||
|
"ror": "1 | a",
|
||||||
|
"rxor": "1 ^ a",
|
||||||
|
"rand": "1 & a",
|
||||||
|
"rfloordiv": "1 // a",
|
||||||
|
"rpow": "1 ** a",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_nested_expressions(self):
|
||||||
|
def f(
|
||||||
|
nested: list[Annotated[set[int], "set of ints", 4j]],
|
||||||
|
set: {a + b}, # single element because order is not guaranteed
|
||||||
|
dict: {a + b: c + d, "key": e + g},
|
||||||
|
list: [a, b, c],
|
||||||
|
tuple: (a, b, c),
|
||||||
|
slice: (a[b:c], a[b:c:d], a[:c], a[b:], a[:], a[::d], a[b::d]),
|
||||||
|
extended_slice: a[:, :, c:d],
|
||||||
|
unpack1: [*a],
|
||||||
|
unpack2: [*a, b, c],
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
anno = annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE)
|
||||||
|
self.assertEqual(
|
||||||
|
anno,
|
||||||
|
{
|
||||||
|
"nested": "list[Annotated[set[int], 'set of ints', 4j]]",
|
||||||
|
"set": "{a + b}",
|
||||||
|
"dict": "{a + b: c + d, 'key': e + g}",
|
||||||
|
"list": "[a, b, c]",
|
||||||
|
"tuple": "(a, b, c)",
|
||||||
|
"slice": "(a[b:c], a[b:c:d], a[:c], a[b:], a[:], a[::d], a[b::d])",
|
||||||
|
"extended_slice": "a[:, :, c:d]",
|
||||||
|
"unpack1": "[*a]",
|
||||||
|
"unpack2": "[*a, b, c]",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unsupported_operations(self):
|
||||||
|
format_msg = "Cannot stringify annotation containing string formatting"
|
||||||
|
|
||||||
|
def f(fstring: f"{a}"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, format_msg):
|
||||||
|
annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE)
|
||||||
|
|
||||||
|
def f(fstring_format: f"{a:02d}"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, format_msg):
|
||||||
|
annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE)
|
||||||
|
|
||||||
|
|
||||||
|
class TestForwardRefClass(unittest.TestCase):
|
||||||
|
def test_special_attrs(self):
|
||||||
|
# Forward refs provide a different introspection API. __name__ and
|
||||||
|
# __qualname__ make little sense for forward refs as they can store
|
||||||
|
# complex typing expressions.
|
||||||
|
fr = annotationlib.ForwardRef("set[Any]")
|
||||||
|
self.assertFalse(hasattr(fr, "__name__"))
|
||||||
|
self.assertFalse(hasattr(fr, "__qualname__"))
|
||||||
|
self.assertEqual(fr.__module__, "annotationlib")
|
||||||
|
# Forward refs are currently unpicklable once they contain a code object.
|
||||||
|
fr.__forward_code__ # fill the cache
|
||||||
|
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
pickle.dumps(fr, proto)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetAnnotations(unittest.TestCase):
|
||||||
|
def test_builtin_type(self):
|
||||||
|
self.assertEqual(annotationlib.get_annotations(int), {})
|
||||||
|
self.assertEqual(annotationlib.get_annotations(object), {})
|
||||||
|
|
||||||
|
def test_custom_metaclass(self):
|
||||||
|
class Meta(type):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class C(metaclass=Meta):
|
||||||
|
x: int
|
||||||
|
|
||||||
|
self.assertEqual(annotationlib.get_annotations(C), {"x": int})
|
||||||
|
|
||||||
|
def test_missing_dunder_dict(self):
|
||||||
|
class NoDict(type):
|
||||||
|
@property
|
||||||
|
def __dict__(cls):
|
||||||
|
raise AttributeError
|
||||||
|
|
||||||
|
b: str
|
||||||
|
|
||||||
|
class C1(metaclass=NoDict):
|
||||||
|
a: int
|
||||||
|
|
||||||
|
self.assertEqual(annotationlib.get_annotations(C1), {"a": int})
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(C1, format=annotationlib.Format.FORWARDREF),
|
||||||
|
{"a": int},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(C1, format=annotationlib.Format.SOURCE),
|
||||||
|
{"a": "int"},
|
||||||
|
)
|
||||||
|
self.assertEqual(annotationlib.get_annotations(NoDict), {"b": str})
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(NoDict, format=annotationlib.Format.FORWARDREF),
|
||||||
|
{"b": str},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(NoDict, format=annotationlib.Format.SOURCE),
|
||||||
|
{"b": "str"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_format(self):
|
||||||
|
def f1(a: int):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def f2(a: undefined):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(f1, format=annotationlib.Format.VALUE),
|
||||||
|
{"a": int},
|
||||||
|
)
|
||||||
|
self.assertEqual(annotationlib.get_annotations(f1, format=1), {"a": int})
|
||||||
|
|
||||||
|
fwd = annotationlib.ForwardRef("undefined")
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(f2, format=annotationlib.Format.FORWARDREF),
|
||||||
|
{"a": fwd},
|
||||||
|
)
|
||||||
|
self.assertEqual(annotationlib.get_annotations(f2, format=2), {"a": fwd})
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(f1, format=annotationlib.Format.SOURCE),
|
||||||
|
{"a": "int"},
|
||||||
|
)
|
||||||
|
self.assertEqual(annotationlib.get_annotations(f1, format=3), {"a": "int"})
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
annotationlib.get_annotations(f1, format=0)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
annotationlib.get_annotations(f1, format=4)
|
||||||
|
|
||||||
|
def test_custom_object_with_annotations(self):
|
||||||
|
class C:
|
||||||
|
def __init__(self):
|
||||||
|
self.__annotations__ = {"x": int, "y": str}
|
||||||
|
|
||||||
|
self.assertEqual(annotationlib.get_annotations(C()), {"x": int, "y": str})
|
||||||
|
|
||||||
|
def test_custom_format_eval_str(self):
|
||||||
|
def foo():
|
||||||
|
pass
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
foo, format=annotationlib.Format.FORWARDREF, eval_str=True
|
||||||
|
)
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
foo, format=annotationlib.Format.SOURCE, eval_str=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stock_annotations(self):
|
||||||
|
def foo(a: int, b: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
for format in (annotationlib.Format.VALUE, annotationlib.Format.FORWARDREF):
|
||||||
|
with self.subTest(format=format):
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(foo, format=format),
|
||||||
|
{"a": int, "b": str},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(foo, format=annotationlib.Format.SOURCE),
|
||||||
|
{"a": "int", "b": "str"},
|
||||||
|
)
|
||||||
|
|
||||||
|
foo.__annotations__ = {"a": "foo", "b": "str"}
|
||||||
|
for format in annotationlib.Format:
|
||||||
|
with self.subTest(format=format):
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(foo, format=format),
|
||||||
|
{"a": "foo", "b": "str"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(foo, eval_str=True, locals=locals()),
|
||||||
|
{"a": foo, "b": str},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(foo, eval_str=True, globals=locals()),
|
||||||
|
{"a": foo, "b": str},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stock_annotations_in_module(self):
|
||||||
|
isa = inspect_stock_annotations
|
||||||
|
|
||||||
|
for kwargs in [
|
||||||
|
{},
|
||||||
|
{"eval_str": False},
|
||||||
|
{"format": annotationlib.Format.VALUE},
|
||||||
|
{"format": annotationlib.Format.FORWARDREF},
|
||||||
|
{"format": annotationlib.Format.VALUE, "eval_str": False},
|
||||||
|
{"format": annotationlib.Format.FORWARDREF, "eval_str": False},
|
||||||
|
]:
|
||||||
|
with self.subTest(**kwargs):
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa, **kwargs), {"a": int, "b": str}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.MyClass, **kwargs),
|
||||||
|
{"a": int, "b": str},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.function, **kwargs),
|
||||||
|
{"a": int, "b": str, "return": isa.MyClass},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.function2, **kwargs),
|
||||||
|
{"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.function3, **kwargs),
|
||||||
|
{"a": "int", "b": "str", "c": "MyClass"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(annotationlib, **kwargs), {}
|
||||||
|
) # annotations module has no annotations
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.UnannotatedClass, **kwargs), {}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.unannotated_function, **kwargs),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
for kwargs in [
|
||||||
|
{"eval_str": True},
|
||||||
|
{"format": annotationlib.Format.VALUE, "eval_str": True},
|
||||||
|
]:
|
||||||
|
with self.subTest(**kwargs):
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa, **kwargs), {"a": int, "b": str}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.MyClass, **kwargs),
|
||||||
|
{"a": int, "b": str},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.function, **kwargs),
|
||||||
|
{"a": int, "b": str, "return": isa.MyClass},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.function2, **kwargs),
|
||||||
|
{"a": int, "b": str, "c": isa.MyClass, "return": isa.MyClass},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.function3, **kwargs),
|
||||||
|
{"a": int, "b": str, "c": isa.MyClass},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(annotationlib, **kwargs), {}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.UnannotatedClass, **kwargs), {}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.unannotated_function, **kwargs),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa, format=annotationlib.Format.SOURCE),
|
||||||
|
{"a": "int", "b": "str"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
isa.MyClass, format=annotationlib.Format.SOURCE
|
||||||
|
),
|
||||||
|
{"a": "int", "b": "str"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
isa.function, format=annotationlib.Format.SOURCE
|
||||||
|
),
|
||||||
|
{"a": "int", "b": "str", "return": "MyClass"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
isa.function2, format=annotationlib.Format.SOURCE
|
||||||
|
),
|
||||||
|
{"a": "int", "b": "str", "c": "MyClass", "return": "MyClass"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
isa.function3, format=annotationlib.Format.SOURCE
|
||||||
|
),
|
||||||
|
{"a": "int", "b": "str", "c": "MyClass"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
annotationlib, format=annotationlib.Format.SOURCE
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
isa.UnannotatedClass, format=annotationlib.Format.SOURCE
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
isa.unannotated_function, format=annotationlib.Format.SOURCE
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stock_annotations_on_wrapper(self):
|
||||||
|
isa = inspect_stock_annotations
|
||||||
|
|
||||||
|
wrapped = times_three(isa.function)
|
||||||
|
self.assertEqual(wrapped(1, "x"), isa.MyClass(3, "xxx"))
|
||||||
|
self.assertIsNot(wrapped.__globals__, isa.function.__globals__)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(wrapped),
|
||||||
|
{"a": int, "b": str, "return": isa.MyClass},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
wrapped, format=annotationlib.Format.FORWARDREF
|
||||||
|
),
|
||||||
|
{"a": int, "b": str, "return": isa.MyClass},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(wrapped, format=annotationlib.Format.SOURCE),
|
||||||
|
{"a": "int", "b": "str", "return": "MyClass"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(wrapped, eval_str=True),
|
||||||
|
{"a": int, "b": str, "return": isa.MyClass},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(wrapped, eval_str=False),
|
||||||
|
{"a": int, "b": str, "return": isa.MyClass},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stringized_annotations_in_module(self):
|
||||||
|
isa = inspect_stringized_annotations
|
||||||
|
for kwargs in [
|
||||||
|
{},
|
||||||
|
{"eval_str": False},
|
||||||
|
{"format": annotationlib.Format.VALUE},
|
||||||
|
{"format": annotationlib.Format.FORWARDREF},
|
||||||
|
{"format": annotationlib.Format.SOURCE},
|
||||||
|
{"format": annotationlib.Format.VALUE, "eval_str": False},
|
||||||
|
{"format": annotationlib.Format.FORWARDREF, "eval_str": False},
|
||||||
|
{"format": annotationlib.Format.SOURCE, "eval_str": False},
|
||||||
|
]:
|
||||||
|
with self.subTest(**kwargs):
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa, **kwargs),
|
||||||
|
{"a": "int", "b": "str"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.MyClass, **kwargs),
|
||||||
|
{"a": "int", "b": "str"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.function, **kwargs),
|
||||||
|
{"a": "int", "b": "str", "return": "MyClass"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.function2, **kwargs),
|
||||||
|
{"a": "int", "b": "'str'", "c": "MyClass", "return": "MyClass"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.function3, **kwargs),
|
||||||
|
{"a": "'int'", "b": "'str'", "c": "'MyClass'"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.UnannotatedClass, **kwargs), {}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.unannotated_function, **kwargs),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
for kwargs in [
|
||||||
|
{"eval_str": True},
|
||||||
|
{"format": annotationlib.Format.VALUE, "eval_str": True},
|
||||||
|
]:
|
||||||
|
with self.subTest(**kwargs):
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa, **kwargs), {"a": int, "b": str}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.MyClass, **kwargs),
|
||||||
|
{"a": int, "b": str},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.function, **kwargs),
|
||||||
|
{"a": int, "b": str, "return": isa.MyClass},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.function2, **kwargs),
|
||||||
|
{"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.function3, **kwargs),
|
||||||
|
{"a": "int", "b": "str", "c": "MyClass"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.UnannotatedClass, **kwargs), {}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.unannotated_function, **kwargs),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stringized_annotations_in_empty_module(self):
|
||||||
|
isa2 = inspect_stringized_annotations_2
|
||||||
|
self.assertEqual(annotationlib.get_annotations(isa2), {})
|
||||||
|
self.assertEqual(annotationlib.get_annotations(isa2, eval_str=True), {})
|
||||||
|
self.assertEqual(annotationlib.get_annotations(isa2, eval_str=False), {})
|
||||||
|
|
||||||
|
def test_stringized_annotations_on_wrapper(self):
|
||||||
|
isa = inspect_stringized_annotations
|
||||||
|
wrapped = times_three(isa.function)
|
||||||
|
self.assertEqual(wrapped(1, "x"), isa.MyClass(3, "xxx"))
|
||||||
|
self.assertIsNot(wrapped.__globals__, isa.function.__globals__)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(wrapped),
|
||||||
|
{"a": "int", "b": "str", "return": "MyClass"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(wrapped, eval_str=True),
|
||||||
|
{"a": int, "b": str, "return": isa.MyClass},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(wrapped, eval_str=False),
|
||||||
|
{"a": "int", "b": "str", "return": "MyClass"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stringized_annotations_on_class(self):
|
||||||
|
isa = inspect_stringized_annotations
|
||||||
|
# test that local namespace lookups work
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(isa.MyClassWithLocalAnnotations),
|
||||||
|
{"x": "mytype"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
isa.MyClassWithLocalAnnotations, eval_str=True
|
||||||
|
),
|
||||||
|
{"x": int},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_modify_annotations(self):
|
||||||
|
def f(x: int):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.assertEqual(annotationlib.get_annotations(f), {"x": int})
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(f, format=annotationlib.Format.FORWARDREF),
|
||||||
|
{"x": int},
|
||||||
|
)
|
||||||
|
|
||||||
|
f.__annotations__["x"] = str
|
||||||
|
# The modification is reflected in VALUE (the default)
|
||||||
|
self.assertEqual(annotationlib.get_annotations(f), {"x": str})
|
||||||
|
# ... but not in FORWARDREF, which uses __annotate__
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(f, format=annotationlib.Format.FORWARDREF),
|
||||||
|
{"x": int},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_pep695_generic_class_with_future_annotations(self):
|
||||||
|
ann_module695 = inspect_stringized_annotations_pep695
|
||||||
|
A_annotations = annotationlib.get_annotations(ann_module695.A, eval_str=True)
|
||||||
|
A_type_params = ann_module695.A.__type_params__
|
||||||
|
self.assertIs(A_annotations["x"], A_type_params[0])
|
||||||
|
self.assertEqual(A_annotations["y"].__args__[0], Unpack[A_type_params[1]])
|
||||||
|
self.assertIs(A_annotations["z"].__args__[0], A_type_params[2])
|
||||||
|
|
||||||
|
def test_pep695_generic_class_with_future_annotations_and_local_shadowing(self):
|
||||||
|
B_annotations = annotationlib.get_annotations(
|
||||||
|
inspect_stringized_annotations_pep695.B, eval_str=True
|
||||||
|
)
|
||||||
|
self.assertEqual(B_annotations, {"x": int, "y": str, "z": bytes})
|
||||||
|
|
||||||
|
def test_pep695_generic_class_with_future_annotations_name_clash_with_global_vars(self):
|
||||||
|
ann_module695 = inspect_stringized_annotations_pep695
|
||||||
|
C_annotations = annotationlib.get_annotations(ann_module695.C, eval_str=True)
|
||||||
|
self.assertEqual(
|
||||||
|
set(C_annotations.values()),
|
||||||
|
set(ann_module695.C.__type_params__)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_pep_695_generic_function_with_future_annotations(self):
|
||||||
|
ann_module695 = inspect_stringized_annotations_pep695
|
||||||
|
generic_func_annotations = annotationlib.get_annotations(
|
||||||
|
ann_module695.generic_function, eval_str=True
|
||||||
|
)
|
||||||
|
func_t_params = ann_module695.generic_function.__type_params__
|
||||||
|
self.assertEqual(
|
||||||
|
generic_func_annotations.keys(), {"x", "y", "z", "zz", "return"}
|
||||||
|
)
|
||||||
|
self.assertIs(generic_func_annotations["x"], func_t_params[0])
|
||||||
|
self.assertEqual(generic_func_annotations["y"], Unpack[func_t_params[1]])
|
||||||
|
self.assertIs(generic_func_annotations["z"].__origin__, func_t_params[2])
|
||||||
|
self.assertIs(generic_func_annotations["zz"].__origin__, func_t_params[2])
|
||||||
|
|
||||||
|
def test_pep_695_generic_function_with_future_annotations_name_clash_with_global_vars(self):
|
||||||
|
self.assertEqual(
|
||||||
|
set(
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
inspect_stringized_annotations_pep695.generic_function_2,
|
||||||
|
eval_str=True
|
||||||
|
).values()
|
||||||
|
),
|
||||||
|
set(
|
||||||
|
inspect_stringized_annotations_pep695.generic_function_2.__type_params__
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_pep_695_generic_method_with_future_annotations(self):
|
||||||
|
ann_module695 = inspect_stringized_annotations_pep695
|
||||||
|
generic_method_annotations = annotationlib.get_annotations(
|
||||||
|
ann_module695.D.generic_method, eval_str=True
|
||||||
|
)
|
||||||
|
params = {
|
||||||
|
param.__name__: param
|
||||||
|
for param in ann_module695.D.generic_method.__type_params__
|
||||||
|
}
|
||||||
|
self.assertEqual(
|
||||||
|
generic_method_annotations,
|
||||||
|
{"x": params["Foo"], "y": params["Bar"], "return": None}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_vars(self):
|
||||||
|
self.assertEqual(
|
||||||
|
set(
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
inspect_stringized_annotations_pep695.D.generic_method_2,
|
||||||
|
eval_str=True
|
||||||
|
).values()
|
||||||
|
),
|
||||||
|
set(
|
||||||
|
inspect_stringized_annotations_pep695.D.generic_method_2.__type_params__
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_and_local_vars(self):
|
||||||
|
self.assertEqual(
|
||||||
|
annotationlib.get_annotations(
|
||||||
|
inspect_stringized_annotations_pep695.E, eval_str=True
|
||||||
|
),
|
||||||
|
{"x": str},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_pep_695_generics_with_future_annotations_nested_in_function(self):
|
||||||
|
results = inspect_stringized_annotations_pep695.nested()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
set(results.F_annotations.values()),
|
||||||
|
set(results.F.__type_params__)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
set(results.F_meth_annotations.values()),
|
||||||
|
set(results.F.generic_method.__type_params__)
|
||||||
|
)
|
||||||
|
self.assertNotEqual(
|
||||||
|
set(results.F_meth_annotations.values()),
|
||||||
|
set(results.F.__type_params__)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
set(results.F_meth_annotations.values()).intersection(results.F.__type_params__),
|
||||||
|
set()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(results.G_annotations, {"x": str})
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
set(results.generic_func_annotations.values()),
|
||||||
|
set(results.generic_func.__type_params__)
|
||||||
|
)
|
|
@ -4807,6 +4807,16 @@ class TestKeywordArgs(unittest.TestCase):
|
||||||
self.assertTrue(fields(B)[0].kw_only)
|
self.assertTrue(fields(B)[0].kw_only)
|
||||||
self.assertFalse(fields(B)[1].kw_only)
|
self.assertFalse(fields(B)[1].kw_only)
|
||||||
|
|
||||||
|
def test_deferred_annotations(self):
|
||||||
|
@dataclass
|
||||||
|
class A:
|
||||||
|
x: undefined
|
||||||
|
y: ClassVar[undefined]
|
||||||
|
|
||||||
|
fs = fields(A)
|
||||||
|
self.assertEqual(len(fs), 1)
|
||||||
|
self.assertEqual(fs[0].name, 'x')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -741,6 +741,26 @@ class TestUpdateWrapper(unittest.TestCase):
|
||||||
self.assertEqual(wrapper.__annotations__, {})
|
self.assertEqual(wrapper.__annotations__, {})
|
||||||
self.assertEqual(wrapper.__type_params__, ())
|
self.assertEqual(wrapper.__type_params__, ())
|
||||||
|
|
||||||
|
def test_update_wrapper_annotations(self):
|
||||||
|
def inner(x: int): pass
|
||||||
|
def wrapper(*args): pass
|
||||||
|
|
||||||
|
functools.update_wrapper(wrapper, inner)
|
||||||
|
self.assertEqual(wrapper.__annotations__, {'x': int})
|
||||||
|
self.assertIs(wrapper.__annotate__, inner.__annotate__)
|
||||||
|
|
||||||
|
def with_forward_ref(x: undefined): pass
|
||||||
|
def wrapper(*args): pass
|
||||||
|
|
||||||
|
functools.update_wrapper(wrapper, with_forward_ref)
|
||||||
|
|
||||||
|
self.assertIs(wrapper.__annotate__, with_forward_ref.__annotate__)
|
||||||
|
with self.assertRaises(NameError):
|
||||||
|
wrapper.__annotations__
|
||||||
|
|
||||||
|
undefined = str
|
||||||
|
self.assertEqual(wrapper.__annotations__, {'x': undefined})
|
||||||
|
|
||||||
|
|
||||||
class TestWraps(TestUpdateWrapper):
|
class TestWraps(TestUpdateWrapper):
|
||||||
|
|
||||||
|
@ -3059,6 +3079,27 @@ class TestSingleDispatch(unittest.TestCase):
|
||||||
self.assertEqual(f(""), "default")
|
self.assertEqual(f(""), "default")
|
||||||
self.assertEqual(f(b""), "default")
|
self.assertEqual(f(b""), "default")
|
||||||
|
|
||||||
|
def test_forward_reference(self):
|
||||||
|
@functools.singledispatch
|
||||||
|
def f(arg, arg2=None):
|
||||||
|
return "default"
|
||||||
|
|
||||||
|
@f.register
|
||||||
|
def _(arg: str, arg2: undefined = None):
|
||||||
|
return "forward reference"
|
||||||
|
|
||||||
|
self.assertEqual(f(1), "default")
|
||||||
|
self.assertEqual(f(""), "forward reference")
|
||||||
|
|
||||||
|
def test_unresolved_forward_reference(self):
|
||||||
|
@functools.singledispatch
|
||||||
|
def f(arg):
|
||||||
|
return "default"
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, "is an unresolved forward reference"):
|
||||||
|
@f.register
|
||||||
|
def _(arg: undefined):
|
||||||
|
return "forward reference"
|
||||||
|
|
||||||
class CachedCostItem:
|
class CachedCostItem:
|
||||||
_cost = 1
|
_cost = 1
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
from test.support import check_syntax_error
|
from test.support import check_syntax_error
|
||||||
from test.support import import_helper
|
from test.support import import_helper
|
||||||
|
import annotationlib
|
||||||
import inspect
|
import inspect
|
||||||
import unittest
|
import unittest
|
||||||
import sys
|
import sys
|
||||||
|
@ -459,7 +460,7 @@ class GrammarTests(unittest.TestCase):
|
||||||
gns = {}; lns = {}
|
gns = {}; lns = {}
|
||||||
exec("'docstring'\n"
|
exec("'docstring'\n"
|
||||||
"x: int = 5\n", gns, lns)
|
"x: int = 5\n", gns, lns)
|
||||||
self.assertEqual(lns["__annotate__"](1), {'x': int})
|
self.assertEqual(lns["__annotate__"](annotationlib.Format.VALUE), {'x': int})
|
||||||
with self.assertRaises(KeyError):
|
with self.assertRaises(KeyError):
|
||||||
gns['__annotate__']
|
gns['__annotate__']
|
||||||
|
|
||||||
|
|
|
@ -45,10 +45,7 @@ from test import support
|
||||||
|
|
||||||
from test.test_inspect import inspect_fodder as mod
|
from test.test_inspect import inspect_fodder as mod
|
||||||
from test.test_inspect import inspect_fodder2 as mod2
|
from test.test_inspect import inspect_fodder2 as mod2
|
||||||
from test.test_inspect import inspect_stock_annotations
|
|
||||||
from test.test_inspect import inspect_stringized_annotations
|
from test.test_inspect import inspect_stringized_annotations
|
||||||
from test.test_inspect import inspect_stringized_annotations_2
|
|
||||||
from test.test_inspect import inspect_stringized_annotations_pep695
|
|
||||||
|
|
||||||
|
|
||||||
# Functions tested in this suite:
|
# Functions tested in this suite:
|
||||||
|
@ -126,7 +123,7 @@ class IsTestBase(unittest.TestCase):
|
||||||
self.assertFalse(other(obj), 'not %s(%s)' % (other.__name__, exp))
|
self.assertFalse(other(obj), 'not %s(%s)' % (other.__name__, exp))
|
||||||
|
|
||||||
def test__all__(self):
|
def test__all__(self):
|
||||||
support.check__all__(self, inspect, not_exported=("modulesbyfile",))
|
support.check__all__(self, inspect, not_exported=("modulesbyfile",), extra=("get_annotations",))
|
||||||
|
|
||||||
def generator_function_example(self):
|
def generator_function_example(self):
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
|
@ -1595,216 +1592,6 @@ class TestClassesAndFunctions(unittest.TestCase):
|
||||||
attrs = [a[0] for a in inspect.getmembers(C)]
|
attrs = [a[0] for a in inspect.getmembers(C)]
|
||||||
self.assertNotIn('missing', attrs)
|
self.assertNotIn('missing', attrs)
|
||||||
|
|
||||||
def test_get_annotations_with_stock_annotations(self):
|
|
||||||
def foo(a:int, b:str): pass
|
|
||||||
self.assertEqual(inspect.get_annotations(foo), {'a': int, 'b': str})
|
|
||||||
|
|
||||||
foo.__annotations__ = {'a': 'foo', 'b':'str'}
|
|
||||||
self.assertEqual(inspect.get_annotations(foo), {'a': 'foo', 'b': 'str'})
|
|
||||||
|
|
||||||
self.assertEqual(inspect.get_annotations(foo, eval_str=True, locals=locals()), {'a': foo, 'b': str})
|
|
||||||
self.assertEqual(inspect.get_annotations(foo, eval_str=True, globals=locals()), {'a': foo, 'b': str})
|
|
||||||
|
|
||||||
isa = inspect_stock_annotations
|
|
||||||
self.assertEqual(inspect.get_annotations(isa), {'a': int, 'b': str})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.MyClass), {'a': int, 'b': str})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function), {'a': int, 'b': str, 'return': isa.MyClass})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function2), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function3), {'a': 'int', 'b': 'str', 'c': 'MyClass'})
|
|
||||||
self.assertEqual(inspect.get_annotations(inspect), {}) # inspect module has no annotations
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.UnannotatedClass), {})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.unannotated_function), {})
|
|
||||||
|
|
||||||
self.assertEqual(inspect.get_annotations(isa, eval_str=True), {'a': int, 'b': str})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=True), {'a': int, 'b': str})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function2, eval_str=True), {'a': int, 'b': str, 'c': isa.MyClass, 'return': isa.MyClass})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function3, eval_str=True), {'a': int, 'b': str, 'c': isa.MyClass})
|
|
||||||
self.assertEqual(inspect.get_annotations(inspect, eval_str=True), {})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=True), {})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=True), {})
|
|
||||||
|
|
||||||
self.assertEqual(inspect.get_annotations(isa, eval_str=False), {'a': int, 'b': str})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=False), {'a': int, 'b': str})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function2, eval_str=False), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function3, eval_str=False), {'a': 'int', 'b': 'str', 'c': 'MyClass'})
|
|
||||||
self.assertEqual(inspect.get_annotations(inspect, eval_str=False), {})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=False), {})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=False), {})
|
|
||||||
|
|
||||||
def times_three(fn):
|
|
||||||
@functools.wraps(fn)
|
|
||||||
def wrapper(a, b):
|
|
||||||
return fn(a*3, b*3)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
wrapped = times_three(isa.function)
|
|
||||||
self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx'))
|
|
||||||
self.assertIsNot(wrapped.__globals__, isa.function.__globals__)
|
|
||||||
self.assertEqual(inspect.get_annotations(wrapped), {'a': int, 'b': str, 'return': isa.MyClass})
|
|
||||||
self.assertEqual(inspect.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass})
|
|
||||||
self.assertEqual(inspect.get_annotations(wrapped, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass})
|
|
||||||
|
|
||||||
def test_get_annotations_with_stringized_annotations(self):
|
|
||||||
isa = inspect_stringized_annotations
|
|
||||||
self.assertEqual(inspect.get_annotations(isa), {'a': 'int', 'b': 'str'})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.MyClass), {'a': 'int', 'b': 'str'})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function), {'a': 'int', 'b': 'str', 'return': 'MyClass'})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function2), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function3), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.UnannotatedClass), {})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.unannotated_function), {})
|
|
||||||
|
|
||||||
self.assertEqual(inspect.get_annotations(isa, eval_str=True), {'a': int, 'b': str})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=True), {'a': int, 'b': str})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function2, eval_str=True), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function3, eval_str=True), {'a': 'int', 'b': 'str', 'c': 'MyClass'})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=True), {})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=True), {})
|
|
||||||
|
|
||||||
self.assertEqual(inspect.get_annotations(isa, eval_str=False), {'a': 'int', 'b': 'str'})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=False), {'a': 'int', 'b': 'str'})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function2, eval_str=False), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.function3, eval_str=False), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=False), {})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=False), {})
|
|
||||||
|
|
||||||
isa2 = inspect_stringized_annotations_2
|
|
||||||
self.assertEqual(inspect.get_annotations(isa2), {})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa2, eval_str=True), {})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa2, eval_str=False), {})
|
|
||||||
|
|
||||||
def times_three(fn):
|
|
||||||
@functools.wraps(fn)
|
|
||||||
def wrapper(a, b):
|
|
||||||
return fn(a*3, b*3)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
wrapped = times_three(isa.function)
|
|
||||||
self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx'))
|
|
||||||
self.assertIsNot(wrapped.__globals__, isa.function.__globals__)
|
|
||||||
self.assertEqual(inspect.get_annotations(wrapped), {'a': 'int', 'b': 'str', 'return': 'MyClass'})
|
|
||||||
self.assertEqual(inspect.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass})
|
|
||||||
self.assertEqual(inspect.get_annotations(wrapped, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'})
|
|
||||||
|
|
||||||
# test that local namespace lookups work
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations), {'x': 'mytype'})
|
|
||||||
self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int})
|
|
||||||
|
|
||||||
def test_pep695_generic_class_with_future_annotations(self):
|
|
||||||
ann_module695 = inspect_stringized_annotations_pep695
|
|
||||||
A_annotations = inspect.get_annotations(ann_module695.A, eval_str=True)
|
|
||||||
A_type_params = ann_module695.A.__type_params__
|
|
||||||
self.assertIs(A_annotations["x"], A_type_params[0])
|
|
||||||
self.assertEqual(A_annotations["y"].__args__[0], Unpack[A_type_params[1]])
|
|
||||||
self.assertIs(A_annotations["z"].__args__[0], A_type_params[2])
|
|
||||||
|
|
||||||
def test_pep695_generic_class_with_future_annotations_and_local_shadowing(self):
|
|
||||||
B_annotations = inspect.get_annotations(
|
|
||||||
inspect_stringized_annotations_pep695.B, eval_str=True
|
|
||||||
)
|
|
||||||
self.assertEqual(B_annotations, {"x": int, "y": str, "z": bytes})
|
|
||||||
|
|
||||||
def test_pep695_generic_class_with_future_annotations_name_clash_with_global_vars(self):
|
|
||||||
ann_module695 = inspect_stringized_annotations_pep695
|
|
||||||
C_annotations = inspect.get_annotations(ann_module695.C, eval_str=True)
|
|
||||||
self.assertEqual(
|
|
||||||
set(C_annotations.values()),
|
|
||||||
set(ann_module695.C.__type_params__)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pep_695_generic_function_with_future_annotations(self):
|
|
||||||
ann_module695 = inspect_stringized_annotations_pep695
|
|
||||||
generic_func_annotations = inspect.get_annotations(
|
|
||||||
ann_module695.generic_function, eval_str=True
|
|
||||||
)
|
|
||||||
func_t_params = ann_module695.generic_function.__type_params__
|
|
||||||
self.assertEqual(
|
|
||||||
generic_func_annotations.keys(), {"x", "y", "z", "zz", "return"}
|
|
||||||
)
|
|
||||||
self.assertIs(generic_func_annotations["x"], func_t_params[0])
|
|
||||||
self.assertEqual(generic_func_annotations["y"], Unpack[func_t_params[1]])
|
|
||||||
self.assertIs(generic_func_annotations["z"].__origin__, func_t_params[2])
|
|
||||||
self.assertIs(generic_func_annotations["zz"].__origin__, func_t_params[2])
|
|
||||||
|
|
||||||
def test_pep_695_generic_function_with_future_annotations_name_clash_with_global_vars(self):
|
|
||||||
self.assertEqual(
|
|
||||||
set(
|
|
||||||
inspect.get_annotations(
|
|
||||||
inspect_stringized_annotations_pep695.generic_function_2,
|
|
||||||
eval_str=True
|
|
||||||
).values()
|
|
||||||
),
|
|
||||||
set(
|
|
||||||
inspect_stringized_annotations_pep695.generic_function_2.__type_params__
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pep_695_generic_method_with_future_annotations(self):
|
|
||||||
ann_module695 = inspect_stringized_annotations_pep695
|
|
||||||
generic_method_annotations = inspect.get_annotations(
|
|
||||||
ann_module695.D.generic_method, eval_str=True
|
|
||||||
)
|
|
||||||
params = {
|
|
||||||
param.__name__: param
|
|
||||||
for param in ann_module695.D.generic_method.__type_params__
|
|
||||||
}
|
|
||||||
self.assertEqual(
|
|
||||||
generic_method_annotations,
|
|
||||||
{"x": params["Foo"], "y": params["Bar"], "return": None}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_vars(self):
|
|
||||||
self.assertEqual(
|
|
||||||
set(
|
|
||||||
inspect.get_annotations(
|
|
||||||
inspect_stringized_annotations_pep695.D.generic_method_2,
|
|
||||||
eval_str=True
|
|
||||||
).values()
|
|
||||||
),
|
|
||||||
set(
|
|
||||||
inspect_stringized_annotations_pep695.D.generic_method_2.__type_params__
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_and_local_vars(self):
|
|
||||||
self.assertEqual(
|
|
||||||
inspect.get_annotations(
|
|
||||||
inspect_stringized_annotations_pep695.E, eval_str=True
|
|
||||||
),
|
|
||||||
{"x": str},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pep_695_generics_with_future_annotations_nested_in_function(self):
|
|
||||||
results = inspect_stringized_annotations_pep695.nested()
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
set(results.F_annotations.values()),
|
|
||||||
set(results.F.__type_params__)
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
set(results.F_meth_annotations.values()),
|
|
||||||
set(results.F.generic_method.__type_params__)
|
|
||||||
)
|
|
||||||
self.assertNotEqual(
|
|
||||||
set(results.F_meth_annotations.values()),
|
|
||||||
set(results.F.__type_params__)
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
set(results.F_meth_annotations.values()).intersection(results.F.__type_params__),
|
|
||||||
set()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(results.G_annotations, {"x": str})
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
set(results.generic_func_annotations.values()),
|
|
||||||
set(results.generic_func.__type_params__)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestFormatAnnotation(unittest.TestCase):
|
class TestFormatAnnotation(unittest.TestCase):
|
||||||
def test_typing_replacement(self):
|
def test_typing_replacement(self):
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
|
import annotationlib
|
||||||
import textwrap
|
import textwrap
|
||||||
import types
|
import types
|
||||||
import unittest
|
import unittest
|
||||||
from test.support import run_code, check_syntax_error
|
from test.support import run_code, check_syntax_error
|
||||||
|
|
||||||
VALUE = 1
|
|
||||||
FORWARDREF = 2
|
|
||||||
SOURCE = 3
|
|
||||||
|
|
||||||
|
|
||||||
class TypeAnnotationTests(unittest.TestCase):
|
class TypeAnnotationTests(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -376,12 +373,12 @@ class DeferredEvaluationTests(unittest.TestCase):
|
||||||
self.assertIsInstance(annotate, types.FunctionType)
|
self.assertIsInstance(annotate, types.FunctionType)
|
||||||
self.assertEqual(annotate.__name__, "__annotate__")
|
self.assertEqual(annotate.__name__, "__annotate__")
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
annotate(FORWARDREF)
|
annotate(annotationlib.Format.FORWARDREF)
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
annotate(SOURCE)
|
annotate(annotationlib.Format.SOURCE)
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
annotate(None)
|
annotate(None)
|
||||||
self.assertEqual(annotate(VALUE), {"x": int})
|
self.assertEqual(annotate(annotationlib.Format.VALUE), {"x": int})
|
||||||
|
|
||||||
def test_comprehension_in_annotation(self):
|
def test_comprehension_in_annotation(self):
|
||||||
# This crashed in an earlier version of the code
|
# This crashed in an earlier version of the code
|
||||||
|
@ -398,7 +395,7 @@ class DeferredEvaluationTests(unittest.TestCase):
|
||||||
f = ns["f"]
|
f = ns["f"]
|
||||||
self.assertIsInstance(f.__annotate__, types.FunctionType)
|
self.assertIsInstance(f.__annotate__, types.FunctionType)
|
||||||
annos = {"x": "int", "return": "int"}
|
annos = {"x": "int", "return": "int"}
|
||||||
self.assertEqual(f.__annotate__(VALUE), annos)
|
self.assertEqual(f.__annotate__(annotationlib.Format.VALUE), annos)
|
||||||
self.assertEqual(f.__annotations__, annos)
|
self.assertEqual(f.__annotations__, annos)
|
||||||
|
|
||||||
def test_name_clash_with_format(self):
|
def test_name_clash_with_format(self):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import annotationlib
|
||||||
import contextlib
|
import contextlib
|
||||||
import collections
|
import collections
|
||||||
import collections.abc
|
import collections.abc
|
||||||
|
@ -45,7 +46,7 @@ import typing
|
||||||
import weakref
|
import weakref
|
||||||
import types
|
import types
|
||||||
|
|
||||||
from test.support import captured_stderr, cpython_only, infinite_recursion, requires_docstrings, import_helper
|
from test.support import captured_stderr, cpython_only, infinite_recursion, requires_docstrings, import_helper, run_code
|
||||||
from test.typinganndata import ann_module695, mod_generics_cache, _typed_dict_helper
|
from test.typinganndata import ann_module695, mod_generics_cache, _typed_dict_helper
|
||||||
|
|
||||||
|
|
||||||
|
@ -7812,6 +7813,48 @@ class NamedTupleTests(BaseTestCase):
|
||||||
def _source(self):
|
def _source(self):
|
||||||
return 'no chance for this as well'
|
return 'no chance for this as well'
|
||||||
|
|
||||||
|
def test_annotation_type_check(self):
|
||||||
|
# These are rejected by _type_check
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
class X(NamedTuple):
|
||||||
|
a: Final
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
class Y(NamedTuple):
|
||||||
|
a: (1, 2)
|
||||||
|
|
||||||
|
# Conversion by _type_convert
|
||||||
|
class Z(NamedTuple):
|
||||||
|
a: None
|
||||||
|
b: "str"
|
||||||
|
annos = {'a': type(None), 'b': ForwardRef("str")}
|
||||||
|
self.assertEqual(Z.__annotations__, annos)
|
||||||
|
self.assertEqual(Z.__annotate__(annotationlib.Format.VALUE), annos)
|
||||||
|
self.assertEqual(Z.__annotate__(annotationlib.Format.FORWARDREF), annos)
|
||||||
|
self.assertEqual(Z.__annotate__(annotationlib.Format.SOURCE), {"a": "None", "b": "str"})
|
||||||
|
|
||||||
|
def test_future_annotations(self):
|
||||||
|
code = """
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import NamedTuple
|
||||||
|
class X(NamedTuple):
|
||||||
|
a: int
|
||||||
|
b: None
|
||||||
|
"""
|
||||||
|
ns = run_code(textwrap.dedent(code))
|
||||||
|
X = ns['X']
|
||||||
|
self.assertEqual(X.__annotations__, {'a': ForwardRef("int"), 'b': ForwardRef("None")})
|
||||||
|
|
||||||
|
def test_deferred_annotations(self):
|
||||||
|
class X(NamedTuple):
|
||||||
|
y: undefined
|
||||||
|
|
||||||
|
self.assertEqual(X._fields, ('y',))
|
||||||
|
with self.assertRaises(NameError):
|
||||||
|
X.__annotations__
|
||||||
|
|
||||||
|
undefined = int
|
||||||
|
self.assertEqual(X.__annotations__, {'y': int})
|
||||||
|
|
||||||
def test_multiple_inheritance(self):
|
def test_multiple_inheritance(self):
|
||||||
class A:
|
class A:
|
||||||
pass
|
pass
|
||||||
|
@ -8126,7 +8169,11 @@ class TypedDictTests(BaseTestCase):
|
||||||
self.assertEqual(Emp.__name__, 'Emp')
|
self.assertEqual(Emp.__name__, 'Emp')
|
||||||
self.assertEqual(Emp.__module__, __name__)
|
self.assertEqual(Emp.__module__, __name__)
|
||||||
self.assertEqual(Emp.__bases__, (dict,))
|
self.assertEqual(Emp.__bases__, (dict,))
|
||||||
self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
|
annos = {'name': str, 'id': int}
|
||||||
|
self.assertEqual(Emp.__annotations__, annos)
|
||||||
|
self.assertEqual(Emp.__annotate__(annotationlib.Format.VALUE), annos)
|
||||||
|
self.assertEqual(Emp.__annotate__(annotationlib.Format.FORWARDREF), annos)
|
||||||
|
self.assertEqual(Emp.__annotate__(annotationlib.Format.SOURCE), {'name': 'str', 'id': 'int'})
|
||||||
self.assertEqual(Emp.__total__, True)
|
self.assertEqual(Emp.__total__, True)
|
||||||
self.assertEqual(Emp.__required_keys__, {'name', 'id'})
|
self.assertEqual(Emp.__required_keys__, {'name', 'id'})
|
||||||
self.assertIsInstance(Emp.__required_keys__, frozenset)
|
self.assertIsInstance(Emp.__required_keys__, frozenset)
|
||||||
|
@ -8487,6 +8534,8 @@ class TypedDictTests(BaseTestCase):
|
||||||
self.assertEqual(A.__bases__, (Generic, dict))
|
self.assertEqual(A.__bases__, (Generic, dict))
|
||||||
self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T]))
|
self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T]))
|
||||||
self.assertEqual(A.__mro__, (A, Generic, dict, object))
|
self.assertEqual(A.__mro__, (A, Generic, dict, object))
|
||||||
|
self.assertEqual(A.__annotations__, {'a': T})
|
||||||
|
self.assertEqual(A.__annotate__(annotationlib.Format.SOURCE), {'a': 'T'})
|
||||||
self.assertEqual(A.__parameters__, (T,))
|
self.assertEqual(A.__parameters__, (T,))
|
||||||
self.assertEqual(A[str].__parameters__, ())
|
self.assertEqual(A[str].__parameters__, ())
|
||||||
self.assertEqual(A[str].__args__, (str,))
|
self.assertEqual(A[str].__args__, (str,))
|
||||||
|
@ -8498,6 +8547,8 @@ class TypedDictTests(BaseTestCase):
|
||||||
self.assertEqual(A.__bases__, (Generic, dict))
|
self.assertEqual(A.__bases__, (Generic, dict))
|
||||||
self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T]))
|
self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T]))
|
||||||
self.assertEqual(A.__mro__, (A, Generic, dict, object))
|
self.assertEqual(A.__mro__, (A, Generic, dict, object))
|
||||||
|
self.assertEqual(A.__annotations__, {'a': T})
|
||||||
|
self.assertEqual(A.__annotate__(annotationlib.Format.SOURCE), {'a': 'T'})
|
||||||
self.assertEqual(A.__parameters__, (T,))
|
self.assertEqual(A.__parameters__, (T,))
|
||||||
self.assertEqual(A[str].__parameters__, ())
|
self.assertEqual(A[str].__parameters__, ())
|
||||||
self.assertEqual(A[str].__args__, (str,))
|
self.assertEqual(A[str].__args__, (str,))
|
||||||
|
@ -8508,6 +8559,8 @@ class TypedDictTests(BaseTestCase):
|
||||||
self.assertEqual(A2.__bases__, (Generic, dict))
|
self.assertEqual(A2.__bases__, (Generic, dict))
|
||||||
self.assertEqual(A2.__orig_bases__, (Generic[T], TypedDict))
|
self.assertEqual(A2.__orig_bases__, (Generic[T], TypedDict))
|
||||||
self.assertEqual(A2.__mro__, (A2, Generic, dict, object))
|
self.assertEqual(A2.__mro__, (A2, Generic, dict, object))
|
||||||
|
self.assertEqual(A2.__annotations__, {'a': T})
|
||||||
|
self.assertEqual(A2.__annotate__(annotationlib.Format.SOURCE), {'a': 'T'})
|
||||||
self.assertEqual(A2.__parameters__, (T,))
|
self.assertEqual(A2.__parameters__, (T,))
|
||||||
self.assertEqual(A2[str].__parameters__, ())
|
self.assertEqual(A2[str].__parameters__, ())
|
||||||
self.assertEqual(A2[str].__args__, (str,))
|
self.assertEqual(A2[str].__args__, (str,))
|
||||||
|
@ -8518,6 +8571,8 @@ class TypedDictTests(BaseTestCase):
|
||||||
self.assertEqual(B.__bases__, (Generic, dict))
|
self.assertEqual(B.__bases__, (Generic, dict))
|
||||||
self.assertEqual(B.__orig_bases__, (A[KT],))
|
self.assertEqual(B.__orig_bases__, (A[KT],))
|
||||||
self.assertEqual(B.__mro__, (B, Generic, dict, object))
|
self.assertEqual(B.__mro__, (B, Generic, dict, object))
|
||||||
|
self.assertEqual(B.__annotations__, {'a': T, 'b': KT})
|
||||||
|
self.assertEqual(B.__annotate__(annotationlib.Format.SOURCE), {'a': 'T', 'b': 'KT'})
|
||||||
self.assertEqual(B.__parameters__, (KT,))
|
self.assertEqual(B.__parameters__, (KT,))
|
||||||
self.assertEqual(B.__total__, False)
|
self.assertEqual(B.__total__, False)
|
||||||
self.assertEqual(B.__optional_keys__, frozenset(['b']))
|
self.assertEqual(B.__optional_keys__, frozenset(['b']))
|
||||||
|
@ -8542,6 +8597,11 @@ class TypedDictTests(BaseTestCase):
|
||||||
'b': KT,
|
'b': KT,
|
||||||
'c': int,
|
'c': int,
|
||||||
})
|
})
|
||||||
|
self.assertEqual(C.__annotate__(annotationlib.Format.SOURCE), {
|
||||||
|
'a': 'T',
|
||||||
|
'b': 'KT',
|
||||||
|
'c': 'int',
|
||||||
|
})
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
C[str]
|
C[str]
|
||||||
|
|
||||||
|
@ -8561,6 +8621,11 @@ class TypedDictTests(BaseTestCase):
|
||||||
'b': T,
|
'b': T,
|
||||||
'c': KT,
|
'c': KT,
|
||||||
})
|
})
|
||||||
|
self.assertEqual(Point3D.__annotate__(annotationlib.Format.SOURCE), {
|
||||||
|
'a': 'T',
|
||||||
|
'b': 'T',
|
||||||
|
'c': 'KT',
|
||||||
|
})
|
||||||
self.assertEqual(Point3D[int, str].__origin__, Point3D)
|
self.assertEqual(Point3D[int, str].__origin__, Point3D)
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
|
@ -8592,6 +8657,11 @@ class TypedDictTests(BaseTestCase):
|
||||||
'b': KT,
|
'b': KT,
|
||||||
'c': int,
|
'c': int,
|
||||||
})
|
})
|
||||||
|
self.assertEqual(WithImplicitAny.__annotate__(annotationlib.Format.SOURCE), {
|
||||||
|
'a': 'T',
|
||||||
|
'b': 'KT',
|
||||||
|
'c': 'int',
|
||||||
|
})
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
WithImplicitAny[str]
|
WithImplicitAny[str]
|
||||||
|
|
||||||
|
@ -8748,6 +8818,54 @@ class TypedDictTests(BaseTestCase):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_annotations(self):
|
||||||
|
# _type_check is applied
|
||||||
|
with self.assertRaisesRegex(TypeError, "Plain typing.Final is not valid as type argument"):
|
||||||
|
class X(TypedDict):
|
||||||
|
a: Final
|
||||||
|
|
||||||
|
# _type_convert is applied
|
||||||
|
class Y(TypedDict):
|
||||||
|
a: None
|
||||||
|
b: "int"
|
||||||
|
fwdref = ForwardRef('int', module='test.test_typing')
|
||||||
|
self.assertEqual(Y.__annotations__, {'a': type(None), 'b': fwdref})
|
||||||
|
self.assertEqual(Y.__annotate__(annotationlib.Format.FORWARDREF), {'a': type(None), 'b': fwdref})
|
||||||
|
|
||||||
|
# _type_check is also applied later
|
||||||
|
class Z(TypedDict):
|
||||||
|
a: undefined
|
||||||
|
|
||||||
|
with self.assertRaises(NameError):
|
||||||
|
Z.__annotations__
|
||||||
|
|
||||||
|
undefined = Final
|
||||||
|
with self.assertRaisesRegex(TypeError, "Plain typing.Final is not valid as type argument"):
|
||||||
|
Z.__annotations__
|
||||||
|
|
||||||
|
undefined = None
|
||||||
|
self.assertEqual(Z.__annotations__, {'a': type(None)})
|
||||||
|
|
||||||
|
def test_deferred_evaluation(self):
|
||||||
|
class A(TypedDict):
|
||||||
|
x: NotRequired[undefined]
|
||||||
|
y: ReadOnly[undefined]
|
||||||
|
z: Required[undefined]
|
||||||
|
|
||||||
|
self.assertEqual(A.__required_keys__, frozenset({'y', 'z'}))
|
||||||
|
self.assertEqual(A.__optional_keys__, frozenset({'x'}))
|
||||||
|
self.assertEqual(A.__readonly_keys__, frozenset({'y'}))
|
||||||
|
self.assertEqual(A.__mutable_keys__, frozenset({'x', 'z'}))
|
||||||
|
|
||||||
|
with self.assertRaises(NameError):
|
||||||
|
A.__annotations__
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
A.__annotate__(annotationlib.Format.SOURCE),
|
||||||
|
{'x': 'NotRequired[undefined]', 'y': 'ReadOnly[undefined]',
|
||||||
|
'z': 'Required[undefined]'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RequiredTests(BaseTestCase):
|
class RequiredTests(BaseTestCase):
|
||||||
|
|
||||||
|
@ -10075,7 +10193,6 @@ class SpecialAttrsTests(BaseTestCase):
|
||||||
typing.ClassVar: 'ClassVar',
|
typing.ClassVar: 'ClassVar',
|
||||||
typing.Concatenate: 'Concatenate',
|
typing.Concatenate: 'Concatenate',
|
||||||
typing.Final: 'Final',
|
typing.Final: 'Final',
|
||||||
typing.ForwardRef: 'ForwardRef',
|
|
||||||
typing.Literal: 'Literal',
|
typing.Literal: 'Literal',
|
||||||
typing.NewType: 'NewType',
|
typing.NewType: 'NewType',
|
||||||
typing.NoReturn: 'NoReturn',
|
typing.NoReturn: 'NoReturn',
|
||||||
|
@ -10087,7 +10204,7 @@ class SpecialAttrsTests(BaseTestCase):
|
||||||
typing.TypeVar: 'TypeVar',
|
typing.TypeVar: 'TypeVar',
|
||||||
typing.Union: 'Union',
|
typing.Union: 'Union',
|
||||||
typing.Self: 'Self',
|
typing.Self: 'Self',
|
||||||
# Subscribed special forms
|
# Subscripted special forms
|
||||||
typing.Annotated[Any, "Annotation"]: 'Annotated',
|
typing.Annotated[Any, "Annotation"]: 'Annotated',
|
||||||
typing.Annotated[int, 'Annotation']: 'Annotated',
|
typing.Annotated[int, 'Annotation']: 'Annotated',
|
||||||
typing.ClassVar[Any]: 'ClassVar',
|
typing.ClassVar[Any]: 'ClassVar',
|
||||||
|
@ -10102,7 +10219,6 @@ class SpecialAttrsTests(BaseTestCase):
|
||||||
typing.Union[Any]: 'Any',
|
typing.Union[Any]: 'Any',
|
||||||
typing.Union[int, float]: 'Union',
|
typing.Union[int, float]: 'Union',
|
||||||
# Incompatible special forms (tested in test_special_attrs2)
|
# Incompatible special forms (tested in test_special_attrs2)
|
||||||
# - typing.ForwardRef('set[Any]')
|
|
||||||
# - typing.NewType('TypeName', Any)
|
# - typing.NewType('TypeName', Any)
|
||||||
# - typing.ParamSpec('SpecialAttrsP')
|
# - typing.ParamSpec('SpecialAttrsP')
|
||||||
# - typing.TypeVar('T')
|
# - typing.TypeVar('T')
|
||||||
|
@ -10121,18 +10237,6 @@ class SpecialAttrsTests(BaseTestCase):
|
||||||
TypeName = typing.NewType('SpecialAttrsTests.TypeName', Any)
|
TypeName = typing.NewType('SpecialAttrsTests.TypeName', Any)
|
||||||
|
|
||||||
def test_special_attrs2(self):
|
def test_special_attrs2(self):
|
||||||
# Forward refs provide a different introspection API. __name__ and
|
|
||||||
# __qualname__ make little sense for forward refs as they can store
|
|
||||||
# complex typing expressions.
|
|
||||||
fr = typing.ForwardRef('set[Any]')
|
|
||||||
self.assertFalse(hasattr(fr, '__name__'))
|
|
||||||
self.assertFalse(hasattr(fr, '__qualname__'))
|
|
||||||
self.assertEqual(fr.__module__, 'typing')
|
|
||||||
# Forward refs are currently unpicklable.
|
|
||||||
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
|
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
pickle.dumps(fr, proto)
|
|
||||||
|
|
||||||
self.assertEqual(SpecialAttrsTests.TypeName.__name__, 'TypeName')
|
self.assertEqual(SpecialAttrsTests.TypeName.__name__, 'TypeName')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
SpecialAttrsTests.TypeName.__qualname__,
|
SpecialAttrsTests.TypeName.__qualname__,
|
||||||
|
|
332
Lib/typing.py
332
Lib/typing.py
|
@ -19,6 +19,8 @@ that may be changed without notice. Use at your own risk!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from abc import abstractmethod, ABCMeta
|
from abc import abstractmethod, ABCMeta
|
||||||
|
import annotationlib
|
||||||
|
from annotationlib import ForwardRef
|
||||||
import collections
|
import collections
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import collections.abc
|
import collections.abc
|
||||||
|
@ -125,6 +127,7 @@ __all__ = [
|
||||||
'cast',
|
'cast',
|
||||||
'clear_overloads',
|
'clear_overloads',
|
||||||
'dataclass_transform',
|
'dataclass_transform',
|
||||||
|
'evaluate_forward_ref',
|
||||||
'final',
|
'final',
|
||||||
'get_args',
|
'get_args',
|
||||||
'get_origin',
|
'get_origin',
|
||||||
|
@ -165,7 +168,7 @@ def _type_convert(arg, module=None, *, allow_special_forms=False):
|
||||||
if arg is None:
|
if arg is None:
|
||||||
return type(None)
|
return type(None)
|
||||||
if isinstance(arg, str):
|
if isinstance(arg, str):
|
||||||
return ForwardRef(arg, module=module, is_class=allow_special_forms)
|
return _make_forward_ref(arg, module=module, is_class=allow_special_forms)
|
||||||
return arg
|
return arg
|
||||||
|
|
||||||
|
|
||||||
|
@ -459,7 +462,8 @@ class _Sentinel:
|
||||||
_sentinel = _Sentinel()
|
_sentinel = _Sentinel()
|
||||||
|
|
||||||
|
|
||||||
def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=frozenset()):
|
def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=frozenset(),
|
||||||
|
format=annotationlib.Format.VALUE, owner=None):
|
||||||
"""Evaluate all forward references in the given type t.
|
"""Evaluate all forward references in the given type t.
|
||||||
|
|
||||||
For use of globalns and localns see the docstring for get_type_hints().
|
For use of globalns and localns see the docstring for get_type_hints().
|
||||||
|
@ -470,11 +474,13 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f
|
||||||
_deprecation_warning_for_no_type_params_passed("typing._eval_type")
|
_deprecation_warning_for_no_type_params_passed("typing._eval_type")
|
||||||
type_params = ()
|
type_params = ()
|
||||||
if isinstance(t, ForwardRef):
|
if isinstance(t, ForwardRef):
|
||||||
return t._evaluate(globalns, localns, type_params, recursive_guard=recursive_guard)
|
return evaluate_forward_ref(t, globals=globalns, locals=localns,
|
||||||
|
type_params=type_params, owner=owner,
|
||||||
|
_recursive_guard=recursive_guard, format=format)
|
||||||
if isinstance(t, (_GenericAlias, GenericAlias, types.UnionType)):
|
if isinstance(t, (_GenericAlias, GenericAlias, types.UnionType)):
|
||||||
if isinstance(t, GenericAlias):
|
if isinstance(t, GenericAlias):
|
||||||
args = tuple(
|
args = tuple(
|
||||||
ForwardRef(arg) if isinstance(arg, str) else arg
|
_make_forward_ref(arg) if isinstance(arg, str) else arg
|
||||||
for arg in t.__args__
|
for arg in t.__args__
|
||||||
)
|
)
|
||||||
is_unpacked = t.__unpacked__
|
is_unpacked = t.__unpacked__
|
||||||
|
@ -487,7 +493,8 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f
|
||||||
|
|
||||||
ev_args = tuple(
|
ev_args = tuple(
|
||||||
_eval_type(
|
_eval_type(
|
||||||
a, globalns, localns, type_params, recursive_guard=recursive_guard
|
a, globalns, localns, type_params, recursive_guard=recursive_guard,
|
||||||
|
format=format, owner=owner,
|
||||||
)
|
)
|
||||||
for a in t.__args__
|
for a in t.__args__
|
||||||
)
|
)
|
||||||
|
@ -1011,111 +1018,77 @@ def TypeIs(self, parameters):
|
||||||
return _GenericAlias(self, (item,))
|
return _GenericAlias(self, (item,))
|
||||||
|
|
||||||
|
|
||||||
class ForwardRef(_Final, _root=True):
|
def _make_forward_ref(code, **kwargs):
|
||||||
"""Internal wrapper to hold a forward reference."""
|
forward_ref = ForwardRef(code, **kwargs)
|
||||||
|
# For compatibility, eagerly compile the forwardref's code.
|
||||||
|
forward_ref.__forward_code__
|
||||||
|
return forward_ref
|
||||||
|
|
||||||
__slots__ = ('__forward_arg__', '__forward_code__',
|
|
||||||
'__forward_evaluated__', '__forward_value__',
|
|
||||||
'__forward_is_argument__', '__forward_is_class__',
|
|
||||||
'__forward_module__')
|
|
||||||
|
|
||||||
def __init__(self, arg, is_argument=True, module=None, *, is_class=False):
|
def evaluate_forward_ref(
|
||||||
if not isinstance(arg, str):
|
forward_ref,
|
||||||
raise TypeError(f"Forward reference must be a string -- got {arg!r}")
|
*,
|
||||||
|
owner=None,
|
||||||
|
globals=None,
|
||||||
|
locals=None,
|
||||||
|
type_params=None,
|
||||||
|
format=annotationlib.Format.VALUE,
|
||||||
|
_recursive_guard=frozenset(),
|
||||||
|
):
|
||||||
|
"""Evaluate a forward reference as a type hint.
|
||||||
|
|
||||||
# If we do `def f(*args: *Ts)`, then we'll have `arg = '*Ts'`.
|
This is similar to calling the ForwardRef.evaluate() method,
|
||||||
# Unfortunately, this isn't a valid expression on its own, so we
|
but unlike that method, evaluate_forward_ref() also:
|
||||||
# do the unpacking manually.
|
|
||||||
if arg.startswith('*'):
|
* Recursively evaluates forward references nested within the type hint.
|
||||||
arg_to_compile = f'({arg},)[0]' # E.g. (*Ts,)[0] or (*tuple[int, int],)[0]
|
* Rejects certain objects that are not valid type hints.
|
||||||
|
* Replaces type hints that evaluate to None with types.NoneType.
|
||||||
|
* Supports the *FORWARDREF* and *SOURCE* formats.
|
||||||
|
|
||||||
|
*forward_ref* must be an instance of ForwardRef. *owner*, if given,
|
||||||
|
should be the object that holds the annotations that the forward reference
|
||||||
|
derived from, such as a module, class object, or function. It is used to
|
||||||
|
infer the namespaces to use for looking up names. *globals* and *locals*
|
||||||
|
can also be explicitly given to provide the global and local namespaces.
|
||||||
|
*type_params* is a tuple of type parameters that are in scope when
|
||||||
|
evaluating the forward reference. This parameter must be provided (though
|
||||||
|
it may be an empty tuple) if *owner* is not given and the forward reference
|
||||||
|
does not already have an owner set. *format* specifies the format of the
|
||||||
|
annotation and is a member of the annoations.Format enum.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if type_params is _sentinel:
|
||||||
|
_deprecation_warning_for_no_type_params_passed("typing.evaluate_forward_ref")
|
||||||
|
type_params = ()
|
||||||
|
if format == annotationlib.Format.SOURCE:
|
||||||
|
return forward_ref.__forward_arg__
|
||||||
|
if forward_ref.__forward_arg__ in _recursive_guard:
|
||||||
|
return forward_ref
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = forward_ref.evaluate(globals=globals, locals=locals,
|
||||||
|
type_params=type_params, owner=owner)
|
||||||
|
except NameError:
|
||||||
|
if format == annotationlib.Format.FORWARDREF:
|
||||||
|
return forward_ref
|
||||||
else:
|
else:
|
||||||
arg_to_compile = arg
|
raise
|
||||||
try:
|
|
||||||
code = compile(arg_to_compile, '<string>', 'eval')
|
|
||||||
except SyntaxError:
|
|
||||||
raise SyntaxError(f"Forward reference must be an expression -- got {arg!r}")
|
|
||||||
|
|
||||||
self.__forward_arg__ = arg
|
type_ = _type_check(
|
||||||
self.__forward_code__ = code
|
value,
|
||||||
self.__forward_evaluated__ = False
|
"Forward references must evaluate to types.",
|
||||||
self.__forward_value__ = None
|
is_argument=forward_ref.__forward_is_argument__,
|
||||||
self.__forward_is_argument__ = is_argument
|
allow_special_forms=forward_ref.__forward_is_class__,
|
||||||
self.__forward_is_class__ = is_class
|
)
|
||||||
self.__forward_module__ = module
|
return _eval_type(
|
||||||
|
type_,
|
||||||
def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard):
|
globals,
|
||||||
if type_params is _sentinel:
|
locals,
|
||||||
_deprecation_warning_for_no_type_params_passed("typing.ForwardRef._evaluate")
|
type_params,
|
||||||
type_params = ()
|
recursive_guard=_recursive_guard | {forward_ref.__forward_arg__},
|
||||||
if self.__forward_arg__ in recursive_guard:
|
format=format,
|
||||||
return self
|
owner=owner,
|
||||||
if not self.__forward_evaluated__ or localns is not globalns:
|
)
|
||||||
if globalns is None and localns is None:
|
|
||||||
globalns = localns = {}
|
|
||||||
elif globalns is None:
|
|
||||||
globalns = localns
|
|
||||||
elif localns is None:
|
|
||||||
localns = globalns
|
|
||||||
if self.__forward_module__ is not None:
|
|
||||||
globalns = getattr(
|
|
||||||
sys.modules.get(self.__forward_module__, None), '__dict__', globalns
|
|
||||||
)
|
|
||||||
|
|
||||||
# type parameters require some special handling,
|
|
||||||
# as they exist in their own scope
|
|
||||||
# but `eval()` does not have a dedicated parameter for that scope.
|
|
||||||
# For classes, names in type parameter scopes should override
|
|
||||||
# names in the global scope (which here are called `localns`!),
|
|
||||||
# but should in turn be overridden by names in the class scope
|
|
||||||
# (which here are called `globalns`!)
|
|
||||||
if type_params:
|
|
||||||
globalns, localns = dict(globalns), dict(localns)
|
|
||||||
for param in type_params:
|
|
||||||
param_name = param.__name__
|
|
||||||
if not self.__forward_is_class__ or param_name not in globalns:
|
|
||||||
globalns[param_name] = param
|
|
||||||
localns.pop(param_name, None)
|
|
||||||
|
|
||||||
type_ = _type_check(
|
|
||||||
eval(self.__forward_code__, globalns, localns),
|
|
||||||
"Forward references must evaluate to types.",
|
|
||||||
is_argument=self.__forward_is_argument__,
|
|
||||||
allow_special_forms=self.__forward_is_class__,
|
|
||||||
)
|
|
||||||
self.__forward_value__ = _eval_type(
|
|
||||||
type_,
|
|
||||||
globalns,
|
|
||||||
localns,
|
|
||||||
type_params,
|
|
||||||
recursive_guard=(recursive_guard | {self.__forward_arg__}),
|
|
||||||
)
|
|
||||||
self.__forward_evaluated__ = True
|
|
||||||
return self.__forward_value__
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if not isinstance(other, ForwardRef):
|
|
||||||
return NotImplemented
|
|
||||||
if self.__forward_evaluated__ and other.__forward_evaluated__:
|
|
||||||
return (self.__forward_arg__ == other.__forward_arg__ and
|
|
||||||
self.__forward_value__ == other.__forward_value__)
|
|
||||||
return (self.__forward_arg__ == other.__forward_arg__ and
|
|
||||||
self.__forward_module__ == other.__forward_module__)
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash((self.__forward_arg__, self.__forward_module__))
|
|
||||||
|
|
||||||
def __or__(self, other):
|
|
||||||
return Union[self, other]
|
|
||||||
|
|
||||||
def __ror__(self, other):
|
|
||||||
return Union[other, self]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
if self.__forward_module__ is None:
|
|
||||||
module_repr = ''
|
|
||||||
else:
|
|
||||||
module_repr = f', module={self.__forward_module__!r}'
|
|
||||||
return f'ForwardRef({self.__forward_arg__!r}{module_repr})'
|
|
||||||
|
|
||||||
|
|
||||||
def _is_unpacked_typevartuple(x: Any) -> bool:
|
def _is_unpacked_typevartuple(x: Any) -> bool:
|
||||||
|
@ -2196,7 +2169,7 @@ class _AnnotatedAlias(_NotIterable, _GenericAlias, _root=True):
|
||||||
"""Runtime representation of an annotated type.
|
"""Runtime representation of an annotated type.
|
||||||
|
|
||||||
At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't'
|
At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't'
|
||||||
with extra annotations. The alias behaves like a normal typing alias.
|
with extra metadata. The alias behaves like a normal typing alias.
|
||||||
Instantiating is the same as instantiating the underlying type; binding
|
Instantiating is the same as instantiating the underlying type; binding
|
||||||
it to types is also the same.
|
it to types is also the same.
|
||||||
|
|
||||||
|
@ -2380,7 +2353,8 @@ _allowed_types = (types.FunctionType, types.BuiltinFunctionType,
|
||||||
WrapperDescriptorType, MethodWrapperType, MethodDescriptorType)
|
WrapperDescriptorType, MethodWrapperType, MethodDescriptorType)
|
||||||
|
|
||||||
|
|
||||||
def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
|
def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
|
||||||
|
*, format=annotationlib.Format.VALUE):
|
||||||
"""Return type hints for an object.
|
"""Return type hints for an object.
|
||||||
|
|
||||||
This is often the same as obj.__annotations__, but it handles
|
This is often the same as obj.__annotations__, but it handles
|
||||||
|
@ -2417,13 +2391,14 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
|
||||||
if isinstance(obj, type):
|
if isinstance(obj, type):
|
||||||
hints = {}
|
hints = {}
|
||||||
for base in reversed(obj.__mro__):
|
for base in reversed(obj.__mro__):
|
||||||
|
ann = annotationlib.get_annotations(base, format=format)
|
||||||
|
if format is annotationlib.Format.SOURCE:
|
||||||
|
hints.update(ann)
|
||||||
|
continue
|
||||||
if globalns is None:
|
if globalns is None:
|
||||||
base_globals = getattr(sys.modules.get(base.__module__, None), '__dict__', {})
|
base_globals = getattr(sys.modules.get(base.__module__, None), '__dict__', {})
|
||||||
else:
|
else:
|
||||||
base_globals = globalns
|
base_globals = globalns
|
||||||
ann = getattr(base, '__annotations__', {})
|
|
||||||
if isinstance(ann, types.GetSetDescriptorType):
|
|
||||||
ann = {}
|
|
||||||
base_locals = dict(vars(base)) if localns is None else localns
|
base_locals = dict(vars(base)) if localns is None else localns
|
||||||
if localns is None and globalns is None:
|
if localns is None and globalns is None:
|
||||||
# This is surprising, but required. Before Python 3.10,
|
# This is surprising, but required. Before Python 3.10,
|
||||||
|
@ -2437,10 +2412,26 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
|
||||||
if value is None:
|
if value is None:
|
||||||
value = type(None)
|
value = type(None)
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = ForwardRef(value, is_argument=False, is_class=True)
|
value = _make_forward_ref(value, is_argument=False, is_class=True)
|
||||||
value = _eval_type(value, base_globals, base_locals, base.__type_params__)
|
value = _eval_type(value, base_globals, base_locals, base.__type_params__,
|
||||||
|
format=format, owner=obj)
|
||||||
hints[name] = value
|
hints[name] = value
|
||||||
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}
|
if include_extras or format is annotationlib.Format.SOURCE:
|
||||||
|
return hints
|
||||||
|
else:
|
||||||
|
return {k: _strip_annotations(t) for k, t in hints.items()}
|
||||||
|
|
||||||
|
hints = annotationlib.get_annotations(obj, format=format)
|
||||||
|
if (
|
||||||
|
not hints
|
||||||
|
and not isinstance(obj, types.ModuleType)
|
||||||
|
and not callable(obj)
|
||||||
|
and not hasattr(obj, '__annotations__')
|
||||||
|
and not hasattr(obj, '__annotate__')
|
||||||
|
):
|
||||||
|
raise TypeError(f"{obj!r} is not a module, class, or callable.")
|
||||||
|
if format is annotationlib.Format.SOURCE:
|
||||||
|
return hints
|
||||||
|
|
||||||
if globalns is None:
|
if globalns is None:
|
||||||
if isinstance(obj, types.ModuleType):
|
if isinstance(obj, types.ModuleType):
|
||||||
|
@ -2455,15 +2446,6 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
|
||||||
localns = globalns
|
localns = globalns
|
||||||
elif localns is None:
|
elif localns is None:
|
||||||
localns = globalns
|
localns = globalns
|
||||||
hints = getattr(obj, '__annotations__', None)
|
|
||||||
if hints is None:
|
|
||||||
# Return empty annotations for something that _could_ have them.
|
|
||||||
if isinstance(obj, _allowed_types):
|
|
||||||
return {}
|
|
||||||
else:
|
|
||||||
raise TypeError('{!r} is not a module, class, method, '
|
|
||||||
'or function.'.format(obj))
|
|
||||||
hints = dict(hints)
|
|
||||||
type_params = getattr(obj, "__type_params__", ())
|
type_params = getattr(obj, "__type_params__", ())
|
||||||
for name, value in hints.items():
|
for name, value in hints.items():
|
||||||
if value is None:
|
if value is None:
|
||||||
|
@ -2471,12 +2453,12 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
# class-level forward refs were handled above, this must be either
|
# class-level forward refs were handled above, this must be either
|
||||||
# a module-level annotation or a function argument annotation
|
# a module-level annotation or a function argument annotation
|
||||||
value = ForwardRef(
|
value = _make_forward_ref(
|
||||||
value,
|
value,
|
||||||
is_argument=not isinstance(obj, types.ModuleType),
|
is_argument=not isinstance(obj, types.ModuleType),
|
||||||
is_class=False,
|
is_class=False,
|
||||||
)
|
)
|
||||||
hints[name] = _eval_type(value, globalns, localns, type_params)
|
hints[name] = _eval_type(value, globalns, localns, type_params, format=format, owner=obj)
|
||||||
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}
|
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2953,22 +2935,34 @@ class SupportsRound[T](Protocol):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _make_nmtuple(name, types, module, defaults = ()):
|
def _make_nmtuple(name, fields, annotate_func, module, defaults = ()):
|
||||||
fields = [n for n, t in types]
|
|
||||||
types = {n: _type_check(t, f"field {n} annotation must be a type")
|
|
||||||
for n, t in types}
|
|
||||||
nm_tpl = collections.namedtuple(name, fields,
|
nm_tpl = collections.namedtuple(name, fields,
|
||||||
defaults=defaults, module=module)
|
defaults=defaults, module=module)
|
||||||
nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = types
|
nm_tpl.__annotate__ = nm_tpl.__new__.__annotate__ = annotate_func
|
||||||
return nm_tpl
|
return nm_tpl
|
||||||
|
|
||||||
|
|
||||||
|
def _make_eager_annotate(types):
|
||||||
|
checked_types = {key: _type_check(val, f"field {key} annotation must be a type")
|
||||||
|
for key, val in types.items()}
|
||||||
|
def annotate(format):
|
||||||
|
if format in (annotationlib.Format.VALUE, annotationlib.Format.FORWARDREF):
|
||||||
|
return checked_types
|
||||||
|
else:
|
||||||
|
return _convert_to_source(types)
|
||||||
|
return annotate
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_to_source(types):
|
||||||
|
return {n: t if isinstance(t, str) else _type_repr(t) for n, t in types.items()}
|
||||||
|
|
||||||
|
|
||||||
# attributes prohibited to set in NamedTuple class syntax
|
# attributes prohibited to set in NamedTuple class syntax
|
||||||
_prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__',
|
_prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__',
|
||||||
'_fields', '_field_defaults',
|
'_fields', '_field_defaults',
|
||||||
'_make', '_replace', '_asdict', '_source'})
|
'_make', '_replace', '_asdict', '_source'})
|
||||||
|
|
||||||
_special = frozenset({'__module__', '__name__', '__annotations__'})
|
_special = frozenset({'__module__', '__name__', '__annotations__', '__annotate__'})
|
||||||
|
|
||||||
|
|
||||||
class NamedTupleMeta(type):
|
class NamedTupleMeta(type):
|
||||||
|
@ -2981,12 +2975,29 @@ class NamedTupleMeta(type):
|
||||||
bases = tuple(tuple if base is _NamedTuple else base for base in bases)
|
bases = tuple(tuple if base is _NamedTuple else base for base in bases)
|
||||||
if "__annotations__" in ns:
|
if "__annotations__" in ns:
|
||||||
types = ns["__annotations__"]
|
types = ns["__annotations__"]
|
||||||
|
field_names = list(types)
|
||||||
|
annotate = _make_eager_annotate(types)
|
||||||
elif "__annotate__" in ns:
|
elif "__annotate__" in ns:
|
||||||
types = ns["__annotate__"](1) # VALUE
|
original_annotate = ns["__annotate__"]
|
||||||
|
types = annotationlib.call_annotate_function(original_annotate, annotationlib.Format.FORWARDREF)
|
||||||
|
field_names = list(types)
|
||||||
|
|
||||||
|
# For backward compatibility, type-check all the types at creation time
|
||||||
|
for typ in types.values():
|
||||||
|
_type_check(typ, "field annotation must be a type")
|
||||||
|
|
||||||
|
def annotate(format):
|
||||||
|
annos = annotationlib.call_annotate_function(original_annotate, format)
|
||||||
|
if format != annotationlib.Format.SOURCE:
|
||||||
|
return {key: _type_check(val, f"field {key} annotation must be a type")
|
||||||
|
for key, val in annos.items()}
|
||||||
|
return annos
|
||||||
else:
|
else:
|
||||||
types = {}
|
# Empty NamedTuple
|
||||||
|
field_names = []
|
||||||
|
annotate = lambda format: {}
|
||||||
default_names = []
|
default_names = []
|
||||||
for field_name in types:
|
for field_name in field_names:
|
||||||
if field_name in ns:
|
if field_name in ns:
|
||||||
default_names.append(field_name)
|
default_names.append(field_name)
|
||||||
elif default_names:
|
elif default_names:
|
||||||
|
@ -2994,7 +3005,7 @@ class NamedTupleMeta(type):
|
||||||
f"cannot follow default field"
|
f"cannot follow default field"
|
||||||
f"{'s' if len(default_names) > 1 else ''} "
|
f"{'s' if len(default_names) > 1 else ''} "
|
||||||
f"{', '.join(default_names)}")
|
f"{', '.join(default_names)}")
|
||||||
nm_tpl = _make_nmtuple(typename, types.items(),
|
nm_tpl = _make_nmtuple(typename, field_names, annotate,
|
||||||
defaults=[ns[n] for n in default_names],
|
defaults=[ns[n] for n in default_names],
|
||||||
module=ns['__module__'])
|
module=ns['__module__'])
|
||||||
nm_tpl.__bases__ = bases
|
nm_tpl.__bases__ = bases
|
||||||
|
@ -3085,7 +3096,11 @@ def NamedTuple(typename, fields=_sentinel, /, **kwargs):
|
||||||
import warnings
|
import warnings
|
||||||
warnings._deprecated(deprecated_thing, message=deprecation_msg, remove=(3, 15))
|
warnings._deprecated(deprecated_thing, message=deprecation_msg, remove=(3, 15))
|
||||||
fields = kwargs.items()
|
fields = kwargs.items()
|
||||||
nt = _make_nmtuple(typename, fields, module=_caller())
|
types = {n: _type_check(t, f"field {n} annotation must be a type")
|
||||||
|
for n, t in fields}
|
||||||
|
field_names = [n for n, _ in fields]
|
||||||
|
|
||||||
|
nt = _make_nmtuple(typename, field_names, _make_eager_annotate(types), module=_caller())
|
||||||
nt.__orig_bases__ = (NamedTuple,)
|
nt.__orig_bases__ = (NamedTuple,)
|
||||||
return nt
|
return nt
|
||||||
|
|
||||||
|
@ -3144,15 +3159,19 @@ class _TypedDictMeta(type):
|
||||||
if not hasattr(tp_dict, '__orig_bases__'):
|
if not hasattr(tp_dict, '__orig_bases__'):
|
||||||
tp_dict.__orig_bases__ = bases
|
tp_dict.__orig_bases__ = bases
|
||||||
|
|
||||||
annotations = {}
|
|
||||||
if "__annotations__" in ns:
|
if "__annotations__" in ns:
|
||||||
|
own_annotate = None
|
||||||
own_annotations = ns["__annotations__"]
|
own_annotations = ns["__annotations__"]
|
||||||
elif "__annotate__" in ns:
|
elif "__annotate__" in ns:
|
||||||
own_annotations = ns["__annotate__"](1) # VALUE
|
own_annotate = ns["__annotate__"]
|
||||||
|
own_annotations = annotationlib.call_annotate_function(
|
||||||
|
own_annotate, annotationlib.Format.FORWARDREF, owner=tp_dict
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
|
own_annotate = None
|
||||||
own_annotations = {}
|
own_annotations = {}
|
||||||
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
|
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
|
||||||
own_annotations = {
|
own_checked_annotations = {
|
||||||
n: _type_check(tp, msg, module=tp_dict.__module__)
|
n: _type_check(tp, msg, module=tp_dict.__module__)
|
||||||
for n, tp in own_annotations.items()
|
for n, tp in own_annotations.items()
|
||||||
}
|
}
|
||||||
|
@ -3162,13 +3181,6 @@ class _TypedDictMeta(type):
|
||||||
mutable_keys = set()
|
mutable_keys = set()
|
||||||
|
|
||||||
for base in bases:
|
for base in bases:
|
||||||
# TODO: Avoid eagerly evaluating annotations in VALUE format.
|
|
||||||
# Instead, evaluate in FORWARDREF format to figure out which
|
|
||||||
# keys have Required/NotRequired/ReadOnly qualifiers, and create
|
|
||||||
# a new __annotate__ function for the resulting TypedDict that
|
|
||||||
# combines the annotations from this class and its parents.
|
|
||||||
annotations.update(base.__annotations__)
|
|
||||||
|
|
||||||
base_required = base.__dict__.get('__required_keys__', set())
|
base_required = base.__dict__.get('__required_keys__', set())
|
||||||
required_keys |= base_required
|
required_keys |= base_required
|
||||||
optional_keys -= base_required
|
optional_keys -= base_required
|
||||||
|
@ -3180,8 +3192,7 @@ class _TypedDictMeta(type):
|
||||||
readonly_keys.update(base.__dict__.get('__readonly_keys__', ()))
|
readonly_keys.update(base.__dict__.get('__readonly_keys__', ()))
|
||||||
mutable_keys.update(base.__dict__.get('__mutable_keys__', ()))
|
mutable_keys.update(base.__dict__.get('__mutable_keys__', ()))
|
||||||
|
|
||||||
annotations.update(own_annotations)
|
for annotation_key, annotation_type in own_checked_annotations.items():
|
||||||
for annotation_key, annotation_type in own_annotations.items():
|
|
||||||
qualifiers = set(_get_typeddict_qualifiers(annotation_type))
|
qualifiers = set(_get_typeddict_qualifiers(annotation_type))
|
||||||
if Required in qualifiers:
|
if Required in qualifiers:
|
||||||
is_required = True
|
is_required = True
|
||||||
|
@ -3212,7 +3223,32 @@ class _TypedDictMeta(type):
|
||||||
f"Required keys overlap with optional keys in {name}:"
|
f"Required keys overlap with optional keys in {name}:"
|
||||||
f" {required_keys=}, {optional_keys=}"
|
f" {required_keys=}, {optional_keys=}"
|
||||||
)
|
)
|
||||||
tp_dict.__annotations__ = annotations
|
|
||||||
|
def __annotate__(format):
|
||||||
|
annos = {}
|
||||||
|
for base in bases:
|
||||||
|
if base is Generic:
|
||||||
|
continue
|
||||||
|
base_annotate = base.__annotate__
|
||||||
|
if base_annotate is None:
|
||||||
|
continue
|
||||||
|
base_annos = annotationlib.call_annotate_function(base.__annotate__, format, owner=base)
|
||||||
|
annos.update(base_annos)
|
||||||
|
if own_annotate is not None:
|
||||||
|
own = annotationlib.call_annotate_function(own_annotate, format, owner=tp_dict)
|
||||||
|
if format != annotationlib.Format.SOURCE:
|
||||||
|
own = {
|
||||||
|
n: _type_check(tp, msg, module=tp_dict.__module__)
|
||||||
|
for n, tp in own.items()
|
||||||
|
}
|
||||||
|
elif format == annotationlib.Format.SOURCE:
|
||||||
|
own = _convert_to_source(own_annotations)
|
||||||
|
else:
|
||||||
|
own = own_checked_annotations
|
||||||
|
annos.update(own)
|
||||||
|
return annos
|
||||||
|
|
||||||
|
tp_dict.__annotate__ = __annotate__
|
||||||
tp_dict.__required_keys__ = frozenset(required_keys)
|
tp_dict.__required_keys__ = frozenset(required_keys)
|
||||||
tp_dict.__optional_keys__ = frozenset(optional_keys)
|
tp_dict.__optional_keys__ = frozenset(optional_keys)
|
||||||
tp_dict.__readonly_keys__ = frozenset(readonly_keys)
|
tp_dict.__readonly_keys__ = frozenset(readonly_keys)
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
As part of implementing :pep:`649` and :pep:`749`, add a new module
|
||||||
|
``annotationlib``. Add support for unresolved forward references in
|
||||||
|
annotations to :mod:`dataclasses`, :class:`typing.TypedDict`, and
|
||||||
|
:class:`typing.NamedTuple`.
|
1
Python/stdlib_module_names.h
generated
1
Python/stdlib_module_names.h
generated
|
@ -99,6 +99,7 @@ static const char* _Py_stdlib_module_names[] = {
|
||||||
"_winapi",
|
"_winapi",
|
||||||
"_zoneinfo",
|
"_zoneinfo",
|
||||||
"abc",
|
"abc",
|
||||||
|
"annotationlib",
|
||||||
"antigravity",
|
"antigravity",
|
||||||
"argparse",
|
"argparse",
|
||||||
"array",
|
"array",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue