diff options
-rw-r--r-- | .github/workflows/tests.yml | 4 | ||||
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | setup.py | 3 | ||||
-rw-r--r-- | tests/conftest.py | 13 | ||||
-rw-r--r-- | tests/test-data/check-immu.test | 73 | ||||
-rw-r--r-- | tests/test_mypy.py | 23 |
6 files changed, 114 insertions, 4 deletions
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7558539..f3e1be7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9] os: [windows-latest, ubuntu-18.04, macos-latest] exclude: # Python 3.5 is unable to properly @@ -52,4 +52,4 @@ jobs: pip install -e .[test] flake8 immutables/ tests/ mypy immutables/ - python -m unittest -v tests.suite + python -m pytest -v @@ -14,7 +14,7 @@ debug: DEBUG_IMMUTABLES=1 $(PYTHON) setup.py build_ext --inplace test: - $(PYTHON) -m unittest -v + $(PYTHON) -m pytest -v rtest: ~/dev/venvs/36-debug/bin/python setup.py build_ext --inplace @@ -10,7 +10,8 @@ TEST_DEPENDENCIES = [ # (example breakage: https://gitlab.com/pycqa/flake8/issues/427) 'flake8~=3.8.4', 'pycodestyle~=2.6.0', - 'mypy>=0.800', + 'mypy>=0.910', + 'pytest~=6.2.4', ] EXTRA_DEPENDENCIES = { diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..9eb9e61 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +# We need the mypy pytest plugin to do the test collection for our +# typing tests. + +# mypy demands that its test-data be present for mypy.test.config to be +# imported, so thwart that check. mypy PR #10919 fixes this. +import unittest.mock +with unittest.mock.patch('os.path.isdir') as isdir: + isdir.return_value = True + import mypy.test.config # noqa + +pytest_plugins = [ + 'mypy.test.data', +] diff --git a/tests/test-data/check-immu.test b/tests/test-data/check-immu.test new file mode 100644 index 0000000..4998542 --- /dev/null +++ b/tests/test-data/check-immu.test @@ -0,0 +1,73 @@ +[case testMypyImmu] +# cmd: mypy test.py +[file test.py] +from immutables import Map +from typing import Dict, Union, Any, cast + +def init() -> None: + def thing(m: Map[str, Union[str, int]]) -> None: + ... + + thing(Map(foo=1)) + thing(Map(foo='bar', baz=1)) + thing(Map([('foo', 'bar'), ('bar', 1)])) + thing(Map(Map(foo=1), bar='foo')) + m = Map({1: 2}) + thing(m) # E: Argument 1 to "thing" has incompatible type "Map[int, int]"; expected "Map[str, Union[str, int]]" + +def assignments() -> None: + m_int__str = Map[int, str]() + m_str__str = Map[str, str]() + m_int_str__str = Map[Union[int, str], str]() + m_str__int_str = Map[str, Union[int, str]]() + + m_int__str = m_str__str # E: Incompatible types in assignment (expression has type "Map[str, str]", variable has type "Map[int, str]") + m_int__str = m_int_str__str # E: Incompatible types in assignment (expression has type "Map[Union[int, str], str]", variable has type "Map[int, str]") + m_int__str = m_str__int_str # E: Incompatible types in assignment (expression has type "Map[str, Union[int, str]]", variable has type "Map[int, str]") + + m_str__str = m_int__str # E: Incompatible types in assignment (expression has type "Map[int, str]", variable has type "Map[str, str]") + m_str__str = m_int_str__str # E: Incompatible types in assignment (expression has type "Map[Union[int, str], str]", variable has type "Map[str, str]") + m_str__str = m_str__int_str # E: Incompatible types in assignment (expression has type "Map[str, Union[int, str]]", variable has type "Map[str, str]") + + m_int_str__str = m_int__str # E: Incompatible types in assignment (expression has type "Map[int, str]", variable has type "Map[Union[int, str], str]") + m_int_str__str = m_str__str # E: Incompatible types in assignment (expression has type "Map[str, str]", variable has type "Map[Union[int, str], str]") + m_int_str__str = m_str__int_str # E: Incompatible types in assignment (expression has type "Map[str, Union[int, str]]", variable has type "Map[Union[int, str], str]") + + m_str__int_str = m_int__str # E: Incompatible types in assignment (expression has type "Map[int, str]", variable has type "Map[str, Union[int, str]]") + m_str__int_str = m_int_str__str # E: Incompatible types in assignment (expression has type "Map[Union[int, str], str]", variable has type "Map[str, Union[int, str]]") + m_str__int_str = m_str__str + +def update() -> None: + m_int__str: Map[int, str] = Map() + m_str__str: Map[str, str] = Map() + m_int_str__str: Map[Union[int, str], str] = Map() + m_str__int_str: Map[str, Union[int, str]] = Map() + + m_int__str.update({1: '2'}) + m_int__str.update({1: '2'}, three='4') # E: Unexpected keyword argument "three" for "update" of "Map" + m_int__str.update({1: 2}) # E: Argument 1 to "update" of "Map" has incompatible type "Dict[int, int]"; expected "Union[IterableItems[int, str], Iterable[Tuple[int, str]]]" + + m_str__str.update({'1': '2'}) + m_str__str.update({'1': '2'}, three='4') + m_str__str.update({'1': 2}) # E: Argument 1 to "update" of "Map" has incompatible type "Dict[str, int]"; expected "Union[IterableItems[str, str], Iterable[Tuple[str, str]]]" + + m_int_str__str.update(cast(Dict[Union[int, str], str], {1: '2', '3': '4'})) + m_int_str__str.update({1: '2'}, three='4') + m_int_str__str.update({'1': 2}) # E: Argument 1 to "update" of "Map" has incompatible type "Dict[str, int]"; expected "Union[IterableItems[Union[int, str], str], Iterable[Tuple[Union[int, str], str]]]" + + m_str__int_str.update({'1': 2, '2': 3}) + m_str__int_str.update({'1': 2, '2': 3}, four='5') + m_str__int_str.update({1: 2}) # E: Argument 1 to "update" of "Map" has incompatible type "Dict[int, int]"; expected "Union[IterableItems[str, Union[int, str]], Iterable[Tuple[str, Union[int, str]]]]" + +def mutate() -> None: + m = Map[str, str]() + + with m.mutate() as mm: + mm[0] = '1' # E: Invalid index type "int" for "MapMutation[str, str]"; expected type "str" + mm['1'] = 0 # E: Incompatible types in assignment (expression has type "int", target has type "str") + mm['1'] = '2' + del mm['1'] + mm.set('3', '4') + m2 = mm.finish() + + reveal_type(m2) # N: Revealed type is "immutables._map.Map[builtins.str*, builtins.str*]" diff --git a/tests/test_mypy.py b/tests/test_mypy.py new file mode 100644 index 0000000..48c7f7f --- /dev/null +++ b/tests/test_mypy.py @@ -0,0 +1,23 @@ +import os +import mypy.test.testcmdline +from mypy.test.helpers import normalize_error_messages + + +# I'm upset. There's no other way to deal with the little 'defined here' +# notes that mypy emits when passing an unexpected keyword argument +# and at no other time. +def renormalize_error_messages(messages): + messages = [x for x in messages if not x.endswith(' defined here')] + return normalize_error_messages(messages) + + +mypy.test.testcmdline.normalize_error_messages = renormalize_error_messages + + +this_file_dir = os.path.dirname(os.path.realpath(__file__)) +test_data_prefix = os.path.join(this_file_dir, 'test-data') + + +class ImmuMypyTest(mypy.test.testcmdline.PythonCmdlineSuite): + data_prefix = test_data_prefix + files = ['check-immu.test'] |