| """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__", |
| ) |
| |
| |
| 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 |
| 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 |
| |
| 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.__forward_arg__ |
| case Format.VALUE: |
| is_forwardref_format = False |
| case Format.FORWARDREF: |
| is_forwardref_format = True |
| case _: |
| raise NotImplementedError(format) |
| if self.__cell__ is not None: |
| 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 locals is None: |
| locals = {} |
| if isinstance(owner, type): |
| locals.update(vars(owner)) |
| |
| if type_params is None and owner is not None: |
| # "Inject" type parameters into the local namespace |
| # (unless they are shadowed by assignments *in* the local namespace), |
| # as a way of emulating annotation scopes when calling `eval()` |
| type_params = getattr(owner, "__type_params__", None) |
| |
| # Type parameters exist in their own scope, which is logically |
| # between the locals and the globals. We simulate this by adding |
| # them to the globals. |
| if type_params is not None: |
| globals = dict(globals) |
| for param in type_params: |
| globals[param.__name__] = param |
| if self.__extra_names__: |
| locals = {**locals, **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 |
| new_locals = _StringifierDict( |
| {**builtins.__dict__, **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() |
| return result |
| |
| def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard): |
| import typing |
| import warnings |
| |
| if type_params is _sentinel: |
| typing._deprecation_warning_for_no_type_params_passed( |
| "typing.ForwardRef._evaluate" |
| ) |
| type_params = () |
| warnings._deprecated( |
| "ForwardRef._evaluate", |
| "{name} is a private API and is retained for compatibility, but will be removed" |
| " in Python 3.16. Use ForwardRef.evaluate() or typing.evaluate_forward_ref() instead.", |
| remove=(3, 16), |
| ) |
| return typing.evaluate_forward_ref( |
| self, |
| globals=globalns, |
| locals=localns, |
| type_params=type_params, |
| _recursive_guard=recursive_guard, |
| ) |
| |
| @property |
| def __forward_arg__(self): |
| if self.__arg__ is not None: |
| return self.__arg__ |
| if self.__ast_node__ is not None: |
| self.__arg__ = ast.unparse(self.__ast_node__) |
| return self.__arg__ |
| raise AssertionError( |
| "Attempted to access '__forward_arg__' on an uninitialized ForwardRef" |
| ) |
| |
| @property |
| def __forward_code__(self): |
| if self.__code__ is not None: |
| return self.__code__ |
| arg = self.__forward_arg__ |
| # If we do `def f(*args: *Ts)`, then we'll have `arg = '*Ts'`. |
| # Unfortunately, this isn't a valid expression on its own, so we |
| # do the unpacking manually. |
| if arg.startswith("*"): |
| arg_to_compile = f"({arg},)[0]" # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] |
| else: |
| arg_to_compile = arg |
| try: |
| self.__code__ = compile(arg_to_compile, "<string>", "eval") |
| except SyntaxError: |
| raise SyntaxError(f"Forward reference must be an expression -- got {arg!r}") |
| return self.__code__ |
| |
| def __eq__(self, other): |
| if not isinstance(other, ForwardRef): |
| return NotImplemented |
| 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__ |
| and self.__cell__ == 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__, |
| 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.__forward_arg__!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 |
| |
| 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): |
| 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 |
| |
| 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. |
| 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 = _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 Exception: |
| pass |
| else: |
| globals.transmogrify() |
| 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 = _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() |
| 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 |
| freevars = annotate.__code__.co_freevars |
| new_closure = [] |
| for i, cell in enumerate(annotate.__closure__): |
| if i < len(freevars): |
| name = freevars[i] |
| else: |
| 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) |
| |
| |
| 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 SOURCE 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: |
| while True: |
| if hasattr(unwrap, "__wrapped__"): |
| unwrap = unwrap.__wrapped__ |
| continue |
| if functools := sys.modules.get("functools"): |
| if isinstance(unwrap, functools.partial): |
| unwrap = unwrap.func |
| continue |
| break |
| if hasattr(unwrap, "__globals__"): |
| obj_globals = unwrap.__globals__ |
| |
| if globals is None: |
| globals = obj_globals |
| if locals is None: |
| locals = obj_locals |
| |
| # "Inject" type parameters into the local namespace |
| # (unless they are shadowed by assignments *in* the local namespace), |
| # as a way of emulating annotation scopes when calling `eval()` |
| if type_params := getattr(obj, "__type_params__", ()): |
| if locals is None: |
| locals = {} |
| locals = {param.__name__: param for param in type_params} | locals |
| |
| return_value = { |
| key: value if not isinstance(value, str) else eval(value, globals, locals) |
| for key, value in ann.items() |
| } |
| return return_value |
| |
| |
| 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 _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 |