bpo-43817: Add inspect.get_annotations(). (#25522)

Add inspect.get_annotations, which safely computes the annotations defined on an object.  It works around the quirks of accessing the annotations from various types of objects, and makes very few assumptions about the object passed in. inspect.get_annotations can also correctly un-stringize stringized annotations.

inspect.signature, inspect.from_callable, and inspect.from_function now call inspect.get_annotations to retrieve annotations.  This means inspect.signature and inspect.from_callable can now un-stringize stringized annotations, too.
This commit is contained in:
larryhastings 2021-04-29 21:16:28 -07:00 • committed by GitHub
parent a62e424de0
commit 74613a46fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 513 additions and 31 deletions

View file

@ -562,7 +562,7 @@ The Signature object represents the call signature of a callable object and its
return annotation. To retrieve a Signature object, use the :func:`signature`
function.
.. function:: signature(callable, *, follow_wrapped=True, globalns=None, localns=None)
.. function:: signature(callable, *, follow_wrapped=True, globals=None, locals=None, eval_str=False)
Return a :class:`Signature` object for the given ``callable``::
@ -584,11 +584,20 @@ function.
Accepts a wide range of Python callables, from plain functions and classes to
:func:`functools.partial` objects.
Raises :exc:`ValueError` if no signature can be provided, and
:exc:`TypeError` if that type of object is not supported.
For objects defined in modules using stringized annotations
(``from __future__ import annotations``), :func:`signature` will
attempt to automatically un-stringize the annotations using
:func:`inspect.get_annotations()`. The
``global``, ``locals``, and ``eval_str`` parameters are passed
into :func:`inspect.get_annotations()` when resolving the
annotations; see the documentation for :func:`inspect.get_annotations()`
for instructions on how to use these parameters.
``globalns`` and ``localns`` are passed into
:func:`typing.get_type_hints` when resolving the annotations.
Raises :exc:`ValueError` if no signature can be provided, and
:exc:`TypeError` if that type of object is not supported. Also,
if the annotations are stringized, and ``eval_str`` is not false,
the ``eval()`` call(s) to un-stringize the annotations could
potentially raise any kind of exception.
A slash(/) in the signature of a function denotes that the parameters prior
to it are positional-only. For more info, see
@ -600,7 +609,7 @@ function.
unwrap decorated callables.)
.. versionadded:: 3.10
``globalns`` and ``localns`` parameters.
``globals``, ``locals``, and ``eval_str`` parameters.
.. note::
@ -608,12 +617,6 @@ function.
Python. For example, in CPython, some built-in functions defined in
C provide no metadata about their arguments.
.. note::
Will first try to resolve the annotations, but when it fails and
encounters with an error while that operation, the annotations will be
returned unchanged (as strings).
.. class:: Signature(parameters=None, *, return_annotation=Signature.empty)
@ -1115,6 +1118,55 @@ Classes and functions
.. versionadded:: 3.4
.. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False)
Compute the annotations dict for an object.
``obj`` may be a callable, class, or module.
Passing in an object of any other type raises :exc:`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 :func:`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 :func:`eval()` on those values:
* If eval_str is true, :func:`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 :func:`eval()`; see the documentation
for :func:`eval()` for more information. If ``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.
.. versionadded:: 3.10
.. _inspect-stack:
The interpreter stack

View file

@ -24,6 +24,8 @@ Here are some of the useful functions provided by this module:
stack(), trace() - get info about frames on the stack or in a traceback
signature() - get a Signature object for the callable
get_annotations() - safely compute an object's annotations
"""
# This module is in the public domain. No warranties.
@ -60,6 +62,122 @@ for k, v in dis.COMPILER_FLAG_NAMES.items():
# See Include/object.h
TPFLAGS_IS_ABSTRACT = 1 << 20
def get_annotations(obj, *, globals=None, locals=None, eval_str=False):
"""Compute the annotations dict for an object.
obj may be a callable, class, or module.
Passing in an object of any other type raises TypeError.
Returns a dict. get_annotations() returns a new dict every time
it's called; calling it twice on the same object will return two
different but equivalent dicts.
This function handles several details for you:
* If eval_str is true, values of type str will
be un-stringized using eval(). This is intended
for use with stringized annotations
("from __future__ import annotations").
* If obj doesn't have an annotations dict, returns an
empty dict. (Functions and methods always have an
annotations dict; classes, modules, and other types of
callables may not.)
* Ignores inherited annotations on classes. If a class
doesn't have its own annotations dict, returns an empty dict.
* All accesses to object members and dict values are done
using getattr() and dict.get() for safety.
* Always, always, always returns a freshly-created dict.
eval_str controls whether or not values of type str are replaced
with the result of calling eval() on those values:
* If eval_str is true, eval() is called on values of type str.
* If eval_str is false (the default), values of type str are unchanged.
globals and locals are passed in to eval(); see the documentation
for eval() for more information. If either globals or locals is
None, this function may replace that value with a context-specific
default, contingent on type(obj):
* If obj is a module, globals defaults to obj.__dict__.
* If obj is a class, globals defaults to
sys.modules[obj.__module__].__dict__ and locals
defaults to the obj class namespace.
* If obj is a callable, globals defaults to obj.__globals__,
although if obj is a wrapped function (using
functools.update_wrapper()) it is first unwrapped.
"""
if isinstance(obj, type):
# class
obj_dict = getattr(obj, '__dict__', None)
if obj_dict and hasattr(obj_dict, 'get'):
ann = obj_dict.get('__annotations__', None)
if isinstance(ann, types.GetSetDescriptorType):
ann = None
else:
ann = None
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.
@ -1165,7 +1283,8 @@ def getfullargspec(func):
sig = _signature_from_callable(func,
follow_wrapper_chains=False,
skip_bound_arg=False,
sigcls=Signature)
sigcls=Signature,
eval_str=False)
except Exception as ex:
# Most of the times 'signature' will raise ValueError.
# But, it can also raise AttributeError, and, maybe something
@ -1898,7 +2017,7 @@ def _signature_is_functionlike(obj):
isinstance(name, str) and
(defaults is None or isinstance(defaults, tuple)) and
(kwdefaults is None or isinstance(kwdefaults, dict)) and
isinstance(annotations, dict))
(isinstance(annotations, (dict)) or annotations is None) )
def _signature_get_bound_param(spec):
@ -2151,7 +2270,7 @@ def _signature_from_builtin(cls, func, skip_bound_arg=True):
def _signature_from_function(cls, func, skip_bound_arg=True,
globalns=None, localns=None):
globals=None, locals=None, eval_str=False):
"""Private helper: constructs Signature for the given python function."""
is_duck_function = False
@ -2177,7 +2296,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True,
positional = arg_names[:pos_count]
keyword_only_count = func_code.co_kwonlyargcount
keyword_only = arg_names[pos_count:pos_count + keyword_only_count]
annotations = func.__annotations__
annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str)
defaults = func.__defaults__
kwdefaults = func.__kwdefaults__
@ -2248,8 +2367,9 @@ def _signature_from_function(cls, func, skip_bound_arg=True,
def _signature_from_callable(obj, *,
follow_wrapper_chains=True,
skip_bound_arg=True,
globalns=None,
localns=None,
globals=None,
locals=None,
eval_str=False,
sigcls):
"""Private helper function to get signature for arbitrary
@ -2259,9 +2379,10 @@ def _signature_from_callable(obj, *,
_get_signature_of = functools.partial(_signature_from_callable,
follow_wrapper_chains=follow_wrapper_chains,
skip_bound_arg=skip_bound_arg,
globalns=globalns,
localns=localns,
sigcls=sigcls)
globals=globals,
locals=locals,
sigcls=sigcls,
eval_str=eval_str)
if not callable(obj):
raise TypeError('{!r} is not a callable object'.format(obj))
@ -2330,7 +2451,7 @@ def _signature_from_callable(obj, *,
# of a Python function (Cython functions, for instance), then:
return _signature_from_function(sigcls, obj,
skip_bound_arg=skip_bound_arg,
globalns=globalns, localns=localns)
globals=globals, locals=locals, eval_str=eval_str)
if _signature_is_builtin(obj):
return _signature_from_builtin(sigcls, obj,
@ -2854,11 +2975,11 @@ class Signature:
@classmethod
def from_callable(cls, obj, *,
follow_wrapped=True, globalns=None, localns=None):
follow_wrapped=True, globals=None, locals=None, eval_str=False):
"""Constructs Signature for the given callable object."""
return _signature_from_callable(obj, sigcls=cls,
follow_wrapper_chains=follow_wrapped,
globalns=globalns, localns=localns)
globals=globals, locals=locals, eval_str=eval_str)
@property
def parameters(self):
@ -3106,10 +3227,10 @@ class Signature:
return rendered
def signature(obj, *, follow_wrapped=True, globalns=None, localns=None):
def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False):
"""Get a signature object for the passed callable."""
return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
globalns=globalns, localns=localns)
globals=globals, locals=locals, eval_str=eval_str)
def _main():

View file

@ -0,0 +1,28 @@
a:int=3
b:str="foo"
class MyClass:
a:int=4
b:str="bar"
def __init__(self, a, b):
self.a = a
self.b = b
def __eq__(self, other):
return isinstance(other, MyClass) and self.a == other.a and self.b == other.b
def function(a:int, b:str) -> MyClass:
return MyClass(a, b)
def function2(a:int, b:"str", c:MyClass) -> MyClass:
pass
def function3(a:"int", b:"str", c:"MyClass"):
pass
class UnannotatedClass:
pass
def unannotated_function(a, b, c): pass

View file

@ -0,0 +1,34 @@
from __future__ import annotations
a:int=3
b:str="foo"
class MyClass:
a:int=4
b:str="bar"
def __init__(self, a, b):
self.a = a
self.b = b
def __eq__(self, other):
return isinstance(other, MyClass) and self.a == other.a and self.b == other.b
def function(a:int, b:str) -> MyClass:
return MyClass(a, b)
def function2(a:int, b:"str", c:MyClass) -> MyClass:
pass
def function3(a:"int", b:"str", c:"MyClass"):
pass
class UnannotatedClass:
pass
def unannotated_function(a, b, c): pass
class MyClassWithLocalAnnotations:
mytype = int
x: mytype

View file

@ -0,0 +1,3 @@
from __future__ import annotations
def foo(a, b, c): pass

View file

@ -32,6 +32,9 @@ from test.support.script_helper import assert_python_ok, assert_python_failure
from test import inspect_fodder as mod
from test import inspect_fodder2 as mod2
from test import support
from test import inspect_stock_annotations
from test import inspect_stringized_annotations
from test import inspect_stringized_annotations_2
from test.test_import import _ready_to_import
@ -1281,6 +1284,106 @@ class TestClassesAndFunctions(unittest.TestCase):
attrs = [a[0] for a in inspect.getmembers(C)]
self.assertNotIn('missing', attrs)
def test_get_annotations_with_stock_annotations(self):
def foo(a:int, b:str): pass
self.assertEqual(inspect.get_annotations(foo), {'a': int, 'b': str})
foo.__annotations__ = {'a': 'foo', 'b':'str'}
self.assertEqual(inspect.get_annotations(foo), {'a': 'foo', 'b': 'str'})
self.assertEqual(inspect.get_annotations(foo, eval_str=True, locals=locals()), {'a': foo, 'b': str})
self.assertEqual(inspect.get_annotations(foo, eval_str=True, globals=locals()), {'a': foo, 'b': str})
isa = inspect_stock_annotations
self.assertEqual(inspect.get_annotations(isa), {'a': int, 'b': str})
self.assertEqual(inspect.get_annotations(isa.MyClass), {'a': int, 'b': str})
self.assertEqual(inspect.get_annotations(isa.function), {'a': int, 'b': str, 'return': isa.MyClass})
self.assertEqual(inspect.get_annotations(isa.function2), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass})
self.assertEqual(inspect.get_annotations(isa.function3), {'a': 'int', 'b': 'str', 'c': 'MyClass'})
self.assertEqual(inspect.get_annotations(inspect), {}) # inspect module has no annotations
self.assertEqual(inspect.get_annotations(isa.UnannotatedClass), {})
self.assertEqual(inspect.get_annotations(isa.unannotated_function), {})
self.assertEqual(inspect.get_annotations(isa, eval_str=True), {'a': int, 'b': str})
self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=True), {'a': int, 'b': str})
self.assertEqual(inspect.get_annotations(isa.function, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass})
self.assertEqual(inspect.get_annotations(isa.function2, eval_str=True), {'a': int, 'b': str, 'c': isa.MyClass, 'return': isa.MyClass})
self.assertEqual(inspect.get_annotations(isa.function3, eval_str=True), {'a': int, 'b': str, 'c': isa.MyClass})
self.assertEqual(inspect.get_annotations(inspect, eval_str=True), {})
self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=True), {})
self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=True), {})
self.assertEqual(inspect.get_annotations(isa, eval_str=False), {'a': int, 'b': str})
self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=False), {'a': int, 'b': str})
self.assertEqual(inspect.get_annotations(isa.function, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass})
self.assertEqual(inspect.get_annotations(isa.function2, eval_str=False), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass})
self.assertEqual(inspect.get_annotations(isa.function3, eval_str=False), {'a': 'int', 'b': 'str', 'c': 'MyClass'})
self.assertEqual(inspect.get_annotations(inspect, eval_str=False), {})
self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=False), {})
self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=False), {})
def times_three(fn):
@functools.wraps(fn)
def wrapper(a, b):
return fn(a*3, b*3)
return wrapper
wrapped = times_three(isa.function)
self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx'))
self.assertIsNot(wrapped.__globals__, isa.function.__globals__)
self.assertEqual(inspect.get_annotations(wrapped), {'a': int, 'b': str, 'return': isa.MyClass})
self.assertEqual(inspect.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass})
self.assertEqual(inspect.get_annotations(wrapped, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass})
def test_get_annotations_with_stringized_annotations(self):
isa = inspect_stringized_annotations
self.assertEqual(inspect.get_annotations(isa), {'a': 'int', 'b': 'str'})
self.assertEqual(inspect.get_annotations(isa.MyClass), {'a': 'int', 'b': 'str'})
self.assertEqual(inspect.get_annotations(isa.function), {'a': 'int', 'b': 'str', 'return': 'MyClass'})
self.assertEqual(inspect.get_annotations(isa.function2), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'})
self.assertEqual(inspect.get_annotations(isa.function3), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"})
self.assertEqual(inspect.get_annotations(isa.UnannotatedClass), {})
self.assertEqual(inspect.get_annotations(isa.unannotated_function), {})
self.assertEqual(inspect.get_annotations(isa, eval_str=True), {'a': int, 'b': str})
self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=True), {'a': int, 'b': str})
self.assertEqual(inspect.get_annotations(isa.function, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass})
self.assertEqual(inspect.get_annotations(isa.function2, eval_str=True), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass})
self.assertEqual(inspect.get_annotations(isa.function3, eval_str=True), {'a': 'int', 'b': 'str', 'c': 'MyClass'})
self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=True), {})
self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=True), {})
self.assertEqual(inspect.get_annotations(isa, eval_str=False), {'a': 'int', 'b': 'str'})
self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=False), {'a': 'int', 'b': 'str'})
self.assertEqual(inspect.get_annotations(isa.function, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'})
self.assertEqual(inspect.get_annotations(isa.function2, eval_str=False), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'})
self.assertEqual(inspect.get_annotations(isa.function3, eval_str=False), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"})
self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=False), {})
self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=False), {})
isa2 = inspect_stringized_annotations_2
self.assertEqual(inspect.get_annotations(isa2), {})
self.assertEqual(inspect.get_annotations(isa2, eval_str=True), {})
self.assertEqual(inspect.get_annotations(isa2, eval_str=False), {})
def times_three(fn):
@functools.wraps(fn)
def wrapper(a, b):
return fn(a*3, b*3)
return wrapper
wrapped = times_three(isa.function)
self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx'))
self.assertIsNot(wrapped.__globals__, isa.function.__globals__)
self.assertEqual(inspect.get_annotations(wrapped), {'a': 'int', 'b': 'str', 'return': 'MyClass'})
self.assertEqual(inspect.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass})
self.assertEqual(inspect.get_annotations(wrapped, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'})
# test that local namespace lookups work
self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations), {'x': 'mytype'})
self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int})
class TestIsDataDescriptor(unittest.TestCase):
def test_custom_descriptors(self):
@ -2786,13 +2889,13 @@ class TestSignatureObject(unittest.TestCase):
pass
ham = partialmethod(test, c=1)
self.assertEqual(self.signature(Spam.ham),
self.assertEqual(self.signature(Spam.ham, eval_str=False),
((('it', ..., ..., 'positional_or_keyword'),
('a', ..., ..., 'positional_or_keyword'),
('c', 1, ..., 'keyword_only')),
'spam'))
self.assertEqual(self.signature(Spam().ham),
self.assertEqual(self.signature(Spam().ham, eval_str=False),
((('a', ..., ..., 'positional_or_keyword'),
('c', 1, ..., 'keyword_only')),
'spam'))
@ -2803,7 +2906,7 @@ class TestSignatureObject(unittest.TestCase):
g = partialmethod(test, 1)
self.assertEqual(self.signature(Spam.g),
self.assertEqual(self.signature(Spam.g, eval_str=False),
((('self', ..., 'anno', 'positional_or_keyword'),),
...))
@ -3265,15 +3368,145 @@ class TestSignatureObject(unittest.TestCase):
self.assertEqual(sig1.return_annotation, int)
self.assertEqual(sig1.parameters['foo'].annotation, Foo)
sig2 = signature_func(func, localns=locals())
sig2 = signature_func(func, locals=locals())
self.assertEqual(sig2.return_annotation, int)
self.assertEqual(sig2.parameters['foo'].annotation, Foo)
sig3 = signature_func(func2, globalns={'Bar': int}, localns=locals())
sig3 = signature_func(func2, globals={'Bar': int}, locals=locals())
self.assertEqual(sig3.return_annotation, int)
self.assertEqual(sig3.parameters['foo'].annotation, Foo)
self.assertEqual(sig3.parameters['bar'].annotation, 'Bar')
def test_signature_eval_str(self):
isa = inspect_stringized_annotations
sig = inspect.Signature
par = inspect.Parameter
PORK = inspect.Parameter.POSITIONAL_OR_KEYWORD
for signature_func in (inspect.signature, inspect.Signature.from_callable):
with self.subTest(signature_func = signature_func):
self.assertEqual(
signature_func(isa.MyClass),
sig(
parameters=(
par('a', PORK),
par('b', PORK),
)))
self.assertEqual(
signature_func(isa.function),
sig(
return_annotation='MyClass',
parameters=(
par('a', PORK, annotation='int'),
par('b', PORK, annotation='str'),
)))
self.assertEqual(
signature_func(isa.function2),
sig(
return_annotation='MyClass',
parameters=(
par('a', PORK, annotation='int'),
par('b', PORK, annotation="'str'"),
par('c', PORK, annotation="MyClass"),
)))
self.assertEqual(
signature_func(isa.function3),
sig(
parameters=(
par('a', PORK, annotation="'int'"),
par('b', PORK, annotation="'str'"),
par('c', PORK, annotation="'MyClass'"),
)))
self.assertEqual(signature_func(isa.UnannotatedClass), sig())
self.assertEqual(signature_func(isa.unannotated_function),
sig(
parameters=(
par('a', PORK),
par('b', PORK),
par('c', PORK),
)))
self.assertEqual(
signature_func(isa.MyClass, eval_str=True),
sig(
parameters=(
par('a', PORK),
par('b', PORK),
)))
self.assertEqual(
signature_func(isa.function, eval_str=True),
sig(
return_annotation=isa.MyClass,
parameters=(
par('a', PORK, annotation=int),
par('b', PORK, annotation=str),
)))
self.assertEqual(
signature_func(isa.function2, eval_str=True),
sig(
return_annotation=isa.MyClass,
parameters=(
par('a', PORK, annotation=int),
par('b', PORK, annotation='str'),
par('c', PORK, annotation=isa.MyClass),
)))
self.assertEqual(
signature_func(isa.function3, eval_str=True),
sig(
parameters=(
par('a', PORK, annotation='int'),
par('b', PORK, annotation='str'),
par('c', PORK, annotation='MyClass'),
)))
globalns = {'int': float, 'str': complex}
localns = {'str': tuple, 'MyClass': dict}
with self.assertRaises(NameError):
signature_func(isa.function, eval_str=True, globals=globalns)
self.assertEqual(
signature_func(isa.function, eval_str=True, locals=localns),
sig(
return_annotation=dict,
parameters=(
par('a', PORK, annotation=int),
par('b', PORK, annotation=tuple),
)))
self.assertEqual(
signature_func(isa.function, eval_str=True, globals=globalns, locals=localns),
sig(
return_annotation=dict,
parameters=(
par('a', PORK, annotation=float),
par('b', PORK, annotation=tuple),
)))
def test_signature_none_annotation(self):
class funclike:
# Has to be callable, and have correct
# __code__, __annotations__, __defaults__, __name__,
# and __kwdefaults__ attributes
def __init__(self, func):
self.__name__ = func.__name__
self.__code__ = func.__code__
self.__annotations__ = func.__annotations__
self.__defaults__ = func.__defaults__
self.__kwdefaults__ = func.__kwdefaults__
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def foo(): pass
foo = funclike(foo)
foo.__annotations__ = None
for signature_func in (inspect.signature, inspect.Signature.from_callable):
with self.subTest(signature_func = signature_func):
self.assertEqual(signature_func(foo), inspect.Signature())
self.assertEqual(inspect.get_annotations(foo), {})
class TestParameterObject(unittest.TestCase):
def test_signature_parameter_kinds(self):

View file

@ -0,0 +1,11 @@
Add :func:`inspect.get_annotations`, which safely computes the annotations
defined on an object. It works around the quirks of accessing the
annotations from various types of objects, and makes very few assumptions
about the object passed in. :func:`inspect.get_annotations` can also
correctly un-stringize stringized annotations.
:func:`inspect.signature`, :func:`inspect.from_callable`, and
:func:`inspect.from_function` now call :func:`inspect.get_annotations`
to retrieve annotations. This means :func:`inspect.signature`
and :func:`inspect.from_callable` can now un-stringize stringized
annotations, too.