Skip to content

Commit

Permalink
Use the new annotations module
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra committed May 31, 2024
1 parent ad8c51c commit c11cc6d
Show file tree
Hide file tree
Showing 7 changed files with 23 additions and 245 deletions.
1 change: 1 addition & 0 deletions Lib/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import ast
import enum
import functools
import sys
import types

Expand Down
232 changes: 1 addition & 231 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"AGEN_CREATED",
"AGEN_RUNNING",
"AGEN_SUSPENDED",
"AnnotationsFormat",
"ArgInfo",
"Arguments",
"Attribute",
Expand All @@ -62,7 +61,6 @@
"ClassFoundException",
"ClosureVars",
"EndOfBlock",
"FORWARDREF",
"FrameInfo",
"FullArgSpec",
"GEN_CLOSED",
Expand Down Expand Up @@ -136,16 +134,15 @@
"istraceback",
"markcoroutinefunction",
"signature",
"SOURCE",
"stack",
"trace",
"unwrap",
"VALUE",
"walktree",
]


import abc
from annotations import get_annotations
import ast
import dis
import collections.abc
Expand Down Expand Up @@ -177,233 +174,6 @@
TPFLAGS_IS_ABSTRACT = 1 << 20


@enum.global_enum
class AnnotationsFormat(enum.IntEnum):
VALUE = 1
FORWARDREF = 2
SOURCE = 3


class _ForwardRef:
def __init__(self, name):
self.name = name


class _ForwardReffer(dict):
def __missing__(self, key):
return _ForwardRef(key)


class Stringifier:
def __init__(self, node):
self.node = node

def _convert(self, other):
if isinstance(other, Stringifier):
return other.node
else:
return ast.Name(id=repr(other))

def _make_binop(op):
def binop(self, other):
return Stringifier(ast.BinOp(self.node, op, self._convert(other)))
return binop

__add__ = _make_binop(ast.Add())
__sub__ = _make_binop(ast.Sub())
__mul__ = _make_binop(ast.Mult())
__matmul__ = _make_binop(ast.MatMult())
__div__ = _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.And())
__floordiv__ = _make_binop(ast.FloorDiv())
__pow__ = _make_binop(ast.Pow())

def _make_unary_op(op):
def unary_op(self):
return Stringifier(ast.UnaryOp(self.node, op))
return unary_op

__invert__ = _make_unary_op(ast.Invert())
__pos__ = _make_binop(ast.UAdd())
__neg__ = _make_binop(ast.USub())

def __getitem__(self, other):
if isinstance(other, tuple):
elts = [self._convert(elt) for elt in other]
other = ast.Tuple(elts)
else:
other = self._convert(other)
return Stringifier(ast.Subscript(self.node, other))

def __getattr__(self, attr):
return Stringifier(ast.Attribute(self.node, attr))

def __call__(self, *args, **kwargs):
return Stringifier(ast.Call(
self.node,
[self._convert(arg) for arg in args],
[ast.keyword(key, self._convert(value)) for key, value in kwargs.items()]
))

def __iter__(self):
return self

def __next__(self):
return Stringifier(ast.Starred(self.node))


class _StringifierDict(dict):
def __missing__(self, key):
return Stringifier(ast.Name(key))


def _call_dunder_annotate(annotate, format):
try:
return annotate(format)
except NotImplementedError:
pass
if format == FORWARDREF:
globals = {**annotate.__builtins__, **annotate.__globals__}
globals = _ForwardReffer(globals)
func = types.FunctionType(
annotate.__code__,
globals,
closure=annotate.__closure__
)
return func(VALUE)
elif format == SOURCE:
globals = _StringifierDict()
func = types.FunctionType(
annotate.__code__,
globals,
# TODO: also replace the closure with stringifiers
closure=annotate.__closure__
)
annos = func(VALUE)
return {
key: ast.unparse(val.node) if isinstance(val, Stringifier) else repr(val)
for key, val in annos.items()
}


def get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=VALUE):
"""Compute the annotations dict for an object.
obj may be a callable, class, or module.
Passing in an object of any other type raises TypeError.
Returns a dict. get_annotations() returns a new dict every time
it's called; calling it twice on the same object will return two
different but equivalent dicts.
This function handles several details for you:
* If eval_str is true, values of type str will
be un-stringized using eval(). This is intended
for use with stringized annotations
("from __future__ import annotations").
* If obj doesn't have an annotations dict, returns an
empty dict. (Functions and methods always have an
annotations dict; classes, modules, and other types of
callables may not.)
* Ignores inherited annotations on classes. If a class
doesn't have its own annotations dict, returns an empty dict.
* All accesses to object members and dict values are done
using getattr() and dict.get() for safety.
* Always, always, always returns a freshly-created dict.
eval_str controls whether or not values of type str are replaced
with the result of calling eval() on those values:
* If eval_str is true, eval() is called on values of type str.
* If eval_str is false (the default), values of type str are unchanged.
globals and locals are passed in to eval(); see the documentation
for eval() for more information. If either globals or locals is
None, this function may replace that value with a context-specific
default, contingent on type(obj):
* If obj is a module, globals defaults to obj.__dict__.
* If obj is a class, globals defaults to
sys.modules[obj.__module__].__dict__ and locals
defaults to the obj class namespace.
* If obj is a callable, globals defaults to obj.__globals__,
although if obj is a wrapped function (using
functools.update_wrapper()) it is first unwrapped.
"""
annotate = getattr(obj, "__annotate__", None)
# TODO remove format != VALUE condition
if annotate is not None and format != VALUE:
ann = _call_dunder_annotate(annotate, format)
elif isinstance(obj, type):
# class
ann = obj.__annotations__

obj_globals = None
module_name = getattr(obj, '__module__', None)
if module_name:
module = sys.modules.get(module_name, None)
if module:
obj_globals = getattr(module, '__dict__', None)
obj_locals = dict(vars(obj))
unwrap = obj
elif isinstance(obj, types.ModuleType):
# module
ann = getattr(obj, '__annotations__', None)
obj_globals = getattr(obj, '__dict__')
obj_locals = None
unwrap = None
elif callable(obj):
# this includes types.Function, types.BuiltinFunctionType,
# types.BuiltinMethodType, functools.partial, functools.singledispatch,
# "class funclike" from Lib/test/test_inspect... on and on it goes.
ann = getattr(obj, '__annotations__', None)
obj_globals = getattr(obj, '__globals__', None)
obj_locals = None
unwrap = obj
else:
raise TypeError(f"{obj!r} is not a module, class, or callable.")

if ann is None:
return {}

if not isinstance(ann, dict):
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")

if not ann:
return {}

if not eval_str:
return dict(ann)

if unwrap is not None:
while True:
if hasattr(unwrap, '__wrapped__'):
unwrap = unwrap.__wrapped__
continue
if isinstance(unwrap, functools.partial):
unwrap = unwrap.func
continue
break
if hasattr(unwrap, "__globals__"):
obj_globals = unwrap.__globals__

if globals is None:
globals = obj_globals
if locals is None:
locals = obj_locals

return_value = {key:
value if not isinstance(value, str) else eval(value, globals, locals)
for key, value in ann.items() }
return return_value


# ----------------------------------------------------------- type-checking
def ismodule(object):
"""Return true if the object is a module."""
Expand Down
12 changes: 12 additions & 0 deletions Lib/test/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
import unittest


class TestFormat(unittest.TestCase):
def test_enum(self):
self.assertEqual(annotations.Format.VALUE.value, 1)
self.assertEqual(annotations.Format.VALUE, 1)

self.assertEqual(annotations.Format.FORWARDREF.value, 2)
self.assertEqual(annotations.Format.FORWARDREF, 2)

self.assertEqual(annotations.Format.SOURCE.value, 3)
self.assertEqual(annotations.Format.SOURCE, 3)


class TestForwardRefFormat(unittest.TestCase):
def test_closure(self):
def inner(arg: x):
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from test.support import check_syntax_error
from test.support import import_helper
import annotations
import inspect
import unittest
import sys
Expand Down Expand Up @@ -459,7 +460,7 @@ def test_var_annot_simple_exec(self):
gns = {}; lns = {}
exec("'docstring'\n"
"x: int = 5\n", gns, lns)
self.assertEqual(lns["__annotate__"](inspect.VALUE), {'x': int})
self.assertEqual(lns["__annotate__"](annotations.Format.VALUE), {'x': int})
with self.assertRaises(KeyError):
gns['__annotate__']

Expand Down
7 changes: 1 addition & 6 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def istest(self, predicate, exp):
self.assertFalse(other(obj), 'not %s(%s)' % (other.__name__, exp))

def test__all__(self):
support.check__all__(self, inspect, not_exported=("modulesbyfile",))
support.check__all__(self, inspect, not_exported=("modulesbyfile",), extra=("get_annotations",))

def generator_function_example(self):
for i in range(2):
Expand Down Expand Up @@ -1592,11 +1592,6 @@ class C(metaclass=M):
attrs = [a[0] for a in inspect.getmembers(C)]
self.assertNotIn('missing', attrs)

def test_annotation_format(self):
self.assertIs(inspect.VALUE, inspect.AnnotationsFormat.VALUE)
self.assertEqual(inspect.VALUE.value, 1)
self.assertEqual(inspect.VALUE, 1)

def test_get_annotations_with_stock_annotations(self):
def foo(a:int, b:str): pass
self.assertEqual(inspect.get_annotations(foo), {'a': int, 'b': str})
Expand Down
8 changes: 4 additions & 4 deletions Lib/test/test_type_annotations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import inspect
import annotations
import textwrap
import types
import unittest
Expand Down Expand Up @@ -373,12 +373,12 @@ class X:
self.assertIsInstance(annotate, types.FunctionType)
self.assertEqual(annotate.__name__, "__annotate__")
with self.assertRaises(NotImplementedError):
annotate(inspect.FORWARDREF)
annotate(annotations.Format.FORWARDREF)
with self.assertRaises(NotImplementedError):
annotate(inspect.SOURCE)
annotate(annotations.Format.SOURCE)
with self.assertRaises(NotImplementedError):
annotate(None)
self.assertEqual(annotate(inspect.VALUE), {"x": int})
self.assertEqual(annotate(annotations.Format.VALUE), {"x": int})

def test_comprehension_in_annotation(self):
# This crashed in an earlier version of the code
Expand Down
5 changes: 2 additions & 3 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import collections.abc
import copyreg
import functools
import inspect
import operator
import sys
import types
Expand Down Expand Up @@ -2937,7 +2936,7 @@ def __new__(cls, typename, bases, ns):
if "__annotations__" in ns:
types = ns["__annotations__"]
elif "__annotate__" in ns:
types = ns["__annotate__"](inspect.VALUE)
types = ns["__annotate__"](annotations.Format.VALUE)
else:
types = {}
default_names = []
Expand Down Expand Up @@ -3103,7 +3102,7 @@ def __new__(cls, name, bases, ns, total=True):
if "__annotations__" in ns:
own_annotations = ns["__annotations__"]
elif "__annotate__" in ns:
own_annotations = ns["__annotate__"](inspect.VALUE)
own_annotations = ns["__annotate__"](annotations.Format.VALUE)
else:
own_annotations = {}
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
Expand Down

0 comments on commit c11cc6d

Please sign in to comment.