diff --git a/.travis.yml b/.travis.yml index 57eb842ff..847addde4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,22 +1,47 @@ language: python -python: - - "nightly" - - "3.7-dev" - - "3.6.2" - - "3.6.1" - - "3.6.0" - - "3.5.3" - - "3.5.2" - - "3.5.1" - - "3.5.0" - - "3.4" - - "2.7" + +jobs: + include: + - name: "3.8-dev" + dist: xenial + python: 3.8-dev + - name: "3.7.3" + dist: xenial + python: 3.7.3 + - name: "3.7.2" + dist: xenial + python: 3.7.2 + - name: "3.7.1" + dist: xenial + python: 3.7.1 + - name: "3.7.0" + dist: xenial + python: 3.7.0 + - name: "3.6.2" + python: 3.6.2 + - name: "3.6.1" + python: 3.6.1 + - name: "3.6.0" + python: 3.6.0 + - name: "3.5.3" + python: 3.5.3 + - name: "3.5.2" + python: 3.5.2 + - name: "3.5.1" + python: 3.5.1 + - name: "3.5.0" + python: 3.5.0 + - name: "3.4" + python: 3.4 + - name: "2.7" + python: 2.7 install: - pip install -r test-requirements.txt script: - - export PYTHONPATH=`python -c "import sys; print('python2' if sys.version.startswith('2') else 'src')"`; py.test $PYTHONPATH + - export PYTHONPATH=`python -c "import sys; print('python2' if sys.version.startswith('2') else 'src')"`; + if [[ $TRAVIS_PYTHON_VERSION < '3.7' ]]; then py.test $PYTHONPATH; fi - if [[ $TRAVIS_PYTHON_VERSION < '3.5' ]]; then python setup.py install; fi - export PYTHONPATH=`python -c "import sys; print('typing_extensions/src_py2' if sys.version.startswith('2') else 'typing_extensions/src_py3')"`; py.test $PYTHONPATH; diff --git a/typing_extensions/src_py3/test_typing_extensions.py b/typing_extensions/src_py3/test_typing_extensions.py index 57d77e65d..5712c2970 100644 --- a/typing_extensions/src_py3/test_typing_extensions.py +++ b/typing_extensions/src_py3/test_typing_extensions.py @@ -190,11 +190,15 @@ def test_basics(self): Final[int][str] def test_repr(self): - self.assertEqual(repr(Final), 'typing_extensions.Final') + if hasattr(typing, 'Final') and sys.version_info[:2] >= (3, 7): + mod_name = 'typing' + else: + mod_name = 'typing_extensions' + self.assertEqual(repr(Final), mod_name + '.Final') cv = Final[int] - self.assertEqual(repr(cv), 'typing_extensions.Final[int]') + self.assertEqual(repr(cv), mod_name + '.Final[int]') cv = Final[Employee] - self.assertEqual(repr(cv), 'typing_extensions.Final[%s.Employee]' % __name__) + self.assertEqual(repr(cv), mod_name + '.Final[%s.Employee]' % __name__) @skipUnless(SUBCLASS_CHECK_FORBIDDEN, "Behavior added in typing 3.5.3") def test_cannot_subclass(self): @@ -242,7 +246,7 @@ def test_basics(self): def test_illegal_parameters_do_not_raise_runtime_errors(self): # Type checkers should reject these types, but we do not - # raise errors at runtime to maintain maximium flexibility + # raise errors at runtime to maintain maximum flexibility Literal[int] Literal[Literal[1, 2], Literal[4, 5]] Literal[3j + 2, ..., ()] @@ -255,11 +259,15 @@ def test_literals_inside_other_types(self): List[Literal[("foo", "bar", "baz")]] def test_repr(self): - self.assertEqual(repr(Literal[1]), "typing_extensions.Literal[1]") - self.assertEqual(repr(Literal[1, True, "foo"]), "typing_extensions.Literal[1, True, 'foo']") - self.assertEqual(repr(Literal[int]), "typing_extensions.Literal[int]") - self.assertEqual(repr(Literal), "typing_extensions.Literal") - self.assertEqual(repr(Literal[None]), "typing_extensions.Literal[None]") + if hasattr(typing, 'Literal'): + mod_name = 'typing' + else: + mod_name = 'typing_extensions' + self.assertEqual(repr(Literal[1]), mod_name + ".Literal[1]") + self.assertEqual(repr(Literal[1, True, "foo"]), mod_name + ".Literal[1, True, 'foo']") + self.assertEqual(repr(Literal[int]), mod_name + ".Literal[int]") + self.assertEqual(repr(Literal), mod_name + ".Literal") + self.assertEqual(repr(Literal[None]), mod_name + ".Literal[None]") def test_cannot_init(self): with self.assertRaises(TypeError): @@ -1303,8 +1311,11 @@ def meth(self): self.assertTrue(P._is_protocol) self.assertTrue(PR._is_protocol) self.assertTrue(PG._is_protocol) - with self.assertRaises(AttributeError): + if hasattr(typing, 'Protocol'): self.assertFalse(P._is_runtime_protocol) + else: + with self.assertRaises(AttributeError): + self.assertFalse(P._is_runtime_protocol) self.assertTrue(PR._is_runtime_protocol) self.assertTrue(PG[int]._is_protocol) self.assertEqual(typing_extensions._get_protocol_attrs(P), {'meth'}) @@ -1419,7 +1430,10 @@ def test_basics_keywords_syntax(self): def test_typeddict_errors(self): Emp = TypedDict('Emp', {'name': str, 'id': int}) - self.assertEqual(TypedDict.__module__, 'typing_extensions') + if hasattr(typing, 'TypedDict'): + self.assertEqual(TypedDict.__module__, 'typing') + else: + self.assertEqual(TypedDict.__module__, 'typing_extensions') jim = Emp(name='Jim', id=1) with self.assertRaises(TypeError): isinstance({}, Emp) diff --git a/typing_extensions/src_py3/typing_extensions.py b/typing_extensions/src_py3/typing_extensions.py index 1b6eb8e51..f257d7d9c 100644 --- a/typing_extensions/src_py3/typing_extensions.py +++ b/typing_extensions/src_py3/typing_extensions.py @@ -129,6 +129,7 @@ def _check_methods_in_mro(C, *methods): 'Counter', 'Deque', 'DefaultDict', + 'TypedDict', # One-off things. 'final', @@ -154,7 +155,7 @@ def _check_methods_in_mro(C, *methods): HAVE_PROTOCOLS = sys.version_info[:3] != (3, 5, 0) if HAVE_PROTOCOLS: - __all__.extend(['Protocol', 'runtime']) + __all__.extend(['Protocol', 'runtime', 'runtime_checkable']) # TODO @@ -348,15 +349,18 @@ class Starship: __type__ = None -if sys.version_info[:2] >= (3, 7): +# On older versions of typing there is an internal class named "Final". +if hasattr(typing, 'Final') and sys.version_info[:2] >= (3, 7): + Final = typing.Final +elif sys.version_info[:2] >= (3, 7): class _FinalForm(typing._SpecialForm, _root=True): def __repr__(self): return 'typing_extensions.' + self._name def __getitem__(self, parameters): - item = _type_check(parameters, - '{} accepts only single type'.format(self._name)) + item = typing._type_check(parameters, + '{} accepts only single type'.format(self._name)) return _GenericAlias(self, (item,)) Final = _FinalForm('Final', doc= @@ -499,27 +503,30 @@ class FastConnector(Connection): __type__ = None -def final(f): - """This decorator can be used to indicate to type checkers that - the decorated method cannot be overridden, and decorated class - cannot be subclassed. For example: - - class Base: +if hasattr(typing, 'final'): + final = typing.final +else: + def final(f): + """This decorator can be used to indicate to type checkers that + the decorated method cannot be overridden, and decorated class + cannot be subclassed. For example: + + class Base: + @final + def done(self) -> None: + ... + class Sub(Base): + def done(self) -> None: # Error reported by type checker + ... @final - def done(self) -> None: + class Leaf: ... - class Sub(Base): - def done(self) -> None: # Error reported by type checker + class Other(Leaf): # Error reported by type checker ... - @final - class Leaf: - ... - class Other(Leaf): # Error reported by type checker - ... - There is no runtime checking of these properties. - """ - return f + There is no runtime checking of these properties. + """ + return f def IntVar(name): @@ -1096,7 +1103,9 @@ def _is_callable_members_only(cls): return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls)) -if HAVE_PROTOCOLS and not PEP_560: +if hasattr(typing, 'Protocol'): + Protocol = typing.Protocol +elif HAVE_PROTOCOLS and not PEP_560: class _ProtocolMeta(GenericMeta): """Internal metaclass for Protocol. @@ -1515,8 +1524,10 @@ def _no_init(self, *args, **kwargs): cls.__init__ = _no_init -if HAVE_PROTOCOLS: - def runtime(cls): +if hasattr(typing, 'runtime_checkable'): + runtime_checkable = typing.runtime_checkable +elif HAVE_PROTOCOLS: + def runtime_checkable(cls): """Mark a protocol class as a runtime protocol, so that it can be used with isinstance() and issubclass(). Raise TypeError if applied to a non-protocol class. @@ -1525,98 +1536,106 @@ def runtime(cls): one-offs in collections.abc such as Hashable. """ if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol: - raise TypeError('@runtime can be only applied to protocol classes,' + raise TypeError('@runtime_checkable can be only applied to protocol classes,' ' got %r' % cls) cls._is_runtime_protocol = True return cls -def _check_fails(cls, other): - try: - if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools', 'typing']: - # Typed dicts are only for static structural subtyping. - raise TypeError('TypedDict does not support instance and class checks') - except (AttributeError, ValueError): - pass - return False +if HAVE_PROTOCOLS: + # Exists for backwards compatibility. + runtime = runtime_checkable + + +if hasattr(typing, 'TypedDict'): + TypedDict = typing.TypedDict +else: + def _check_fails(cls, other): + try: + if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools', 'typing']: + # Typed dicts are only for static structural subtyping. + raise TypeError('TypedDict does not support instance and class checks') + except (AttributeError, ValueError): + pass + return False -def _dict_new(cls, *args, **kwargs): - return dict(*args, **kwargs) + def _dict_new(cls, *args, **kwargs): + return dict(*args, **kwargs) -def _typeddict_new(cls, _typename, _fields=None, **kwargs): - total = kwargs.pop('total', True) - if _fields is None: - _fields = kwargs - elif kwargs: - raise TypeError("TypedDict takes either a dict or keyword arguments," - " but not both") + def _typeddict_new(cls, _typename, _fields=None, **kwargs): + total = kwargs.pop('total', True) + if _fields is None: + _fields = kwargs + elif kwargs: + raise TypeError("TypedDict takes either a dict or keyword arguments," + " but not both") - ns = {'__annotations__': dict(_fields), '__total__': total} - try: - # Setting correct module is necessary to make typed dict classes pickleable. - ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - pass + ns = {'__annotations__': dict(_fields), '__total__': total} + try: + # Setting correct module is necessary to make typed dict classes pickleable. + ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + pass - return _TypedDictMeta(_typename, (), ns) + return _TypedDictMeta(_typename, (), ns) -class _TypedDictMeta(type): - def __new__(cls, name, bases, ns, total=True): - # Create new typed dict class object. - # This method is called directly when TypedDict is subclassed, - # or via _typeddict_new when TypedDict is instantiated. This way - # TypedDict supports all three syntaxes described in its docstring. - # Subclasses and instances of TypedDict return actual dictionaries - # via _dict_new. - ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new - tp_dict = super(_TypedDictMeta, cls).__new__(cls, name, (dict,), ns) + class _TypedDictMeta(type): + def __new__(cls, name, bases, ns, total=True): + # Create new typed dict class object. + # This method is called directly when TypedDict is subclassed, + # or via _typeddict_new when TypedDict is instantiated. This way + # TypedDict supports all three syntaxes described in its docstring. + # Subclasses and instances of TypedDict return actual dictionaries + # via _dict_new. + ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new + tp_dict = super(_TypedDictMeta, cls).__new__(cls, name, (dict,), ns) - anns = ns.get('__annotations__', {}) - msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" - anns = {n: typing._type_check(tp, msg) for n, tp in anns.items()} - for base in bases: - anns.update(base.__dict__.get('__annotations__', {})) - tp_dict.__annotations__ = anns - if not hasattr(tp_dict, '__total__'): - tp_dict.__total__ = total - return tp_dict + anns = ns.get('__annotations__', {}) + msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" + anns = {n: typing._type_check(tp, msg) for n, tp in anns.items()} + for base in bases: + anns.update(base.__dict__.get('__annotations__', {})) + tp_dict.__annotations__ = anns + if not hasattr(tp_dict, '__total__'): + tp_dict.__total__ = total + return tp_dict - __instancecheck__ = __subclasscheck__ = _check_fails + __instancecheck__ = __subclasscheck__ = _check_fails -TypedDict = _TypedDictMeta('TypedDict', (dict,), {}) -TypedDict.__module__ = __name__ -TypedDict.__doc__ = \ - """A simple typed name space. At runtime it is equivalent to a plain dict. + TypedDict = _TypedDictMeta('TypedDict', (dict,), {}) + TypedDict.__module__ = __name__ + TypedDict.__doc__ = \ + """A simple typed name space. At runtime it is equivalent to a plain dict. - TypedDict creates a dictionary type that expects all of its - instances to have a certain set of keys, with each key - associated with a value of a consistent type. This expectation - is not checked at runtime but is only enforced by type checkers. - Usage:: + TypedDict creates a dictionary type that expects all of its + instances to have a certain set of keys, with each key + associated with a value of a consistent type. This expectation + is not checked at runtime but is only enforced by type checkers. + Usage:: - class Point2D(TypedDict): - x: int - y: int - label: str + class Point2D(TypedDict): + x: int + y: int + label: str - a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK - b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check + a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK + b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check - assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first') + assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first') - The type info could be accessed via Point2D.__annotations__. TypedDict - supports two additional equivalent forms:: + The type info could be accessed via Point2D.__annotations__. TypedDict + supports two additional equivalent forms:: - Point2D = TypedDict('Point2D', x=int, y=int, label=str) - Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) + Point2D = TypedDict('Point2D', x=int, y=int, label=str) + Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) - The class syntax is only supported in Python 3.6+, while two other - syntax forms work for Python 2.7 and 3.2+ - """ + The class syntax is only supported in Python 3.6+, while two other + syntax forms work for Python 2.7 and 3.2+ + """ if PEP_560: