From 991d14fee1805e17647940a2a8cbf4f62f0f09ea Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 9 Nov 2016 13:12:51 -0800 Subject: Issue #28556: More typing.py updates from upstream. --- Lib/test/test_typing.py | 63 +++++++++++---- Lib/typing.py | 206 +++++++++++++++++------------------------------- 2 files changed, 119 insertions(+), 150 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 7a5b415..ae9de61 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -378,6 +378,16 @@ class CallableTests(BaseTestCase): with self.assertRaises(TypeError): type(c)() + def test_callable_wrong_forms(self): + with self.assertRaises(TypeError): + Callable[[...], int] + with self.assertRaises(TypeError): + Callable[(), int] + with self.assertRaises(TypeError): + Callable[[()], int] + with self.assertRaises(TypeError): + Callable[[int, 1], 2] + def test_callable_instance_works(self): def f(): pass @@ -1296,9 +1306,10 @@ PY36 = sys.version_info[:2] >= (3, 6) PY36_TESTS = """ from test import ann_module, ann_module2, ann_module3 -from collections import ChainMap -class B: +class A: + y: float +class B(A): x: ClassVar[Optional['B']] = None y: int class CSub(B): @@ -1317,6 +1328,15 @@ if PY36: gth = get_type_hints class GetTypeHintTests(BaseTestCase): + def test_get_type_hints_from_various_objects(self): + # For invalid objects should fail with TypeError (not AttributeError etc). + with self.assertRaises(TypeError): + gth(123) + with self.assertRaises(TypeError): + gth('abc') + with self.assertRaises(TypeError): + gth(None) + @skipUnless(PY36, 'Python 3.6 required') def test_get_type_hints_modules(self): self.assertEqual(gth(ann_module), {1: 2, 'f': Tuple[int, int], 'x': int, 'y': str}) @@ -1326,18 +1346,15 @@ class GetTypeHintTests(BaseTestCase): @skipUnless(PY36, 'Python 3.6 required') def test_get_type_hints_classes(self): self.assertEqual(gth(ann_module.C, ann_module.__dict__), - ChainMap({'y': Optional[ann_module.C]}, {})) - self.assertEqual(repr(gth(ann_module.j_class)), 'ChainMap({}, {})') - self.assertEqual(gth(ann_module.M), ChainMap({'123': 123, 'o': type}, - {}, {})) + {'y': Optional[ann_module.C]}) + self.assertIsInstance(gth(ann_module.j_class), dict) + self.assertEqual(gth(ann_module.M), {'123': 123, 'o': type}) self.assertEqual(gth(ann_module.D), - ChainMap({'j': str, 'k': str, - 'y': Optional[ann_module.C]}, {})) - self.assertEqual(gth(ann_module.Y), ChainMap({'z': int}, {})) + {'j': str, 'k': str, 'y': Optional[ann_module.C]}) + self.assertEqual(gth(ann_module.Y), {'z': int}) self.assertEqual(gth(ann_module.h_class), - ChainMap({}, {'y': Optional[ann_module.C]}, {})) - self.assertEqual(gth(ann_module.S), ChainMap({'x': str, 'y': str}, - {})) + {'y': Optional[ann_module.C]}) + self.assertEqual(gth(ann_module.S), {'x': str, 'y': str}) self.assertEqual(gth(ann_module.foo), {'x': int}) @skipUnless(PY36, 'Python 3.6 required') @@ -1355,20 +1372,34 @@ class GetTypeHintTests(BaseTestCase): class Der(ABase): ... self.assertEqual(gth(ABase.meth), {'x': int}) + def test_get_type_hints_for_builins(self): + # Should not fail for built-in classes and functions. + self.assertEqual(gth(int), {}) + self.assertEqual(gth(type), {}) + self.assertEqual(gth(dir), {}) + self.assertEqual(gth(len), {}) def test_previous_behavior(self): def testf(x, y): ... testf.__annotations__['x'] = 'int' self.assertEqual(gth(testf), {'x': int}) + def test_get_type_hints_for_object_with_annotations(self): + class A: ... + class B: ... + b = B() + b.__annotations__ = {'x': 'A'} + self.assertEqual(gth(b, locals()), {'x': A}) + @skipUnless(PY36, 'Python 3.6 required') def test_get_type_hints_ClassVar(self): + self.assertEqual(gth(ann_module2.CV, ann_module2.__dict__), + {'var': typing.ClassVar[ann_module2.CV]}) self.assertEqual(gth(B, globals()), - ChainMap({'y': int, 'x': ClassVar[Optional[B]]}, {})) + {'y': int, 'x': ClassVar[Optional[B]]}) self.assertEqual(gth(CSub, globals()), - ChainMap({'z': ClassVar[CSub]}, - {'y': int, 'x': ClassVar[Optional[B]]}, {})) - self.assertEqual(gth(G), ChainMap({'lst': ClassVar[List[T]]},{},{})) + {'z': ClassVar[CSub], 'y': int, 'x': ClassVar[Optional[B]]}) + self.assertEqual(gth(G), {'lst': ClassVar[List[T]]}) class CollectionsAbcTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index 2a1ea08..e748fd9 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -10,8 +10,6 @@ try: import collections.abc as collections_abc except ImportError: import collections as collections_abc # Fallback for PY3.2. -if sys.version_info[:2] >= (3, 3): - from collections import ChainMap # Please keep __all__ alphabetized within each category. @@ -1194,14 +1192,12 @@ class CallableMeta(GenericMeta): # super()._tree_repr() for nice formatting. arg_list = [] for arg in tree[1:]: - if arg == (): - arg_list.append('[]') - elif not isinstance(arg, tuple): + if not isinstance(arg, tuple): arg_list.append(_type_repr(arg)) else: arg_list.append(arg[0]._tree_repr(arg)) - if len(arg_list) == 2: - return repr(tree[0]) + '[%s]' % ', '.join(arg_list) + if arg_list[0] == '...': + return repr(tree[0]) + '[..., %s]' % arg_list[1] return (repr(tree[0]) + '[[%s], %s]' % (', '.join(arg_list[:-1]), arg_list[-1])) @@ -1216,26 +1212,22 @@ class CallableMeta(GenericMeta): raise TypeError("Callable must be used as " "Callable[[arg, ...], result].") args, result = parameters - if args is ...: - parameters = (..., result) - elif args == []: - parameters = ((), result) + if args is Ellipsis: + parameters = (Ellipsis, result) else: if not isinstance(args, list): raise TypeError("Callable[args, result]: args must be a list." " Got %.100r." % (args,)) - parameters = tuple(args) + (result,) + parameters = (tuple(args), result) return self.__getitem_inner__(parameters) @_tp_cache def __getitem_inner__(self, parameters): - *args, result = parameters + args, result = parameters msg = "Callable[args, result]: result must be a type." result = _type_check(result, msg) - if args == [...,]: + if args is Ellipsis: return super().__getitem__((_TypingEllipsis, result)) - if args == [(),]: - return super().__getitem__((_TypingEmpty, result)) msg = "Callable[[arg, ...], result]: each arg must be a type." args = tuple(_type_check(arg, msg) for arg in args) parameters = args + (result,) @@ -1332,7 +1324,11 @@ def cast(typ, val): def _get_defaults(func): """Internal helper to extract the default arguments, by name.""" - code = func.__code__ + try: + code = func.__code__ + except AttributeError: + # Some built-in functions don't have __code__, __defaults__, etc. + return {} pos_count = code.co_argcount arg_names = code.co_varnames arg_names = arg_names[:pos_count] @@ -1346,138 +1342,80 @@ def _get_defaults(func): return res -if sys.version_info[:2] >= (3, 3): - def get_type_hints(obj, globalns=None, localns=None): - """Return type hints for an object. +def get_type_hints(obj, globalns=None, localns=None): + """Return type hints for an object. - This is often the same as obj.__annotations__, but it handles - forward references encoded as string literals, and if necessary - adds Optional[t] if a default value equal to None is set. + This is often the same as obj.__annotations__, but it handles + forward references encoded as string literals, and if necessary + adds Optional[t] if a default value equal to None is set. - The argument may be a module, class, method, or function. The annotations - are returned as a dictionary, or in the case of a class, a ChainMap of - dictionaries. + The argument may be a module, class, method, or function. The annotations + are returned as a dictionary. For classes, annotations include also + inherited members. - TypeError is raised if the argument is not of a type that can contain - annotations, and an empty dictionary is returned if no annotations are - present. + TypeError is raised if the argument is not of a type that can contain + annotations, and an empty dictionary is returned if no annotations are + present. - BEWARE -- the behavior of globalns and localns is counterintuitive - (unless you are familiar with how eval() and exec() work). The - search order is locals first, then globals. + BEWARE -- the behavior of globalns and localns is counterintuitive + (unless you are familiar with how eval() and exec() work). The + search order is locals first, then globals. - - If no dict arguments are passed, an attempt is made to use the - globals from obj, and these are also used as the locals. If the - object does not appear to have globals, an exception is raised. + - If no dict arguments are passed, an attempt is made to use the + globals from obj, and these are also used as the locals. If the + object does not appear to have globals, an exception is raised. - - If one dict argument is passed, it is used for both globals and - locals. + - If one dict argument is passed, it is used for both globals and + locals. - - If two dict arguments are passed, they specify globals and - locals, respectively. - """ + - If two dict arguments are passed, they specify globals and + locals, respectively. + """ - if getattr(obj, '__no_type_check__', None): - return {} - if globalns is None: - globalns = getattr(obj, '__globals__', {}) - if localns is None: - localns = globalns - elif localns is None: + if getattr(obj, '__no_type_check__', None): + return {} + if globalns is None: + globalns = getattr(obj, '__globals__', {}) + if localns is None: localns = globalns - - if (isinstance(obj, types.FunctionType) or - isinstance(obj, types.BuiltinFunctionType) or - isinstance(obj, types.MethodType)): - defaults = _get_defaults(obj) - hints = obj.__annotations__ - for name, value in hints.items(): - if value is None: - value = type(None) - if isinstance(value, str): - value = _ForwardRef(value) - value = _eval_type(value, globalns, localns) - if name in defaults and defaults[name] is None: - value = Optional[value] - hints[name] = value - return hints - - if isinstance(obj, types.ModuleType): - try: - hints = obj.__annotations__ - except AttributeError: - return {} - for name, value in hints.items(): + elif localns is None: + localns = globalns + # Classes require a special treatment. + if isinstance(obj, type): + hints = {} + for base in reversed(obj.__mro__): + ann = base.__dict__.get('__annotations__', {}) + for name, value in ann.items(): if value is None: value = type(None) if isinstance(value, str): value = _ForwardRef(value) value = _eval_type(value, globalns, localns) hints[name] = value - return hints - - if isinstance(object, type): - cmap = None - for base in reversed(obj.__mro__): - new_map = collections.ChainMap if cmap is None else cmap.new_child - try: - hints = base.__dict__['__annotations__'] - except KeyError: - cmap = new_map() - else: - for name, value in hints.items(): - if value is None: - value = type(None) - if isinstance(value, str): - value = _ForwardRef(value) - value = _eval_type(value, globalns, localns) - hints[name] = value - cmap = new_map(hints) - return cmap - - raise TypeError('{!r} is not a module, class, method, ' - 'or function.'.format(obj)) - -else: - def get_type_hints(obj, globalns=None, localns=None): - """Return type hints for a function or method object. - - This is often the same as obj.__annotations__, but it handles - forward references encoded as string literals, and if necessary - adds Optional[t] if a default value equal to None is set. - - BEWARE -- the behavior of globalns and localns is counterintuitive - (unless you are familiar with how eval() and exec() work). The - search order is locals first, then globals. - - - If no dict arguments are passed, an attempt is made to use the - globals from obj, and these are also used as the locals. If the - object does not appear to have globals, an exception is raised. - - - If one dict argument is passed, it is used for both globals and - locals. - - - If two dict arguments are passed, they specify globals and - locals, respectively. - """ - if getattr(obj, '__no_type_check__', None): - return {} - if globalns is None: - globalns = getattr(obj, '__globals__', {}) - if localns is None: - localns = globalns - elif localns is None: - localns = globalns - defaults = _get_defaults(obj) - hints = dict(obj.__annotations__) - for name, value in hints.items(): - if isinstance(value, str): - value = _ForwardRef(value) - value = _eval_type(value, globalns, localns) - if name in defaults and defaults[name] is None: - value = Optional[value] - hints[name] = value return hints + hints = getattr(obj, '__annotations__', None) + if hints is None: + # Return empty annotations for something that _could_ have them. + if (isinstance(obj, types.FunctionType) or + isinstance(obj, types.BuiltinFunctionType) or + isinstance(obj, types.MethodType) or + isinstance(obj, types.ModuleType)): + return {} + else: + raise TypeError('{!r} is not a module, class, method, ' + 'or function.'.format(obj)) + defaults = _get_defaults(obj) + hints = dict(hints) + for name, value in hints.items(): + if value is None: + value = type(None) + if isinstance(value, str): + value = _ForwardRef(value) + value = _eval_type(value, globalns, localns) + if name in defaults and defaults[name] is None: + value = Optional[value] + hints[name] = value + return hints def no_type_check(arg): @@ -2160,7 +2098,7 @@ class TextIO(IO[str]): pass @abstractproperty - def errors(self) -> str: + def errors(self) -> Optional[str]: pass @abstractproperty -- cgit v0.12