summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/tests.yml4
-rw-r--r--Makefile2
-rw-r--r--setup.py3
-rw-r--r--tests/conftest.py13
-rw-r--r--tests/test-data/check-immu.test73
-rw-r--r--tests/test_mypy.py23
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
diff --git a/Makefile b/Makefile
index b2d44d2..481d190 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/setup.py b/setup.py
index 530013c..b21c05a 100644
--- a/setup.py
+++ b/setup.py
@@ -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']