diff --git a/noxfile.py b/noxfile.py index c67c777..ceb2f31 100644 --- a/noxfile.py +++ b/noxfile.py @@ -3,6 +3,7 @@ import logging import nox # noqa +from nox.command import CommandFailed from pathlib import Path # noqa import sys @@ -28,7 +29,6 @@ PY37: {"coverage": True, "pkg_specs": {"pip": ">19"}}, # , "pytest-html": "1.9.0" } - # set the default activated sessions, minimal for CI nox.options.sessions = ["tests", "flake8"] # , "docs", "gh_pages" nox.options.reuse_existing_virtualenvs = True # this can be done using -r @@ -98,7 +98,10 @@ def tests(session: PowerSession, coverage, pkg_specs): conda_prefix = Path(session.bin) if conda_prefix.name == "bin": conda_prefix = conda_prefix.parent - session.run2("conda list", env={"CONDA_PREFIX": str(conda_prefix), "CONDA_DEFAULT_ENV": session.get_session_id()}) + try: + session.run2("conda list", env={"CONDA_PREFIX": str(conda_prefix), "CONDA_DEFAULT_ENV": session.get_session_id()}) + except CommandFailed: + pass # Fail if the assumed python version is not the actual one session.run2("python ci_tools/check_python_version.py %s" % session.python) diff --git a/setup.cfg b/setup.cfg index ff0a6d7..8588c21 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,12 +35,16 @@ setup_requires = pytest-runner install_requires = makefun>=1.5.0 + typing-extensions>=4.2;python_version>'3.6' and python_version<'3.10' # note: do not use double quotes in these, this triggers a weird bug in PyCharm in debug mode only funcsigs;python_version<'3.3' enum34;python_version<'3.4' tests_require = pytest pytest_cases + # syrupy and pyright is used for testing type correctness. + syrupy>2;python_version>'3.6' + pyright;python_version>'3.6' # for some reason these pytest dependencies were not declared in old versions of pytest six;python_version<'3.6' attr;python_version<'3.6' diff --git a/src/decopatch/main.pyi b/src/decopatch/main.pyi index 8649211..1cdeb76 100644 --- a/src/decopatch/main.pyi +++ b/src/decopatch/main.pyi @@ -1,12 +1,14 @@ from typing import Any, Callable, Optional, Protocol, TypeVar, overload -from decopatch.utils_disambiguation import FirstArgDisambiguation -from decopatch.utils_modes import SignatureInfo - try: - from typing import ParamSpec -except ImportError: + # We're importing typing_extensions version first, becouse it will + # detect best available implementation depending on python version. from typing_extensions import ParamSpec +except ImportError: + from typing import ParamSpec + +from decopatch.utils_disambiguation import FirstArgDisambiguation +from decopatch.utils_modes import SignatureInfo P = ParamSpec("P") F = TypeVar("F", bound=Callable[..., Any]) diff --git a/tests/pyright/__init__.py b/tests/pyright/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pyright/__snapshots__/test_typing.ambr b/tests/pyright/__snapshots__/test_typing.ambr new file mode 100644 index 0000000..d1c328c --- /dev/null +++ b/tests/pyright/__snapshots__/test_typing.ambr @@ -0,0 +1,61 @@ +# This file is generated by [syrupy](https://github.com/tophat/syrupy), +# do not edit manually. +# To update - run `pytest --snapshot-update` +# name: test_typing + list([ + dict({ + 'message': ''' + No overloads for "function_decorator" match the provided arguments +   Argument types: (Literal[True]) + ''', + 'range': dict({ + 'end': dict({ + 'character': 47, + 'line': 15, + }), + 'start': dict({ + 'character': 9, + 'line': 15, + }), + }), + 'rule': 'reportGeneralTypeIssues', + 'severity': 'error', + }), + dict({ + 'message': ''' + Argument of type "Literal[2]" cannot be assigned to parameter "scope" of type "str" in function "__call__" +   "Literal[2]" is incompatible with "str" + ''', + 'range': dict({ + 'end': dict({ + 'character': 22, + 'line': 36, + }), + 'start': dict({ + 'character': 21, + 'line': 36, + }), + }), + 'rule': 'reportGeneralTypeIssues', + 'severity': 'error', + }), + dict({ + 'message': ''' + Argument of type "Literal[2]" cannot be assigned to parameter "scope" of type "str" in function "__call__" +   "Literal[2]" is incompatible with "str" + ''', + 'range': dict({ + 'end': dict({ + 'character': 34, + 'line': 52, + }), + 'start': dict({ + 'character': 33, + 'line': 52, + }), + }), + 'rule': 'reportGeneralTypeIssues', + 'severity': 'error', + }), + ]) +# --- diff --git a/tests/pyright/base.py b/tests/pyright/base.py new file mode 100644 index 0000000..324789a --- /dev/null +++ b/tests/pyright/base.py @@ -0,0 +1,35 @@ +import json +import shutil +import subprocess + +__all__ = ["run_pyright", "pyright_installed"] + +try: + pyright_bin = shutil.which("pyright") + pyright_installed = pyright_bin is not None +except AttributeError: + # shutil.which works from python 3.3 onward + pyright_bin = None + pyright_installed = False + + +def run_pyright(filename): + """ + Executes pyright type checker against a file, and returns json output. + + Used together with syrupy snapshot to check if typing is working as expected. + """ + result = subprocess.run( + [pyright_bin, "--outputjson", filename], + capture_output=True, + text=True, + ) + assert result.stdout, result.stderr + output = json.loads(result.stdout) + + def format_row(data): + # Remove "file" from json report, it has no use here. + del data["file"] + return data + + return [format_row(row) for row in output["generalDiagnostics"]] diff --git a/tests/test_typing.py b/tests/pyright/test_file.py similarity index 61% rename from tests/test_typing.py rename to tests/pyright/test_file.py index 9fbb395..f9db440 100644 --- a/tests/test_typing.py +++ b/tests/pyright/test_file.py @@ -1,6 +1,7 @@ -# This is test file for typing, -# No automatic testing is used at the moment. Just use your type checker and see if it works. -# Pytest here is used to make sure that runtime behavir matches with type checker expecter errors. +""" +Tests in this file do almost nothing at runtime, but serve as a source for +testing with pyright from test_typing.py +""" from typing import Any, Callable import pytest @@ -10,16 +11,16 @@ def test_invalid_parameter(): with pytest.raises(TypeError): - # Error, invalid argument + # Error, invalid argument. + # This triggers error in type checking and in runtime. @function_decorator(invalid_param=True) - def decorator_wint_invalid_param(fn=DECORATED): + def decorator_with_invalid_param(fn=DECORATED): return fn def test_normal_decorator(): @function_decorator def decorator(scope="test", fn=DECORATED): # type: (str, Any) -> Callable[..., Any] - assert isinstance(scope, str) return fn # Ok @@ -27,20 +28,15 @@ def decorator(scope="test", fn=DECORATED): # type: (str, Any) -> Callable[..., def decorated_flat(): pass - assert decorated_flat - - with pytest.raises(AssertionError): - # Error, Literal[2] is incompatible with str - @decorator(scope=2) - def decorated_with_invalid_options(): - pass - # Ok, should reveal correct type for `scope` @decorator(scope="success") def decorated_with_valid_options(): pass - assert decorated_with_valid_options + # Error, Literal[2] is incompatible with str + @decorator(scope=2) + def decorated_with_invalid_options(): + pass def test_function_decorator_with_params(): @@ -54,4 +50,6 @@ def decorator_with_params(scope = "test", fn=DECORATED): # type: (str, Any) -> def decorated_with_valid_options(): pass - assert decorated_with_valid_options + @decorator_with_params(scope=2) + def decorated_with_invalid_options(): + pass diff --git a/tests/pyright/test_typing.py b/tests/pyright/test_typing.py new file mode 100644 index 0000000..c9ac45d --- /dev/null +++ b/tests/pyright/test_typing.py @@ -0,0 +1,19 @@ +import sys + +import pytest + +from .base import run_pyright, pyright_installed + + +@pytest.mark.skipif( + sys.version_info < (3, 7), + reason="Requires Python 3.7+", +) +@pytest.mark.skipif( + not pyright_installed, + reason="Pyright not installed", +) +def test_typing(snapshot): + """Test that pyright detects the typing issues on `test_file` correctly.""" + actual = run_pyright("tests/pyright/test_file.py") + assert actual == snapshot