From 0e37975a962e06fa3bcbb516d6ba33b17a7a86e7 Mon Sep 17 00:00:00 2001 From: Ramon Hagenaars <37958579+ramonhagenaars@users.noreply.github.com> Date: Sun, 19 Jun 2022 12:47:31 +0200 Subject: [PATCH] Release/2.1.3 (#82) * Added history. * Added helper for writing test Python files. * Changed the Invoke tasks to also add sources alongside the wheels * package_info test should only run locally. * Fixed incorrect error message. * Fix for Pyright that didn't like Shape and Structure. * Version bump. * Added the sources to the wheel test. * Writing some history. Co-authored-by: Ramon --- HISTORY.md | 5 +++ constraints.txt | 4 ++ dependencies/qa-requirements.txt | 9 +++-- nptyping/ndarray.py | 2 +- nptyping/package_info.py | 2 +- nptyping/shape.pyi | 7 +++- nptyping/structure.pyi | 9 ++++- tasks.py | 1 + tests/test_helpers/__init__.py | 0 tests/test_helpers/temp_file.py | 15 +++++++ tests/test_mypy.py | 67 +++++++++++++++----------------- tests/test_package_info.py | 3 ++ tests/test_pyright.py | 48 +++++++++++++++++++++++ tests/test_wheel.py | 16 ++++---- 14 files changed, 135 insertions(+), 53 deletions(-) create mode 100644 tests/test_helpers/__init__.py create mode 100644 tests/test_helpers/temp_file.py create mode 100644 tests/test_pyright.py diff --git a/HISTORY.md b/HISTORY.md index defef1f..113df46 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # History +## 2.1.3 (2022-06-19) + +- Fixed typing issue with Pyright/Pylance that caused the message: "Literal" is not a class +- Fixed wrong error message when an invalid `Structure` was provided to `NDArray`. + ## 2.1.2 (2022-06-08) - Fixed bug that caused MyPy to fail with the message: Value of type variable "_DType_co" of "ndarray" cannot be "floating[Any]" diff --git a/constraints.txt b/constraints.txt index 80d2973..aee8b30 100644 --- a/constraints.txt +++ b/constraints.txt @@ -52,6 +52,8 @@ mypy-extensions==0.4.3 # via # black # mypy +nodeenv==1.6.0 + # via pyright numpy==1.22.4 ; python_version >= "3.8" # via -r ./dependencies\requirements.txt pathspec==0.9.0 @@ -68,6 +70,8 @@ pyflakes==2.4.0 # via autoflake pylint==2.14.1 # via -r ./dependencies\qa-requirements.txt +pyright==1.1.254 + # via -r ./dependencies\qa-requirements.txt requests==2.27.1 # via codecov sgmllib3k==1.0.0 diff --git a/dependencies/qa-requirements.txt b/dependencies/qa-requirements.txt index 08571b6..5f8749d 100644 --- a/dependencies/qa-requirements.txt +++ b/dependencies/qa-requirements.txt @@ -1,13 +1,14 @@ autoflake +beartype<0.10.0; python_version<'3.10' +beartype>=0.10.0; python_version>='3.10' black coverage codecov>=2.1.0 +feedparser isort mypy pylint +pyright setuptools -wheel -beartype<0.10.0; python_version<'3.10' -beartype>=0.10.0; python_version>='3.10' typeguard -feedparser +wheel diff --git a/nptyping/ndarray.py b/nptyping/ndarray.py index d1fafcc..35c5053 100644 --- a/nptyping/ndarray.py +++ b/nptyping/ndarray.py @@ -147,7 +147,7 @@ def _get_dtype(cls, dtype_candidate: Any) -> DType: raise InvalidArgumentsError( f"Unexpected argument '{dtype_candidate}', expecting" " Structure[]" - " or Literal[]" + " or Literal[]" " or a dtype" " or typing.Any." ) diff --git a/nptyping/package_info.py b/nptyping/package_info.py index e2ccab4..b3da4c0 100644 --- a/nptyping/package_info.py +++ b/nptyping/package_info.py @@ -22,7 +22,7 @@ SOFTWARE. """ __title__ = "nptyping" -__version__ = "2.1.2" +__version__ = "2.1.3" __author__ = "Ramon Hagenaars" __author_email__ = "ramon.hagenaars@gmail.com" __description__ = "Type hints for NumPy." diff --git a/nptyping/shape.pyi b/nptyping/shape.pyi index 3c18b79..c6941ea 100644 --- a/nptyping/shape.pyi +++ b/nptyping/shape.pyi @@ -26,6 +26,11 @@ try: except ImportError: from typing_extensions import Literal # type: ignore[attr-defined,misc] -from typing import cast +from typing import Any, cast +# For MyPy: Shape = cast(Literal, Shape) # type: ignore[has-type,misc] + +# For PyRight: +class Shape: # type: ignore[no-redef] + def __class_getitem__(cls, item: Any) -> Any: ... diff --git a/nptyping/structure.pyi b/nptyping/structure.pyi index de57346..d10fdbe 100644 --- a/nptyping/structure.pyi +++ b/nptyping/structure.pyi @@ -26,6 +26,13 @@ try: except ImportError: from typing_extensions import Literal # type: ignore[attr-defined,misc] -from typing import cast +from typing import Any, cast +import numpy as np + +# For MyPy: Structure = cast(Literal, Structure) # type: ignore[has-type,misc] + +# For PyRight: +class Structure(np.dtype[Any]): # type: ignore[no-redef] + def __class_getitem__(cls, item: Any) -> Any: ... diff --git a/tasks.py b/tasks.py index 94cfdbf..1aaa0fa 100644 --- a/tasks.py +++ b/tasks.py @@ -145,6 +145,7 @@ def init(context, py=None): def wheel(context, py=None): """Build a wheel.""" print(f"Installing dependencies into: {_DEFAULT_VENV}") + context.run(f"{get_py(py)} setup.py sdist") context.run(f"{get_py(py)} setup.py bdist_wheel") diff --git a/tests/test_helpers/__init__.py b/tests/test_helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_helpers/temp_file.py b/tests/test_helpers/temp_file.py new file mode 100644 index 0000000..aa733c0 --- /dev/null +++ b/tests/test_helpers/temp_file.py @@ -0,0 +1,15 @@ +import contextlib +import os +from pathlib import Path +from tempfile import TemporaryDirectory +from textwrap import dedent + + +@contextlib.contextmanager +def temp_file(python_code: str, file_name: str = "test_file.py"): + file_content = dedent(python_code).strip() + os.linesep + with TemporaryDirectory() as directory_name: + path_to_file = Path(directory_name) / file_name + with open(path_to_file, "w") as file: + file.write(file_content) + yield path_to_file diff --git a/tests/test_mypy.py b/tests/test_mypy.py index 48a9303..dfcf364 100644 --- a/tests/test_mypy.py +++ b/tests/test_mypy.py @@ -1,25 +1,20 @@ -import os -from pathlib import Path -from tempfile import TemporaryDirectory -from textwrap import dedent +from typing import Tuple from unittest import TestCase from mypy import api +from tests.test_helpers.temp_file import temp_file -def _check_mypy_on_code(python_code: str) -> str: - file_content = dedent(python_code).strip() + os.linesep - with TemporaryDirectory() as directory_name: - path_to_file = Path(directory_name) / "mypy_test.py" - with open(path_to_file, "w") as file: - file.write(file_content) - mypy_findings, _, _ = api.run([str(path_to_file)]) - return mypy_findings + +def _check_mypy_on_code(python_code: str) -> Tuple[int, str, str]: + with temp_file(python_code) as path_to_file: + stdout, stderr, exit_code = api.run([str(path_to_file)]) + return exit_code, stdout, stderr class MyPyTest(TestCase): def test_mypy_accepts_ndarray_with_any(self): - mypy_findings = _check_mypy_on_code( + exit_code, stdout, stderr = _check_mypy_on_code( """ from typing import Any from nptyping import NDArray @@ -28,10 +23,10 @@ def test_mypy_accepts_ndarray_with_any(self): NDArray[Any, Any] """ ) - self.assertIn("Success", mypy_findings) + self.assertEqual(0, exit_code, stdout) def test_mypy_accepts_ndarray_with_shape(self): - mypy_findings = _check_mypy_on_code( + exit_code, stdout, stderr = _check_mypy_on_code( """ from typing import Any from nptyping import NDArray, Shape @@ -41,10 +36,10 @@ def test_mypy_accepts_ndarray_with_shape(self): """ ) - self.assertIn("Success", mypy_findings) + self.assertEqual(0, exit_code, stdout) def test_mypy_accepts_ndarray_with_structure(self): - mypy_findings = _check_mypy_on_code( + exit_code, stdout, stderr = _check_mypy_on_code( """ from typing import Any from nptyping import NDArray, RecArray, Structure @@ -54,10 +49,10 @@ def test_mypy_accepts_ndarray_with_structure(self): """ ) - self.assertIn("Success", mypy_findings) + self.assertEqual(0, exit_code, stdout) def test_mypy_disapproves_ndarray_with_wrong_function_arguments(self): - mypy_findings = _check_mypy_on_code( + exit_code, stdout, stderr = _check_mypy_on_code( """ from typing import Any import numpy as np @@ -72,12 +67,12 @@ def func(_: NDArray[Shape["2, 2"], Any]) -> None: """ ) - self.assertIn('Argument 1 to "func" has incompatible type "str"', mypy_findings) - self.assertIn('expected "ndarray[Any, Any]"', mypy_findings) - self.assertIn("Found 1 error in 1 file", mypy_findings) + self.assertIn('Argument 1 to "func" has incompatible type "str"', stdout) + self.assertIn('expected "ndarray[Any, Any]"', stdout) + self.assertIn("Found 1 error in 1 file", stdout) def test_mypy_accepts_ndarrays_as_function_arguments(self): - mypy_findings = _check_mypy_on_code( + exit_code, stdout, stderr = _check_mypy_on_code( """ from typing import Any import numpy as np @@ -92,10 +87,10 @@ def func(_: NDArray[Shape["2, 2"], Any]) -> None: """ ) - self.assertIn("Success", mypy_findings) + self.assertEqual(0, exit_code, stdout) def test_mypy_accepts_ndarrays_as_variable_hints(self): - mypy_findings = _check_mypy_on_code( + exit_code, stdout, stderr = _check_mypy_on_code( """ from typing import Any import numpy as np @@ -106,10 +101,10 @@ def test_mypy_accepts_ndarrays_as_variable_hints(self): """ ) - self.assertIn("Success", mypy_findings) + self.assertEqual(0, exit_code, stdout) def test_mypy_accepts_recarray_with_structure(self): - mypy_findings = _check_mypy_on_code( + exit_code, stdout, stderr = _check_mypy_on_code( """ from typing import Any from nptyping import RecArray, Structure @@ -119,10 +114,10 @@ def test_mypy_accepts_recarray_with_structure(self): """ ) - self.assertIn("Success", mypy_findings) + self.assertEqual(0, exit_code, stdout) def test_mypy_accepts_numpy_types(self): - mypy_findings = _check_mypy_on_code( + exit_code, stdout, stderr = _check_mypy_on_code( """ from typing import Any from nptyping import NDArray @@ -136,10 +131,10 @@ def test_mypy_accepts_numpy_types(self): """ ) - self.assertIn("Success", mypy_findings) + self.assertEqual(0, exit_code, stdout) def test_mypy_wont_accept_numpy_types_without_dtype(self): - mypy_findings = _check_mypy_on_code( + exit_code, stdout, stderr = _check_mypy_on_code( """ from nptyping import NDArray from typing import Any @@ -152,13 +147,13 @@ def test_mypy_wont_accept_numpy_types_without_dtype(self): self.assertIn( 'Value of type variable "_DType_co" of "ndarray" cannot be "signedinteger[Any]"', - mypy_findings, + stdout, ) def test_mypy_knows_of_ndarray_methods(self): # If MyPy knows of some arbitrary ndarray methods, we can assume that # code completion works. - mypy_findings = _check_mypy_on_code( + exit_code, stdout, stderr = _check_mypy_on_code( """ from typing import Any from nptyping import NDArray @@ -173,10 +168,10 @@ def test_mypy_knows_of_ndarray_methods(self): """ ) - self.assertIn("Success", mypy_findings) + self.assertEqual(0, exit_code, stdout) def test_mypy_accepts_nptyping_types(self): - mypy_findings = _check_mypy_on_code( + exit_code, stdout, stderr = _check_mypy_on_code( """ from typing import Any import numpy as np @@ -314,4 +309,4 @@ def test_mypy_accepts_nptyping_types(self): """ ) - self.assertIn("Success", mypy_findings) + self.assertEqual(0, exit_code, stdout) diff --git a/tests/test_package_info.py b/tests/test_package_info.py index 7fb96af..1a36070 100644 --- a/tests/test_package_info.py +++ b/tests/test_package_info.py @@ -1,4 +1,6 @@ +import os from unittest import TestCase +from unittest.case import skipIf import feedparser @@ -6,6 +8,7 @@ class PackageInfoTest(TestCase): + @skipIf(os.environ.get("CI"), reason="Only run locally") def test_version_bump(self): releases = feedparser.parse( "https://pypi.org/rss/project/nptyping/releases.xml" diff --git a/tests/test_pyright.py b/tests/test_pyright.py new file mode 100644 index 0000000..451c199 --- /dev/null +++ b/tests/test_pyright.py @@ -0,0 +1,48 @@ +from functools import partial +from subprocess import PIPE, run +from typing import Tuple +from unittest import TestCase + +import pyright + +from tests.test_helpers.temp_file import temp_file + + +def _check_pyright_on_code(python_code: str) -> Tuple[int, str, str]: + pyright.node.subprocess.run = partial(run, stdout=PIPE, stderr=PIPE) + try: + with temp_file(python_code) as path_to_file: + result = pyright.run(str(path_to_file)) + return ( + result.returncode, + bytes.decode(result.stdout), + bytes.decode(result.stderr), + ) + finally: + pyright.node.subprocess.run = run + + +class PyrightTest(TestCase): + def test_pyright_accepts_array_with_shape(self): + exit_code, stdout, sterr = _check_pyright_on_code( + """ + from typing import Any + from nptyping import NDArray, Shape + + + NDArray[Shape["*, ..."], Any] + """ + ) + self.assertEqual(0, exit_code, stdout) + + def test_pyright_accepts_array_with_structure(self): + exit_code, stdout, sterr = _check_pyright_on_code( + """ + from typing import Any + from nptyping import NDArray, Structure + + + NDArray[Any, Structure["x: Int, y: Float"]] + """ + ) + self.assertEqual(0, exit_code, stdout) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index b6d02a8..2c3a1a0 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -45,11 +45,8 @@ def determine_order(_: Any, x: str, __: str) -> int: - if x == "test_wheel_is_built_correctly": - return -1 - if x == "test_wheel_can_be_installed": - return -1 - return 1 + prio_tests = ("test_wheel_is_built_correctly", "test_wheel_can_be_installed") + return -1 if x in prio_tests else 1 TestLoader.sortTestMethodsUsing = determine_order @@ -83,11 +80,12 @@ def tearDownClass(cls) -> None: def test_wheel_is_built_correctly(self): with working_dir(_ROOT): - subprocess.check_output( - f"{sys.executable} setup.py bdist_wheel", shell=True - ) - wheel_files = glob(f"dist/*{__version__}*") + subprocess.check_output(f"{sys.executable} -m invoke wheel", shell=True) + wheel_files = glob(f"dist/*{__version__}*.whl") + src_files = glob(f"dist/*{__version__}*.tar.gz") + self.assertEqual(1, len(wheel_files)) + self.assertEqual(1, len(src_files)) with ZipFile(_ROOT / Path(wheel_files[0]), "r") as zip_: files_in_wheel = set(