diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7d150e2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,46 @@ +language: python +# cache package wheels (1 cache per python version) +cache: pip +# newer python versions are available only on xenial (while some older only on trusty) Ubuntu distribution +dist: trusty +sudo: required + +env: + TOXENV=py + +jobs: + include: + - python: 2.7 + - python: 3.4 + - python: 3.5.0 + - python: 3.5.1 + - python: 3.5.2 + - python: 3.5.3 + - python: 3.5 + - python: 3.6.0 + - python: 3.6.1 + - python: 3.6.2 + - python: 3.6 + dist: xenial + - python: 3.7.0 + dist: xenial + - python: 3.7.1 + dist: xenial + - python: 3.7 + dist: xenial + - python: 3.8-dev + dist: xenial + + - name: "check code style with flake8" + python: 3.7 + dist: xenial + env: + - TOXENV=lint + +install: +- pip install -U pip setuptools +- pip install -U tox +- tox --notest + +script: +- tox diff --git a/tests/testextensions.py b/tests/testextensions.py new file mode 100644 index 0000000..a76f565 --- /dev/null +++ b/tests/testextensions.py @@ -0,0 +1,142 @@ +import sys +import pickle +import typing +from unittest import TestCase, main, skipUnless +from mypy_extensions import TypedDict + + +class BaseTestCase(TestCase): + + def assertIsSubclass(self, cls, class_or_tuple, msg=None): + if not issubclass(cls, class_or_tuple): + message = '%r is not a subclass of %r' % (cls, class_or_tuple) + if msg is not None: + message += ' : %s' % msg + raise self.failureException(message) + + def assertNotIsSubclass(self, cls, class_or_tuple, msg=None): + if issubclass(cls, class_or_tuple): + message = '%r is a subclass of %r' % (cls, class_or_tuple) + if msg is not None: + message += ' : %s' % msg + raise self.failureException(message) + + +PY36 = sys.version_info[:2] >= (3, 6) + +PY36_TESTS = """ +Label = TypedDict('Label', [('label', str)]) + +class Point2D(TypedDict): + x: int + y: int + +class LabelPoint2D(Point2D, Label): ... + +class Options(TypedDict, total=False): + log_level: int + log_path: str +""" + +if PY36: + exec(PY36_TESTS) + + +class TypedDictTests(BaseTestCase): + + def test_basics_iterable_syntax(self): + Emp = TypedDict('Emp', {'name': str, 'id': int}) + self.assertIsSubclass(Emp, dict) + self.assertIsSubclass(Emp, typing.MutableMapping) + if sys.version_info[0] >= 3: + import collections.abc + self.assertNotIsSubclass(Emp, collections.abc.Sequence) + jim = Emp(name='Jim', id=1) + self.assertIs(type(jim), dict) + self.assertEqual(jim['name'], 'Jim') + self.assertEqual(jim['id'], 1) + self.assertEqual(Emp.__name__, 'Emp') + self.assertEqual(Emp.__module__, __name__) + self.assertEqual(Emp.__bases__, (dict,)) + self.assertEqual(Emp.__annotations__, {'name': str, 'id': int}) + self.assertEqual(Emp.__total__, True) + + def test_basics_keywords_syntax(self): + Emp = TypedDict('Emp', name=str, id=int) + self.assertIsSubclass(Emp, dict) + self.assertIsSubclass(Emp, typing.MutableMapping) + if sys.version_info[0] >= 3: + import collections.abc + self.assertNotIsSubclass(Emp, collections.abc.Sequence) + jim = Emp(name='Jim', id=1) # type: ignore # mypy doesn't support keyword syntax yet + self.assertIs(type(jim), dict) + self.assertEqual(jim['name'], 'Jim') + self.assertEqual(jim['id'], 1) + self.assertEqual(Emp.__name__, 'Emp') + self.assertEqual(Emp.__module__, __name__) + self.assertEqual(Emp.__bases__, (dict,)) + self.assertEqual(Emp.__annotations__, {'name': str, 'id': int}) + self.assertEqual(Emp.__total__, True) + + def test_typeddict_errors(self): + Emp = TypedDict('Emp', {'name': str, 'id': int}) + self.assertEqual(TypedDict.__module__, 'mypy_extensions') + jim = Emp(name='Jim', id=1) + with self.assertRaises(TypeError): + isinstance({}, Emp) # type: ignore + with self.assertRaises(TypeError): + isinstance(jim, Emp) # type: ignore + with self.assertRaises(TypeError): + issubclass(dict, Emp) # type: ignore + with self.assertRaises(TypeError): + TypedDict('Hi', x=1) + with self.assertRaises(TypeError): + TypedDict('Hi', [('x', int), ('y', 1)]) + with self.assertRaises(TypeError): + TypedDict('Hi', [('x', int)], y=int) + + @skipUnless(PY36, 'Python 3.6 required') + def test_py36_class_syntax_usage(self): + self.assertEqual(LabelPoint2D.__annotations__, {'x': int, 'y': int, 'label': str}) # noqa + self.assertEqual(LabelPoint2D.__bases__, (dict,)) # noqa + self.assertEqual(LabelPoint2D.__total__, True) # noqa + self.assertNotIsSubclass(LabelPoint2D, typing.Sequence) # noqa + not_origin = Point2D(x=0, y=1) # noqa + self.assertEqual(not_origin['x'], 0) + self.assertEqual(not_origin['y'], 1) + other = LabelPoint2D(x=0, y=1, label='hi') # noqa + self.assertEqual(other['label'], 'hi') + + def test_pickle(self): + global EmpD # pickle wants to reference the class by name + EmpD = TypedDict('EmpD', name=str, id=int) + jane = EmpD({'name': 'jane', 'id': 37}) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + z = pickle.dumps(jane, proto) + jane2 = pickle.loads(z) + self.assertEqual(jane2, jane) + self.assertEqual(jane2, {'name': 'jane', 'id': 37}) + ZZ = pickle.dumps(EmpD, proto) + EmpDnew = pickle.loads(ZZ) + self.assertEqual(EmpDnew({'name': 'jane', 'id': 37}), jane) + + def test_optional(self): + EmpD = TypedDict('EmpD', name=str, id=int) + + self.assertEqual(typing.Optional[EmpD], typing.Union[None, EmpD]) + self.assertNotEqual(typing.List[EmpD], typing.Tuple[EmpD]) + + def test_total(self): + D = TypedDict('D', {'x': int}, total=False) + self.assertEqual(D(), {}) + self.assertEqual(D(x=1), {'x': 1}) + self.assertEqual(D.__total__, False) + + if PY36: + self.assertEqual(Options(), {}) # noqa + self.assertEqual(Options(log_level=2), {'log_level': 2}) # noqa + self.assertEqual(Options.__total__, False) # noqa + + +if __name__ == '__main__': + main() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..fe02312 --- /dev/null +++ b/tox.ini @@ -0,0 +1,20 @@ +[tox] +minversion = 2.9.1 +skip_missing_interpreters = true +envlist = py27, py34, py35, py36, py37 + +[testenv] +description = run the test driver with {basepython} +commands = python -m unittest discover tests + +[testenv:lint] +description = check the code style +basepython = python3.7 +deps = flake8 +commands = flake8 -j0 {posargs} + +[flake8] +max-line-length = 99 +ignore = + # multiple statements on one line (colon) (we use this for classes with empty body) + E701