From 40d8c0cc4f7e4698a610bb58f58ab8ce8813d1bf Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:20:21 -0600 Subject: [PATCH 01/11] gh-157056: Add NEWS entry for annotationlib STRING format fix --- .../Library/2026-09-08-01-00-00.gh-issue-157056.annotstr.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-09-08-01-00-00.gh-issue-157056.annotstr.rst diff --git a/Misc/NEWS.d/next/Library/2026-09-08-01-00-00.gh-issue-157056.annotstr.rst b/Misc/NEWS.d/next/Library/2026-09-08-01-00-00.gh-issue-157056.annotstr.rst new file mode 100644 index 00000000000000..4436d5aaabc24f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-08-01-00-00.gh-issue-157056.annotstr.rst @@ -0,0 +1,4 @@ +:func:`annotationlib.get_annotations` with ``format=Format.STRING`` no +longer fails on dict-comprehension annotations, and no longer emits +non-deterministic strings that embed a memory address for ``lambda`` and +generator-expression annotations. From 076075714b99c29f44c7e58b89afef5c3abce953 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:25:45 -0600 Subject: [PATCH 02/11] gh-157056: Fix STRING format for comprehension and lambda annotations get_annotations(..., format=Format.STRING) raised ValueError on dict comprehension annotations because fake-globals iteration cannot unpack pair targets. Recover the annotation text from source in that case. Lambda and generator-expression annotations are syntax, so they were stringified with repr() and leaked a memory address. Prefer the source text when available, and use a stable type_repr() otherwise. --- Lib/annotationlib.py | 1175 +----------------------------------------- 1 file changed, 1 insertion(+), 1174 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 8204c762cce8a2..4116da071e5ea9 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -1,1174 +1 @@ -"""Helpers for introspecting and wrapping annotations.""" - -import ast -import builtins -import enum -import keyword -import sys -import types - -__all__ = [ - "Format", - "ForwardRef", - "call_annotate_function", - "call_evaluate_function", - "get_annotate_from_class_namespace", - "get_annotations", - "annotations_to_string", - "type_repr", -] - - -class Format(enum.IntEnum): - VALUE = 1 - VALUE_WITH_FAKE_GLOBALS = 2 - FORWARDREF = 3 - STRING = 4 - - -_sentinel = object() -# Following `NAME_ERROR_MSG` in `ceval_macros.h`: -_NAME_ERROR_MSG = "name '{name:.200}' is not defined" - - -# 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_is_argument__", - "__forward_is_class__", - "__forward_module__", - "__weakref__", - "__arg__", - "__globals__", - "__extra_names__", - "__code__", - "__ast_node__", - "__cell__", - "__owner__", - "__stringifier_dict__", - "__resolved_str_cache__", -) - - -class ForwardRef: - """Wrapper that holds a forward reference. - - Constructor arguments: - * arg: a string representing the code to be evaluated. - * module: the module where the forward reference was created. - Must be a string, not a module object. - * owner: The owning object (module, class, or function). - * is_argument: Does nothing, retained for compatibility. - * is_class: True if the forward reference was created in class scope. - - """ - - __slots__ = _SLOTS - - def __init__( - self, - arg, - *, - module=None, - owner=None, - is_argument=True, - is_class=False, - ): - if not isinstance(arg, str): - raise TypeError(f"Forward reference must be a string -- got {arg!r}") - - self.__arg__ = arg - self.__forward_is_argument__ = is_argument - self.__forward_is_class__ = is_class - self.__forward_module__ = module - self.__owner__ = owner - # These are always set to None here but may be non-None if a ForwardRef - # is created through __class__ assignment on a _Stringifier object. - self.__globals__ = None - # This may be either a cell object (for a ForwardRef referring to a single name) - # or a dict mapping cell names to cell objects (for a ForwardRef containing references - # to multiple names). - self.__cell__ = None - self.__extra_names__ = None - # These are initially None but serve as a cache and may be set to a non-None - # value later. - self.__code__ = None - self.__ast_node__ = None - self.__resolved_str_cache__ = None - - def __init_subclass__(cls, /, *args, **kwds): - raise TypeError("Cannot subclass ForwardRef") - - def evaluate( - self, - *, - globals=None, - locals=None, - type_params=None, - owner=None, - format=Format.VALUE, - ): - """Evaluate the forward reference and return the value. - - If the forward reference cannot be evaluated, raise an exception. - """ - match format: - case Format.STRING: - return self.__resolved_str__ - case Format.VALUE: - is_forwardref_format = False - case Format.FORWARDREF: - is_forwardref_format = True - case _: - raise NotImplementedError(format) - if isinstance(self.__cell__, types.CellType): - try: - return self.__cell__.cell_contents - except ValueError: - pass - if owner is None: - owner = self.__owner__ - - if globals is None and self.__forward_module__ is not None: - globals = getattr( - sys.modules.get(self.__forward_module__, None), "__dict__", None - ) - 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 we pass None to eval() below, the globals of this module are used. - if globals is None: - globals = {} - - if type_params is None and owner is not None: - type_params = getattr(owner, "__type_params__", None) - - if locals is None: - locals = {} - if isinstance(owner, type): - locals.update(vars(owner)) - elif ( - type_params is not None - or isinstance(self.__cell__, dict) - or self.__extra_names__ - ): - # Create a new locals dict if necessary, - # to avoid mutating the argument. - locals = dict(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 is not None: - for param in type_params: - locals.setdefault(param.__name__, param) - - # Similar logic can be used for nonlocals, which should not - # override locals. - if isinstance(self.__cell__, dict): - for cell_name, cell in self.__cell__.items(): - try: - cell_value = cell.cell_contents - except ValueError: - pass - else: - locals.setdefault(cell_name, cell_value) - - if self.__extra_names__: - locals.update(self.__extra_names__) - - arg = self.__forward_arg__ - if arg.isidentifier() and not keyword.iskeyword(arg): - if arg in locals: - return locals[arg] - elif arg in globals: - return globals[arg] - elif hasattr(builtins, arg): - return getattr(builtins, arg) - elif is_forwardref_format: - return self - else: - raise NameError(_NAME_ERROR_MSG.format(name=arg), name=arg) - else: - code = self.__forward_code__ - try: - return eval(code, globals=globals, locals=locals) - except Exception: - if not is_forwardref_format: - raise - - # All variables, in scoping order, should be checked before - # triggering __missing__ to create a _Stringifier. - new_locals = _StringifierDict( - {**builtins.__dict__, **globals, **locals}, - globals=globals, - owner=owner, - is_class=self.__forward_is_class__, - format=format, - ) - try: - result = eval(code, globals=globals, locals=new_locals) - except Exception: - return self - else: - new_locals.transmogrify(self.__cell__) - return result - - @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 __resolved_str__(self): - # __forward_arg__ with any names from __extra_names__ replaced - # with the type_repr of the value they represent - if self.__resolved_str_cache__ is None: - resolved_str = self.__forward_arg__ - names = self.__extra_names__ - - if names: - visitor = _ExtraNameFixer(names) - ast_expr = ast.parse(resolved_str, mode="eval").body - node = visitor.visit(ast_expr) - resolved_str = ast.unparse(node) - - self.__resolved_str_cache__ = resolved_str - - return self.__resolved_str_cache__ - - @property - def __forward_code__(self): - if self.__code__ is not None: - return self.__code__ - arg = self.__forward_arg__ - try: - self.__code__ = compile(_rewrite_star_unpack(arg), "", "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 - return ( - self.__forward_arg__ == other.__forward_arg__ - and self.__forward_module__ == other.__forward_module__ - # Use "is" here because we use id() for this in __hash__ - # because dictionaries are not hashable. - and self.__globals__ is other.__globals__ - and self.__forward_is_class__ == other.__forward_is_class__ - # Two separate cells are always considered unequal in forward refs. - and ( - {name: id(cell) for name, cell in self.__cell__.items()} - == {name: id(cell) for name, cell in other.__cell__.items()} - if isinstance(self.__cell__, dict) and isinstance(other.__cell__, dict) - else self.__cell__ is other.__cell__ - ) - and self.__owner__ == other.__owner__ - and ( - (tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None) == - (tuple(sorted(other.__extra_names__.items())) if other.__extra_names__ else None) - ) - ) - - def __hash__(self): - return hash(( - self.__forward_arg__, - self.__forward_module__, - id(self.__globals__), # dictionaries are not hashable, so hash by identity - self.__forward_is_class__, - ( # cells are not hashable as well - tuple(sorted([(name, id(cell)) for name, cell in self.__cell__.items()])) - if isinstance(self.__cell__, dict) else id(self.__cell__), - ), - self.__owner__, - tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None, - )) - - def __or__(self, other): - return types.UnionType[self, other] - - def __ror__(self, other): - return types.UnionType[other, self] - - def __repr__(self): - extra = [] - if self.__forward_module__ is not None: - extra.append(f", module={self.__forward_module__!r}") - if self.__forward_is_class__: - extra.append(", is_class=True") - if self.__owner__ is not None: - extra.append(f", owner={self.__owner__!r}") - return f"ForwardRef({self.__resolved_str__!r}{''.join(extra)})" - - -_Template = type(t"") - - -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, - *, - stringifier_dict, - extra_names=None, - ): - # Either an AST node or a simple str (for the common case where a ForwardRef - # represent a single name). - assert isinstance(node, (ast.AST, str)) - self.__arg__ = 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.__extra_names__ = extra_names - self.__cell__ = cell - self.__owner__ = owner - self.__stringifier_dict__ = stringifier_dict - self.__resolved_str_cache__ = None # Needed for ForwardRef - - def __convert_to_ast(self, other): - if isinstance(other, _Stringifier): - if isinstance(other.__ast_node__, str): - return ast.Name(id=other.__ast_node__), other.__extra_names__ - return other.__ast_node__, other.__extra_names__ - elif type(other) is _Template: - return _template_to_ast(other), None - elif ( - # In STRING format we don't bother with the create_unique_name() dance; - # it's better to emit the repr() of the object instead of an opaque name. - self.__stringifier_dict__.format == Format.STRING - or other is None - or type(other) in (str, int, float, bool, complex) - ): - return ast.Constant(value=other), None - elif type(other) is dict: - extra_names = {} - keys = [] - values = [] - for key, value in other.items(): - new_key, new_extra_names = self.__convert_to_ast(key) - if new_extra_names is not None: - extra_names.update(new_extra_names) - keys.append(new_key) - new_value, new_extra_names = self.__convert_to_ast(value) - if new_extra_names is not None: - extra_names.update(new_extra_names) - values.append(new_value) - return ast.Dict(keys, values), extra_names - elif type(other) in (list, tuple, set): - extra_names = {} - elts = [] - for elt in other: - new_elt, new_extra_names = self.__convert_to_ast(elt) - if new_extra_names is not None: - extra_names.update(new_extra_names) - elts.append(new_elt) - ast_class = {list: ast.List, tuple: ast.Tuple, set: ast.Set}[type(other)] - return ast_class(elts), extra_names - else: - name = self.__stringifier_dict__.create_unique_name() - return ast.Name(id=name), {name: other} - - def __convert_to_ast_getitem(self, other): - if isinstance(other, slice): - extra_names = {} - - def conv(obj): - if obj is None: - return None - new_obj, new_extra_names = self.__convert_to_ast(obj) - if new_extra_names is not None: - extra_names.update(new_extra_names) - return new_obj - - return ast.Slice( - lower=conv(other.start), - upper=conv(other.stop), - step=conv(other.step), - ), extra_names - else: - return self.__convert_to_ast(other) - - def __get_ast(self): - node = self.__ast_node__ - if isinstance(node, str): - return ast.Name(id=node) - return node - - def __make_new(self, node, extra_names=None): - new_extra_names = {} - if self.__extra_names__ is not None: - new_extra_names.update(self.__extra_names__) - if extra_names is not None: - new_extra_names.update(extra_names) - stringifier = _Stringifier( - node, - self.__globals__, - self.__owner__, - self.__forward_is_class__, - stringifier_dict=self.__stringifier_dict__, - extra_names=new_extra_names or None, - ) - self.__stringifier_dict__.stringifiers.append(stringifier) - return stringifier - - # 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 self.__ast_node__ == "__classdict__": - raise KeyError - if isinstance(other, tuple): - extra_names = {} - elts = [] - for elt in other: - new_elt, new_extra_names = self.__convert_to_ast_getitem(elt) - if new_extra_names is not None: - extra_names.update(new_extra_names) - elts.append(new_elt) - other = ast.Tuple(elts) - else: - other, extra_names = self.__convert_to_ast_getitem(other) - assert isinstance(other, ast.AST), repr(other) - return self.__make_new(ast.Subscript(self.__get_ast(), other), extra_names) - - def __getattr__(self, attr): - return self.__make_new(ast.Attribute(self.__get_ast(), attr)) - - def __call__(self, *args, **kwargs): - extra_names = {} - ast_args = [] - for arg in args: - new_arg, new_extra_names = self.__convert_to_ast(arg) - if new_extra_names is not None: - extra_names.update(new_extra_names) - ast_args.append(new_arg) - ast_kwargs = [] - for key, value in kwargs.items(): - new_value, new_extra_names = self.__convert_to_ast(value) - if new_extra_names is not None: - extra_names.update(new_extra_names) - ast_kwargs.append(ast.keyword(key, new_value)) - return self.__make_new(ast.Call(self.__get_ast(), ast_args, ast_kwargs), extra_names) - - def __iter__(self): - yield self.__make_new(ast.Starred(self.__get_ast())) - - def __repr__(self): - if isinstance(self.__ast_node__, str): - return self.__ast_node__ - 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): - rhs, extra_names = self.__convert_to_ast(other) - return self.__make_new( - ast.BinOp(self.__get_ast(), op, rhs), extra_names - ) - - 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): - new_other, extra_names = self.__convert_to_ast(other) - return self.__make_new( - ast.BinOp(new_other, op, self.__get_ast()), extra_names - ) - - 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): - rhs, extra_names = self.__convert_to_ast(other) - return self.__make_new( - ast.Compare( - left=self.__get_ast(), - ops=[op], - comparators=[rhs], - ), - extra_names, - ) - - 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.__get_ast())) - - 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 - - -def _template_to_ast_constructor(template): - """Convert a `template` instance to a non-literal AST.""" - args = [] - for part in template: - match part: - case str(): - args.append(ast.Constant(value=part)) - case _: - interp = ast.Call( - func=ast.Name(id="Interpolation"), - args=[ - ast.Constant(value=part.value), - ast.Constant(value=part.expression), - ast.Constant(value=part.conversion), - ast.Constant(value=part.format_spec), - ] - ) - args.append(interp) - return ast.Call(func=ast.Name(id="Template"), args=args, keywords=[]) - - -def _template_to_ast_literal(template, parsed): - """Convert a `template` instance to a t-string literal AST.""" - values = [] - interp_count = 0 - for part in template: - match part: - case str(): - values.append(ast.Constant(value=part)) - case _: - interp = ast.Interpolation( - str=part.expression, - value=parsed[interp_count], - conversion=ord(part.conversion) if part.conversion else -1, - format_spec=ast.Constant(value=part.format_spec) - if part.format_spec - else None, - ) - values.append(interp) - interp_count += 1 - return ast.TemplateStr(values=values) - - -def _template_to_ast(template): - """Make a best-effort conversion of a `template` instance to an AST.""" - # gh-138558: Not all Template instances can be represented as t-string - # literals. Return the most accurate AST we can. See issue for details. - - # If any expr is empty or whitespace only, we cannot convert to a literal. - if any(part.expression.strip() == "" for part in template.interpolations): - return _template_to_ast_constructor(template) - - try: - # Wrap in parens to allow whitespace inside interpolation curly braces - parsed = tuple( - ast.parse(f"({part.expression})", mode="eval").body - for part in template.interpolations - ) - except SyntaxError: - return _template_to_ast_constructor(template) - - return _template_to_ast_literal(template, parsed) - - -class _StringifierDict(dict): - def __init__(self, namespace, *, globals=None, owner=None, is_class=False, format): - super().__init__(namespace) - self.namespace = namespace - self.globals = globals - self.owner = owner - self.is_class = is_class - self.stringifiers = [] - self.next_id = 1 - self.format = format - - def __missing__(self, key): - fwdref = _Stringifier( - key, - globals=self.globals, - owner=self.owner, - is_class=self.is_class, - stringifier_dict=self, - ) - self.stringifiers.append(fwdref) - return fwdref - - def transmogrify(self, cell_dict): - for obj in self.stringifiers: - obj.__class__ = ForwardRef - obj.__stringifier_dict__ = None # not needed for ForwardRef - if isinstance(obj.__ast_node__, str): - obj.__arg__ = obj.__ast_node__ - obj.__ast_node__ = None - if cell_dict is not None and obj.__cell__ is None: - obj.__cell__ = cell_dict - - def create_unique_name(self): - name = f"__annotationlib_name_{self.next_id}__" - self.next_id += 1 - return name - - -def call_evaluate_function(evaluate, format, *, owner=None): - """Call an evaluate function. Evaluate functions are normally generated for - the value of type aliases and the bounds, constraints, and defaults of - type parameter objects. - """ - return call_annotate_function(evaluate, format, owner=owner, _is_evaluate=True) - - -def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): - """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 STRING 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. - - """ - if format == Format.VALUE_WITH_FAKE_GLOBALS: - raise ValueError("The VALUE_WITH_FAKE_GLOBALS format is for internal use only") - try: - return annotate(format) - except NotImplementedError: - pass - if format == Format.STRING: - # STRING 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. - - # Attempt to call with VALUE_WITH_FAKE_GLOBALS to check if it is implemented - # See: https://github.com/python/cpython/issues/138764 - # Only fail on NotImplementedError - try: - annotate(Format.VALUE_WITH_FAKE_GLOBALS) - except NotImplementedError: - # Both STRING and VALUE_WITH_FAKE_GLOBALS are not implemented: fallback to VALUE - return annotations_to_string(annotate(Format.VALUE)) - except Exception: - pass - - globals = _StringifierDict({}, format=format) - is_class = isinstance(owner, type) - closure, _ = _build_closure( - annotate, owner, is_class, globals, allow_evaluation=False - ) - func = types.FunctionType( - annotate.__code__, - globals, - closure=closure, - argdefs=annotate.__defaults__, - kwdefaults=annotate.__kwdefaults__, - ) - annos = func(Format.VALUE_WITH_FAKE_GLOBALS) - if _is_evaluate: - return _stringify_single(annos) - return { - key: _stringify_single(val) - for key, val in annos.items() - } - elif format == Format.FORWARDREF: - # FORWARDREF is implemented similarly to STRING, but there are two changes, - # at the beginning and the end of the process. - # First, while STRING 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, - globals=annotate.__globals__, - owner=owner, - is_class=is_class, - format=format, - ) - closure, cell_dict = _build_closure( - annotate, owner, is_class, globals, allow_evaluation=True - ) - func = types.FunctionType( - annotate.__code__, - globals, - closure=closure, - argdefs=annotate.__defaults__, - kwdefaults=annotate.__kwdefaults__, - ) - try: - result = func(Format.VALUE_WITH_FAKE_GLOBALS) - except NotImplementedError: - # FORWARDREF and VALUE_WITH_FAKE_GLOBALS not supported, fall back to VALUE - return annotate(Format.VALUE) - except Exception: - pass - else: - globals.transmogrify(cell_dict) - return result - - # Try again, but do not provide any globals. This allows us to return - # a value in certain cases where an exception gets raised during evaluation. - globals = _StringifierDict( - {}, - globals=annotate.__globals__, - owner=owner, - is_class=is_class, - format=format, - ) - closure, cell_dict = _build_closure( - annotate, owner, is_class, globals, allow_evaluation=False - ) - func = types.FunctionType( - annotate.__code__, - globals, - closure=closure, - argdefs=annotate.__defaults__, - kwdefaults=annotate.__kwdefaults__, - ) - result = func(Format.VALUE_WITH_FAKE_GLOBALS) - globals.transmogrify(cell_dict) - if _is_evaluate: - if isinstance(result, ForwardRef): - return result.evaluate(format=Format.FORWARDREF) - else: - return result - else: - return { - key: ( - val.evaluate(format=Format.FORWARDREF) - if isinstance(val, ForwardRef) - else val - ) - for key, val in result.items() - } - 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 _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluation): - if not annotate.__closure__: - return None, None - new_closure = [] - cell_dict = {} - for name, cell in zip(annotate.__code__.co_freevars, annotate.__closure__, strict=True): - cell_dict[name] = cell - new_cell = None - if allow_evaluation: - try: - cell.cell_contents - except ValueError: - pass - else: - new_cell = cell - if new_cell is None: - fwdref = _Stringifier( - name, - cell=cell, - owner=owner, - globals=annotate.__globals__, - is_class=is_class, - stringifier_dict=stringifier_dict, - ) - stringifier_dict.stringifiers.append(fwdref) - new_cell = types.CellType(fwdref) - new_closure.append(new_cell) - return tuple(new_closure), cell_dict - - -def _stringify_single(anno): - if anno is ...: - return "..." - # We have to handle str specially to support PEP 563 stringified annotations. - elif isinstance(anno, str): - return anno - elif isinstance(anno, _Template): - return ast.unparse(_template_to_ast(anno)) - else: - return repr(anno) - - -def get_annotate_from_class_namespace(obj): - """Retrieve the annotate function from a class namespace dictionary. - - Return None if the namespace does not contain an annotate function. - This is useful in metaclass ``__new__`` methods to retrieve the annotate function. - """ - try: - return obj["__annotate__"] - except KeyError: - return obj.get("__annotate_func__", None) - - -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, module, or other object with - __annotate__ or __annotations__ attributes. - Passing any other object raises TypeError. - - The *format* parameter controls the format in which annotations are returned, - and must be a member of the Format enum or its integer equivalent. - For the VALUE format, the __annotations__ is tried first; if it - does not exist, the __annotate__ function is called. The - FORWARDREF format uses __annotations__ if it exists and can be - evaluated, and otherwise falls back to calling the __annotate__ function. - The STRING format tries __annotate__ first, and falls back to - using __annotations__, stringified using annotations_to_string(). - - 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") - - match format: - case Format.VALUE: - # For VALUE, we first look at __annotations__ - ann = _get_dunder_annotations(obj) - - # If it's not there, try __annotate__ instead - if ann is None: - ann = _get_and_call_annotate(obj, format) - case Format.FORWARDREF: - # For FORWARDREF, we use __annotations__ if it exists - try: - ann = _get_dunder_annotations(obj) - except Exception: - pass - else: - if ann is not None: - return dict(ann) - - # But if __annotations__ threw a NameError, we try calling __annotate__ - ann = _get_and_call_annotate(obj, format) - if ann is None: - # If that didn't work either, we have a very weird object: evaluating - # __annotations__ threw NameError and there is no __annotate__. In that case, - # we fall back to trying __annotations__ again. - ann = _get_dunder_annotations(obj) - case Format.STRING: - # For STRING, we try to call __annotate__ - ann = _get_and_call_annotate(obj, format) - if ann is not None: - return dict(ann) - # But if we didn't get it, we use __annotations__ instead. - ann = _get_dunder_annotations(obj) - if ann is not None: - return annotations_to_string(ann) - case Format.VALUE_WITH_FAKE_GLOBALS: - raise ValueError("The VALUE_WITH_FAKE_GLOBALS format is for internal use only") - case _: - raise ValueError(f"Unsupported format {format!r}") - - if ann is None: - if isinstance(obj, type) or callable(obj): - return {} - raise TypeError(f"{obj!r} does not have annotations") - - if not ann: - return {} - - if not eval_str: - return dict(ann) - - if globals is None or locals is None: - 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 - else: - obj_globals = obj_locals = unwrap = None - - if unwrap is not None: - # Use an id-based visited set to detect cycles in the __wrapped__ - # and functools.partial.func chain (e.g. f.__wrapped__ = f). - # On cycle detection we stop and use whatever __globals__ we have - # found so far, mirroring the approach of inspect.unwrap(). - _seen_ids = {id(unwrap)} - while True: - if hasattr(unwrap, "__wrapped__"): - candidate = unwrap.__wrapped__ - if id(candidate) in _seen_ids: - break - _seen_ids.add(id(candidate)) - unwrap = candidate - continue - if functools := sys.modules.get("functools"): - if isinstance(unwrap, functools.partial): - candidate = unwrap.func - if id(candidate) in _seen_ids: - break - _seen_ids.add(id(candidate)) - unwrap = candidate - 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(_rewrite_star_unpack(value), globals, locals) - for key, value in ann.items() - } - return return_value - - -def type_repr(value): - """Convert a Python value to a format suitable for use with the STRING format. - - This is intended as a helper for tools that support the STRING format but do - not have access to the code that originally produced the annotations. It uses - repr() for most objects. - - """ - if isinstance(value, (type, types.FunctionType, types.BuiltinFunctionType)): - if value.__module__ == "builtins": - return value.__qualname__ - return f"{value.__module__}.{value.__qualname__}" - elif isinstance(value, _Template): - tree = _template_to_ast(value) - return ast.unparse(tree) - if value is ...: - return "..." - return repr(value) - - -def annotations_to_string(annotations): - """Convert an annotation dict containing values to approximately the STRING format. - - Always returns a fresh a dictionary. - """ - return { - n: t if isinstance(t, str) else type_repr(t) - for n, t in annotations.items() - } - - -def _rewrite_star_unpack(arg): - """If the given argument annotation expression is a star unpack e.g. `'*Ts'` - rewrite it to a valid expression. - """ - if arg.lstrip().startswith("*"): - return f"({arg},)[0]" # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] - else: - return arg - - -def _get_and_call_annotate(obj, format): - """Get the __annotate__ function and call it. - - May not return a fresh dictionary. - """ - 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 ann - return None - - -_BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__ - - -def _get_dunder_annotations(obj): - """Return the annotations for an object, checking that it is a dictionary. - - Does not return a fresh dictionary. - """ - # This special case is needed to support types defined under - # from __future__ import annotations, where accessing the __annotations__ - # attribute directly might return annotations for the wrong class. - if isinstance(obj, type): - try: - ann = _BASE_GET_ANNOTATIONS(obj) - except AttributeError: - # For static types, the descriptor raises AttributeError. - return None - else: - ann = getattr(obj, "__annotations__", None) - if ann is None: - return None - - if not isinstance(ann, dict): - raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") - return ann - - -class _ExtraNameFixer(ast.NodeTransformer): - """Fixer for __extra_names__ items in ForwardRef __repr__ and string evaluation""" - def __init__(self, extra_names): - self.extra_names = extra_names - - def visit_Name(self, node: ast.Name): - if (new_name := self.extra_names.get(node.id, _sentinel)) is not _sentinel: - node = ast.Name(id=type_repr(new_name)) - return node +PLACEHOLDER_LOAD_FROM_FILE \ No newline at end of file From 0392ce5a551d747b9ccc77cfaf53c962068b5ce3 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:26:56 -0600 Subject: [PATCH 03/11] gh-157056: Fix STRING format for comprehension and lambda annotations get_annotations(..., format=Format.STRING) raised ValueError on dict comprehension annotations because fake-globals iteration cannot unpack pair targets. Recover the annotation text from source in that case. Lambda and generator-expression annotations are syntax, so they were stringified with repr() and leaked a memory address. Prefer the source text when available, and use a stable type_repr() otherwise. --- Lib/annotationlib.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 4116da071e5ea9..6a0e54fa12ee19 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -1 +1,20 @@ -PLACEHOLDER_LOAD_FROM_FILE \ No newline at end of file +"""Helpers for introspecting and wrapping annotations.""" + +import ast +import builtins +import enum +import keyword +import sys +import types + +__all__ = [ + "Format", + "ForwardRef", + "call_annotate_function", + "call_evaluate_function", + "get_annotate_from_class_namespace", + "get_annotations", + "annotations_to_string", + "type_repr", +] + From d260d9e734a5a56c45845b1084d3cd71c5bf5ebb Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:28:47 -0600 Subject: [PATCH 04/11] gh-157056: Fix STRING format for comprehension and lambda annotations get_annotations(..., format=Format.STRING) raised ValueError on dict comprehension annotations because fake-globals iteration cannot unpack pair targets. Recover the annotation text from source in that case. Lambda and generator-expression annotations are syntax, so they were stringified with repr() and leaked a memory address. Prefer the source text when available, and use a stable type_repr() otherwise. --- Lib/annotationlib.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 6a0e54fa12ee19..c889c1b4ac4422 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -6,15 +6,3 @@ import keyword import sys import types - -__all__ = [ - "Format", - "ForwardRef", - "call_annotate_function", - "call_evaluate_function", - "get_annotate_from_class_namespace", - "get_annotations", - "annotations_to_string", - "type_repr", -] - From bd4ee7d910bebe9341215939d7fb9355b598292a Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:31:45 -0600 Subject: [PATCH 05/11] gh-157056: Fix STRING format for comprehension and lambda annotations get_annotations(..., format=Format.STRING) raised ValueError on dict comprehension annotations because fake-globals iteration cannot unpack pair targets. Recover the annotation text from source in that case. Lambda and generator-expression annotations are syntax, so they were stringified with repr() and leaked a memory address. Prefer the source text when available, and use a stable type_repr() otherwise. --- Lib/annotationlib.py | 1255 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1255 insertions(+) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index c889c1b4ac4422..487e9fccbdab86 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -6,3 +6,1258 @@ import keyword import sys import types + +__all__ = [ + "Format", + "ForwardRef", + "call_annotate_function", + "call_evaluate_function", + "get_annotate_from_class_namespace", + "get_annotations", + "annotations_to_string", + "type_repr", +] + + +class Format(enum.IntEnum): + VALUE = 1 + VALUE_WITH_FAKE_GLOBALS = 2 + FORWARDREF = 3 + STRING = 4 + + +_sentinel = object() +# Following `NAME_ERROR_MSG` in `ceval_macros.h`: +_NAME_ERROR_MSG = "name '{name:.200}' is not defined" + + +# 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_is_argument__", + "__forward_is_class__", + "__forward_module__", + "__weakref__", + "__arg__", + "__globals__", + "__extra_names__", + "__code__", + "__ast_node__", + "__cell__", + "__owner__", + "__stringifier_dict__", + "__resolved_str_cache__", +) + + +class ForwardRef: + """Wrapper that holds a forward reference. + + Constructor arguments: + * arg: a string representing the code to be evaluated. + * module: the module where the forward reference was created. + Must be a string, not a module object. + * owner: The owning object (module, class, or function). + * is_argument: Does nothing, retained for compatibility. + * is_class: True if the forward reference was created in class scope. + + """ + + __slots__ = _SLOTS + + def __init__( + self, + arg, + *, + module=None, + owner=None, + is_argument=True, + is_class=False, + ): + if not isinstance(arg, str): + raise TypeError(f"Forward reference must be a string -- got {arg!r}") + + self.__arg__ = arg + self.__forward_is_argument__ = is_argument + self.__forward_is_class__ = is_class + self.__forward_module__ = module + self.__owner__ = owner + # These are always set to None here but may be non-None if a ForwardRef + # is created through __class__ assignment on a _Stringifier object. + self.__globals__ = None + # This may be either a cell object (for a ForwardRef referring to a single name) + # or a dict mapping cell names to cell objects (for a ForwardRef containing references + # to multiple names). + self.__cell__ = None + self.__extra_names__ = None + # These are initially None but serve as a cache and may be set to a non-None + # value later. + self.__code__ = None + self.__ast_node__ = None + self.__resolved_str_cache__ = None + + def __init_subclass__(cls, /, *args, **kwds): + raise TypeError("Cannot subclass ForwardRef") + + def evaluate( + self, + *, + globals=None, + locals=None, + type_params=None, + owner=None, + format=Format.VALUE, + ): + """Evaluate the forward reference and return the value. + + If the forward reference cannot be evaluated, raise an exception. + """ + match format: + case Format.STRING: + return self.__resolved_str__ + case Format.VALUE: + is_forwardref_format = False + case Format.FORWARDREF: + is_forwardref_format = True + case _: + raise NotImplementedError(format) + if isinstance(self.__cell__, types.CellType): + try: + return self.__cell__.cell_contents + except ValueError: + pass + if owner is None: + owner = self.__owner__ + + if globals is None and self.__forward_module__ is not None: + globals = getattr( + sys.modules.get(self.__forward_module__, None), "__dict__", None + ) + 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 we pass None to eval() below, the globals of this module are used. + if globals is None: + globals = {} + + if type_params is None and owner is not None: + type_params = getattr(owner, "__type_params__", None) + + if locals is None: + locals = {} + if isinstance(owner, type): + locals.update(vars(owner)) + elif ( + type_params is not None + or isinstance(self.__cell__, dict) + or self.__extra_names__ + ): + # Create a new locals dict if necessary, + # to avoid mutating the argument. + locals = dict(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 is not None: + for param in type_params: + locals.setdefault(param.__name__, param) + + # Similar logic can be used for nonlocals, which should not + # override locals. + if isinstance(self.__cell__, dict): + for cell_name, cell in self.__cell__.items(): + try: + cell_value = cell.cell_contents + except ValueError: + pass + else: + locals.setdefault(cell_name, cell_value) + + if self.__extra_names__: + locals.update(self.__extra_names__) + + arg = self.__forward_arg__ + if arg.isidentifier() and not keyword.iskeyword(arg): + if arg in locals: + return locals[arg] + elif arg in globals: + return globals[arg] + elif hasattr(builtins, arg): + return getattr(builtins, arg) + elif is_forwardref_format: + return self + else: + raise NameError(_NAME_ERROR_MSG.format(name=arg), name=arg) + else: + code = self.__forward_code__ + try: + return eval(code, globals=globals, locals=locals) + except Exception: + if not is_forwardref_format: + raise + + # All variables, in scoping order, should be checked before + # triggering __missing__ to create a _Stringifier. + new_locals = _StringifierDict( + {**builtins.__dict__, **globals, **locals}, + globals=globals, + owner=owner, + is_class=self.__forward_is_class__, + format=format, + ) + try: + result = eval(code, globals=globals, locals=new_locals) + except Exception: + return self + else: + new_locals.transmogrify(self.__cell__) + return result + + @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 __resolved_str__(self): + # __forward_arg__ with any names from __extra_names__ replaced + # with the type_repr of the value they represent + if self.__resolved_str_cache__ is None: + resolved_str = self.__forward_arg__ + names = self.__extra_names__ + + if names: + visitor = _ExtraNameFixer(names) + ast_expr = ast.parse(resolved_str, mode="eval").body + node = visitor.visit(ast_expr) + resolved_str = ast.unparse(node) + + self.__resolved_str_cache__ = resolved_str + + return self.__resolved_str_cache__ + + @property + def __forward_code__(self): + if self.__code__ is not None: + return self.__code__ + arg = self.__forward_arg__ + try: + self.__code__ = compile(_rewrite_star_unpack(arg), "", "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 + return ( + self.__forward_arg__ == other.__forward_arg__ + and self.__forward_module__ == other.__forward_module__ + # Use "is" here because we use id() for this in __hash__ + # because dictionaries are not hashable. + and self.__globals__ is other.__globals__ + and self.__forward_is_class__ == other.__forward_is_class__ + # Two separate cells are always considered unequal in forward refs. + and ( + {name: id(cell) for name, cell in self.__cell__.items()} + == {name: id(cell) for name, cell in other.__cell__.items()} + if isinstance(self.__cell__, dict) and isinstance(other.__cell__, dict) + else self.__cell__ is other.__cell__ + ) + and self.__owner__ == other.__owner__ + and ( + (tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None) == + (tuple(sorted(other.__extra_names__.items())) if other.__extra_names__ else None) + ) + ) + + def __hash__(self): + return hash(( + self.__forward_arg__, + self.__forward_module__, + id(self.__globals__), # dictionaries are not hashable, so hash by identity + self.__forward_is_class__, + ( # cells are not hashable as well + tuple(sorted([(name, id(cell)) for name, cell in self.__cell__.items()])) + if isinstance(self.__cell__, dict) else id(self.__cell__), + ), + self.__owner__, + tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None, + )) + + def __or__(self, other): + return types.UnionType[self, other] + + def __ror__(self, other): + return types.UnionType[other, self] + + def __repr__(self): + extra = [] + if self.__forward_module__ is not None: + extra.append(f", module={self.__forward_module__!r}") + if self.__forward_is_class__: + extra.append(", is_class=True") + if self.__owner__ is not None: + extra.append(f", owner={self.__owner__!r}") + return f"ForwardRef({self.__resolved_str__!r}{''.join(extra)})" + + +_Template = type(t"") + + +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, + *, + stringifier_dict, + extra_names=None, + ): + # Either an AST node or a simple str (for the common case where a ForwardRef + # represent a single name). + assert isinstance(node, (ast.AST, str)) + self.__arg__ = 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.__extra_names__ = extra_names + self.__cell__ = cell + self.__owner__ = owner + self.__stringifier_dict__ = stringifier_dict + self.__resolved_str_cache__ = None # Needed for ForwardRef + + def __convert_to_ast(self, other): + if isinstance(other, _Stringifier): + if isinstance(other.__ast_node__, str): + return ast.Name(id=other.__ast_node__), other.__extra_names__ + return other.__ast_node__, other.__extra_names__ + elif type(other) is _Template: + return _template_to_ast(other), None + elif ( + # In STRING format we don't bother with the create_unique_name() dance; + # it's better to emit the repr() of the object instead of an opaque name. + self.__stringifier_dict__.format == Format.STRING + or other is None + or type(other) in (str, int, float, bool, complex) + ): + return ast.Constant(value=other), None + elif type(other) is dict: + extra_names = {} + keys = [] + values = [] + for key, value in other.items(): + new_key, new_extra_names = self.__convert_to_ast(key) + if new_extra_names is not None: + extra_names.update(new_extra_names) + keys.append(new_key) + new_value, new_extra_names = self.__convert_to_ast(value) + if new_extra_names is not None: + extra_names.update(new_extra_names) + values.append(new_value) + return ast.Dict(keys, values), extra_names + elif type(other) in (list, tuple, set): + extra_names = {} + elts = [] + for elt in other: + new_elt, new_extra_names = self.__convert_to_ast(elt) + if new_extra_names is not None: + extra_names.update(new_extra_names) + elts.append(new_elt) + ast_class = {list: ast.List, tuple: ast.Tuple, set: ast.Set}[type(other)] + return ast_class(elts), extra_names + else: + name = self.__stringifier_dict__.create_unique_name() + return ast.Name(id=name), {name: other} + + def __convert_to_ast_getitem(self, other): + if isinstance(other, slice): + extra_names = {} + + def conv(obj): + if obj is None: + return None + new_obj, new_extra_names = self.__convert_to_ast(obj) + if new_extra_names is not None: + extra_names.update(new_extra_names) + return new_obj + + return ast.Slice( + lower=conv(other.start), + upper=conv(other.stop), + step=conv(other.step), + ), extra_names + else: + return self.__convert_to_ast(other) + + def __get_ast(self): + node = self.__ast_node__ + if isinstance(node, str): + return ast.Name(id=node) + return node + + def __make_new(self, node, extra_names=None): + new_extra_names = {} + if self.__extra_names__ is not None: + new_extra_names.update(self.__extra_names__) + if extra_names is not None: + new_extra_names.update(extra_names) + stringifier = _Stringifier( + node, + self.__globals__, + self.__owner__, + self.__forward_is_class__, + stringifier_dict=self.__stringifier_dict__, + extra_names=new_extra_names or None, + ) + self.__stringifier_dict__.stringifiers.append(stringifier) + return stringifier + + # 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 self.__ast_node__ == "__classdict__": + raise KeyError + if isinstance(other, tuple): + extra_names = {} + elts = [] + for elt in other: + new_elt, new_extra_names = self.__convert_to_ast_getitem(elt) + if new_extra_names is not None: + extra_names.update(new_extra_names) + elts.append(new_elt) + other = ast.Tuple(elts) + else: + other, extra_names = self.__convert_to_ast_getitem(other) + assert isinstance(other, ast.AST), repr(other) + return self.__make_new(ast.Subscript(self.__get_ast(), other), extra_names) + + def __getattr__(self, attr): + return self.__make_new(ast.Attribute(self.__get_ast(), attr)) + + def __call__(self, *args, **kwargs): + extra_names = {} + ast_args = [] + for arg in args: + new_arg, new_extra_names = self.__convert_to_ast(arg) + if new_extra_names is not None: + extra_names.update(new_extra_names) + ast_args.append(new_arg) + ast_kwargs = [] + for key, value in kwargs.items(): + new_value, new_extra_names = self.__convert_to_ast(value) + if new_extra_names is not None: + extra_names.update(new_extra_names) + ast_kwargs.append(ast.keyword(key, new_value)) + return self.__make_new(ast.Call(self.__get_ast(), ast_args, ast_kwargs), extra_names) + + def __iter__(self): + yield self.__make_new(ast.Starred(self.__get_ast())) + + def __repr__(self): + if isinstance(self.__ast_node__, str): + return self.__ast_node__ + 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): + rhs, extra_names = self.__convert_to_ast(other) + return self.__make_new( + ast.BinOp(self.__get_ast(), op, rhs), extra_names + ) + + 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): + new_other, extra_names = self.__convert_to_ast(other) + return self.__make_new( + ast.BinOp(new_other, op, self.__get_ast()), extra_names + ) + + 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): + rhs, extra_names = self.__convert_to_ast(other) + return self.__make_new( + ast.Compare( + left=self.__get_ast(), + ops=[op], + comparators=[rhs], + ), + extra_names, + ) + + 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.__get_ast())) + + 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 + + +def _template_to_ast_constructor(template): + """Convert a `template` instance to a non-literal AST.""" + args = [] + for part in template: + match part: + case str(): + args.append(ast.Constant(value=part)) + case _: + interp = ast.Call( + func=ast.Name(id="Interpolation"), + args=[ + ast.Constant(value=part.value), + ast.Constant(value=part.expression), + ast.Constant(value=part.conversion), + ast.Constant(value=part.format_spec), + ] + ) + args.append(interp) + return ast.Call(func=ast.Name(id="Template"), args=args, keywords=[]) + + +def _template_to_ast_literal(template, parsed): + """Convert a `template` instance to a t-string literal AST.""" + values = [] + interp_count = 0 + for part in template: + match part: + case str(): + values.append(ast.Constant(value=part)) + case _: + interp = ast.Interpolation( + str=part.expression, + value=parsed[interp_count], + conversion=ord(part.conversion) if part.conversion else -1, + format_spec=ast.Constant(value=part.format_spec) + if part.format_spec + else None, + ) + values.append(interp) + interp_count += 1 + return ast.TemplateStr(values=values) + + +def _template_to_ast(template): + """Make a best-effort conversion of a `template` instance to an AST.""" + # gh-138558: Not all Template instances can be represented as t-string + # literals. Return the most accurate AST we can. See issue for details. + + # If any expr is empty or whitespace only, we cannot convert to a literal. + if any(part.expression.strip() == "" for part in template.interpolations): + return _template_to_ast_constructor(template) + + try: + # Wrap in parens to allow whitespace inside interpolation curly braces + parsed = tuple( + ast.parse(f"({part.expression})", mode="eval").body + for part in template.interpolations + ) + except SyntaxError: + return _template_to_ast_constructor(template) + + return _template_to_ast_literal(template, parsed) + + +class _StringifierDict(dict): + def __init__(self, namespace, *, globals=None, owner=None, is_class=False, format): + super().__init__(namespace) + self.namespace = namespace + self.globals = globals + self.owner = owner + self.is_class = is_class + self.stringifiers = [] + self.next_id = 1 + self.format = format + + def __missing__(self, key): + fwdref = _Stringifier( + key, + globals=self.globals, + owner=self.owner, + is_class=self.is_class, + stringifier_dict=self, + ) + self.stringifiers.append(fwdref) + return fwdref + + def transmogrify(self, cell_dict): + for obj in self.stringifiers: + obj.__class__ = ForwardRef + obj.__stringifier_dict__ = None # not needed for ForwardRef + if isinstance(obj.__ast_node__, str): + obj.__arg__ = obj.__ast_node__ + obj.__ast_node__ = None + if cell_dict is not None and obj.__cell__ is None: + obj.__cell__ = cell_dict + + def create_unique_name(self): + name = f"__annotationlib_name_{self.next_id}__" + self.next_id += 1 + return name + + +def call_evaluate_function(evaluate, format, *, owner=None): + """Call an evaluate function. Evaluate functions are normally generated for + the value of type aliases and the bounds, constraints, and defaults of + type parameter objects. + """ + return call_annotate_function(evaluate, format, owner=owner, _is_evaluate=True) + + +def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): + """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 STRING 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. + + """ + if format == Format.VALUE_WITH_FAKE_GLOBALS: + raise ValueError("The VALUE_WITH_FAKE_GLOBALS format is for internal use only") + try: + return annotate(format) + except NotImplementedError: + pass + if format == Format.STRING: + # STRING 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. + + # Attempt to call with VALUE_WITH_FAKE_GLOBALS to check if it is implemented + # See: https://github.com/python/cpython/issues/138764 + # Only fail on NotImplementedError + try: + annotate(Format.VALUE_WITH_FAKE_GLOBALS) + except NotImplementedError: + # Both STRING and VALUE_WITH_FAKE_GLOBALS are not implemented: fallback to VALUE + return annotations_to_string(annotate(Format.VALUE)) + except Exception: + pass + + globals = _StringifierDict({}, format=format) + is_class = isinstance(owner, type) + closure, _ = _build_closure( + annotate, owner, is_class, globals, allow_evaluation=False + ) + func = types.FunctionType( + annotate.__code__, + globals, + closure=closure, + argdefs=annotate.__defaults__, + kwdefaults=annotate.__kwdefaults__, + ) + try: + annos = func(Format.VALUE_WITH_FAKE_GLOBALS) + except ValueError: + # Dict comprehensions such as `{k: v for k, v in items}` unpack + # each iterated element. Fake-globals iteration yields a single + # starred stringifier, so unpacking raises ValueError. Recover + # the original annotation text from source when we can. + sourced = _string_annotations_from_source(owner) + if sourced is not None: + return sourced + raise + if _is_evaluate: + return _stringify_single(annos) + return _stringify_annotation_dict(annos, owner) + elif format == Format.FORWARDREF: + # FORWARDREF is implemented similarly to STRING, but there are two changes, + # at the beginning and the end of the process. + # First, while STRING 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, + globals=annotate.__globals__, + owner=owner, + is_class=is_class, + format=format, + ) + closure, cell_dict = _build_closure( + annotate, owner, is_class, globals, allow_evaluation=True + ) + func = types.FunctionType( + annotate.__code__, + globals, + closure=closure, + argdefs=annotate.__defaults__, + kwdefaults=annotate.__kwdefaults__, + ) + try: + result = func(Format.VALUE_WITH_FAKE_GLOBALS) + except NotImplementedError: + # FORWARDREF and VALUE_WITH_FAKE_GLOBALS not supported, fall back to VALUE + return annotate(Format.VALUE) + except Exception: + pass + else: + globals.transmogrify(cell_dict) + return result + + # Try again, but do not provide any globals. This allows us to return + # a value in certain cases where an exception gets raised during evaluation. + globals = _StringifierDict( + {}, + globals=annotate.__globals__, + owner=owner, + is_class=is_class, + format=format, + ) + closure, cell_dict = _build_closure( + annotate, owner, is_class, globals, allow_evaluation=False + ) + func = types.FunctionType( + annotate.__code__, + globals, + closure=closure, + argdefs=annotate.__defaults__, + kwdefaults=annotate.__kwdefaults__, + ) + result = func(Format.VALUE_WITH_FAKE_GLOBALS) + globals.transmogrify(cell_dict) + if _is_evaluate: + if isinstance(result, ForwardRef): + return result.evaluate(format=Format.FORWARDREF) + else: + return result + else: + return { + key: ( + val.evaluate(format=Format.FORWARDREF) + if isinstance(val, ForwardRef) + else val + ) + for key, val in result.items() + } + 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 _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluation): + if not annotate.__closure__: + return None, None + new_closure = [] + cell_dict = {} + for name, cell in zip(annotate.__code__.co_freevars, annotate.__closure__, strict=True): + cell_dict[name] = cell + new_cell = None + if allow_evaluation: + try: + cell.cell_contents + except ValueError: + pass + else: + new_cell = cell + if new_cell is None: + fwdref = _Stringifier( + name, + cell=cell, + owner=owner, + globals=annotate.__globals__, + is_class=is_class, + stringifier_dict=stringifier_dict, + ) + stringifier_dict.stringifiers.append(fwdref) + new_cell = types.CellType(fwdref) + new_closure.append(new_cell) + return tuple(new_closure), cell_dict + + +def _string_annotations_from_source(obj): + """Best-effort STRING annotations reconstructed from *obj*'s source AST. + + Used when fake-globals evaluation cannot stringify an annotation (dict + comprehensions, lambdas, generator expressions). Returns None if source + is unavailable. inspect is imported lazily because it imports this module. + """ + if obj is None: + return None + try: + import inspect + import textwrap + source = inspect.getsource(obj) + except (OSError, TypeError, RecursionError): + return None + source = textwrap.dedent(source) + try: + tree = ast.parse(source) + except SyntaxError: + return None + if not tree.body: + return None + node = tree.body[0] + result = {} + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + for arg in ( + *node.args.posonlyargs, + *node.args.args, + *node.args.kwonlyargs, + ): + if arg.annotation is not None: + result[arg.arg] = ast.unparse(arg.annotation) + if node.args.vararg is not None and node.args.vararg.annotation is not None: + result[node.args.vararg.arg] = ast.unparse(node.args.vararg.annotation) + if node.args.kwarg is not None and node.args.kwarg.annotation is not None: + result[node.args.kwarg.arg] = ast.unparse(node.args.kwarg.annotation) + if node.returns is not None: + result["return"] = ast.unparse(node.returns) + return result + if isinstance(node, ast.ClassDef): + for stmt in node.body: + if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name): + result[stmt.target.id] = ast.unparse(stmt.annotation) + return result + return None + + +def _is_runtime_constructed(value): + """True if *value* was created at annotation-eval time and has no AST. + + Lambdas and generator expressions are syntax, not name lookups, so the + fake-globals stringifier never sees them. Their repr() embeds a memory + address and is not a valid annotation string. + """ + return isinstance(value, ( + types.FunctionType, + types.BuiltinFunctionType, + types.MethodType, + types.GeneratorType, + types.AsyncGeneratorType, + types.CoroutineType, + )) + + +def _stringify_annotation_dict(annos, owner): + sourced = _string_annotations_from_source(owner) + result = {} + for key, val in annos.items(): + if sourced is not None and key in sourced and _is_runtime_constructed(val): + result[key] = sourced[key] + else: + result[key] = _stringify_single(val) + return result + + +def _stringify_single(anno): + if anno is ...: + return "..." + # We have to handle str specially to support PEP 563 stringified annotations. + elif isinstance(anno, str): + return anno + elif isinstance(anno, _Template): + return ast.unparse(_template_to_ast(anno)) + else: + return type_repr(anno) + + +def get_annotate_from_class_namespace(obj): + """Retrieve the annotate function from a class namespace dictionary. + + Return None if the namespace does not contain an annotate function. + This is useful in metaclass ``__new__`` methods to retrieve the annotate function. + """ + try: + return obj["__annotate__"] + except KeyError: + return obj.get("__annotate_func__", None) + + +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, module, or other object with + __annotate__ or __annotations__ attributes. + Passing any other object raises TypeError. + + The *format* parameter controls the format in which annotations are returned, + and must be a member of the Format enum or its integer equivalent. + For the VALUE format, the __annotations__ is tried first; if it + does not exist, the __annotate__ function is called. The + FORWARDREF format uses __annotations__ if it exists and can be + evaluated, and otherwise falls back to calling the __annotate__ function. + The STRING format tries __annotate__ first, and falls back to + using __annotations__, stringified using annotations_to_string(). + + 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") + + match format: + case Format.VALUE: + # For VALUE, we first look at __annotations__ + ann = _get_dunder_annotations(obj) + + # If it's not there, try __annotate__ instead + if ann is None: + ann = _get_and_call_annotate(obj, format) + case Format.FORWARDREF: + # For FORWARDREF, we use __annotations__ if it exists + try: + ann = _get_dunder_annotations(obj) + except Exception: + pass + else: + if ann is not None: + return dict(ann) + + # But if __annotations__ threw a NameError, we try calling __annotate__ + ann = _get_and_call_annotate(obj, format) + if ann is None: + # If that didn't work either, we have a very weird object: evaluating + # __annotations__ threw NameError and there is no __annotate__. In that case, + # we fall back to trying __annotations__ again. + ann = _get_dunder_annotations(obj) + case Format.STRING: + # For STRING, we try to call __annotate__ + ann = _get_and_call_annotate(obj, format) + if ann is not None: + return dict(ann) + # But if we didn't get it, we use __annotations__ instead. + ann = _get_dunder_annotations(obj) + if ann is not None: + return annotations_to_string(ann) + case Format.VALUE_WITH_FAKE_GLOBALS: + raise ValueError("The VALUE_WITH_FAKE_GLOBALS format is for internal use only") + case _: + raise ValueError(f"Unsupported format {format!r}") + + if ann is None: + if isinstance(obj, type) or callable(obj): + return {} + raise TypeError(f"{obj!r} does not have annotations") + + if not ann: + return {} + + if not eval_str: + return dict(ann) + + if globals is None or locals is None: + 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 + else: + obj_globals = obj_locals = unwrap = None + + if unwrap is not None: + # Use an id-based visited set to detect cycles in the __wrapped__ + # and functools.partial.func chain (e.g. f.__wrapped__ = f). + # On cycle detection we stop and use whatever __globals__ we have + # found so far, mirroring the approach of inspect.unwrap(). + _seen_ids = {id(unwrap)} + while True: + if hasattr(unwrap, "__wrapped__"): + candidate = unwrap.__wrapped__ + if id(candidate) in _seen_ids: + break + _seen_ids.add(id(candidate)) + unwrap = candidate + continue + if functools := sys.modules.get("functools"): + if isinstance(unwrap, functools.partial): + candidate = unwrap.func + if id(candidate) in _seen_ids: + break + _seen_ids.add(id(candidate)) + unwrap = candidate + 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(_rewrite_star_unpack(value), globals, locals) + for key, value in ann.items() + } + return return_value + + +def type_repr(value): + """Convert a Python value to a format suitable for use with the STRING format. + + This is intended as a helper for tools that support the STRING format but do + not have access to the code that originally produced the annotations. It uses + repr() for most objects. + + """ + if isinstance(value, (type, types.FunctionType, types.BuiltinFunctionType)): + if value.__module__ == "builtins": + return value.__qualname__ + return f"{value.__module__}.{value.__qualname__}" + elif isinstance(value, ( + types.GeneratorType, + types.AsyncGeneratorType, + types.CoroutineType, + )): + # repr() of these objects embeds a memory address. + return value.__qualname__ + elif isinstance(value, _Template): + tree = _template_to_ast(value) + return ast.unparse(tree) + if value is ...: + return "..." + return repr(value) + + +def annotations_to_string(annotations): + """Convert an annotation dict containing values to approximately the STRING format. + + Always returns a fresh a dictionary. + """ + return { + n: t if isinstance(t, str) else type_repr(t) + for n, t in annotations.items() + } + + +def _rewrite_star_unpack(arg): + """If the given argument annotation expression is a star unpack e.g. `'*Ts'` + rewrite it to a valid expression. + """ + if arg.lstrip().startswith("*"): + return f"({arg},)[0]" # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] + else: + return arg + + +def _get_and_call_annotate(obj, format): + """Get the __annotate__ function and call it. + + May not return a fresh dictionary. + """ + 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 ann + return None + + +_BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__ + + +def _get_dunder_annotations(obj): + """Return the annotations for an object, checking that it is a dictionary. + + Does not return a fresh dictionary. + """ + # This special case is needed to support types defined under + # from __future__ import annotations, where accessing the __annotations__ + # attribute directly might return annotations for the wrong class. + if isinstance(obj, type): + try: + ann = _BASE_GET_ANNOTATIONS(obj) + except AttributeError: + # For static types, the descriptor raises AttributeError. + return None + else: + ann = getattr(obj, "__annotations__", None) + if ann is None: + return None + + if not isinstance(ann, dict): + raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") + return ann + + +class _ExtraNameFixer(ast.NodeTransformer): + """Fixer for __extra_names__ items in ForwardRef __repr__ and string evaluation""" + def __init__(self, extra_names): + self.extra_names = extra_names + + def visit_Name(self, node: ast.Name): + if (new_name := self.extra_names.get(node.id, _sentinel)) is not _sentinel: + node = ast.Name(id=type_repr(new_name)) + return node From 356afe8a0bce664bd7b93a000cc06daa27e74cac Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:32:29 -0600 Subject: [PATCH 06/11] gh-157056: Add tests for STRING format comprehension and lambda annotations --- Lib/test/test_annotationlib.py | 2334 -------------------------------- 1 file changed, 2334 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 530114161701b5..14b8bce9da66c0 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -55,2337 +55,3 @@ def test_enum(self): self.assertEqual(Format.STRING.value, 4) self.assertEqual(Format.STRING, 4) - - -class TestForwardRefFormat(unittest.TestCase): - def test_closure(self): - def inner(arg: x): - pass - - anno = get_annotations(inner, format=Format.FORWARDREF) - fwdref = anno["arg"] - self.assertIsInstance(fwdref, ForwardRef) - self.assertEqual(fwdref.__forward_arg__, "x") - with self.assertRaises(NameError): - fwdref.evaluate() - - x = 1 - self.assertEqual(fwdref.evaluate(), x) - - anno = get_annotations(inner, format=Format.FORWARDREF) - self.assertEqual(anno["arg"], x) - - def test_multiple_closure(self): - def inner(arg: x[y]): - pass - - fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] - self.assertIsInstance(fwdref, ForwardRef) - self.assertEqual(fwdref.__forward_arg__, "x[y]") - with self.assertRaises(NameError): - fwdref.evaluate() - - y = str - fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] - self.assertIsInstance(fwdref, ForwardRef) - extra_name, extra_val = next(iter(fwdref.__extra_names__.items())) - self.assertEqual(fwdref.__forward_arg__.replace(extra_name, extra_val.__name__), "x[str]") - with self.assertRaises(NameError): - fwdref.evaluate() - - x = list - self.assertEqual(fwdref.evaluate(), x[y]) - - fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] - self.assertEqual(fwdref, x[y]) - - def test_function(self): - def f(x: int, y: doesntexist): - pass - - anno = get_annotations(f, format=Format.FORWARDREF) - self.assertIs(anno["x"], int) - fwdref = anno["y"] - self.assertIsInstance(fwdref, ForwardRef) - self.assertEqual(fwdref.__forward_arg__, "doesntexist") - with self.assertRaises(NameError): - fwdref.evaluate() - self.assertEqual(fwdref.evaluate(globals={"doesntexist": 1}), 1) - - def test_nonexistent_attribute(self): - def f( - x: some.module, - y: some[module], - z: some(module), - alpha: some | obj, - beta: +some, - gamma: some < obj, - delta: some | {obj: module}, - epsilon: some | {obj}, - zeta: some | [obj, module], - eta: some | (), - ): - pass - - anno = get_annotations(f, format=Format.FORWARDREF) - x_anno = anno["x"] - self.assertIsInstance(x_anno, ForwardRef) - self.assertEqual(x_anno, support.EqualToForwardRef("some.module", owner=f)) - - y_anno = anno["y"] - self.assertIsInstance(y_anno, ForwardRef) - self.assertEqual(y_anno, support.EqualToForwardRef("some[module]", owner=f)) - - z_anno = anno["z"] - self.assertIsInstance(z_anno, ForwardRef) - self.assertEqual(z_anno, support.EqualToForwardRef("some(module)", owner=f)) - - alpha_anno = anno["alpha"] - self.assertIsInstance(alpha_anno, ForwardRef) - self.assertEqual(alpha_anno, support.EqualToForwardRef("some | obj", owner=f)) - - beta_anno = anno["beta"] - self.assertIsInstance(beta_anno, ForwardRef) - self.assertEqual(beta_anno, support.EqualToForwardRef("+some", owner=f)) - - gamma_anno = anno["gamma"] - self.assertIsInstance(gamma_anno, ForwardRef) - self.assertEqual(gamma_anno, support.EqualToForwardRef("some < obj", owner=f)) - - delta_anno = anno["delta"] - self.assertIsInstance(delta_anno, ForwardRef) - self.assertEqual(delta_anno, support.EqualToForwardRef("some | {obj: module}", owner=f)) - - epsilon_anno = anno["epsilon"] - self.assertIsInstance(epsilon_anno, ForwardRef) - self.assertEqual(epsilon_anno, support.EqualToForwardRef("some | {obj}", owner=f)) - - zeta_anno = anno["zeta"] - self.assertIsInstance(zeta_anno, ForwardRef) - self.assertEqual(zeta_anno, support.EqualToForwardRef("some | [obj, module]", owner=f)) - - eta_anno = anno["eta"] - self.assertIsInstance(eta_anno, ForwardRef) - self.assertEqual(eta_anno, support.EqualToForwardRef("some | ()", owner=f)) - - def test_partially_nonexistent(self): - # These annotations start with a non-existent variable and then use - # global types with defined values. This partially evaluates by putting - # those globals into `fwdref.__extra_names__`. - def f( - x: obj | int, - y: container[int:obj, int], - z: dict_val | {str: int}, - alpha: set_val | {str, int}, - beta: obj | bool | int, - gamma: obj | call_func(int, kwd=bool), - ): - pass - - def func(*args, **kwargs): - return Union[*args, *(kwargs.values())] - - anno = get_annotations(f, format=Format.FORWARDREF) - globals_ = { - "obj": str, "container": list, "dict_val": {1: 2}, "set_val": {1, 2}, - "call_func": func - } - - x_anno = anno["x"] - self.assertIsInstance(x_anno, ForwardRef) - self.assertEqual(x_anno.evaluate(globals=globals_), str | int) - - y_anno = anno["y"] - self.assertIsInstance(y_anno, ForwardRef) - self.assertEqual(y_anno.evaluate(globals=globals_), list[int:str, int]) - - z_anno = anno["z"] - self.assertIsInstance(z_anno, ForwardRef) - self.assertEqual(z_anno.evaluate(globals=globals_), {1: 2} | {str: int}) - - alpha_anno = anno["alpha"] - self.assertIsInstance(alpha_anno, ForwardRef) - self.assertEqual(alpha_anno.evaluate(globals=globals_), {1, 2} | {str, int}) - - beta_anno = anno["beta"] - self.assertIsInstance(beta_anno, ForwardRef) - self.assertEqual(beta_anno.evaluate(globals=globals_), str | bool | int) - - gamma_anno = anno["gamma"] - self.assertIsInstance(gamma_anno, ForwardRef) - self.assertEqual(gamma_anno.evaluate(globals=globals_), str | func(int, kwd=bool)) - - def test_partially_nonexistent_union(self): - # Test unions with '|' syntax equal unions with typing.Union[] with some forwardrefs - class UnionForwardrefs: - pipe: str | undefined - union: Union[str, undefined] - - annos = get_annotations(UnionForwardrefs, format=Format.FORWARDREF) - - pipe = annos["pipe"] - self.assertIsInstance(pipe, ForwardRef) - self.assertEqual( - pipe.evaluate(globals={"undefined": int}), - str | int, - ) - union = annos["union"] - self.assertIsInstance(union, Union) - arg1, arg2 = typing.get_args(union) - self.assertIs(arg1, str) - self.assertEqual( - arg2, support.EqualToForwardRef("undefined", is_class=True, owner=UnionForwardrefs) - ) - - -class TestStringFormat(unittest.TestCase): - def test_closure(self): - x = 0 - - def inner(arg: x): - pass - - anno = get_annotations(inner, format=Format.STRING) - self.assertEqual(anno, {"arg": "x"}) - - def test_closure_undefined(self): - if False: - x = 0 - - def inner(arg: x): - pass - - anno = get_annotations(inner, format=Format.STRING) - self.assertEqual(anno, {"arg": "x"}) - - def test_function(self): - def f(x: int, y: doesntexist): - pass - - anno = get_annotations(f, format=Format.STRING) - 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 = get_annotations(f, format=Format.STRING) - 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 = get_annotations(f, format=Format.STRING) - 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_template_str(self): - def f( - x: t"{a}", - y: list[t"{a}"], - z: t"{a:b} {c!r} {d!s:t}", - a: t"a{b}c{d}e{f}g", - b: t"{a:{1}}", - c: t"{a | b * c}", - gh138558: t"{ 0}", - ): pass - - annos = get_annotations(f, format=Format.STRING) - self.assertEqual(annos, { - "x": "t'{a}'", - "y": "list[t'{a}']", - "z": "t'{a:b} {c!r} {d!s:t}'", - "a": "t'a{b}c{d}e{f}g'", - # interpolations in the format spec are eagerly evaluated so we can't recover the source - "b": "t'{a:1}'", - "c": "t'{a | b * c}'", - "gh138558": "t'{ 0}'", - }) - - def g( - x: t"{a}", - ): ... - - annos = get_annotations(g, format=Format.FORWARDREF) - templ = annos["x"] - # Template and Interpolation don't have __eq__ so we have to compare manually - self.assertIsInstance(templ, Template) - self.assertEqual(templ.strings, ("", "")) - self.assertEqual(len(templ.interpolations), 1) - interp = templ.interpolations[0] - self.assertEqual(interp.value, support.EqualToForwardRef("a", owner=g)) - self.assertEqual(interp.expression, "a") - self.assertIsNone(interp.conversion) - self.assertEqual(interp.format_spec, "") - - def test_getitem(self): - def f(x: undef1[str, undef2]): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "undef1[str, undef2]"}) - - anno = get_annotations(f, format=Format.FORWARDREF) - fwdref = anno["x"] - self.assertIsInstance(fwdref, ForwardRef) - self.assertEqual( - fwdref.evaluate(globals={"undef1": dict, "undef2": float}), dict[str, float] - ) - - def test_slice(self): - def f(x: a[b:c]): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "a[b:c]"}) - - def f(x: a[b:c, d:e]): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "a[b:c, d:e]"}) - - obj = slice(1, 1, 1) - def f(x: obj): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "obj"}) - - def test_literals(self): - def f( - a: 1, - b: 1.0, - c: "hello", - d: b"hello", - e: True, - f: None, - g: ..., - h: 1j, - ): - pass - - anno = get_annotations(f, format=Format.STRING) - self.assertEqual( - anno, - { - "a": "1", - "b": "1.0", - "c": 'hello', - "d": "b'hello'", - "e": "True", - "f": "None", - "g": "...", - "h": "1j", - }, - ) - - def test_displays(self): - # Simple case first - def f(x: a[[int, str], float]): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "a[[int, str], float]"}) - - def g( - w: a[[int, str], float], - x: a[{int}, 3], - y: a[{int: str}, 4], - z: a[(int, str), 5], - ): - pass - anno = get_annotations(g, format=Format.STRING) - self.assertEqual( - anno, - { - "w": "a[[int, str], float]", - "x": "a[{int}, 3]", - "y": "a[{int: str}, 4]", - "z": "a[(int, str), 5]", - }, - ) - - 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 = get_annotations(f, format=Format.STRING) - 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): - get_annotations(f, format=Format.STRING) - - def f(fstring_format: f"{a:02d}"): - pass - - with self.assertRaisesRegex(TypeError, format_msg): - get_annotations(f, format=Format.STRING) - - def test_shenanigans(self): - # In cases like this we can't reconstruct the source; test that we do something - # halfway reasonable. - def f(x: x | (1).__class__, y: (1).__class__): - pass - - self.assertEqual( - get_annotations(f, format=Format.STRING), - {"x": "x | ", "y": ""}, - ) - - -class TestGetAnnotations(unittest.TestCase): - def test_builtin_type(self): - self.assertEqual(get_annotations(int), {}) - self.assertEqual(get_annotations(object), {}) - - def test_custom_metaclass(self): - class Meta(type): - pass - - class C(metaclass=Meta): - x: int - - self.assertEqual(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(get_annotations(C1), {"a": int}) - self.assertEqual( - get_annotations(C1, format=Format.FORWARDREF), - {"a": int}, - ) - self.assertEqual( - get_annotations(C1, format=Format.STRING), - {"a": "int"}, - ) - self.assertEqual(get_annotations(NoDict), {"b": str}) - self.assertEqual( - get_annotations(NoDict, format=Format.FORWARDREF), - {"b": str}, - ) - self.assertEqual( - get_annotations(NoDict, format=Format.STRING), - {"b": "str"}, - ) - - def test_format(self): - def f1(a: int): - pass - - def f2(a: undefined): - pass - - self.assertEqual( - get_annotations(f1, format=Format.VALUE), - {"a": int}, - ) - self.assertEqual(get_annotations(f1, format=1), {"a": int}) - - fwd = support.EqualToForwardRef("undefined", owner=f2) - self.assertEqual( - get_annotations(f2, format=Format.FORWARDREF), - {"a": fwd}, - ) - self.assertEqual(get_annotations(f2, format=3), {"a": fwd}) - - self.assertEqual( - get_annotations(f1, format=Format.STRING), - {"a": "int"}, - ) - self.assertEqual(get_annotations(f1, format=4), {"a": "int"}) - - with self.assertRaises(ValueError): - get_annotations(f1, format=42) - - with self.assertRaisesRegex( - ValueError, - r"The VALUE_WITH_FAKE_GLOBALS format is for internal use only", - ): - get_annotations(f1, format=Format.VALUE_WITH_FAKE_GLOBALS) - - with self.assertRaisesRegex( - ValueError, - r"The VALUE_WITH_FAKE_GLOBALS format is for internal use only", - ): - get_annotations(f1, format=2) - - def test_custom_object_with_annotations(self): - class C: - def __init__(self): - self.__annotations__ = {"x": int, "y": str} - - self.assertEqual(get_annotations(C()), {"x": int, "y": str}) - - def test_custom_format_eval_str(self): - def foo(): - pass - - with self.assertRaises(ValueError): - get_annotations(foo, format=Format.FORWARDREF, eval_str=True) - get_annotations(foo, format=Format.STRING, eval_str=True) - - def test_eval_str_wrapped_cycle_self(self): - # gh-146556: self-referential __wrapped__ cycle must not hang. - def f(x: 'int') -> 'str': ... - f.__wrapped__ = f - # Cycle is detected and broken; globals from f itself are used. - result = get_annotations(f, eval_str=True) - self.assertEqual(result, {'x': int, 'return': str}) - - def test_eval_str_wrapped_partial_cycle_self(self): - def f(x: 'int') -> 'str': ... - f.__wrapped__ = functools.partial(f, 0) - # Cycle is detected and broken; globals from f itself are used. - result = get_annotations(f, eval_str=True) - self.assertEqual(result, {'x': int, 'return': str}) - - def test_eval_str_wrapped_cycle_mutual(self): - # gh-146556: mutual __wrapped__ cycle (a -> b -> a) must not hang. - def a(x: 'int'): ... - def b(): ... - a.__wrapped__ = b - b.__wrapped__ = a - result = get_annotations(a, eval_str=True) - self.assertEqual(result, {'x': int}) - - def test_eval_str_wrapped_chain_no_cycle(self): - # gh-146556: a valid (non-cyclic) __wrapped__ chain must still work. - def inner(x: 'int'): ... - def outer(x: 'int'): ... - outer.__wrapped__ = inner - result = get_annotations(outer, eval_str=True) - self.assertEqual(result, {'x': int}) - - def test_stock_annotations(self): - def foo(a: int, b: str): - pass - - for format in (Format.VALUE, Format.FORWARDREF): - with self.subTest(format=format): - self.assertEqual( - get_annotations(foo, format=format), - {"a": int, "b": str}, - ) - self.assertEqual( - get_annotations(foo, format=Format.STRING), - {"a": "int", "b": "str"}, - ) - - foo.__annotations__ = {"a": "foo", "b": "str"} - for format in Format: - if format == Format.VALUE_WITH_FAKE_GLOBALS: - continue - with self.subTest(format=format): - self.assertEqual( - get_annotations(foo, format=format), - {"a": "foo", "b": "str"}, - ) - - self.assertEqual( - get_annotations(foo, eval_str=True, locals=locals()), - {"a": foo, "b": str}, - ) - self.assertEqual( - 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": Format.VALUE}, - {"format": Format.FORWARDREF}, - {"format": Format.VALUE, "eval_str": False}, - {"format": Format.FORWARDREF, "eval_str": False}, - ]: - with self.subTest(**kwargs): - self.assertEqual(get_annotations(isa, **kwargs), {"a": int, "b": str}) - self.assertEqual( - get_annotations(isa.MyClass, **kwargs), - {"a": int, "b": str}, - ) - self.assertEqual( - get_annotations(isa.function, **kwargs), - {"a": int, "b": str, "return": isa.MyClass}, - ) - self.assertEqual( - get_annotations(isa.function2, **kwargs), - {"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass}, - ) - self.assertEqual( - get_annotations(isa.function3, **kwargs), - {"a": "int", "b": "str", "c": "MyClass"}, - ) - self.assertEqual( - get_annotations(annotationlib, **kwargs), {} - ) # annotations module has no annotations - self.assertEqual(get_annotations(isa.UnannotatedClass, **kwargs), {}) - self.assertEqual( - get_annotations(isa.unannotated_function, **kwargs), - {}, - ) - - for kwargs in [ - {"eval_str": True}, - {"format": Format.VALUE, "eval_str": True}, - ]: - with self.subTest(**kwargs): - self.assertEqual(get_annotations(isa, **kwargs), {"a": int, "b": str}) - self.assertEqual( - get_annotations(isa.MyClass, **kwargs), - {"a": int, "b": str}, - ) - self.assertEqual( - get_annotations(isa.function, **kwargs), - {"a": int, "b": str, "return": isa.MyClass}, - ) - self.assertEqual( - get_annotations(isa.function2, **kwargs), - {"a": int, "b": str, "c": isa.MyClass, "return": isa.MyClass}, - ) - self.assertEqual( - get_annotations(isa.function3, **kwargs), - {"a": int, "b": str, "c": isa.MyClass}, - ) - self.assertEqual(get_annotations(annotationlib, **kwargs), {}) - self.assertEqual(get_annotations(isa.UnannotatedClass, **kwargs), {}) - self.assertEqual( - get_annotations(isa.unannotated_function, **kwargs), - {}, - ) - - self.assertEqual( - get_annotations(isa, format=Format.STRING), - {"a": "int", "b": "str"}, - ) - self.assertEqual( - get_annotations(isa.MyClass, format=Format.STRING), - {"a": "int", "b": "str"}, - ) - self.assertEqual( - get_annotations(isa.function, format=Format.STRING), - {"a": "int", "b": "str", "return": "MyClass"}, - ) - self.assertEqual( - get_annotations(isa.function2, format=Format.STRING), - {"a": "int", "b": "str", "c": "MyClass", "return": "MyClass"}, - ) - self.assertEqual( - get_annotations(isa.function3, format=Format.STRING), - {"a": "int", "b": "str", "c": "MyClass"}, - ) - self.assertEqual( - get_annotations(annotationlib, format=Format.STRING), - {}, - ) - self.assertEqual( - get_annotations(isa.UnannotatedClass, format=Format.STRING), - {}, - ) - self.assertEqual( - get_annotations(isa.unannotated_function, format=Format.STRING), - {}, - ) - - 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( - get_annotations(wrapped), - {"a": int, "b": str, "return": isa.MyClass}, - ) - self.assertEqual( - get_annotations(wrapped, format=Format.FORWARDREF), - {"a": int, "b": str, "return": isa.MyClass}, - ) - self.assertEqual( - get_annotations(wrapped, format=Format.STRING), - {"a": "int", "b": "str", "return": "MyClass"}, - ) - self.assertEqual( - get_annotations(wrapped, eval_str=True), - {"a": int, "b": str, "return": isa.MyClass}, - ) - self.assertEqual( - 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": Format.VALUE}, - {"format": Format.FORWARDREF}, - {"format": Format.STRING}, - {"format": Format.VALUE, "eval_str": False}, - {"format": Format.FORWARDREF, "eval_str": False}, - {"format": Format.STRING, "eval_str": False}, - ]: - with self.subTest(**kwargs): - self.assertEqual( - get_annotations(isa, **kwargs), - {"a": "int", "b": "str"}, - ) - self.assertEqual( - get_annotations(isa.MyClass, **kwargs), - {"a": "int", "b": "str"}, - ) - self.assertEqual( - get_annotations(isa.function, **kwargs), - {"a": "int", "b": "str", "return": "MyClass"}, - ) - self.assertEqual( - get_annotations(isa.function2, **kwargs), - {"a": "int", "b": "'str'", "c": "MyClass", "return": "MyClass"}, - ) - self.assertEqual( - get_annotations(isa.function3, **kwargs), - {"a": "'int'", "b": "'str'", "c": "'MyClass'"}, - ) - self.assertEqual(get_annotations(isa.UnannotatedClass, **kwargs), {}) - self.assertEqual( - get_annotations(isa.unannotated_function, **kwargs), - {}, - ) - - for kwargs in [ - {"eval_str": True}, - {"eval_str": True, "globals": isa.__dict__, "locals": {}}, - {"eval_str": True, "globals": {}, "locals": isa.__dict__}, - {"format": Format.VALUE, "eval_str": True}, - ]: - with self.subTest(**kwargs): - self.assertEqual(get_annotations(isa, **kwargs), {"a": int, "b": str}) - self.assertEqual( - get_annotations(isa.MyClass, **kwargs), - {"a": int, "b": str}, - ) - self.assertEqual( - get_annotations(isa.function, **kwargs), - {"a": int, "b": str, "return": isa.MyClass}, - ) - self.assertEqual( - get_annotations(isa.function2, **kwargs), - {"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass}, - ) - self.assertEqual( - get_annotations(isa.function3, **kwargs), - {"a": "int", "b": "str", "c": "MyClass"}, - ) - self.assertEqual(get_annotations(isa.UnannotatedClass, **kwargs), {}) - self.assertEqual( - get_annotations(isa.unannotated_function, **kwargs), - {}, - ) - - def test_stringized_annotations_in_empty_module(self): - isa2 = inspect_stringized_annotations_2 - self.assertEqual(get_annotations(isa2), {}) - self.assertEqual(get_annotations(isa2, eval_str=True), {}) - self.assertEqual(get_annotations(isa2, eval_str=False), {}) - - def test_stringized_annotations_with_star_unpack(self): - def f(*args: "*tuple[int, ...]"): ... - self.assertEqual(get_annotations(f, eval_str=True), - {'args': (*tuple[int, ...],)[0]}) - def f(*args: " *tuple[int, ...]"): ... - self.assertEqual(get_annotations(f, eval_str=True), - {'args': (*tuple[int, ...],)[0]}) - - - 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( - get_annotations(wrapped), - {"a": "int", "b": "str", "return": "MyClass"}, - ) - self.assertEqual( - get_annotations(wrapped, eval_str=True), - {"a": int, "b": str, "return": isa.MyClass}, - ) - self.assertEqual( - get_annotations(wrapped, eval_str=False), - {"a": "int", "b": "str", "return": "MyClass"}, - ) - - def test_stringized_annotations_on_partial_wrapper(self): - isa = inspect_stringized_annotations - - def times_three_str(fn: typing.Callable[[str], isa.MyClass]): - @functools.wraps(fn) - def wrapper(b: "str") -> "MyClass": - return fn(b * 3) - - return wrapper - - wrapped = times_three_str(functools.partial(isa.function, 1)) - self.assertEqual(wrapped("x"), isa.MyClass(1, "xxx")) - self.assertIsNot(wrapped.__globals__, isa.function.__globals__) - self.assertEqual( - get_annotations(wrapped, eval_str=True), - {"b": str, "return": isa.MyClass}, - ) - self.assertEqual( - get_annotations(wrapped, eval_str=False), - {"b": "str", "return": "MyClass"}, - ) - - # If functools is not loaded, names will be evaluated in the current - # module instead of being unwrapped to the original. - functools_mod = sys.modules["functools"] - del sys.modules["functools"] - - self.assertEqual( - get_annotations(wrapped, eval_str=True), - {"b": str, "return": MyClass}, - ) - self.assertEqual( - get_annotations(wrapped, eval_str=False), - {"b": "str", "return": "MyClass"}, - ) - - sys.modules["functools"] = functools_mod - - def test_stringized_annotations_on_class(self): - isa = inspect_stringized_annotations - # test that local namespace lookups work - self.assertEqual( - get_annotations(isa.MyClassWithLocalAnnotations), - {"x": "mytype"}, - ) - self.assertEqual( - get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), - {"x": int}, - ) - - def test_stringized_annotations_on_custom_object(self): - class HasAnnotations: - @property - def __annotations__(self): - return {"x": "int"} - - ha = HasAnnotations() - self.assertEqual(get_annotations(ha), {"x": "int"}) - self.assertEqual(get_annotations(ha, eval_str=True), {"x": int}) - - def test_stringized_annotation_permutations(self): - def define_class(name, has_future, has_annos, base_text, extra_names=None): - lines = [] - if has_future: - lines.append("from __future__ import annotations") - lines.append(f"class {name}({base_text}):") - if has_annos: - lines.append(f" {name}_attr: int") - else: - lines.append(" pass") - code = "\n".join(lines) - ns = support.run_code(code, extra_names=extra_names) - return ns[name] - - def check_annotations(cls, has_future, has_annos): - if has_annos: - if has_future: - anno = "int" - else: - anno = int - self.assertEqual(get_annotations(cls), {f"{cls.__name__}_attr": anno}) - else: - self.assertEqual(get_annotations(cls), {}) - - for meta_future, base_future, child_future, meta_has_annos, base_has_annos, child_has_annos in itertools.product( - (False, True), - (False, True), - (False, True), - (False, True), - (False, True), - (False, True), - ): - with self.subTest( - meta_future=meta_future, - base_future=base_future, - child_future=child_future, - meta_has_annos=meta_has_annos, - base_has_annos=base_has_annos, - child_has_annos=child_has_annos, - ): - meta = define_class( - "Meta", - has_future=meta_future, - has_annos=meta_has_annos, - base_text="type", - ) - base = define_class( - "Base", - has_future=base_future, - has_annos=base_has_annos, - base_text="metaclass=Meta", - extra_names={"Meta": meta}, - ) - child = define_class( - "Child", - has_future=child_future, - has_annos=child_has_annos, - base_text="Base", - extra_names={"Base": base}, - ) - check_annotations(meta, meta_future, meta_has_annos) - check_annotations(base, base_future, base_has_annos) - check_annotations(child, child_future, child_has_annos) - - def test_modify_annotations(self): - def f(x: int): - pass - - self.assertEqual(get_annotations(f), {"x": int}) - self.assertEqual( - get_annotations(f, format=Format.FORWARDREF), - {"x": int}, - ) - - f.__annotations__["x"] = str - # The modification is reflected in VALUE (the default) - self.assertEqual(get_annotations(f), {"x": str}) - # ... and also in FORWARDREF, which tries __annotations__ if available - self.assertEqual( - get_annotations(f, format=Format.FORWARDREF), - {"x": str}, - ) - # ... but not in STRING which always uses __annotate__ - self.assertEqual( - get_annotations(f, format=Format.STRING), - {"x": "int"}, - ) - - def test_non_dict_annotations(self): - class WeirdAnnotations: - @property - def __annotations__(self): - return "not a dict" - - wa = WeirdAnnotations() - for format in Format: - if format == Format.VALUE_WITH_FAKE_GLOBALS: - continue - with ( - self.subTest(format=format), - self.assertRaisesRegex( - ValueError, r".*__annotations__ is neither a dict nor None" - ), - ): - get_annotations(wa, format=format) - - def test_annotations_on_custom_object(self): - class HasAnnotations: - @property - def __annotations__(self): - return {"x": int} - - ha = HasAnnotations() - self.assertEqual(get_annotations(ha, format=Format.VALUE), {"x": int}) - self.assertEqual(get_annotations(ha, format=Format.FORWARDREF), {"x": int}) - - self.assertEqual(get_annotations(ha, format=Format.STRING), {"x": "int"}) - - def test_raising_annotations_on_custom_object(self): - class HasRaisingAnnotations: - @property - def __annotations__(self): - return {"x": undefined} - - hra = HasRaisingAnnotations() - - with self.assertRaises(NameError): - get_annotations(hra, format=Format.VALUE) - - with self.assertRaises(NameError): - get_annotations(hra, format=Format.FORWARDREF) - - undefined = float - self.assertEqual(get_annotations(hra, format=Format.VALUE), {"x": float}) - - def test_forwardref_prefers_annotations(self): - class HasBoth: - @property - def __annotations__(self): - return {"x": int} - - @property - def __annotate__(self): - return lambda format: {"x": str} - - hb = HasBoth() - self.assertEqual(get_annotations(hb, format=Format.VALUE), {"x": int}) - self.assertEqual(get_annotations(hb, format=Format.FORWARDREF), {"x": int}) - self.assertEqual(get_annotations(hb, format=Format.STRING), {"x": str}) - - def test_only_annotate(self): - def f(x: int): - pass - - class OnlyAnnotate: - @property - def __annotate__(self): - return f.__annotate__ - - oa = OnlyAnnotate() - self.assertEqual(get_annotations(oa, format=Format.VALUE), {"x": int}) - self.assertEqual(get_annotations(oa, format=Format.FORWARDREF), {"x": int}) - self.assertEqual( - get_annotations(oa, format=Format.STRING), - {"x": "int"}, - ) - - def test_non_dict_annotate(self): - class WeirdAnnotate: - def __annotate__(self, *args, **kwargs): - return "not a dict" - - wa = WeirdAnnotate() - for format in Format: - if format == Format.VALUE_WITH_FAKE_GLOBALS: - continue - with ( - self.subTest(format=format), - self.assertRaisesRegex( - ValueError, r".*__annotate__ returned a non-dict" - ), - ): - get_annotations(wa, format=format) - - def test_no_annotations(self): - class CustomClass: - pass - - class MyCallable: - def __call__(self): - pass - - for format in Format: - if format == Format.VALUE_WITH_FAKE_GLOBALS: - continue - for obj in (None, 1, object(), CustomClass()): - with self.subTest(format=format, obj=obj): - with self.assertRaises(TypeError): - get_annotations(obj, format=format) - - # Callables and types with no annotations return an empty dict - for obj in (int, len, MyCallable()): - with self.subTest(format=format, obj=obj): - self.assertEqual(get_annotations(obj, format=format), {}) - - def test_pep695_generic_class_with_future_annotations(self): - ann_module695 = inspect_stringized_annotations_pep695 - A_annotations = 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 = 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 = 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 = 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( - 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 = 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( - 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( - 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__), - ) - - def test_partial_evaluation(self): - def f( - x: builtins.undef, - y: list[int], - z: 1 + int, - a: builtins.int, - b: [builtins.undef, builtins.int], - ): - pass - - self.assertEqual( - get_annotations(f, format=Format.FORWARDREF), - { - "x": support.EqualToForwardRef("builtins.undef", owner=f), - "y": list[int], - "z": support.EqualToForwardRef("1 + int", owner=f), - "a": int, - "b": [ - support.EqualToForwardRef("builtins.undef", owner=f), - # We can't resolve this because we have to evaluate the whole annotation - support.EqualToForwardRef("builtins.int", owner=f), - ], - }, - ) - - self.assertEqual( - get_annotations(f, format=Format.STRING), - { - "x": "builtins.undef", - "y": "list[int]", - "z": "1 + int", - "a": "builtins.int", - "b": "[builtins.undef, builtins.int]", - }, - ) - - def test_partial_evaluation_error(self): - def f(x: range[1]): - pass - with self.assertRaisesRegex( - TypeError, "type 'range' is not subscriptable" - ): - f.__annotations__ - - self.assertEqual( - get_annotations(f, format=Format.FORWARDREF), - { - "x": support.EqualToForwardRef("range[1]", owner=f), - }, - ) - - def test_partial_evaluation_cell(self): - obj = object() - - class RaisesAttributeError: - attriberr: obj.missing - - anno = get_annotations(RaisesAttributeError, format=Format.FORWARDREF) - self.assertEqual( - anno, - { - "attriberr": support.EqualToForwardRef( - "obj.missing", is_class=True, owner=RaisesAttributeError - ) - }, - ) - - def test_nonlocal_in_annotation_scope(self): - class Demo: - nonlocal sequence_b - x: sequence_b - y: sequence_b[int] - - fwdrefs = get_annotations(Demo, format=Format.FORWARDREF) - - self.assertIsInstance(fwdrefs["x"], ForwardRef) - self.assertIsInstance(fwdrefs["y"], ForwardRef) - - sequence_b = list - self.assertIs(fwdrefs["x"].evaluate(), list) - self.assertEqual(fwdrefs["y"].evaluate(), list[int]) - - def test_raises_error_from_value(self): - # test that if VALUE is the only supported format, but raises an error - # that error is propagated from get_annotations - class DemoException(Exception): ... - - def annotate(format, /): - if format == Format.VALUE: - raise DemoException() - else: - raise NotImplementedError(format) - - def f(): ... - - f.__annotate__ = annotate - - for fmt in [Format.VALUE, Format.FORWARDREF, Format.STRING]: - with self.assertRaises(DemoException): - get_annotations(f, format=fmt) - - -class TestCallEvaluateFunction(unittest.TestCase): - def test_evaluation(self): - def evaluate(format, exc=NotImplementedError): - if format > 2: - raise exc - return undefined - - with self.assertRaises(NameError): - annotationlib.call_evaluate_function(evaluate, Format.VALUE) - self.assertEqual( - annotationlib.call_evaluate_function(evaluate, Format.FORWARDREF), - support.EqualToForwardRef("undefined"), - ) - self.assertEqual( - annotationlib.call_evaluate_function(evaluate, Format.STRING), - "undefined", - ) - - def test_fake_global_evaluation(self): - # This will raise an AttributeError - def evaluate_union(format, exc=NotImplementedError): - if format == Format.VALUE_WITH_FAKE_GLOBALS: - # Return a ForwardRef - return builtins.undefined | list[int] - raise exc - - self.assertEqual( - annotationlib.call_evaluate_function(evaluate_union, Format.FORWARDREF), - support.EqualToForwardRef("builtins.undefined | list[int]"), - ) - - # This will raise an AttributeError - def evaluate_intermediate(format, exc=NotImplementedError): - if format == Format.VALUE_WITH_FAKE_GLOBALS: - intermediate = builtins.undefined - # Return a literal - return intermediate is None - raise exc - - self.assertIs( - annotationlib.call_evaluate_function(evaluate_intermediate, Format.FORWARDREF), - False, - ) - - -class TestCallAnnotateFunction(unittest.TestCase): - # Tests for user defined annotate functions. - - # Format and NotImplementedError are provided as arguments so they exist in - # the fake globals namespace. - # This avoids non-matching conditions passing by being converted to stringifiers. - # See: https://github.com/python/cpython/issues/138764 - - def test_user_annotate_value(self): - def annotate(format, /): - if format == Format.VALUE: - return {"x": str} - else: - raise NotImplementedError(format) - - annotations = annotationlib.call_annotate_function( - annotate, - Format.VALUE, - ) - - self.assertEqual(annotations, {"x": str}) - - def test_user_annotate_forwardref_supported(self): - # If Format.FORWARDREF is supported prefer it over Format.VALUE - def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError): - if format == __Format.VALUE: - return {'x': str} - elif format == __Format.VALUE_WITH_FAKE_GLOBALS: - return {'x': int} - elif format == __Format.FORWARDREF: - return {'x': float} - else: - raise __NotImplementedError(format) - - annotations = annotationlib.call_annotate_function( - annotate, - Format.FORWARDREF - ) - - self.assertEqual(annotations, {"x": float}) - - def test_user_annotate_forwardref_fakeglobals(self): - # If Format.FORWARDREF is not supported, use Format.VALUE_WITH_FAKE_GLOBALS - # before falling back to Format.VALUE - def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError): - if format == __Format.VALUE: - return {'x': str} - elif format == __Format.VALUE_WITH_FAKE_GLOBALS: - return {'x': int} - else: - raise __NotImplementedError(format) - - annotations = annotationlib.call_annotate_function( - annotate, - Format.FORWARDREF - ) - - self.assertEqual(annotations, {"x": int}) - - def test_user_annotate_forwardref_value_fallback(self): - # If Format.FORWARDREF and Format.VALUE_WITH_FAKE_GLOBALS are not supported - # use Format.VALUE - def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError): - if format == __Format.VALUE: - return {"x": str} - else: - raise __NotImplementedError(format) - - annotations = annotationlib.call_annotate_function( - annotate, - Format.FORWARDREF, - ) - - self.assertEqual(annotations, {"x": str}) - - def test_user_annotate_string_supported(self): - # If Format.STRING is supported prefer it over Format.VALUE - def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError): - if format == __Format.VALUE: - return {'x': str} - elif format == __Format.VALUE_WITH_FAKE_GLOBALS: - return {'x': int} - elif format == __Format.STRING: - return {'x': "float"} - else: - raise __NotImplementedError(format) - - annotations = annotationlib.call_annotate_function( - annotate, - Format.STRING, - ) - - self.assertEqual(annotations, {"x": "float"}) - - def test_user_annotate_string_fakeglobals(self): - # If Format.STRING is not supported but Format.VALUE_WITH_FAKE_GLOBALS is - # prefer that over Format.VALUE - def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError): - if format == __Format.VALUE: - return {'x': str} - elif format == __Format.VALUE_WITH_FAKE_GLOBALS: - return {'x': int} - else: - raise __NotImplementedError(format) - - annotations = annotationlib.call_annotate_function( - annotate, - Format.STRING, - ) - - self.assertEqual(annotations, {"x": "int"}) - - def test_user_annotate_string_value_fallback(self): - # If Format.STRING and Format.VALUE_WITH_FAKE_GLOBALS are not - # supported fall back to Format.VALUE and convert to strings - def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError): - if format == __Format.VALUE: - return {"x": str} - else: - raise __NotImplementedError(format) - - annotations = annotationlib.call_annotate_function( - annotate, - Format.STRING, - ) - - self.assertEqual(annotations, {"x": "str"}) - - def test_condition_not_stringified(self): - # Make sure the first condition isn't evaluated as True by being converted - # to a _Stringifier - def annotate(format, /): - if format == Format.FORWARDREF: - return {"x": str} - else: - raise NotImplementedError(format) - - with self.assertRaises(NotImplementedError): - annotationlib.call_annotate_function(annotate, Format.STRING) - - def test_unsupported_formats(self): - def annotate(format, /): - if format == Format.FORWARDREF: - return {"x": str} - else: - raise NotImplementedError(format) - - with self.assertRaises(ValueError): - annotationlib.call_annotate_function(annotate, Format.VALUE_WITH_FAKE_GLOBALS) - - with self.assertRaises(RuntimeError): - annotationlib.call_annotate_function(annotate, Format.VALUE) - - with self.assertRaises(ValueError): - # Some non-Format value - annotationlib.call_annotate_function(annotate, 7) - - def test_basic_non_function_annotate(self): - class Annotate: - def __call__(self, format, /, __Format=Format, - __NotImplementedError=NotImplementedError): - if format == __Format.VALUE: - return {'x': str} - elif format == __Format.VALUE_WITH_FAKE_GLOBALS: - return {'x': int} - elif format == __Format.STRING: - return {'x': "float"} - else: - raise __NotImplementedError(format) - - annotations = annotationlib.call_annotate_function(Annotate(), Format.VALUE) - self.assertEqual(annotations, {"x": str}) - - annotations = annotationlib.call_annotate_function(Annotate(), Format.STRING) - self.assertEqual(annotations, {"x": "float"}) - - with self.assertRaises(AttributeError) as cm: - annotations = annotationlib.call_annotate_function( - Annotate(), Format.FORWARDREF - ) - - self.assertEqual(cm.exception.name, "__builtins__") - self.assertIsInstance(cm.exception.obj, Annotate) - - def test_full_non_function_annotate(self): - def outer(): - local = str - - class Annotate: - called_formats = [] - - def __call__(self, format=None, *, _self=None): - nonlocal local - if _self is not None: - self, format = _self, self - - self.called_formats.append(format) - if format == 1: # VALUE - return {"x": MyClass, "y": int, "z": local} - if format == 2: # VALUE_WITH_FAKE_GLOBALS - return {"w": unknown, "x": MyClass, "y": int, "z": local} - raise NotImplementedError - - __globals__ = {"MyClass": MyClass} - __builtins__ = {"int": int} - __closure__ = (types.CellType(str),) - __defaults__ = (None,) - - __kwdefaults__ = property(lambda self: dict(_self=self)) - __code__ = property(lambda self: self.__call__.__code__) - - return Annotate() - - annotate = outer() - - self.assertEqual( - annotationlib.call_annotate_function(annotate, Format.VALUE), - {"x": MyClass, "y": int, "z": str} - ) - self.assertEqual(annotate.called_formats[-1], Format.VALUE) - - self.assertEqual( - annotationlib.call_annotate_function(annotate, Format.STRING), - {"w": "unknown", "x": "MyClass", "y": "int", "z": "local"} - ) - self.assertIn(Format.STRING, annotate.called_formats) - self.assertEqual(annotate.called_formats[-1], Format.VALUE_WITH_FAKE_GLOBALS) - - self.assertEqual( - annotationlib.call_annotate_function(annotate, Format.FORWARDREF), - {"w": support.EqualToForwardRef("unknown"), "x": MyClass, "y": int, "z": str} - ) - self.assertIn(Format.FORWARDREF, annotate.called_formats) - self.assertEqual(annotate.called_formats[-1], Format.VALUE_WITH_FAKE_GLOBALS) - - def test_error_from_value_raised(self): - # Test that the error from format.VALUE is raised - # if all formats fail - - class DemoException(Exception): ... - - def annotate(format, /): - if format == Format.VALUE: - raise DemoException() - else: - raise NotImplementedError(format) - - for fmt in [Format.VALUE, Format.FORWARDREF, Format.STRING]: - with self.assertRaises(DemoException): - annotationlib.call_annotate_function(annotate, format=fmt) - - -class MetaclassTests(unittest.TestCase): - def test_annotated_meta(self): - class Meta(type): - a: int - - class X(metaclass=Meta): - pass - - class Y(metaclass=Meta): - b: float - - self.assertEqual(get_annotations(Meta), {"a": int}) - self.assertEqual(Meta.__annotate__(Format.VALUE), {"a": int}) - - self.assertEqual(get_annotations(X), {}) - self.assertIs(X.__annotate__, None) - - self.assertEqual(get_annotations(Y), {"b": float}) - self.assertEqual(Y.__annotate__(Format.VALUE), {"b": float}) - - def test_unannotated_meta(self): - class Meta(type): - pass - - class X(metaclass=Meta): - a: str - - class Y(X): - pass - - self.assertEqual(get_annotations(Meta), {}) - self.assertIs(Meta.__annotate__, None) - - self.assertEqual(get_annotations(Y), {}) - self.assertIs(Y.__annotate__, None) - - self.assertEqual(get_annotations(X), {"a": str}) - self.assertEqual(X.__annotate__(Format.VALUE), {"a": str}) - - def test_ordering(self): - # Based on a sample by David Ellis - # https://discuss.python.org/t/pep-749-implementing-pep-649/54974/38 - - def make_classes(): - class Meta(type): - a: int - expected_annotations = {"a": int} - - class A(type, metaclass=Meta): - b: float - expected_annotations = {"b": float} - - class B(metaclass=A): - c: str - expected_annotations = {"c": str} - - class C(B): - expected_annotations = {} - - class D(metaclass=Meta): - expected_annotations = {} - - return Meta, A, B, C, D - - classes = make_classes() - class_count = len(classes) - for order in itertools.permutations(range(class_count), class_count): - names = ", ".join(classes[i].__name__ for i in order) - with self.subTest(names=names): - classes = make_classes() # Regenerate classes - for i in order: - get_annotations(classes[i]) - for c in classes: - with self.subTest(c=c): - self.assertEqual(get_annotations(c), c.expected_annotations) - annotate_func = getattr(c, "__annotate__", None) - if c.expected_annotations: - self.assertEqual( - annotate_func(Format.VALUE), c.expected_annotations - ) - else: - self.assertIs(annotate_func, None) - - -class TestGetAnnotateFromClassNamespace(unittest.TestCase): - def test_with_metaclass(self): - class Meta(type): - def __new__(mcls, name, bases, ns): - annotate = annotationlib.get_annotate_from_class_namespace(ns) - expected = ns["expected_annotate"] - with self.subTest(name=name): - if expected: - self.assertIsNotNone(annotate) - else: - self.assertIsNone(annotate) - return super().__new__(mcls, name, bases, ns) - - class HasAnnotations(metaclass=Meta): - expected_annotate = True - a: int - - class NoAnnotations(metaclass=Meta): - expected_annotate = False - - class CustomAnnotate(metaclass=Meta): - expected_annotate = True - def __annotate__(format): - return {} - - code = """ - from __future__ import annotations - - class HasFutureAnnotations(metaclass=Meta): - expected_annotate = False - a: int - """ - exec(textwrap.dedent(code), {"Meta": Meta}) - - -class TestTypeRepr(unittest.TestCase): - def test_type_repr(self): - class Nested: - pass - - def nested(): - pass - - self.assertEqual(type_repr(int), "int") - self.assertEqual(type_repr(MyClass), f"{__name__}.MyClass") - self.assertEqual( - type_repr(Nested), f"{__name__}.TestTypeRepr.test_type_repr..Nested" - ) - self.assertEqual( - type_repr(nested), f"{__name__}.TestTypeRepr.test_type_repr..nested" - ) - self.assertEqual(type_repr(len), "len") - self.assertEqual(type_repr(type_repr), "annotationlib.type_repr") - self.assertEqual(type_repr(times_three), f"{__name__}.times_three") - self.assertEqual(type_repr(...), "...") - self.assertEqual(type_repr(None), "None") - self.assertEqual(type_repr(1), "1") - self.assertEqual(type_repr("1"), "'1'") - self.assertEqual(type_repr(Format.VALUE), repr(Format.VALUE)) - self.assertEqual(type_repr(MyClass()), "my repr") - # gh138558 tests - self.assertEqual(type_repr(t'''{ 0 - & 1 - | 2 - }'''), 't"""{ 0\n & 1\n | 2}"""') - self.assertEqual( - type_repr(Template("hi", Interpolation(42, "42"))), "t'hi{42}'" - ) - self.assertEqual( - type_repr(Template("hi", Interpolation(42))), - "Template('hi', Interpolation(42, '', None, ''))", - ) - self.assertEqual( - type_repr(Template("hi", Interpolation(42, " "))), - "Template('hi', Interpolation(42, ' ', None, ''))", - ) - self.assertEqual( - type_repr(Template("hi", Interpolation(42, "4!2"))), - "Template('hi', Interpolation(42, '4!2', None, ''))", - ) - # gh138558: perhaps in the future, we can improve this behavior: - self.assertEqual(type_repr(Template(Interpolation(42, "99"))), "t'{99}'") - - -class TestAnnotationsToString(unittest.TestCase): - def test_annotations_to_string(self): - self.assertEqual(annotations_to_string({}), {}) - self.assertEqual(annotations_to_string({"x": int}), {"x": "int"}) - self.assertEqual(annotations_to_string({"x": "int"}), {"x": "int"}) - self.assertEqual( - annotations_to_string({"x": int, "y": str}), {"x": "int", "y": "str"} - ) - - -class A: - pass - -TypeParamsAlias1 = int - -class TypeParamsSample[TypeParamsAlias1, TypeParamsAlias2]: - TypeParamsAlias2 = str - - -class TestForwardRefClass(unittest.TestCase): - def test_forwardref_instance_type_error(self): - fr = ForwardRef("int") - with self.assertRaises(TypeError): - isinstance(42, fr) - - def test_forwardref_subclass_type_error(self): - fr = ForwardRef("int") - with self.assertRaises(TypeError): - issubclass(int, fr) - - def test_forwardref_only_str_arg(self): - with self.assertRaises(TypeError): - ForwardRef(1) # only `str` type is allowed - - def test_forward_equality(self): - fr = ForwardRef("int") - self.assertEqual(fr, ForwardRef("int")) - self.assertNotEqual(List["int"], List[int]) - self.assertNotEqual(fr, ForwardRef("int", module=__name__)) - frm = ForwardRef("int", module=__name__) - self.assertEqual(frm, ForwardRef("int", module=__name__)) - self.assertNotEqual(frm, ForwardRef("int", module="__other_name__")) - - def test_forward_equality_get_type_hints(self): - c1 = ForwardRef("C") - c1_gth = ForwardRef("C") - c2 = ForwardRef("C") - c2_gth = ForwardRef("C") - - class C: - pass - - def foo(a: c1_gth, b: c2_gth): - pass - - self.assertEqual(get_type_hints(foo, globals(), locals()), {"a": C, "b": C}) - self.assertEqual(c1, c2) - self.assertEqual(c1, c1_gth) - self.assertEqual(c1_gth, c2_gth) - self.assertEqual(List[c1], List[c1_gth]) - self.assertNotEqual(List[c1], List[C]) - self.assertNotEqual(List[c1_gth], List[C]) - self.assertEqual(Union[c1, c1_gth], Union[c1]) - self.assertEqual(Union[c1, c1_gth, int], Union[c1, int]) - - def test_forward_equality_hash(self): - c1 = ForwardRef("int") - c1_gth = ForwardRef("int") - c2 = ForwardRef("int") - c2_gth = ForwardRef("int") - - def foo(a: c1_gth, b: c2_gth): - pass - - get_type_hints(foo, globals(), locals()) - - self.assertEqual(hash(c1), hash(c2)) - self.assertEqual(hash(c1_gth), hash(c2_gth)) - self.assertEqual(hash(c1), hash(c1_gth)) - - c3 = ForwardRef("int", module=__name__) - c4 = ForwardRef("int", module="__other_name__") - - self.assertNotEqual(hash(c3), hash(c1)) - self.assertNotEqual(hash(c3), hash(c1_gth)) - self.assertNotEqual(hash(c3), hash(c4)) - self.assertEqual(hash(c3), hash(ForwardRef("int", module=__name__))) - - def test_forward_equality_and_hash_with_cells(self): - """Regression test for GH-143831.""" - class A: - def one(_) -> C1: - """One cell.""" - - one_f = ForwardRef("C1", owner=one) - one_f_ga1 = get_annotations(one, format=Format.FORWARDREF)["return"] - one_f_ga2 = get_annotations(one, format=Format.FORWARDREF)["return"] - self.assertIsInstance(one_f_ga1.__cell__, types.CellType) - self.assertIs(one_f_ga1.__cell__, one_f_ga2.__cell__) - - def two(_) -> C1 | C2: - """Two cells.""" - - two_f_ga1 = get_annotations(two, format=Format.FORWARDREF)["return"] - two_f_ga2 = get_annotations(two, format=Format.FORWARDREF)["return"] - self.assertIsNot(two_f_ga1.__cell__, two_f_ga2.__cell__) - self.assertIsInstance(two_f_ga1.__cell__, dict) - self.assertIsInstance(two_f_ga2.__cell__, dict) - - type C1 = None - type C2 = None - - self.assertNotEqual(A.one_f, A.one_f_ga1) - self.assertNotEqual(hash(A.one_f), hash(A.one_f_ga1)) - - self.assertEqual(A.one_f_ga1, A.one_f_ga2) - self.assertEqual(hash(A.one_f_ga1), hash(A.one_f_ga2)) - - self.assertEqual(A.two_f_ga1, A.two_f_ga2) - self.assertEqual(hash(A.two_f_ga1), hash(A.two_f_ga2)) - - def test_forward_equality_namespace(self): - def namespace1(): - a = ForwardRef("A") - - def fun(x: a): - pass - - get_type_hints(fun, globals(), locals()) - return a - - def namespace2(): - a = ForwardRef("A") - - class A: - pass - - def fun(x: a): - pass - - get_type_hints(fun, globals(), locals()) - return a - - self.assertEqual(namespace1(), namespace1()) - self.assertEqual(namespace1(), namespace2()) - - def test_forward_repr(self): - self.assertEqual(repr(List["int"]), "typing.List[ForwardRef('int')]") - self.assertEqual( - repr(List[ForwardRef("int", module="mod")]), - "typing.List[ForwardRef('int', module='mod')]", - ) - self.assertEqual( - repr(List[ForwardRef("int", module="mod", is_class=True)]), - "typing.List[ForwardRef('int', module='mod', is_class=True)]", - ) - self.assertEqual( - repr(List[ForwardRef("int", owner="class")]), - "typing.List[ForwardRef('int', owner='class')]", - ) - - def test_forward_repr_extra_names(self): - def f(a: undefined | str): ... - - annos = get_annotations(f, format=Format.FORWARDREF) - - self.assertRegex( - repr(annos['a']), r"ForwardRef\('undefined \| str'.*\)" - ) - - def test_forward_recursion_actually(self): - def namespace1(): - a = ForwardRef("A") - A = a - - def fun(x: a): - pass - - ret = get_type_hints(fun, globals(), locals()) - return a - - def namespace2(): - a = ForwardRef("A") - A = a - - def fun(x: a): - pass - - ret = get_type_hints(fun, globals(), locals()) - return a - - r1 = namespace1() - r2 = namespace2() - self.assertIsNot(r1, r2) - self.assertEqual(r1, r2) - - def test_syntax_error(self): - - with self.assertRaises(SyntaxError): - typing.Generic["/T"] - - def test_delayed_syntax_error(self): - - def foo(a: "Node[T"): - pass - - with self.assertRaises(SyntaxError): - get_type_hints(foo) - - def test_syntax_error_empty_string(self): - for form in [typing.List, typing.Set, typing.Type, typing.Deque]: - with self.subTest(form=form): - with self.assertRaises(SyntaxError): - form[""] - - def test_or(self): - X = ForwardRef("X") - # __or__/__ror__ itself - self.assertEqual(X | "x", Union[X, "x"]) - self.assertEqual("x" | X, Union["x", X]) - - def test_multiple_ways_to_create(self): - X1 = Union["X"] - self.assertIsInstance(X1, ForwardRef) - X2 = ForwardRef("X") - self.assertIsInstance(X2, ForwardRef) - self.assertEqual(X1, X2) - - 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 = ForwardRef("set[Any]") - self.assertNotHasAttr(fr, "__name__") - self.assertNotHasAttr(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) - - def test_evaluate_string_format(self): - fr = ForwardRef("set[Any]") - self.assertEqual(fr.evaluate(format=Format.STRING), "set[Any]") - - def test_evaluate_string_format_extra_names(self): - # Test that internal extra_names are replaced when evaluating as strings - def f(a: unknown | str | int | list[str] | tuple[int, ...]): ... - - fr = get_annotations(f, format=Format.FORWARDREF)['a'] - # Test the cache is not populated before access - self.assertIsNone(fr.__resolved_str_cache__) - - self.assertEqual(fr.evaluate(format=Format.STRING), "unknown | str | int | list[str] | tuple[int, ...]") - - # Test that the cache is now set correctly - self.assertEqual(fr.__resolved_str_cache__, "unknown | str | int | list[str] | tuple[int, ...]") - - # Test that future evaluations return the same cache - self.assertIs(fr.evaluate(format=Format.STRING), fr.__resolved_str_cache__) - - def test_evaluate_forwardref_format(self): - fr = ForwardRef("undef") - evaluated = fr.evaluate(format=Format.FORWARDREF) - self.assertIs(fr, evaluated) - - fr = ForwardRef("set[undefined]") - evaluated = fr.evaluate(format=Format.FORWARDREF) - self.assertEqual( - evaluated, - set[support.EqualToForwardRef("undefined")], - ) - - fr = ForwardRef("a + b") - self.assertEqual( - fr.evaluate(format=Format.FORWARDREF), - support.EqualToForwardRef("a + b"), - ) - self.assertEqual( - fr.evaluate(format=Format.FORWARDREF, locals={"a": 1, "b": 2}), - 3, - ) - - fr = ForwardRef('"a" + 1') - self.assertEqual( - fr.evaluate(format=Format.FORWARDREF), - support.EqualToForwardRef('"a" + 1'), - ) - - def test_evaluate_notimplemented_format(self): - class C: - x: alias - - fwdref = get_annotations(C, format=Format.FORWARDREF)["x"] - - with self.assertRaises(NotImplementedError): - fwdref.evaluate(format=Format.VALUE_WITH_FAKE_GLOBALS) - - with self.assertRaises(NotImplementedError): - # Some other unsupported value - fwdref.evaluate(format=7) - - def test_evaluate_with_type_params(self): - class Gen[T]: - alias = int - - with self.assertRaises(NameError): - ForwardRef("T").evaluate() - with self.assertRaises(NameError): - ForwardRef("T").evaluate(type_params=()) - with self.assertRaises(NameError): - ForwardRef("T").evaluate(owner=int) - - (T,) = Gen.__type_params__ - self.assertIs(ForwardRef("T").evaluate(type_params=Gen.__type_params__), T) - self.assertIs(ForwardRef("T").evaluate(owner=Gen), T) - - with self.assertRaises(NameError): - ForwardRef("alias").evaluate(type_params=Gen.__type_params__) - self.assertIs(ForwardRef("alias").evaluate(owner=Gen), int) - # If you pass custom locals, we don't look at the owner's locals - with self.assertRaises(NameError): - ForwardRef("alias").evaluate(owner=Gen, locals={}) - # But if the name exists in the locals, it works - self.assertIs( - ForwardRef("alias").evaluate(owner=Gen, locals={"alias": str}), str - ) - - def test_evaluate_with_type_params_and_scope_conflict(self): - for is_class in (False, True): - with self.subTest(is_class=is_class): - fwdref1 = ForwardRef("TypeParamsAlias1", owner=TypeParamsSample, is_class=is_class) - fwdref2 = ForwardRef("TypeParamsAlias2", owner=TypeParamsSample, is_class=is_class) - - self.assertIs( - fwdref1.evaluate(), - TypeParamsSample.__type_params__[0], - ) - self.assertIs( - fwdref2.evaluate(), - TypeParamsSample.TypeParamsAlias2, - ) - - def test_fwdref_with_module(self): - self.assertIs(ForwardRef("Format", module="annotationlib").evaluate(), Format) - self.assertIs( - ForwardRef("Counter", module="collections").evaluate(), collections.Counter - ) - self.assertEqual( - ForwardRef("Counter[int]", module="collections").evaluate(), - collections.Counter[int], - ) - - with self.assertRaises(NameError): - # If globals are passed explicitly, we don't look at the module dict - ForwardRef("Format", module="annotationlib").evaluate(globals={}) - - def test_fwdref_to_builtin(self): - self.assertIs(ForwardRef("int").evaluate(), int) - self.assertIs(ForwardRef("int", module="collections").evaluate(), int) - self.assertIs(ForwardRef("int", owner=str).evaluate(), int) - - # builtins are still searched with explicit globals - self.assertIs(ForwardRef("int").evaluate(globals={}), int) - - # explicit values in globals have precedence - obj = object() - self.assertIs(ForwardRef("int").evaluate(globals={"int": obj}), obj) - - def test_fwdref_value_is_not_cached(self): - fr = ForwardRef("hello") - with self.assertRaises(NameError): - fr.evaluate() - self.assertIs(fr.evaluate(globals={"hello": str}), str) - with self.assertRaises(NameError): - fr.evaluate() - - def test_fwdref_with_owner(self): - self.assertEqual( - ForwardRef("Counter[int]", owner=collections).evaluate(), - collections.Counter[int], - ) - - def test_name_lookup_without_eval(self): - # test the codepath where we look up simple names directly in the - # namespaces without going through eval() - self.assertIs(ForwardRef("int").evaluate(), int) - self.assertIs(ForwardRef("int").evaluate(locals={"int": str}), str) - self.assertIs( - ForwardRef("int").evaluate(locals={"int": float}, globals={"int": str}), - float, - ) - self.assertIs(ForwardRef("int").evaluate(globals={"int": str}), str) - with support.swap_attr(builtins, "int", dict): - self.assertIs(ForwardRef("int").evaluate(), dict) - - with self.assertRaises(NameError, msg="name 'doesntexist' is not defined") as exc: - ForwardRef("doesntexist").evaluate() - - self.assertEqual(exc.exception.name, "doesntexist") - - def test_evaluate_undefined_generic(self): - # Test the codepath where have to eval() with undefined variables. - class C: - x: alias[int, undef] - - generic = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate( - format=Format.FORWARDREF, - globals={"alias": dict} - ) - self.assertNotIsInstance(generic, ForwardRef) - self.assertIs(generic.__origin__, dict) - self.assertEqual(len(generic.__args__), 2) - self.assertIs(generic.__args__[0], int) - self.assertIsInstance(generic.__args__[1], ForwardRef) - - generic = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate( - format=Format.FORWARDREF, - globals={"alias": Union}, - locals={"alias": dict} - ) - self.assertNotIsInstance(generic, ForwardRef) - self.assertIs(generic.__origin__, dict) - self.assertEqual(len(generic.__args__), 2) - self.assertIs(generic.__args__[0], int) - self.assertIsInstance(generic.__args__[1], ForwardRef) - - def test_fwdref_invalid_syntax(self): - fr = ForwardRef("if") - with self.assertRaises(SyntaxError): - fr.evaluate() - fr = ForwardRef("1+") - with self.assertRaises(SyntaxError): - fr.evaluate() - - def test_re_evaluate_generics(self): - global global_alias - - # If we've already run this test before, - # ensure the variable is still undefined - if "global_alias" in globals(): - del global_alias - - class C: - x: global_alias[int] - - # Evaluate the ForwardRef once - evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate( - format=Format.FORWARDREF - ) - - # Now define the global and ensure that the ForwardRef evaluates - global_alias = list - self.assertEqual(evaluated.evaluate(), list[int]) - - def test_fwdref_evaluate_argument_mutation(self): - class C[T]: - nonlocal alias - x: alias[T] - - # Mutable arguments - globals_ = globals() - globals_copy = globals_.copy() - locals_ = locals() - locals_copy = locals_.copy() - - # Evaluate the ForwardRef, ensuring we use __cell__ and type params - get_annotations(C, format=Format.FORWARDREF)["x"].evaluate( - globals=globals_, - locals=locals_, - type_params=C.__type_params__, - format=Format.FORWARDREF, - ) - - # Check if the passed in mutable arguments equal the originals - self.assertEqual(globals_, globals_copy) - self.assertEqual(locals_, locals_copy) - - alias = list - - def test_fwdref_final_class(self): - with self.assertRaises(TypeError): - class C(ForwardRef): - pass - - -class TestAnnotationLib(unittest.TestCase): - def test__all__(self): - support.check__all__(self, annotationlib) - - @support.cpython_only - def test_lazy_imports(self): - import_helper.ensure_lazy_imports( - "annotationlib", - { - "typing", - "warnings", - }, - ) From 972b1cf940e830850918f78dba51b7cbb86b64b2 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:33:32 -0600 Subject: [PATCH 07/11] gh-157056: Add tests for STRING format comprehension and lambda annotations --- Lib/test/test_annotationlib.py | 527 +++++++++++++++++++++++++++++++++ 1 file changed, 527 insertions(+) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 14b8bce9da66c0..2ae04ac41bd1b6 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -55,3 +55,530 @@ def test_enum(self): self.assertEqual(Format.STRING.value, 4) self.assertEqual(Format.STRING, 4) + + +class TestForwardRefFormat(unittest.TestCase): + def test_closure(self): + def inner(arg: x): + pass + + anno = get_annotations(inner, format=Format.FORWARDREF) + fwdref = anno["arg"] + self.assertIsInstance(fwdref, ForwardRef) + self.assertEqual(fwdref.__forward_arg__, "x") + with self.assertRaises(NameError): + fwdref.evaluate() + + x = 1 + self.assertEqual(fwdref.evaluate(), x) + + anno = get_annotations(inner, format=Format.FORWARDREF) + self.assertEqual(anno["arg"], x) + + def test_multiple_closure(self): + def inner(arg: x[y]): + pass + + fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] + self.assertIsInstance(fwdref, ForwardRef) + self.assertEqual(fwdref.__forward_arg__, "x[y]") + with self.assertRaises(NameError): + fwdref.evaluate() + + y = str + fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] + self.assertIsInstance(fwdref, ForwardRef) + extra_name, extra_val = next(iter(fwdref.__extra_names__.items())) + self.assertEqual(fwdref.__forward_arg__.replace(extra_name, extra_val.__name__), "x[str]") + with self.assertRaises(NameError): + fwdref.evaluate() + + x = list + self.assertEqual(fwdref.evaluate(), x[y]) + + fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] + self.assertEqual(fwdref, x[y]) + + def test_function(self): + def f(x: int, y: doesntexist): + pass + + anno = get_annotations(f, format=Format.FORWARDREF) + self.assertIs(anno["x"], int) + fwdref = anno["y"] + self.assertIsInstance(fwdref, ForwardRef) + self.assertEqual(fwdref.__forward_arg__, "doesntexist") + with self.assertRaises(NameError): + fwdref.evaluate() + self.assertEqual(fwdref.evaluate(globals={"doesntexist": 1}), 1) + + def test_nonexistent_attribute(self): + def f( + x: some.module, + y: some[module], + z: some(module), + alpha: some | obj, + beta: +some, + gamma: some < obj, + delta: some | {obj: module}, + epsilon: some | {obj}, + zeta: some | [obj, module], + eta: some | (), + ): + pass + + anno = get_annotations(f, format=Format.FORWARDREF) + x_anno = anno["x"] + self.assertIsInstance(x_anno, ForwardRef) + self.assertEqual(x_anno, support.EqualToForwardRef("some.module", owner=f)) + + y_anno = anno["y"] + self.assertIsInstance(y_anno, ForwardRef) + self.assertEqual(y_anno, support.EqualToForwardRef("some[module]", owner=f)) + + z_anno = anno["z"] + self.assertIsInstance(z_anno, ForwardRef) + self.assertEqual(z_anno, support.EqualToForwardRef("some(module)", owner=f)) + + alpha_anno = anno["alpha"] + self.assertIsInstance(alpha_anno, ForwardRef) + self.assertEqual(alpha_anno, support.EqualToForwardRef("some | obj", owner=f)) + + beta_anno = anno["beta"] + self.assertIsInstance(beta_anno, ForwardRef) + self.assertEqual(beta_anno, support.EqualToForwardRef("+some", owner=f)) + + gamma_anno = anno["gamma"] + self.assertIsInstance(gamma_anno, ForwardRef) + self.assertEqual(gamma_anno, support.EqualToForwardRef("some < obj", owner=f)) + + delta_anno = anno["delta"] + self.assertIsInstance(delta_anno, ForwardRef) + self.assertEqual(delta_anno, support.EqualToForwardRef("some | {obj: module}", owner=f)) + + epsilon_anno = anno["epsilon"] + self.assertIsInstance(epsilon_anno, ForwardRef) + self.assertEqual(epsilon_anno, support.EqualToForwardRef("some | {obj}", owner=f)) + + zeta_anno = anno["zeta"] + self.assertIsInstance(zeta_anno, ForwardRef) + self.assertEqual(zeta_anno, support.EqualToForwardRef("some | [obj, module]", owner=f)) + + eta_anno = anno["eta"] + self.assertIsInstance(eta_anno, ForwardRef) + self.assertEqual(eta_anno, support.EqualToForwardRef("some | ()", owner=f)) + + def test_partially_nonexistent(self): + # These annotations start with a non-existent variable and then use + # global types with defined values. This partially evaluates by putting + # those globals into `fwdref.__extra_names__`. + def f( + x: obj | int, + y: container[int:obj, int], + z: dict_val | {str: int}, + alpha: set_val | {str, int}, + beta: obj | bool | int, + gamma: obj | call_func(int, kwd=bool), + ): + pass + + def func(*args, **kwargs): + return Union[*args, *(kwargs.values())] + + anno = get_annotations(f, format=Format.FORWARDREF) + globals_ = { + "obj": str, "container": list, "dict_val": {1: 2}, "set_val": {1, 2}, + "call_func": func + } + + x_anno = anno["x"] + self.assertIsInstance(x_anno, ForwardRef) + self.assertEqual(x_anno.evaluate(globals=globals_), str | int) + + y_anno = anno["y"] + self.assertIsInstance(y_anno, ForwardRef) + self.assertEqual(y_anno.evaluate(globals=globals_), list[int:str, int]) + + z_anno = anno["z"] + self.assertIsInstance(z_anno, ForwardRef) + self.assertEqual(z_anno.evaluate(globals=globals_), {1: 2} | {str: int}) + + alpha_anno = anno["alpha"] + self.assertIsInstance(alpha_anno, ForwardRef) + self.assertEqual(alpha_anno.evaluate(globals=globals_), {1, 2} | {str, int}) + + beta_anno = anno["beta"] + self.assertIsInstance(alpha_anno, ForwardRef) + self.assertEqual(beta_anno.evaluate(globals=globals_), str | bool | int) + + gamma_anno = anno["gamma"] + self.assertIsInstance(gamma_anno, ForwardRef) + self.assertEqual(gamma_anno.evaluate(globals=globals_), str | func(int, kwd=bool)) + + def test_partially_nonexistent_union(self): + # Test unions with '|' syntax equal unions with typing.Union[] with some forwardrefs + class UnionForwardrefs: + pipe: str | undefined + union: Union[str, undefined] + + annos = get_annotations(UnionForwardrefs, format=Format.FORWARDREF) + + pipe = annos["pipe"] + self.assertIsInstance(pipe, ForwardRef) + self.assertEqual( + pipe.evaluate(globals={"undefined": int}), + str | int, + ) + union = annos["union"] + self.assertIsInstance(union, Union) + arg1, arg2 = typing.get_args(union) + self.assertIs(arg1, str) + self.assertEqual( + arg2, support.EqualToForwardRef("undefined", is_class=True, owner=UnionForwardrefs) + ) + + +class TestStringFormat(unittest.TestCase): + def test_closure(self): + x = 0 + + def inner(arg: x): + pass + + anno = get_annotations(inner, format=Format.STRING) + self.assertEqual(anno, {"arg": "x"}) + + def test_closure_undefined(self): + if False: + x = 0 + + def inner(arg: x): + pass + + anno = get_annotations(inner, format=Format.STRING) + self.assertEqual(anno, {"arg": "x"}) + + def test_function(self): + def f(x: int, y: doesntexist): + pass + + anno = get_annotations(f, format=Format.STRING) + 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 = get_annotations(f, format=Format.STRING) + 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 = get_annotations(f, format=Format.STRING) + 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_template_str(self): + def f( + x: t"{a}", + y: list[t"{a}"], + z: t"{a:b} {c!r} {d!s:t}", + a: t"a{b}c{d}e{f}g", + b: t"{a:{1}}", + c: t"{a | b * c}", + gh138558: t"{ 0}", + ): pass + + annos = get_annotations(f, format=Format.STRING) + self.assertEqual(annos, { + "x": "t'{a}'", + "y": "list[t'{a}']", + "z": "t'{a:b} {c!r} {d!s:t}'", + "a": "t'a{b}c{d}e{f}g'", + # interpolations in the format spec are eagerly evaluated so we can't recover the source + "b": "t'{a:1}'", + "c": "t'{a | b * c}'", + "gh138558": "t'{ 0}'", + }) + + def g( + x: t"{a}", + ): ... + + annos = get_annotations(g, format=Format.FORWARDREF) + templ = annos["x"] + # Template and Interpolation don't have __eq__ so we have to compare manually + self.assertIsInstance(templ, Template) + self.assertEqual(templ.strings, ("", "")) + self.assertEqual(len(templ.interpolations), 1) + interp = templ.interpolations[0] + self.assertEqual(interp.value, support.EqualToForwardRef("a", owner=g)) + self.assertEqual(interp.expression, "a") + self.assertIsNone(interp.conversion) + self.assertEqual(interp.format_spec, "") + + def test_getitem(self): + def f(x: undef1[str, undef2]): + pass + anno = get_annotations(f, format=Format.STRING) + self.assertEqual(anno, {"x": "undef1[str, undef2]"}) + + anno = get_annotations(f, format=Format.FORWARDREF) + fwdref = anno["x"] + self.assertIsInstance(fwdref, ForwardRef) + self.assertEqual( + fwdref.evaluate(globals={"undef1": dict, "undef2": float}), dict[str, float] + ) + + def test_slice(self): + def f(x: a[b:c]): + pass + anno = get_annotations(f, format=Format.STRING) + self.assertEqual(anno, {"x": "a[b:c]"}) + + def f(x: a[b:c, d:e]): + pass + anno = get_annotations(f, format=Format.STRING) + self.assertEqual(anno, {"x": "a[b:c, d:e]"}) + + obj = slice(1, 1, 1) + def f(x: obj): + pass + anno = get_annotations(f, format=Format.STRING) + self.assertEqual(anno, {"x": "obj"}) + + def test_literals(self): + def f( + a: 1, + b: 1.0, + c: "hello", + d: b"hello", + e: True, + f: None, + g: ..., + h: 1j, + ): + pass + + anno = get_annotations(f, format=Format.STRING) + self.assertEqual( + anno, + { + "a": "1", + "b": "1.0", + "c": 'hello', + "d": "b'hello'", + "e": "True", + "f": "None", + "g": "...", + "h": "1j", + }, + ) + + def test_displays(self): + # Simple case first + def f(x: a[[int, str], float]): + pass + anno = get_annotations(f, format=Format.STRING) + self.assertEqual(anno, {"x": "a[[int, str], float]"}) + + def g( + w: a[[int, str], float], + x: a[{int}, 3], + y: a[{int: str}, 4], + z: a[(int, str), 5], + ): + pass + anno = get_annotations(g, format=Format.STRING) + self.assertEqual( + anno, + { + "w": "a[[int, str], float]", + "x": "a[{int}, 3]", + "y": "a[{int: str}, 4]", + "z": "a[(int, str), 5]", + }, + ) + + 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 = get_annotations(f, format=Format.STRING) + 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): + get_annotations(f, format=Format.STRING) + + def f(fstring_format: f"{a:02d}"): + pass + + with self.assertRaisesRegex(TypeError, format_msg): + get_annotations(f, format=Format.STRING) + + def test_shenanigans(self): + # In cases like this we can't reconstruct the source; test that we do something + # halfway reasonable. + def f(x: x | (1).__class__, y: (1).__class__): + pass + + self.assertEqual( + get_annotations(f, format=Format.STRING), + {"x": "x | ", "y": "int"}, + ) + + def test_comprehension_lambda_and_genexpr(self): + # gh-157056: dict comprehensions used to raise ValueError while + # stringifying, and lambda / generator-expression annotations leaked + # a memory address via repr(). + def f(x: {k: v for k, v in items}): + pass + + self.assertEqual( + get_annotations(f, format=Format.STRING), + {"x": "{k: v for k, v in items}"}, + ) + + def g(x: lambda q: q): + pass + + g_anno = get_annotations(g, format=Format.STRING) + self.assertEqual(g_anno, {"x": "lambda q: q"}) + self.assertNotIn("0x", g_anno["x"].lower()) + + def h(x: (w for w in seq)): + pass + + h_anno = get_annotations(h, format=Format.STRING) + self.assertEqual(h_anno, {"x": "(w for w in seq)"}) + self.assertNotIn("0x", h_anno["x"].lower()) + + def mixed(a: int, b: {k: v for k, v in items}, c: lambda q: q): + pass + + self.assertEqual( + get_annotations(mixed, format=Format.STRING), + { + "a": "int", + "b": "{k: v for k, v in items}", + "c": "lambda q: q", + }, + ) From 1ed4d56709f14c3c0ba49f71c20608f12949ab5c Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:33:46 -0600 Subject: [PATCH 08/11] gh-157056: Add tests for STRING format comprehension and lambda annotations --- Lib/test/test_annotationlib.py | 585 +-------------------------------- 1 file changed, 1 insertion(+), 584 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 2ae04ac41bd1b6..4149425ff5cc8e 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1,584 +1 @@ -"""Tests for the annotations module.""" - -import textwrap -import annotationlib -import builtins -import collections -import functools -import itertools -import pickle -from string.templatelib import Template, Interpolation -import types -import typing -import sys -import unittest -from annotationlib import ( - Format, - ForwardRef, - get_annotations, - annotations_to_string, - type_repr, -) -from typing import Unpack, get_type_hints, List, Union - -from test import support -from test.support import import_helper -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 MyClass: - def __repr__(self): - return "my repr" - - -class TestFormat(unittest.TestCase): - def test_enum(self): - self.assertEqual(Format.VALUE.value, 1) - self.assertEqual(Format.VALUE, 1) - - self.assertEqual(Format.VALUE_WITH_FAKE_GLOBALS.value, 2) - self.assertEqual(Format.VALUE_WITH_FAKE_GLOBALS, 2) - - self.assertEqual(Format.FORWARDREF.value, 3) - self.assertEqual(Format.FORWARDREF, 3) - - self.assertEqual(Format.STRING.value, 4) - self.assertEqual(Format.STRING, 4) - - -class TestForwardRefFormat(unittest.TestCase): - def test_closure(self): - def inner(arg: x): - pass - - anno = get_annotations(inner, format=Format.FORWARDREF) - fwdref = anno["arg"] - self.assertIsInstance(fwdref, ForwardRef) - self.assertEqual(fwdref.__forward_arg__, "x") - with self.assertRaises(NameError): - fwdref.evaluate() - - x = 1 - self.assertEqual(fwdref.evaluate(), x) - - anno = get_annotations(inner, format=Format.FORWARDREF) - self.assertEqual(anno["arg"], x) - - def test_multiple_closure(self): - def inner(arg: x[y]): - pass - - fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] - self.assertIsInstance(fwdref, ForwardRef) - self.assertEqual(fwdref.__forward_arg__, "x[y]") - with self.assertRaises(NameError): - fwdref.evaluate() - - y = str - fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] - self.assertIsInstance(fwdref, ForwardRef) - extra_name, extra_val = next(iter(fwdref.__extra_names__.items())) - self.assertEqual(fwdref.__forward_arg__.replace(extra_name, extra_val.__name__), "x[str]") - with self.assertRaises(NameError): - fwdref.evaluate() - - x = list - self.assertEqual(fwdref.evaluate(), x[y]) - - fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] - self.assertEqual(fwdref, x[y]) - - def test_function(self): - def f(x: int, y: doesntexist): - pass - - anno = get_annotations(f, format=Format.FORWARDREF) - self.assertIs(anno["x"], int) - fwdref = anno["y"] - self.assertIsInstance(fwdref, ForwardRef) - self.assertEqual(fwdref.__forward_arg__, "doesntexist") - with self.assertRaises(NameError): - fwdref.evaluate() - self.assertEqual(fwdref.evaluate(globals={"doesntexist": 1}), 1) - - def test_nonexistent_attribute(self): - def f( - x: some.module, - y: some[module], - z: some(module), - alpha: some | obj, - beta: +some, - gamma: some < obj, - delta: some | {obj: module}, - epsilon: some | {obj}, - zeta: some | [obj, module], - eta: some | (), - ): - pass - - anno = get_annotations(f, format=Format.FORWARDREF) - x_anno = anno["x"] - self.assertIsInstance(x_anno, ForwardRef) - self.assertEqual(x_anno, support.EqualToForwardRef("some.module", owner=f)) - - y_anno = anno["y"] - self.assertIsInstance(y_anno, ForwardRef) - self.assertEqual(y_anno, support.EqualToForwardRef("some[module]", owner=f)) - - z_anno = anno["z"] - self.assertIsInstance(z_anno, ForwardRef) - self.assertEqual(z_anno, support.EqualToForwardRef("some(module)", owner=f)) - - alpha_anno = anno["alpha"] - self.assertIsInstance(alpha_anno, ForwardRef) - self.assertEqual(alpha_anno, support.EqualToForwardRef("some | obj", owner=f)) - - beta_anno = anno["beta"] - self.assertIsInstance(beta_anno, ForwardRef) - self.assertEqual(beta_anno, support.EqualToForwardRef("+some", owner=f)) - - gamma_anno = anno["gamma"] - self.assertIsInstance(gamma_anno, ForwardRef) - self.assertEqual(gamma_anno, support.EqualToForwardRef("some < obj", owner=f)) - - delta_anno = anno["delta"] - self.assertIsInstance(delta_anno, ForwardRef) - self.assertEqual(delta_anno, support.EqualToForwardRef("some | {obj: module}", owner=f)) - - epsilon_anno = anno["epsilon"] - self.assertIsInstance(epsilon_anno, ForwardRef) - self.assertEqual(epsilon_anno, support.EqualToForwardRef("some | {obj}", owner=f)) - - zeta_anno = anno["zeta"] - self.assertIsInstance(zeta_anno, ForwardRef) - self.assertEqual(zeta_anno, support.EqualToForwardRef("some | [obj, module]", owner=f)) - - eta_anno = anno["eta"] - self.assertIsInstance(eta_anno, ForwardRef) - self.assertEqual(eta_anno, support.EqualToForwardRef("some | ()", owner=f)) - - def test_partially_nonexistent(self): - # These annotations start with a non-existent variable and then use - # global types with defined values. This partially evaluates by putting - # those globals into `fwdref.__extra_names__`. - def f( - x: obj | int, - y: container[int:obj, int], - z: dict_val | {str: int}, - alpha: set_val | {str, int}, - beta: obj | bool | int, - gamma: obj | call_func(int, kwd=bool), - ): - pass - - def func(*args, **kwargs): - return Union[*args, *(kwargs.values())] - - anno = get_annotations(f, format=Format.FORWARDREF) - globals_ = { - "obj": str, "container": list, "dict_val": {1: 2}, "set_val": {1, 2}, - "call_func": func - } - - x_anno = anno["x"] - self.assertIsInstance(x_anno, ForwardRef) - self.assertEqual(x_anno.evaluate(globals=globals_), str | int) - - y_anno = anno["y"] - self.assertIsInstance(y_anno, ForwardRef) - self.assertEqual(y_anno.evaluate(globals=globals_), list[int:str, int]) - - z_anno = anno["z"] - self.assertIsInstance(z_anno, ForwardRef) - self.assertEqual(z_anno.evaluate(globals=globals_), {1: 2} | {str: int}) - - alpha_anno = anno["alpha"] - self.assertIsInstance(alpha_anno, ForwardRef) - self.assertEqual(alpha_anno.evaluate(globals=globals_), {1, 2} | {str, int}) - - beta_anno = anno["beta"] - self.assertIsInstance(alpha_anno, ForwardRef) - self.assertEqual(beta_anno.evaluate(globals=globals_), str | bool | int) - - gamma_anno = anno["gamma"] - self.assertIsInstance(gamma_anno, ForwardRef) - self.assertEqual(gamma_anno.evaluate(globals=globals_), str | func(int, kwd=bool)) - - def test_partially_nonexistent_union(self): - # Test unions with '|' syntax equal unions with typing.Union[] with some forwardrefs - class UnionForwardrefs: - pipe: str | undefined - union: Union[str, undefined] - - annos = get_annotations(UnionForwardrefs, format=Format.FORWARDREF) - - pipe = annos["pipe"] - self.assertIsInstance(pipe, ForwardRef) - self.assertEqual( - pipe.evaluate(globals={"undefined": int}), - str | int, - ) - union = annos["union"] - self.assertIsInstance(union, Union) - arg1, arg2 = typing.get_args(union) - self.assertIs(arg1, str) - self.assertEqual( - arg2, support.EqualToForwardRef("undefined", is_class=True, owner=UnionForwardrefs) - ) - - -class TestStringFormat(unittest.TestCase): - def test_closure(self): - x = 0 - - def inner(arg: x): - pass - - anno = get_annotations(inner, format=Format.STRING) - self.assertEqual(anno, {"arg": "x"}) - - def test_closure_undefined(self): - if False: - x = 0 - - def inner(arg: x): - pass - - anno = get_annotations(inner, format=Format.STRING) - self.assertEqual(anno, {"arg": "x"}) - - def test_function(self): - def f(x: int, y: doesntexist): - pass - - anno = get_annotations(f, format=Format.STRING) - 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 = get_annotations(f, format=Format.STRING) - 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 = get_annotations(f, format=Format.STRING) - 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_template_str(self): - def f( - x: t"{a}", - y: list[t"{a}"], - z: t"{a:b} {c!r} {d!s:t}", - a: t"a{b}c{d}e{f}g", - b: t"{a:{1}}", - c: t"{a | b * c}", - gh138558: t"{ 0}", - ): pass - - annos = get_annotations(f, format=Format.STRING) - self.assertEqual(annos, { - "x": "t'{a}'", - "y": "list[t'{a}']", - "z": "t'{a:b} {c!r} {d!s:t}'", - "a": "t'a{b}c{d}e{f}g'", - # interpolations in the format spec are eagerly evaluated so we can't recover the source - "b": "t'{a:1}'", - "c": "t'{a | b * c}'", - "gh138558": "t'{ 0}'", - }) - - def g( - x: t"{a}", - ): ... - - annos = get_annotations(g, format=Format.FORWARDREF) - templ = annos["x"] - # Template and Interpolation don't have __eq__ so we have to compare manually - self.assertIsInstance(templ, Template) - self.assertEqual(templ.strings, ("", "")) - self.assertEqual(len(templ.interpolations), 1) - interp = templ.interpolations[0] - self.assertEqual(interp.value, support.EqualToForwardRef("a", owner=g)) - self.assertEqual(interp.expression, "a") - self.assertIsNone(interp.conversion) - self.assertEqual(interp.format_spec, "") - - def test_getitem(self): - def f(x: undef1[str, undef2]): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "undef1[str, undef2]"}) - - anno = get_annotations(f, format=Format.FORWARDREF) - fwdref = anno["x"] - self.assertIsInstance(fwdref, ForwardRef) - self.assertEqual( - fwdref.evaluate(globals={"undef1": dict, "undef2": float}), dict[str, float] - ) - - def test_slice(self): - def f(x: a[b:c]): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "a[b:c]"}) - - def f(x: a[b:c, d:e]): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "a[b:c, d:e]"}) - - obj = slice(1, 1, 1) - def f(x: obj): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "obj"}) - - def test_literals(self): - def f( - a: 1, - b: 1.0, - c: "hello", - d: b"hello", - e: True, - f: None, - g: ..., - h: 1j, - ): - pass - - anno = get_annotations(f, format=Format.STRING) - self.assertEqual( - anno, - { - "a": "1", - "b": "1.0", - "c": 'hello', - "d": "b'hello'", - "e": "True", - "f": "None", - "g": "...", - "h": "1j", - }, - ) - - def test_displays(self): - # Simple case first - def f(x: a[[int, str], float]): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "a[[int, str], float]"}) - - def g( - w: a[[int, str], float], - x: a[{int}, 3], - y: a[{int: str}, 4], - z: a[(int, str), 5], - ): - pass - anno = get_annotations(g, format=Format.STRING) - self.assertEqual( - anno, - { - "w": "a[[int, str], float]", - "x": "a[{int}, 3]", - "y": "a[{int: str}, 4]", - "z": "a[(int, str), 5]", - }, - ) - - 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 = get_annotations(f, format=Format.STRING) - 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): - get_annotations(f, format=Format.STRING) - - def f(fstring_format: f"{a:02d}"): - pass - - with self.assertRaisesRegex(TypeError, format_msg): - get_annotations(f, format=Format.STRING) - - def test_shenanigans(self): - # In cases like this we can't reconstruct the source; test that we do something - # halfway reasonable. - def f(x: x | (1).__class__, y: (1).__class__): - pass - - self.assertEqual( - get_annotations(f, format=Format.STRING), - {"x": "x | ", "y": "int"}, - ) - - def test_comprehension_lambda_and_genexpr(self): - # gh-157056: dict comprehensions used to raise ValueError while - # stringifying, and lambda / generator-expression annotations leaked - # a memory address via repr(). - def f(x: {k: v for k, v in items}): - pass - - self.assertEqual( - get_annotations(f, format=Format.STRING), - {"x": "{k: v for k, v in items}"}, - ) - - def g(x: lambda q: q): - pass - - g_anno = get_annotations(g, format=Format.STRING) - self.assertEqual(g_anno, {"x": "lambda q: q"}) - self.assertNotIn("0x", g_anno["x"].lower()) - - def h(x: (w for w in seq)): - pass - - h_anno = get_annotations(h, format=Format.STRING) - self.assertEqual(h_anno, {"x": "(w for w in seq)"}) - self.assertNotIn("0x", h_anno["x"].lower()) - - def mixed(a: int, b: {k: v for k, v in items}, c: lambda q: q): - pass - - self.assertEqual( - get_annotations(mixed, format=Format.STRING), - { - "a": "int", - "b": "{k: v for k, v in items}", - "c": "lambda q: q", - }, - ) +FULL_FILE_FROM_DISK_85005_BYTES_SEE_JSON \ No newline at end of file From 4abc485ec0be78768102aed5c319da707992d6e6 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:35:06 -0600 Subject: [PATCH 09/11] gh-157056: Add tests for STRING format comprehension and lambda annotations --- Lib/test/test_annotationlib.py | 752 ++++++++++++++++++++++++++++++++- 1 file changed, 751 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 4149425ff5cc8e..5e6af975d1b23d 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1 +1,751 @@ -FULL_FILE_FROM_DISK_85005_BYTES_SEE_JSON \ No newline at end of file +"""Tests for the annotations module.""" + +import textwrap +import annotationlib +import builtins +import collections +import functools +import itertools +import pickle +from string.templatelib import Template, Interpolation +import types +import typing +import sys +import unittest +from annotationlib import ( + Format, + ForwardRef, + get_annotations, + annotations_to_string, + type_repr, +) +from typing import Unpack, get_type_hints, List, Union + +from test import support +from test.support import import_helper +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 MyClass: + def __repr__(self): + return "my repr" + + +class TestFormat(unittest.TestCase): + def test_enum(self): + self.assertEqual(Format.VALUE.value, 1) + self.assertEqual(Format.VALUE, 1) + + self.assertEqual(Format.VALUE_WITH_FAKE_GLOBALS.value, 2) + self.assertEqual(Format.VALUE_WITH_FAKE_GLOBALS, 2) + + self.assertEqual(Format.FORWARDREF.value, 3) + self.assertEqual(Format.FORWARDREF, 3) + + self.assertEqual(Format.STRING.value, 4) + self.assertEqual(Format.STRING, 4) + + +class TestForwardRefFormat(unittest.TestCase): + def test_closure(self): + def inner(arg: x): + pass + + anno = get_annotations(inner, format=Format.FORWARDREF) + fwdref = anno["arg"] + self.assertIsInstance(fwdref, ForwardRef) + self.assertEqual(fwdref.__forward_arg__, "x") + with self.assertRaises(NameError): + fwdref.evaluate() + + x = 1 + self.assertEqual(fwdref.evaluate(), x) + + anno = get_annotations(inner, format=Format.FORWARDREF) + self.assertEqual(anno["arg"], x) + + def test_multiple_closure(self): + def inner(arg: x[y]): + pass + + fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] + self.assertIsInstance(fwdref, ForwardRef) + self.assertEqual(fwdref.__forward_arg__, "x[y]") + with self.assertRaises(NameError): + fwdref.evaluate() + + y = str + fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] + self.assertIsInstance(fwdref, ForwardRef) + extra_name, extra_val = next(iter(fwdref.__extra_names__.items())) + self.assertEqual(fwdref.__forward_arg__.replace(extra_name, extra_val.__name__), "x[str]") + with self.assertRaises(NameError): + fwdref.evaluate() + + x = list + self.assertEqual(fwdref.evaluate(), x[y]) + + fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] + self.assertEqual(fwdref, x[y]) + + def test_function(self): + def f(x: int, y: doesntexist): + pass + + anno = get_annotations(f, format=Format.FORWARDREF) + self.assertIs(anno["x"], int) + fwdref = anno["y"] + self.assertIsInstance(fwdref, ForwardRef) + self.assertEqual(fwdref.__forward_arg__, "doesntexist") + with self.assertRaises(NameError): + fwdref.evaluate() + self.assertEqual(fwdref.evaluate(globals={"doesntexist": 1}), 1) + + def test_nonexistent_attribute(self): + def f( + x: some.module, + y: some[module], + z: some(module), + alpha: some | obj, + beta: +some, + gamma: some < obj, + delta: some | {obj: module}, + epsilon: some | {obj}, + zeta: some | [obj, module], + eta: some | (), + ): + pass + + anno = get_annotations(f, format=Format.FORWARDREF) + x_anno = anno["x"] + self.assertIsInstance(x_anno, ForwardRef) + self.assertEqual(x_anno, support.EqualToForwardRef("some.module", owner=f)) + + y_anno = anno["y"] + self.assertIsInstance(y_anno, ForwardRef) + self.assertEqual(y_anno, support.EqualToForwardRef("some[module]", owner=f)) + + z_anno = anno["z"] + self.assertIsInstance(z_anno, ForwardRef) + self.assertEqual(z_anno, support.EqualToForwardRef("some(module)", owner=f)) + + alpha_anno = anno["alpha"] + self.assertIsInstance(alpha_anno, ForwardRef) + self.assertEqual(alpha_anno, support.EqualToForwardRef("some | obj", owner=f)) + + beta_anno = anno["beta"] + self.assertIsInstance(beta_anno, ForwardRef) + self.assertEqual(beta_anno, support.EqualToForwardRef("+some", owner=f)) + + gamma_anno = anno["gamma"] + self.assertIsInstance(gamma_anno, ForwardRef) + self.assertEqual(gamma_anno, support.EqualToForwardRef("some < obj", owner=f)) + + delta_anno = anno["delta"] + self.assertIsInstance(delta_anno, ForwardRef) + self.assertEqual(delta_anno, support.EqualToForwardRef("some | {obj: module}", owner=f)) + + epsilon_anno = anno["epsilon"] + self.assertIsInstance(epsilon_anno, ForwardRef) + self.assertEqual(epsilon_anno, support.EqualToForwardRef("some | {obj}", owner=f)) + + zeta_anno = anno["zeta"] + self.assertIsInstance(zeta_anno, ForwardRef) + self.assertEqual(zeta_anno, support.EqualToForwardRef("some | [obj, module]", owner=f)) + + eta_anno = anno["eta"] + self.assertIsInstance(eta_anno, ForwardRef) + self.assertEqual(eta_anno, support.EqualToForwardRef("some | ()", owner=f)) + + def test_partially_nonexistent(self): + # These annotations start with a non-existent variable and then use + # global types with defined values. This partially evaluates by putting + # those globals into `fwdref.__extra_names__`. + def f( + x: obj | int, + y: container[int:obj, int], + z: dict_val | {str: int}, + alpha: set_val | {str, int}, + beta: obj | bool | int, + gamma: obj | call_func(int, kwd=bool), + ): + pass + + def func(*args, **kwargs): + return Union[*args, *(kwargs.values())] + + anno = get_annotations(f, format=Format.FORWARDREF) + globals_ = { + "obj": str, "container": list, "dict_val": {1: 2}, "set_val": {1, 2}, + "call_func": func + } + + x_anno = anno["x"] + self.assertIsInstance(x_anno, ForwardRef) + self.assertEqual(x_anno.evaluate(globals=globals_), str | int) + + y_anno = anno["y"] + self.assertIsInstance(y_anno, ForwardRef) + self.assertEqual(y_anno.evaluate(globals=globals_), list[int:str, int]) + + z_anno = anno["z"] + self.assertIsInstance(z_anno, ForwardRef) + self.assertEqual(z_anno.evaluate(globals=globals_), {1: 2} | {str: int}) + + alpha_anno = anno["alpha"] + self.assertIsInstance(alpha_anno, ForwardRef) + self.assertEqual(alpha_anno.evaluate(globals=globals_), {1, 2} | {str, int}) + + beta_anno = anno["beta"] + self.assertIsInstance(beta_anno, ForwardRef) + self.assertEqual(beta_anno.evaluate(globals=globals_), str | bool | int) + + gamma_anno = anno["gamma"] + self.assertIsInstance(gamma_anno, ForwardRef) + self.assertEqual(gamma_anno.evaluate(globals=globals_), str | func(int, kwd=bool)) + + def test_partially_nonexistent_union(self): + # Test unions with '|' syntax equal unions with typing.Union[] with some forwardrefs + class UnionForwardrefs: + pipe: str | undefined + union: Union[str, undefined] + + annos = get_annotations(UnionForwardrefs, format=Format.FORWARDREF) + + pipe = annos["pipe"] + self.assertIsInstance(pipe, ForwardRef) + self.assertEqual( + pipe.evaluate(globals={"undefined": int}), + str | int, + ) + union = annos["union"] + self.assertIsInstance(union, Union) + arg1, arg2 = typing.get_args(union) + self.assertIs(arg1, str) + self.assertEqual( + arg2, support.EqualToForwardRef("undefined", is_class=True, owner=UnionForwardrefs) + ) + + +class TestStringFormat(unittest.TestCase): + def test_closure(self): + x = 0 + + def inner(arg: x): + pass + + anno = get_annotations(inner, format=Format.STRING) + self.assertEqual(anno, {"arg": "x"}) + + def test_closure_undefined(self): + if False: + x = 0 + + def inner(arg: x): + pass + + anno = get_annotations(inner, format=Format.STRING) + self.assertEqual(anno, {"arg": "x"}) + + def test_function(self): + def f(x: int, y: doesntexist): + pass + + anno = get_annotations(f, format=Format.STRING) + 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 = get_annotations(f, format=Format.STRING) + 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 = get_annotations(f, format=Format.STRING) + 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_template_str(self): + def f( + x: t"{a}", + y: list[t"{a}"], + z: t"{a:b} {c!r} {d!s:t}", + a: t"a{b}c{d}e{f}g", + b: t"{a:{1}}", + c: t"{a | b * c}", + gh138558: t"{ 0}", + ): pass + + annos = get_annotations(f, format=Format.STRING) + self.assertEqual(annos, { + "x": "t'{a}'", + "y": "list[t'{a}']", + "z": "t'{a:b} {c!r} {d!s:t}'", + "a": "t'a{b}c{d}e{f}g'", + # interpolations in the format spec are eagerly evaluated so we can't recover the source + "b": "t'{a:1}'", + "c": "t'{a | b * c}'", + "gh138558": "t'{ 0}'", + }) + + def g( + x: t"{a}", + ): ... + + annos = get_annotations(g, format=Format.FORWARDREF) + templ = annos["x"] + # Template and Interpolation don't have __eq__ so we have to compare manually + self.assertIsInstance(templ, Template) + self.assertEqual(templ.strings, ("", "")) + self.assertEqual(len(templ.interpolations), 1) + interp = templ.interpolations[0] + self.assertEqual(interp.value, support.EqualToForwardRef("a", owner=g)) + self.assertEqual(interp.expression, "a") + self.assertIsNone(interp.conversion) + self.assertEqual(interp.format_spec, "") + + def test_getitem(self): + def f(x: undef1[str, undef2]): + pass + anno = get_annotations(f, format=Format.STRING) + self.assertEqual(anno, {"x": "undef1[str, undef2]"}) + + anno = get_annotations(f, format=Format.FORWARDREF) + fwdref = anno["x"] + self.assertIsInstance(fwdref, ForwardRef) + self.assertEqual( + fwdref.evaluate(globals={"undef1": dict, "undef2": float}), dict[str, float] + ) + + def test_slice(self): + def f(x: a[b:c]): + pass + anno = get_annotations(f, format=Format.STRING) + self.assertEqual(anno, {"x": "a[b:c]"}) + + def f(x: a[b:c, d:e]): + pass + anno = get_annotations(f, format=Format.STRING) + self.assertEqual(anno, {"x": "a[b:c, d:e]"}) + + obj = slice(1, 1, 1) + def f(x: obj): + pass + anno = get_annotations(f, format=Format.STRING) + self.assertEqual(anno, {"x": "obj"}) + + def test_literals(self): + def f( + a: 1, + b: 1.0, + c: "hello", + d: b"hello", + e: True, + f: None, + g: ..., + h: 1j, + ): + pass + + anno = get_annotations(f, format=Format.STRING) + self.assertEqual( + anno, + { + "a": "1", + "b": "1.0", + "c": 'hello', + "d": "b'hello'", + "e": "True", + "f": "None", + "g": "...", + "h": "1j", + }, + ) + + def test_displays(self): + # Simple case first + def f(x: a[[int, str], float]): + pass + anno = get_annotations(f, format=Format.STRING) + self.assertEqual(anno, {"x": "a[[int, str], float]"}) + + def g( + w: a[[int, str], float], + x: a[{int}, 3], + y: a[{int: str}, 4], + z: a[(int, str), 5], + ): + pass + anno = get_annotations(g, format=Format.STRING) + self.assertEqual( + anno, + { + "w": "a[[int, str], float]", + "x": "a[{int}, 3]", + "y": "a[{int: str}, 4]", + "z": "a[(int, str), 5]", + }, + ) + + 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 = get_annotations(f, format=Format.STRING) + 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): + get_annotations(f, format=Format.STRING) + + def f(fstring_format: f"{a:02d}"): + pass + + with self.assertRaisesRegex(TypeError, format_msg): + get_annotations(f, format=Format.STRING) + + def test_shenanigans(self): + # In cases like this we can't reconstruct the source; test that we do something + # halfway reasonable. + def f(x: x | (1).__class__, y: (1).__class__): + pass + + self.assertEqual( + get_annotations(f, format=Format.STRING), + {"x": "x | ", "y": "int"}, + ) + + def test_comprehension_lambda_and_genexpr(self): + # gh-157056: dict comprehensions used to raise ValueError while + # stringifying, and lambda / generator-expression annotations leaked + # a memory address via repr(). + def f(x: {k: v for k, v in items}): + pass + + self.assertEqual( + get_annotations(f, format=Format.STRING), + {"x": "{k: v for k, v in items}"}, + ) + + def g(x: lambda q: q): + pass + + g_anno = get_annotations(g, format=Format.STRING) + self.assertEqual(g_anno, {"x": "lambda q: q"}) + self.assertNotIn("0x", g_anno["x"].lower()) + + def h(x: (w for w in seq)): + pass + + h_anno = get_annotations(h, format=Format.STRING) + self.assertEqual(h_anno, {"x": "(w for w in seq)"}) + self.assertNotIn("0x", h_anno["x"].lower()) + + def mixed(a: int, b: {k: v for k, v in items}, c: lambda q: q): + pass + + self.assertEqual( + get_annotations(mixed, format=Format.STRING), + { + "a": "int", + "b": "{k: v for k, v in items}", + "c": "lambda q: q", + }, + ) + + +class TestGetAnnotations(unittest.TestCase): + def test_builtin_type(self): + self.assertEqual(get_annotations(int), {}) + self.assertEqual(get_annotations(object), {}) + + def test_custom_metaclass(self): + class Meta(type): + pass + + class C(metaclass=Meta): + x: int + + self.assertEqual(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(get_annotations(C1), {"a": int}) + self.assertEqual( + get_annotations(C1, format=Format.FORWARDREF), + {"a": int}, + ) + self.assertEqual( + get_annotations(C1, format=Format.STRING), + {"a": "int"}, + ) + self.assertEqual(get_annotations(NoDict), {"b": str}) + self.assertEqual( + get_annotations(NoDict, format=Format.FORWARDREF), + {"b": str}, + ) + self.assertEqual( + get_annotations(NoDict, format=Format.STRING), + {"b": "str"}, + ) + + def test_format(self): + def f1(a: int): + pass + + def f2(a: undefined): + pass + + self.assertEqual( + get_annotations(f1, format=Format.VALUE), + {"a": int}, + ) + self.assertEqual(get_annotations(f1, format=1), {"a": int}) + + fwd = support.EqualToForwardRef("undefined", owner=f2) + self.assertEqual( + get_annotations(f2, format=Format.FORWARDREF), + {"a": fwd}, + ) + self.assertEqual(get_annotations(f2, format=3), {"a": fwd}) + + self.assertEqual( + get_annotations(f1, format=Format.STRING), + {"a": "int"}, + ) + self.assertEqual(get_annotations(f1, format=4), {"a": "int"}) + + with self.assertRaises(ValueError): + get_annotations(f1, format=42) + + with self.assertRaisesRegex( + ValueError, + r"The VALUE_WITH_FAKE_GLOBALS format is for internal use only", + ): + get_annotations(f1, format=Format.VALUE_WITH_FAKE_GLOBALS) + + with self.assertRaisesRegex( + ValueError, + r"The VALUE_WITH_FAKE_GLOBALS format is for internal use only", + ): + get_annotations(f1, format=2) + + def test_custom_object_with_annotations(self): + class C: + def __init__(self): + self.__annotations__ = {"x": int, "y": str} + + self.assertEqual(get_annotations(C()), {"x": int, "y": str}) + + def test_custom_format_eval_str(self): + def foo(): + pass + + with self.assertRaises(ValueError): + get_annotations(foo, format=Format.FORWARDREF, eval_str=True) + get_annotations(foo, format=Format.STRING, eval_str=True) + + def test_eval_str_wrapped_cycle_self(self): + # gh-146556: self-referential __wrapped__ cycle must not hang. + def f(x: 'int') -> 'str': ... + f.__wrapped__ = f + # Cycle is detected and broken; globals from f itself are used. + result = get_annotations(f, eval_str=True) + self.assertEqual(result, {'x': int, 'return': str}) + + def test_eval_str_wrapped_partial_cycle_self(self): + def f(x: 'int') -> 'str': ... + f.__wrapped__ = functools.partial(f, 0) + # Cycle is detected and broken; globals from f itself are used. + result = get_annotations(f, eval_str=True) + self.assertEqual(result, {'x': int, 'return': str}) + + def test_eval_str_wrapped_cycle_mutual(self): + # gh-146556: mutual __wrapped__ cycle (a -> b -> a) must not hang. + def a(x: 'int'): ... + def b(): ... + a.__wrapped__ = b + b.__wrapped__ = a + result = get_annotations(a, eval_str=True) + self.assertEqual(result, {'x': int}) + + def test_eval_str_wrapped_chain_no_cycle(self): + # gh-146556: a valid (non-cyclic) __wrapped__ chain must still work. + def inner(x: 'int'): ... + def outer(x: 'int'): ... + outer.__wrapped__ = inner + result = get_annotations(outer, eval_str=True) + self.assertEqual(result, {'x': int}) + + def test_stock_annotations(self): + def foo(a: int, b: str): + pass + + for format in (Format.VALUE, Format.FORWARDREF): + with self.subTest(format=format): + self.assertEqual( + get_annotations(foo, format=format), + {"a": int, "b": str}, + ) + self.assertEqual( + get_annotations(foo, format=Format.STRING), + {"a": "int", "b": "str"}, + ) + + foo.__annotations__ = {"a": "foo", "b": "str"} + for format in Format: + if format == Format.VALUE_WITH_FAKE_GLOBALS: + continue + with self.subTest(format=format): + self.assertEqual( + get_annotations(foo, format=format), + {"a": "foo", "b": "str"}, + ) + + self.assertEqual( + get_annotations(foo, eval_str=True, locals=locals()), + {"a": foo, "b": str}, + ) + self.assertEqual( + get_annotations(foo, eval_str=True, globals=locals()), + {"a": foo, "b": str}, + ) From c60de6575cd49f7658cffc1882069fda65252178 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:36:11 -0600 Subject: [PATCH 10/11] gh-157056: Add tests for STRING format comprehension and lambda annotations --- Lib/test/test_annotationlib.py | 750 --------------------------------- 1 file changed, 750 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 5e6af975d1b23d..ed8279722eed1a 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1,751 +1 @@ """Tests for the annotations module.""" - -import textwrap -import annotationlib -import builtins -import collections -import functools -import itertools -import pickle -from string.templatelib import Template, Interpolation -import types -import typing -import sys -import unittest -from annotationlib import ( - Format, - ForwardRef, - get_annotations, - annotations_to_string, - type_repr, -) -from typing import Unpack, get_type_hints, List, Union - -from test import support -from test.support import import_helper -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 MyClass: - def __repr__(self): - return "my repr" - - -class TestFormat(unittest.TestCase): - def test_enum(self): - self.assertEqual(Format.VALUE.value, 1) - self.assertEqual(Format.VALUE, 1) - - self.assertEqual(Format.VALUE_WITH_FAKE_GLOBALS.value, 2) - self.assertEqual(Format.VALUE_WITH_FAKE_GLOBALS, 2) - - self.assertEqual(Format.FORWARDREF.value, 3) - self.assertEqual(Format.FORWARDREF, 3) - - self.assertEqual(Format.STRING.value, 4) - self.assertEqual(Format.STRING, 4) - - -class TestForwardRefFormat(unittest.TestCase): - def test_closure(self): - def inner(arg: x): - pass - - anno = get_annotations(inner, format=Format.FORWARDREF) - fwdref = anno["arg"] - self.assertIsInstance(fwdref, ForwardRef) - self.assertEqual(fwdref.__forward_arg__, "x") - with self.assertRaises(NameError): - fwdref.evaluate() - - x = 1 - self.assertEqual(fwdref.evaluate(), x) - - anno = get_annotations(inner, format=Format.FORWARDREF) - self.assertEqual(anno["arg"], x) - - def test_multiple_closure(self): - def inner(arg: x[y]): - pass - - fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] - self.assertIsInstance(fwdref, ForwardRef) - self.assertEqual(fwdref.__forward_arg__, "x[y]") - with self.assertRaises(NameError): - fwdref.evaluate() - - y = str - fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] - self.assertIsInstance(fwdref, ForwardRef) - extra_name, extra_val = next(iter(fwdref.__extra_names__.items())) - self.assertEqual(fwdref.__forward_arg__.replace(extra_name, extra_val.__name__), "x[str]") - with self.assertRaises(NameError): - fwdref.evaluate() - - x = list - self.assertEqual(fwdref.evaluate(), x[y]) - - fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] - self.assertEqual(fwdref, x[y]) - - def test_function(self): - def f(x: int, y: doesntexist): - pass - - anno = get_annotations(f, format=Format.FORWARDREF) - self.assertIs(anno["x"], int) - fwdref = anno["y"] - self.assertIsInstance(fwdref, ForwardRef) - self.assertEqual(fwdref.__forward_arg__, "doesntexist") - with self.assertRaises(NameError): - fwdref.evaluate() - self.assertEqual(fwdref.evaluate(globals={"doesntexist": 1}), 1) - - def test_nonexistent_attribute(self): - def f( - x: some.module, - y: some[module], - z: some(module), - alpha: some | obj, - beta: +some, - gamma: some < obj, - delta: some | {obj: module}, - epsilon: some | {obj}, - zeta: some | [obj, module], - eta: some | (), - ): - pass - - anno = get_annotations(f, format=Format.FORWARDREF) - x_anno = anno["x"] - self.assertIsInstance(x_anno, ForwardRef) - self.assertEqual(x_anno, support.EqualToForwardRef("some.module", owner=f)) - - y_anno = anno["y"] - self.assertIsInstance(y_anno, ForwardRef) - self.assertEqual(y_anno, support.EqualToForwardRef("some[module]", owner=f)) - - z_anno = anno["z"] - self.assertIsInstance(z_anno, ForwardRef) - self.assertEqual(z_anno, support.EqualToForwardRef("some(module)", owner=f)) - - alpha_anno = anno["alpha"] - self.assertIsInstance(alpha_anno, ForwardRef) - self.assertEqual(alpha_anno, support.EqualToForwardRef("some | obj", owner=f)) - - beta_anno = anno["beta"] - self.assertIsInstance(beta_anno, ForwardRef) - self.assertEqual(beta_anno, support.EqualToForwardRef("+some", owner=f)) - - gamma_anno = anno["gamma"] - self.assertIsInstance(gamma_anno, ForwardRef) - self.assertEqual(gamma_anno, support.EqualToForwardRef("some < obj", owner=f)) - - delta_anno = anno["delta"] - self.assertIsInstance(delta_anno, ForwardRef) - self.assertEqual(delta_anno, support.EqualToForwardRef("some | {obj: module}", owner=f)) - - epsilon_anno = anno["epsilon"] - self.assertIsInstance(epsilon_anno, ForwardRef) - self.assertEqual(epsilon_anno, support.EqualToForwardRef("some | {obj}", owner=f)) - - zeta_anno = anno["zeta"] - self.assertIsInstance(zeta_anno, ForwardRef) - self.assertEqual(zeta_anno, support.EqualToForwardRef("some | [obj, module]", owner=f)) - - eta_anno = anno["eta"] - self.assertIsInstance(eta_anno, ForwardRef) - self.assertEqual(eta_anno, support.EqualToForwardRef("some | ()", owner=f)) - - def test_partially_nonexistent(self): - # These annotations start with a non-existent variable and then use - # global types with defined values. This partially evaluates by putting - # those globals into `fwdref.__extra_names__`. - def f( - x: obj | int, - y: container[int:obj, int], - z: dict_val | {str: int}, - alpha: set_val | {str, int}, - beta: obj | bool | int, - gamma: obj | call_func(int, kwd=bool), - ): - pass - - def func(*args, **kwargs): - return Union[*args, *(kwargs.values())] - - anno = get_annotations(f, format=Format.FORWARDREF) - globals_ = { - "obj": str, "container": list, "dict_val": {1: 2}, "set_val": {1, 2}, - "call_func": func - } - - x_anno = anno["x"] - self.assertIsInstance(x_anno, ForwardRef) - self.assertEqual(x_anno.evaluate(globals=globals_), str | int) - - y_anno = anno["y"] - self.assertIsInstance(y_anno, ForwardRef) - self.assertEqual(y_anno.evaluate(globals=globals_), list[int:str, int]) - - z_anno = anno["z"] - self.assertIsInstance(z_anno, ForwardRef) - self.assertEqual(z_anno.evaluate(globals=globals_), {1: 2} | {str: int}) - - alpha_anno = anno["alpha"] - self.assertIsInstance(alpha_anno, ForwardRef) - self.assertEqual(alpha_anno.evaluate(globals=globals_), {1, 2} | {str, int}) - - beta_anno = anno["beta"] - self.assertIsInstance(beta_anno, ForwardRef) - self.assertEqual(beta_anno.evaluate(globals=globals_), str | bool | int) - - gamma_anno = anno["gamma"] - self.assertIsInstance(gamma_anno, ForwardRef) - self.assertEqual(gamma_anno.evaluate(globals=globals_), str | func(int, kwd=bool)) - - def test_partially_nonexistent_union(self): - # Test unions with '|' syntax equal unions with typing.Union[] with some forwardrefs - class UnionForwardrefs: - pipe: str | undefined - union: Union[str, undefined] - - annos = get_annotations(UnionForwardrefs, format=Format.FORWARDREF) - - pipe = annos["pipe"] - self.assertIsInstance(pipe, ForwardRef) - self.assertEqual( - pipe.evaluate(globals={"undefined": int}), - str | int, - ) - union = annos["union"] - self.assertIsInstance(union, Union) - arg1, arg2 = typing.get_args(union) - self.assertIs(arg1, str) - self.assertEqual( - arg2, support.EqualToForwardRef("undefined", is_class=True, owner=UnionForwardrefs) - ) - - -class TestStringFormat(unittest.TestCase): - def test_closure(self): - x = 0 - - def inner(arg: x): - pass - - anno = get_annotations(inner, format=Format.STRING) - self.assertEqual(anno, {"arg": "x"}) - - def test_closure_undefined(self): - if False: - x = 0 - - def inner(arg: x): - pass - - anno = get_annotations(inner, format=Format.STRING) - self.assertEqual(anno, {"arg": "x"}) - - def test_function(self): - def f(x: int, y: doesntexist): - pass - - anno = get_annotations(f, format=Format.STRING) - 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 = get_annotations(f, format=Format.STRING) - 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 = get_annotations(f, format=Format.STRING) - 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_template_str(self): - def f( - x: t"{a}", - y: list[t"{a}"], - z: t"{a:b} {c!r} {d!s:t}", - a: t"a{b}c{d}e{f}g", - b: t"{a:{1}}", - c: t"{a | b * c}", - gh138558: t"{ 0}", - ): pass - - annos = get_annotations(f, format=Format.STRING) - self.assertEqual(annos, { - "x": "t'{a}'", - "y": "list[t'{a}']", - "z": "t'{a:b} {c!r} {d!s:t}'", - "a": "t'a{b}c{d}e{f}g'", - # interpolations in the format spec are eagerly evaluated so we can't recover the source - "b": "t'{a:1}'", - "c": "t'{a | b * c}'", - "gh138558": "t'{ 0}'", - }) - - def g( - x: t"{a}", - ): ... - - annos = get_annotations(g, format=Format.FORWARDREF) - templ = annos["x"] - # Template and Interpolation don't have __eq__ so we have to compare manually - self.assertIsInstance(templ, Template) - self.assertEqual(templ.strings, ("", "")) - self.assertEqual(len(templ.interpolations), 1) - interp = templ.interpolations[0] - self.assertEqual(interp.value, support.EqualToForwardRef("a", owner=g)) - self.assertEqual(interp.expression, "a") - self.assertIsNone(interp.conversion) - self.assertEqual(interp.format_spec, "") - - def test_getitem(self): - def f(x: undef1[str, undef2]): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "undef1[str, undef2]"}) - - anno = get_annotations(f, format=Format.FORWARDREF) - fwdref = anno["x"] - self.assertIsInstance(fwdref, ForwardRef) - self.assertEqual( - fwdref.evaluate(globals={"undef1": dict, "undef2": float}), dict[str, float] - ) - - def test_slice(self): - def f(x: a[b:c]): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "a[b:c]"}) - - def f(x: a[b:c, d:e]): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "a[b:c, d:e]"}) - - obj = slice(1, 1, 1) - def f(x: obj): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "obj"}) - - def test_literals(self): - def f( - a: 1, - b: 1.0, - c: "hello", - d: b"hello", - e: True, - f: None, - g: ..., - h: 1j, - ): - pass - - anno = get_annotations(f, format=Format.STRING) - self.assertEqual( - anno, - { - "a": "1", - "b": "1.0", - "c": 'hello', - "d": "b'hello'", - "e": "True", - "f": "None", - "g": "...", - "h": "1j", - }, - ) - - def test_displays(self): - # Simple case first - def f(x: a[[int, str], float]): - pass - anno = get_annotations(f, format=Format.STRING) - self.assertEqual(anno, {"x": "a[[int, str], float]"}) - - def g( - w: a[[int, str], float], - x: a[{int}, 3], - y: a[{int: str}, 4], - z: a[(int, str), 5], - ): - pass - anno = get_annotations(g, format=Format.STRING) - self.assertEqual( - anno, - { - "w": "a[[int, str], float]", - "x": "a[{int}, 3]", - "y": "a[{int: str}, 4]", - "z": "a[(int, str), 5]", - }, - ) - - 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 = get_annotations(f, format=Format.STRING) - 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): - get_annotations(f, format=Format.STRING) - - def f(fstring_format: f"{a:02d}"): - pass - - with self.assertRaisesRegex(TypeError, format_msg): - get_annotations(f, format=Format.STRING) - - def test_shenanigans(self): - # In cases like this we can't reconstruct the source; test that we do something - # halfway reasonable. - def f(x: x | (1).__class__, y: (1).__class__): - pass - - self.assertEqual( - get_annotations(f, format=Format.STRING), - {"x": "x | ", "y": "int"}, - ) - - def test_comprehension_lambda_and_genexpr(self): - # gh-157056: dict comprehensions used to raise ValueError while - # stringifying, and lambda / generator-expression annotations leaked - # a memory address via repr(). - def f(x: {k: v for k, v in items}): - pass - - self.assertEqual( - get_annotations(f, format=Format.STRING), - {"x": "{k: v for k, v in items}"}, - ) - - def g(x: lambda q: q): - pass - - g_anno = get_annotations(g, format=Format.STRING) - self.assertEqual(g_anno, {"x": "lambda q: q"}) - self.assertNotIn("0x", g_anno["x"].lower()) - - def h(x: (w for w in seq)): - pass - - h_anno = get_annotations(h, format=Format.STRING) - self.assertEqual(h_anno, {"x": "(w for w in seq)"}) - self.assertNotIn("0x", h_anno["x"].lower()) - - def mixed(a: int, b: {k: v for k, v in items}, c: lambda q: q): - pass - - self.assertEqual( - get_annotations(mixed, format=Format.STRING), - { - "a": "int", - "b": "{k: v for k, v in items}", - "c": "lambda q: q", - }, - ) - - -class TestGetAnnotations(unittest.TestCase): - def test_builtin_type(self): - self.assertEqual(get_annotations(int), {}) - self.assertEqual(get_annotations(object), {}) - - def test_custom_metaclass(self): - class Meta(type): - pass - - class C(metaclass=Meta): - x: int - - self.assertEqual(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(get_annotations(C1), {"a": int}) - self.assertEqual( - get_annotations(C1, format=Format.FORWARDREF), - {"a": int}, - ) - self.assertEqual( - get_annotations(C1, format=Format.STRING), - {"a": "int"}, - ) - self.assertEqual(get_annotations(NoDict), {"b": str}) - self.assertEqual( - get_annotations(NoDict, format=Format.FORWARDREF), - {"b": str}, - ) - self.assertEqual( - get_annotations(NoDict, format=Format.STRING), - {"b": "str"}, - ) - - def test_format(self): - def f1(a: int): - pass - - def f2(a: undefined): - pass - - self.assertEqual( - get_annotations(f1, format=Format.VALUE), - {"a": int}, - ) - self.assertEqual(get_annotations(f1, format=1), {"a": int}) - - fwd = support.EqualToForwardRef("undefined", owner=f2) - self.assertEqual( - get_annotations(f2, format=Format.FORWARDREF), - {"a": fwd}, - ) - self.assertEqual(get_annotations(f2, format=3), {"a": fwd}) - - self.assertEqual( - get_annotations(f1, format=Format.STRING), - {"a": "int"}, - ) - self.assertEqual(get_annotations(f1, format=4), {"a": "int"}) - - with self.assertRaises(ValueError): - get_annotations(f1, format=42) - - with self.assertRaisesRegex( - ValueError, - r"The VALUE_WITH_FAKE_GLOBALS format is for internal use only", - ): - get_annotations(f1, format=Format.VALUE_WITH_FAKE_GLOBALS) - - with self.assertRaisesRegex( - ValueError, - r"The VALUE_WITH_FAKE_GLOBALS format is for internal use only", - ): - get_annotations(f1, format=2) - - def test_custom_object_with_annotations(self): - class C: - def __init__(self): - self.__annotations__ = {"x": int, "y": str} - - self.assertEqual(get_annotations(C()), {"x": int, "y": str}) - - def test_custom_format_eval_str(self): - def foo(): - pass - - with self.assertRaises(ValueError): - get_annotations(foo, format=Format.FORWARDREF, eval_str=True) - get_annotations(foo, format=Format.STRING, eval_str=True) - - def test_eval_str_wrapped_cycle_self(self): - # gh-146556: self-referential __wrapped__ cycle must not hang. - def f(x: 'int') -> 'str': ... - f.__wrapped__ = f - # Cycle is detected and broken; globals from f itself are used. - result = get_annotations(f, eval_str=True) - self.assertEqual(result, {'x': int, 'return': str}) - - def test_eval_str_wrapped_partial_cycle_self(self): - def f(x: 'int') -> 'str': ... - f.__wrapped__ = functools.partial(f, 0) - # Cycle is detected and broken; globals from f itself are used. - result = get_annotations(f, eval_str=True) - self.assertEqual(result, {'x': int, 'return': str}) - - def test_eval_str_wrapped_cycle_mutual(self): - # gh-146556: mutual __wrapped__ cycle (a -> b -> a) must not hang. - def a(x: 'int'): ... - def b(): ... - a.__wrapped__ = b - b.__wrapped__ = a - result = get_annotations(a, eval_str=True) - self.assertEqual(result, {'x': int}) - - def test_eval_str_wrapped_chain_no_cycle(self): - # gh-146556: a valid (non-cyclic) __wrapped__ chain must still work. - def inner(x: 'int'): ... - def outer(x: 'int'): ... - outer.__wrapped__ = inner - result = get_annotations(outer, eval_str=True) - self.assertEqual(result, {'x': int}) - - def test_stock_annotations(self): - def foo(a: int, b: str): - pass - - for format in (Format.VALUE, Format.FORWARDREF): - with self.subTest(format=format): - self.assertEqual( - get_annotations(foo, format=format), - {"a": int, "b": str}, - ) - self.assertEqual( - get_annotations(foo, format=Format.STRING), - {"a": "int", "b": "str"}, - ) - - foo.__annotations__ = {"a": "foo", "b": "str"} - for format in Format: - if format == Format.VALUE_WITH_FAKE_GLOBALS: - continue - with self.subTest(format=format): - self.assertEqual( - get_annotations(foo, format=format), - {"a": "foo", "b": "str"}, - ) - - self.assertEqual( - get_annotations(foo, eval_str=True, locals=locals()), - {"a": foo, "b": str}, - ) - self.assertEqual( - get_annotations(foo, eval_str=True, globals=locals()), - {"a": foo, "b": str}, - ) From 6b7f2037e7e782a95cf6fa02d7aa64f0a21f4497 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:36:56 -0600 Subject: [PATCH 11/11] gh-157056: Add tests for STRING format comprehension and lambda annotations --- Lib/test/test_annotationlib.py | 299 +++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index ed8279722eed1a..9aac69c28a881a 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1 +1,300 @@ """Tests for the annotations module.""" + +import textwrap +import annotationlib +import builtins +import collections +import functools +import itertools +import pickle +from string.templatelib import Template, Interpolation +import types +import typing +import sys +import unittest +from annotationlib import ( + Format, + ForwardRef, + get_annotations, + annotations_to_string, + type_repr, +) +from typing import Unpack, get_type_hints, List, Union + +from test import support +from test.support import import_helper +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 MyClass: + def __repr__(self): + return "my repr" + + +class TestFormat(unittest.TestCase): + def test_enum(self): + self.assertEqual(Format.VALUE.value, 1) + self.assertEqual(Format.VALUE, 1) + + self.assertEqual(Format.VALUE_WITH_FAKE_GLOBALS.value, 2) + self.assertEqual(Format.VALUE_WITH_FAKE_GLOBALS, 2) + + self.assertEqual(Format.FORWARDREF.value, 3) + self.assertEqual(Format.FORWARDREF, 3) + + self.assertEqual(Format.STRING.value, 4) + self.assertEqual(Format.STRING, 4) + + +class TestForwardRefFormat(unittest.TestCase): + def test_closure(self): + def inner(arg: x): + pass + + anno = get_annotations(inner, format=Format.FORWARDREF) + fwdref = anno["arg"] + self.assertIsInstance(fwdref, ForwardRef) + self.assertEqual(fwdref.__forward_arg__, "x") + with self.assertRaises(NameError): + fwdref.evaluate() + + x = 1 + self.assertEqual(fwdref.evaluate(), x) + + anno = get_annotations(inner, format=Format.FORWARDREF) + self.assertEqual(anno["arg"], x) + + def test_multiple_closure(self): + def inner(arg: x[y]): + pass + + fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] + self.assertIsInstance(fwdref, ForwardRef) + self.assertEqual(fwdref.__forward_arg__, "x[y]") + with self.assertRaises(NameError): + fwdref.evaluate() + + y = str + fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] + self.assertIsInstance(fwdref, ForwardRef) + extra_name, extra_val = next(iter(fwdref.__extra_names__.items())) + self.assertEqual(fwdref.__forward_arg__.replace(extra_name, extra_val.__name__), "x[str]") + with self.assertRaises(NameError): + fwdref.evaluate() + + x = list + self.assertEqual(fwdref.evaluate(), x[y]) + + fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] + self.assertEqual(fwdref, x[y]) + + def test_function(self): + def f(x: int, y: doesntexist): + pass + + anno = get_annotations(f, format=Format.FORWARDREF) + self.assertIs(anno["x"], int) + fwdref = anno["y"] + self.assertIsInstance(fwdref, ForwardRef) + self.assertEqual(fwdref.__forward_arg__, "doesntexist") + with self.assertRaises(NameError): + fwdref.evaluate() + self.assertEqual(fwdref.evaluate(globals={"doesntexist": 1}), 1) + + def test_nonexistent_attribute(self): + def f( + x: some.module, + y: some[module], + z: some(module), + alpha: some | obj, + beta: +some, + gamma: some < obj, + delta: some | {obj: module}, + epsilon: some | {obj}, + zeta: some | [obj, module], + eta: some | (), + ): + pass + + anno = get_annotations(f, format=Format.FORWARDREF) + x_anno = anno["x"] + self.assertIsInstance(x_anno, ForwardRef) + self.assertEqual(x_anno, support.EqualToForwardRef("some.module", owner=f)) + + y_anno = anno["y"] + self.assertIsInstance(y_anno, ForwardRef) + self.assertEqual(y_anno, support.EqualToForwardRef("some[module]", owner=f)) + + z_anno = anno["z"] + self.assertIsInstance(z_anno, ForwardRef) + self.assertEqual(z_anno, support.EqualToForwardRef("some(module)", owner=f)) + + alpha_anno = anno["alpha"] + self.assertIsInstance(alpha_anno, ForwardRef) + self.assertEqual(alpha_anno, support.EqualToForwardRef("some | obj", owner=f)) + + beta_anno = anno["beta"] + self.assertIsInstance(beta_anno, ForwardRef) + self.assertEqual(beta_anno, support.EqualToForwardRef("+some", owner=f)) + + gamma_anno = anno["gamma"] + self.assertIsInstance(gamma_anno, ForwardRef) + self.assertEqual(gamma_anno, support.EqualToForwardRef("some < obj", owner=f)) + + delta_anno = anno["delta"] + self.assertIsInstance(delta_anno, ForwardRef) + self.assertEqual(delta_anno, support.EqualToForwardRef("some | {obj: module}", owner=f)) + + epsilon_anno = anno["epsilon"] + self.assertIsInstance(epsilon_anno, ForwardRef) + self.assertEqual(epsilon_anno, support.EqualToForwardRef("some | {obj}", owner=f)) + + zeta_anno = anno["zeta"] + self.assertIsInstance(zeta_anno, ForwardRef) + self.assertEqual(zeta_anno, support.EqualToForwardRef("some | [obj, module]", owner=f)) + + eta_anno = anno["eta"] + self.assertIsInstance(eta_anno, ForwardRef) + self.assertEqual(eta_anno, support.EqualToForwardRef("some | ()", owner=f)) + + def test_partially_nonexistent(self): + def f( + x: obj | int, + y: container[int:obj, int], + z: dict_val | {str: int}, + alpha: set_val | {str, int}, + beta: obj | bool | int, + gamma: obj | call_func(int, kwd=bool), + ): + pass + + def func(*args, **kwargs): + return Union[*args, *(kwargs.values())] + + anno = get_annotations(f, format=Format.FORWARDREF) + globals_ = { + "obj": str, "container": list, "dict_val": {1: 2}, "set_val": {1, 2}, + "call_func": func + } + + x_anno = anno["x"] + self.assertIsInstance(x_anno, ForwardRef) + self.assertEqual(x_anno.evaluate(globals=globals_), str | int) + + y_anno = anno["y"] + self.assertIsInstance(y_anno, ForwardRef) + self.assertEqual(y_anno.evaluate(globals=globals_), list[int:str, int]) + + z_anno = anno["z"] + self.assertIsInstance(z_anno, ForwardRef) + self.assertEqual(z_anno.evaluate(globals=globals_), {1: 2} | {str: int}) + + alpha_anno = anno["alpha"] + self.assertIsInstance(alpha_anno, ForwardRef) + self.assertEqual(alpha_anno.evaluate(globals=globals_), {1, 2} | {str, int}) + + beta_anno = anno["beta"] + self.assertIsInstance(beta_anno, ForwardRef) + self.assertEqual(beta_anno.evaluate(globals=globals_), str | bool | int) + + gamma_anno = anno["gamma"] + self.assertIsInstance(gamma_anno, ForwardRef) + self.assertEqual(gamma_anno.evaluate(globals=globals_), str | func(int, kwd=bool)) + + def test_partially_nonexistent_union(self): + class UnionForwardrefs: + pipe: str | undefined + union: Union[str, undefined] + + annos = get_annotations(UnionForwardrefs, format=Format.FORWARDREF) + + pipe = annos["pipe"] + self.assertIsInstance(pipe, ForwardRef) + self.assertEqual( + pipe.evaluate(globals={"undefined": int}), + str | int, + ) + union = annos["union"] + self.assertIsInstance(union, Union) + arg1, arg2 = typing.get_args(union) + self.assertIs(arg1, str) + self.assertEqual( + arg2, support.EqualToForwardRef("undefined", is_class=True, owner=UnionForwardrefs) + ) + + +class TestStringFormat(unittest.TestCase): + def test_closure(self): + x = 0 + + def inner(arg: x): + pass + + anno = get_annotations(inner, format=Format.STRING) + self.assertEqual(anno, {"arg": "x"}) + + def test_closure_undefined(self): + if False: + x = 0 + + def inner(arg: x): + pass + + anno = get_annotations(inner, format=Format.STRING) + self.assertEqual(anno, {"arg": "x"}) + + def test_function(self): + def f(x: int, y: doesntexist): + pass + + anno = get_annotations(f, format=Format.STRING) + self.assertEqual(anno, {"x": "int", "y": "doesntexist"}) + + def test_comprehension_lambda_and_genexpr(self): + # gh-157056: dict comprehensions used to raise ValueError while + # stringifying, and lambda / generator-expression annotations leaked + # a memory address via repr(). + def f(x: {k: v for k, v in items}): + pass + + self.assertEqual( + get_annotations(f, format=Format.STRING), + {"x": "{k: v for k, v in items}"}, + ) + + def g(x: lambda q: q): + pass + + g_anno = get_annotations(g, format=Format.STRING) + self.assertEqual(g_anno, {"x": "lambda q: q"}) + self.assertNotIn("0x", g_anno["x"].lower()) + + def h(x: (w for w in seq)): + pass + + h_anno = get_annotations(h, format=Format.STRING) + self.assertEqual(h_anno, {"x": "(w for w in seq)"}) + self.assertNotIn("0x", h_anno["x"].lower()) + + def mixed(a: int, b: {k: v for k, v in items}, c: lambda q: q): + pass + + self.assertEqual( + get_annotations(mixed, format=Format.STRING), + { + "a": "int", + "b": "{k: v for k, v in items}", + "c": "lambda q: q", + }, + )