From da8a96739b11fb5994096dca71fbf03ebca94502 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Tue, 15 Jun 2021 20:17:12 -0700 Subject: [PATCH 01/66] Update build deps --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0a59365..f26c121 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ['setuptools ~= 50.3.0', 'setuptools-scm[toml] ~= 5.0.0', 'wheel ~= 0.36.0'] -build-backend = 'setuptools.build_meta' +requires = ["setuptools >= 59.6", "setuptools-scm[toml] >= 6.4", "wheel >= 0.37.1"] +build-backend = "setuptools.build_meta" [tool.setuptools_scm] From 253ab8ef8334e322d7329aaf16a228f361b62591 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Tue, 22 Nov 2022 13:31:34 -0800 Subject: [PATCH 02/66] Update publish env deps --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 474eadf..ff5684c 100644 --- a/tox.ini +++ b/tox.ini @@ -91,7 +91,7 @@ testpaths = tests [testenv:publish] passenv = TWINE_* deps = - build ~= 0.8.0 + build[virtualenv] ~= 0.9.0 twine ~= 4.0.0 commands = {envpython} -m build --outdir {distdir} . From d4a5bad32e10760485a749ccafea2ce2656664a9 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Tue, 22 Nov 2022 13:44:01 -0800 Subject: [PATCH 03/66] Update the changelog --- changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog.md b/changelog.md index 160b7bd..7fdccf1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,8 @@ # Changelog +## [0.10.2](https://github.com/dbader/pytest-mypy/milestone/20) +* Update and loosen [build-system] requirements. + ## [0.10.1](https://github.com/dbader/pytest-mypy/milestone/19) * Work around https://github.com/python/mypy/issues/14042. * Add support for Python 3.11. From c0f1bb3b34661e303d1c6d770b4edfcc08a917a5 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 11 Dec 2022 09:58:16 -0800 Subject: [PATCH 04/66] Update the validation GH Action ubuntu-20.04 is required for py36 now. --- .github/workflows/validation.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 3ef36e0..cca2f1d 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -2,13 +2,13 @@ name: Validation on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - run: python -m pip install --upgrade tox-gh-actions From a8637bbbc3f3ca81b29b0a7227d1d71c18f9155e Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sat, 12 Nov 2022 13:41:07 -0800 Subject: [PATCH 05/66] Stop failing if mypy only produces notes --- changelog.md | 3 +++ src/pytest_mypy.py | 13 ++++++++++++- tests/test_pytest_mypy.py | 27 +++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 7fdccf1..d0393fe 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,8 @@ # Changelog +## [0.10.3](https://github.com/dbader/pytest-mypy/milestone/21) +* Stop failing if mypy only produces notes. + ## [0.10.2](https://github.com/dbader/pytest-mypy/milestone/20) * Update and loosen [build-system] requirements. diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 668e9f1..faa02a7 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -5,6 +5,7 @@ from pathlib import Path from tempfile import NamedTemporaryFile from typing import Dict, List, Optional, TextIO +import warnings import attr from filelock import FileLock # type: ignore @@ -202,7 +203,13 @@ def runtest(self): abspath = os.path.abspath(str(self.fspath)) errors = results.abspath_errors.get(abspath) if errors: - raise MypyError(file_error_formatter(self, results, errors)) + if not all( + error.partition(":")[2].partition(":")[0].strip() == "note" + for error in errors + ): + raise MypyError(file_error_formatter(self, results, errors)) + # This line cannot be easily covered on mypy < 0.990: + warnings.warn("\n" + "\n".join(errors), MypyWarning) # pragma: no cover def reportinfo(self): """Produce a heading for the test report.""" @@ -314,6 +321,10 @@ class MypyError(Exception): """ +class MypyWarning(pytest.PytestWarning): + """A non-failure message regarding the mypy run.""" + + def pytest_terminal_summary(terminalreporter, config): """Report stderr and unrecognized lines from stdout.""" try: diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index 0f10dc5..6e86af3 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -98,6 +98,33 @@ def pyfunc(x: int) -> str: assert result.ret != 0 +def test_mypy_annotation_unchecked(testdir, xdist_args): + """Verify that annotation-unchecked warnings do not manifest as an error.""" + testdir.makepyfile( + """ + def pyfunc(x): + y: int = 2 + return x * y + """, + ) + result = testdir.runpytest_subprocess(*xdist_args) + result.assert_outcomes() + result = testdir.runpytest_subprocess("--mypy", *xdist_args) + mypy_file_checks = 1 + mypy_status_check = 1 + mypy_checks = mypy_file_checks + mypy_status_check + outcomes = {"passed": mypy_checks} + # mypy doesn't emit annotation-unchecked warnings until 0.990: + min_mypy_version = Version("0.990") + if MYPY_VERSION >= min_mypy_version and PYTEST_VERSION >= Version("7.0"): + # assert_outcomes does not support `warnings` until 7.x. + outcomes["warnings"] = 1 + result.assert_outcomes(**outcomes) + if MYPY_VERSION >= min_mypy_version: + result.stdout.fnmatch_lines(["*MypyWarning*"]) + assert result.ret == 0 + + def test_mypy_ignore_missings_imports(testdir, xdist_args): """ Verify that --mypy-ignore-missing-imports From 4b46e9565ea64ffb26c03783206f7ac73d153a84 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sat, 6 May 2023 13:06:42 -0700 Subject: [PATCH 06/66] Add test_pyproject_toml --- tests/test_pytest_mypy.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index 0f10dc5..29a406d 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -348,6 +348,30 @@ def pytest_configure(config): assert result.ret != 0 +@pytest.mark.xfail( + Version("0.900") > MYPY_VERSION, + reason="Mypy added pyproject.toml configuration in 0.900.", +) +def test_pyproject_toml(testdir, xdist_args): + """Ensure that the plugin allows configuration with pyproject.toml.""" + testdir.makefile( + ".toml", + pyproject=""" + [tool.mypy] + disallow_untyped_defs = true + """, + ) + testdir.makepyfile( + conftest=""" + def pyfunc(x): + return x * 2 + """, + ) + result = testdir.runpytest_subprocess("--mypy", *xdist_args) + result.stdout.fnmatch_lines(["1: error: Function is missing a type annotation*"]) + assert result.ret != 0 + + def test_setup_cfg(testdir, xdist_args): """Ensure that the plugin allows configuration with setup.cfg.""" testdir.makefile( From 4badf3cc2a78000f1a4483afe790b05205a2a82d Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sat, 6 May 2023 14:01:17 -0700 Subject: [PATCH 07/66] Remove references to Travis CI --- CONTRIBUTING.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8768342..26b71fb 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -4,10 +4,6 @@ Contributing Contributions are very welcome. Tests can be run with `tox `_. Please ensure the coverage at least stays the same before you submit a pull request. -.. image:: https://travis-ci.org/dbader/pytest-mypy.svg?branch=master - :target: https://travis-ci.org/dbader/pytest-mypy - :alt: See Build Status on Travis CI - Development Environment Setup ----------------------------- @@ -21,7 +17,7 @@ Here's how to install pytest-mypy in development mode so you can test your chang How to publish a new version to PyPI ------------------------------------ -Push a tag, and Travis CI will publish it automatically. +Push a tag, and the release will be published automatically. To publish manually: .. code-block:: bash From d969552e17c808366916f487ebe701f2c02be840 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sat, 6 May 2023 13:50:40 -0700 Subject: [PATCH 08/66] Update references to dbader/pytest-mypy The repo was migrated to realpython/pytest-mypy on 2022-10-14. --- README.rst | 4 ++-- changelog.md | 40 ++++++++++++++++++++-------------------- setup.py | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 8c91fea..a71c336 100644 --- a/README.rst +++ b/README.rst @@ -63,11 +63,11 @@ Meta Daniel Bader – `@dbader_org`_ – https://dbader.org – mail@dbader.org -https://github.com/dbader/pytest-mypy +https://github.com/realpython/pytest-mypy .. _`MIT`: http://opensource.org/licenses/MIT -.. _`file an issue`: https://github.com/dbader/pytest-mypy/issues +.. _`file an issue`: https://github.com/realpython/pytest-mypy/issues .. _`pip`: https://pypi.python.org/pypi/pip/ .. _`PyPI`: https://pypi.python.org/pypi .. _`mypy`: http://mypy-lang.org/ diff --git a/changelog.md b/changelog.md index d0393fe..26f4138 100644 --- a/changelog.md +++ b/changelog.md @@ -1,79 +1,79 @@ # Changelog -## [0.10.3](https://github.com/dbader/pytest-mypy/milestone/21) +## [0.10.3](https://github.com/realpython/pytest-mypy/milestone/21) * Stop failing if mypy only produces notes. -## [0.10.2](https://github.com/dbader/pytest-mypy/milestone/20) +## [0.10.2](https://github.com/realpython/pytest-mypy/milestone/20) * Update and loosen [build-system] requirements. -## [0.10.1](https://github.com/dbader/pytest-mypy/milestone/19) +## [0.10.1](https://github.com/realpython/pytest-mypy/milestone/19) * Work around https://github.com/python/mypy/issues/14042. * Add support for Python 3.11. -## [0.10.0](https://github.com/dbader/pytest-mypy/milestone/18) +## [0.10.0](https://github.com/realpython/pytest-mypy/milestone/18) * Drop support for python<3.6. -## [0.9.1](https://github.com/dbader/pytest-mypy/milestone/17) +## [0.9.1](https://github.com/realpython/pytest-mypy/milestone/17) * Add support for pytest 7. -## [0.9.0](https://github.com/dbader/pytest-mypy/milestone/14) +## [0.9.0](https://github.com/realpython/pytest-mypy/milestone/14) * Drop support for pytest<4.6. * Add --mypy-config-file. -## [0.8.1](https://github.com/dbader/pytest-mypy/milestone/16) +## [0.8.1](https://github.com/realpython/pytest-mypy/milestone/16) * Add a partial workaround for https://github.com/pytest-dev/pytest/issues/8016. -## [0.8.0](https://github.com/dbader/pytest-mypy/milestone/15) +## [0.8.0](https://github.com/realpython/pytest-mypy/milestone/15) * Add support for Python 3.9. * Stop injecting `MypyStatusItem` in `pytest_collection_modifyitems` to fix `--looponfail`. -## [0.7.0](https://github.com/dbader/pytest-mypy/milestone/13) +## [0.7.0](https://github.com/realpython/pytest-mypy/milestone/13) * Remove the upper bound on `python_requires`. * Require Python 3.5 or greater. * Enable custom error formatting. * Fix compatibility with pytest-xdist 2. -## [0.6.2](https://github.com/dbader/pytest-mypy/milestone/12) +## [0.6.2](https://github.com/realpython/pytest-mypy/milestone/12) * Stop ignoring `.pyi` files. -## [0.6.1](https://github.com/dbader/pytest-mypy/milestone/11) +## [0.6.1](https://github.com/realpython/pytest-mypy/milestone/11) * Fix a PytestDeprecationWarning emitted by pytest>=5.4 -## [0.6.0](https://github.com/dbader/pytest-mypy/milestone/10) +## [0.6.0](https://github.com/realpython/pytest-mypy/milestone/10) * Inject a test that checks the mypy exit status -## [0.5.0](https://github.com/dbader/pytest-mypy/milestone/9) +## [0.5.0](https://github.com/realpython/pytest-mypy/milestone/9) * Remove `MypyItem.mypy_path` * Add support for pytest-xdist * Add a configurable name to MypyItem node IDs -## [0.4.2](https://github.com/dbader/pytest-mypy/milestone/8) +## [0.4.2](https://github.com/realpython/pytest-mypy/milestone/8) * Make success message green instead of red * Remove Python 3.8 beta/dev references * Stop blacklisting early 0.5x and 0.7x mypy releases -## [0.4.1](https://github.com/dbader/pytest-mypy/milestone/7) +## [0.4.1](https://github.com/realpython/pytest-mypy/milestone/7) * Stop overlapping `python_version`s in `install_requires` -## [0.4.0](https://github.com/dbader/pytest-mypy/milestone/6) +## [0.4.0](https://github.com/realpython/pytest-mypy/milestone/6) * Run mypy once per session instead of once per file * Stop passing --incremental (which mypy now defaults to) * Support configuring the plugin in a conftest.py * Add support for Python 3.8 -## [0.3.3](https://github.com/dbader/pytest-mypy/milestone/3) +## [0.3.3](https://github.com/realpython/pytest-mypy/milestone/3) * Register `mypy` marker. * Add a PEP 518 `[build-system]` * Add dependency pins for Python 3.4 * Add support for Python 3.7 -## [0.3.2](https://github.com/dbader/pytest-mypy/milestone/2) +## [0.3.2](https://github.com/realpython/pytest-mypy/milestone/2) * Add `mypy` marker to run mypy checks only -## [0.3.1](https://github.com/dbader/pytest-mypy/milestone/1) +## [0.3.1](https://github.com/realpython/pytest-mypy/milestone/1) * Only depend on `mypy.api` * Add `--mypy-ignore-missing-imports` * Invoke `mypy` with `--incremental` -## [0.3.0](https://github.com/dbader/pytest-mypy/milestone/5) +## [0.3.0](https://github.com/realpython/pytest-mypy/milestone/5) * Change `mypy` dependency to pull in `mypy` instead of `mypy-lang` diff --git a/setup.py b/setup.py index 9dbc660..6abbd5a 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ def read(fname): maintainer="David Tucker", maintainer_email="david@tucker.name", license="MIT", - url="https://github.com/dbader/pytest-mypy", + url="https://github.com/realpython/pytest-mypy", description="Mypy static type checker plugin for Pytest", long_description=read("README.rst"), long_description_content_type="text/x-rst", From 6ee477d6507df658b98681bde259d00f0c64780f Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 7 May 2023 11:21:04 -0700 Subject: [PATCH 09/66] Note that the Changelog has moved --- changelog.md | 78 +--------------------------------------------------- 1 file changed, 1 insertion(+), 77 deletions(-) diff --git a/changelog.md b/changelog.md index 26f4138..ef37f37 100644 --- a/changelog.md +++ b/changelog.md @@ -1,79 +1,3 @@ # Changelog -## [0.10.3](https://github.com/realpython/pytest-mypy/milestone/21) -* Stop failing if mypy only produces notes. - -## [0.10.2](https://github.com/realpython/pytest-mypy/milestone/20) -* Update and loosen [build-system] requirements. - -## [0.10.1](https://github.com/realpython/pytest-mypy/milestone/19) -* Work around https://github.com/python/mypy/issues/14042. -* Add support for Python 3.11. - -## [0.10.0](https://github.com/realpython/pytest-mypy/milestone/18) -* Drop support for python<3.6. - -## [0.9.1](https://github.com/realpython/pytest-mypy/milestone/17) -* Add support for pytest 7. - -## [0.9.0](https://github.com/realpython/pytest-mypy/milestone/14) -* Drop support for pytest<4.6. -* Add --mypy-config-file. - -## [0.8.1](https://github.com/realpython/pytest-mypy/milestone/16) -* Add a partial workaround for https://github.com/pytest-dev/pytest/issues/8016. - -## [0.8.0](https://github.com/realpython/pytest-mypy/milestone/15) -* Add support for Python 3.9. -* Stop injecting `MypyStatusItem` in `pytest_collection_modifyitems` to fix `--looponfail`. - -## [0.7.0](https://github.com/realpython/pytest-mypy/milestone/13) -* Remove the upper bound on `python_requires`. -* Require Python 3.5 or greater. -* Enable custom error formatting. -* Fix compatibility with pytest-xdist 2. - -## [0.6.2](https://github.com/realpython/pytest-mypy/milestone/12) -* Stop ignoring `.pyi` files. - -## [0.6.1](https://github.com/realpython/pytest-mypy/milestone/11) -* Fix a PytestDeprecationWarning emitted by pytest>=5.4 - -## [0.6.0](https://github.com/realpython/pytest-mypy/milestone/10) -* Inject a test that checks the mypy exit status - -## [0.5.0](https://github.com/realpython/pytest-mypy/milestone/9) -* Remove `MypyItem.mypy_path` -* Add support for pytest-xdist -* Add a configurable name to MypyItem node IDs - -## [0.4.2](https://github.com/realpython/pytest-mypy/milestone/8) -* Make success message green instead of red -* Remove Python 3.8 beta/dev references -* Stop blacklisting early 0.5x and 0.7x mypy releases - -## [0.4.1](https://github.com/realpython/pytest-mypy/milestone/7) -* Stop overlapping `python_version`s in `install_requires` - -## [0.4.0](https://github.com/realpython/pytest-mypy/milestone/6) -* Run mypy once per session instead of once per file -* Stop passing --incremental (which mypy now defaults to) -* Support configuring the plugin in a conftest.py -* Add support for Python 3.8 - -## [0.3.3](https://github.com/realpython/pytest-mypy/milestone/3) -* Register `mypy` marker. -* Add a PEP 518 `[build-system]` -* Add dependency pins for Python 3.4 -* Add support for Python 3.7 - -## [0.3.2](https://github.com/realpython/pytest-mypy/milestone/2) -* Add `mypy` marker to run mypy checks only - -## [0.3.1](https://github.com/realpython/pytest-mypy/milestone/1) -* Only depend on `mypy.api` -* Add `--mypy-ignore-missing-imports` -* Invoke `mypy` with `--incremental` - -## [0.3.0](https://github.com/realpython/pytest-mypy/milestone/5) -* Change `mypy` dependency to pull in `mypy` instead of `mypy-lang` +The Changelog has moved to https://github.com/realpython/pytest-mypy/releases From 237952cf5fa9db14a8e554cccc38444517815b90 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sat, 6 May 2023 14:06:04 -0700 Subject: [PATCH 10/66] Drop support for Python 3.6 --- .github/workflows/validation.yml | 2 +- setup.py | 5 ++--- tox.ini | 2 -- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index cca2f1d..3edcdb3 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 diff --git a/setup.py b/setup.py index 6abbd5a..ce0b0c5 100644 --- a/setup.py +++ b/setup.py @@ -29,12 +29,12 @@ def read(fname): py_modules=[ os.path.splitext(os.path.basename(path))[0] for path in glob.glob("src/*.py") ], - python_requires=">=3.6", + python_requires=">=3.7", setup_requires=["setuptools-scm>=3.5"], install_requires=[ "attrs>=19.0", "filelock>=3.0", - 'pytest>=4.6; python_version>="3.6" and python_version<"3.10"', + 'pytest>=4.6; python_version<"3.10"', 'pytest>=6.2; python_version>="3.10"', 'mypy>=0.500; python_version<"3.8"', 'mypy>=0.700; python_version>="3.8" and python_version<"3.9"', @@ -48,7 +48,6 @@ def read(fname): "Topic :: Software Development :: Testing", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", diff --git a/tox.ini b/tox.ini index ff5684c..1781e33 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,6 @@ minversion = 3.20 isolated_build = true envlist = - py36-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.5x, 0.60, 0.6x, 0.70, 0.7x, 0.80, 0.8x, 0.90, 0.9x} py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.5x, 0.60, 0.6x, 0.70, 0.7x, 0.80, 0.8x, 0.90, 0.9x} py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.71, 0.7x, 0.80, 0.8x, 0.90, 0.9x} py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x} @@ -14,7 +13,6 @@ envlist = [gh-actions] python = - 3.6: py36-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.5x, 0.60, 0.6x, 0.70, 0.7x, 0.80, 0.8x, 0.90, 0.9x} 3.7: py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.5x, 0.60, 0.6x, 0.70, 0.7x, 0.80, 0.8x, 0.90, 0.9x} 3.8: py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.71, 0.7x, 0.80, 0.8x, 0.90, 0.9x}, publish, static 3.9: py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x} From 833a87a5b712c49816a6d7d1f9a587df7eca8449 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sat, 6 May 2023 13:39:34 -0700 Subject: [PATCH 11/66] Replace setup.py with PEP 621 metadata --- pyproject.toml | 45 ++++++++++++++++++++++++++++++++++++- setup.py | 60 -------------------------------------------------- tox.ini | 8 +++---- 3 files changed, 48 insertions(+), 65 deletions(-) delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml index f26c121..6b70512 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,48 @@ [build-system] -requires = ["setuptools >= 59.6", "setuptools-scm[toml] >= 6.4", "wheel >= 0.37.1"] +requires = ["setuptools >= 61.0", "setuptools-scm >= 7.1", "wheel >= 0.40"] build-backend = "setuptools.build_meta" +[project] +name = "pytest-mypy" +dynamic = ["version"] +description = "A Pytest Plugin for Mypy" +readme = "README.rst" +license = {file = "LICENSE"} +maintainers = [ + {name = "David Tucker", email = "david@tucker.name"} +] +classifiers = [ + "Development Status :: 4 - Beta", + "Framework :: Pytest", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Software Development :: Testing", +] +requires-python = ">=3.7" +dependencies = [ + "attrs>=19.0", + "filelock>=3.0", + "pytest>=4.6; python_version<'3.10'", + "pytest>=6.2; python_version>='3.10'", + "mypy>=0.500; python_version<'3.8'", + "mypy>=0.700; python_version>='3.8' and python_version<'3.9'", + "mypy>=0.780; python_version>='3.9' and python_version<'3.11'", + "mypy>=0.900; python_version>='3.11'", +] + +[project.urls] +homepage = "https://github.com/realpython/pytest-mypy" + +[project.entry-points.pytest11] +mypy = "pytest_mypy" + [tool.setuptools_scm] diff --git a/setup.py b/setup.py deleted file mode 100644 index ce0b0c5..0000000 --- a/setup.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import glob -import os -import codecs -from setuptools import setup, find_packages # type: ignore - - -def read(fname): - file_path = os.path.join(os.path.dirname(__file__), fname) - return codecs.open(file_path, encoding="utf-8").read() - - -setup( - name="pytest-mypy", - use_scm_version=True, - author="Daniel Bader", - author_email="mail@dbader.org", - maintainer="David Tucker", - maintainer_email="david@tucker.name", - license="MIT", - url="https://github.com/realpython/pytest-mypy", - description="Mypy static type checker plugin for Pytest", - long_description=read("README.rst"), - long_description_content_type="text/x-rst", - packages=find_packages("src"), - package_dir={"": "src"}, - py_modules=[ - os.path.splitext(os.path.basename(path))[0] for path in glob.glob("src/*.py") - ], - python_requires=">=3.7", - setup_requires=["setuptools-scm>=3.5"], - install_requires=[ - "attrs>=19.0", - "filelock>=3.0", - 'pytest>=4.6; python_version<"3.10"', - 'pytest>=6.2; python_version>="3.10"', - 'mypy>=0.500; python_version<"3.8"', - 'mypy>=0.700; python_version>="3.8" and python_version<"3.9"', - 'mypy>=0.780; python_version>="3.9" and python_version<"3.11"', - 'mypy>=0.900; python_version>="3.11"', - ], - classifiers=[ - "Development Status :: 4 - Beta", - "Framework :: Pytest", - "Intended Audience :: Developers", - "Topic :: Software Development :: Testing", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: Implementation :: CPython", - "Operating System :: OS Independent", - "License :: OSI Approved :: MIT License", - ], - entry_points={"pytest11": ["mypy = pytest_mypy"]}, -) diff --git a/tox.ini b/tox.ini index 1781e33..0762b8a 100644 --- a/tox.ini +++ b/tox.ini @@ -102,10 +102,10 @@ deps = flake8 ~= 4.0.0 mypy >= 0.900, < 0.910 commands = - black --check src setup.py tests - flake8 src setup.py tests - mypy src setup.py - bandit --recursive src setup.py + black --check src tests + flake8 src tests + mypy src + bandit --recursive src [flake8] max-line-length = 88 From 9b9ee9e878f11128c4d81717b23118a019fe9051 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 7 May 2023 14:59:29 -0700 Subject: [PATCH 12/66] Use envtmpdir instead of distdir in tox.ini --- pyproject.toml | 6 +++--- tox.ini | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6b70512..9ee67de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,10 +39,10 @@ dependencies = [ "mypy>=0.900; python_version>='3.11'", ] -[project.urls] -homepage = "https://github.com/realpython/pytest-mypy" - [project.entry-points.pytest11] mypy = "pytest_mypy" +[project.urls] +homepage = "https://github.com/realpython/pytest-mypy" + [tool.setuptools_scm] diff --git a/tox.ini b/tox.ini index 0762b8a..ba812fe 100644 --- a/tox.ini +++ b/tox.ini @@ -92,8 +92,8 @@ deps = build[virtualenv] ~= 0.9.0 twine ~= 4.0.0 commands = - {envpython} -m build --outdir {distdir} . - twine {posargs:check} {distdir}/* + {envpython} -m build --outdir {envtmpdir} . + twine {posargs:check} {envtmpdir}/* [testenv:static] deps = From de0a150738cbb0c1313c0558b71dfcd61eec9835 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 7 May 2023 15:29:27 -0700 Subject: [PATCH 13/66] Test with mypy 1.x --- tox.ini | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tox.ini b/tox.ini index ba812fe..bbf03f3 100644 --- a/tox.ini +++ b/tox.ini @@ -3,21 +3,21 @@ minversion = 3.20 isolated_build = true envlist = - py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.5x, 0.60, 0.6x, 0.70, 0.7x, 0.80, 0.8x, 0.90, 0.9x} - py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.71, 0.7x, 0.80, 0.8x, 0.90, 0.9x} - py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x} - py310-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x} - py311-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.9x} + py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.5x, 0.60, 0.6x, 0.70, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} + py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.71, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} + py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} + py310-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} + py311-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.9x, 1.0, 1.x} publish static [gh-actions] python = - 3.7: py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.5x, 0.60, 0.6x, 0.70, 0.7x, 0.80, 0.8x, 0.90, 0.9x} - 3.8: py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.71, 0.7x, 0.80, 0.8x, 0.90, 0.9x}, publish, static - 3.9: py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x} - 3.10: py310-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x} - 3.11: py311-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.9x} + 3.7: py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.5x, 0.60, 0.6x, 0.70, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} + 3.8: py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.71, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x}, publish, static + 3.9: py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} + 3.10: py310-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} + 3.11: py311-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.9x, 1.0, 1.x} [testenv] deps = @@ -74,6 +74,8 @@ deps = mypy0.98: mypy >= 0.980, < 0.990 mypy0.99: mypy >= 0.990, <= 0.999 mypy0.9x: mypy >= 0.900, <= 0.999 + mypy1.0: mypy ~= 1.0.0 + mypy1.x: mypy ~= 1.0 packaging ~= 21.3 pexpect ~= 4.8.0 From 6d0191dd1c5e687a2f217fb243e2e96ac37f8b88 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 7 May 2023 18:20:36 -0700 Subject: [PATCH 14/66] Reduce mypy testing scope --- tox.ini | 63 ++++++++++----------------------------------------------- 1 file changed, 11 insertions(+), 52 deletions(-) diff --git a/tox.ini b/tox.ini index bbf03f3..4817b10 100644 --- a/tox.ini +++ b/tox.ini @@ -3,21 +3,21 @@ minversion = 3.20 isolated_build = true envlist = - py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.5x, 0.60, 0.6x, 0.70, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} - py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.71, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} - py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} - py310-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} - py311-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.9x, 1.0, 1.x} + py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.x, 1.0, 1.x} + py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.71, 0.x, 1.0, 1.x} + py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.x, 1.0, 1.x} + py310-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.78, 0.x, 1.0, 1.x} + py311-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.x, 1.0, 1.x} publish static [gh-actions] python = - 3.7: py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.5x, 0.60, 0.6x, 0.70, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} - 3.8: py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.71, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x}, publish, static - 3.9: py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} - 3.10: py310-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.78, 0.7x, 0.80, 0.8x, 0.90, 0.9x, 1.0, 1.x} - 3.11: py311-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.9x, 1.0, 1.x} + 3.7: py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.x, 1.0, 1.x} + 3.8: py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.71, 0.x, 1.0, 1.x}, publish, static + 3.9: py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.x, 1.0, 1.x} + 3.10: py310-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.78, 0.x, 1.0, 1.x} + 3.11: py311-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.x, 1.0, 1.x} [testenv] deps = @@ -25,55 +25,14 @@ deps = pytest5.0: pytest ~= 5.0.0 pytest5.x: pytest ~= 5.0 pytest6.0: pytest ~= 6.0.0 - pytest6.2: pytest ~= 6.2.0 pytest6.x: pytest ~= 6.0 pytest7.0: pytest ~= 7.0.0 pytest7.x: pytest ~= 7.0 mypy0.50: mypy >= 0.500, < 0.510 - mypy0.51: mypy >= 0.510, < 0.520 - mypy0.52: mypy >= 0.520, < 0.530 - mypy0.53: mypy >= 0.530, < 0.540 - mypy0.54: mypy >= 0.540, < 0.550 - mypy0.55: mypy >= 0.550, < 0.560 - mypy0.56: mypy >= 0.560, < 0.570 - mypy0.57: mypy >= 0.570, < 0.580 - mypy0.58: mypy >= 0.580, < 0.590 - mypy0.59: mypy >= 0.590, < 0.600 - mypy0.5x: mypy >= 0.500, < 0.600 - mypy0.60: mypy >= 0.600, < 0.610 - mypy0.61: mypy >= 0.610, < 0.620 - mypy0.62: mypy >= 0.620, < 0.630 - mypy0.63: mypy >= 0.630, < 0.640 - mypy0.64: mypy >= 0.640, < 0.650 - mypy0.65: mypy >= 0.650, < 0.660 - mypy0.66: mypy >= 0.660, < 0.670 - mypy0.67: mypy >= 0.670, < 0.680 - mypy0.6x: mypy >= 0.600, < 0.700 - mypy0.70: mypy >= 0.700, < 0.710 mypy0.71: mypy >= 0.710, < 0.720 - mypy0.72: mypy >= 0.720, < 0.730 - mypy0.73: mypy >= 0.730, < 0.740 - mypy0.74: mypy >= 0.740, < 0.750 - mypy0.75: mypy >= 0.750, < 0.760 - mypy0.76: mypy >= 0.760, < 0.770 - mypy0.77: mypy >= 0.770, < 0.780 mypy0.78: mypy >= 0.780, < 0.790 - mypy0.79: mypy >= 0.790, < 0.800 - mypy0.7x: mypy >= 0.700, < 0.800 - mypy0.80: mypy >= 0.800, < 0.810 - mypy0.81: mypy >= 0.810, < 0.820 - mypy0.8x: mypy >= 0.800, < 0.900 mypy0.90: mypy >= 0.900, < 0.910 - mypy0.91: mypy >= 0.910, < 0.920 - mypy0.92: mypy >= 0.920, < 0.930 - mypy0.93: mypy >= 0.930, < 0.940 - mypy0.94: mypy >= 0.940, < 0.950 - mypy0.95: mypy >= 0.950, < 0.960 - mypy0.96: mypy >= 0.960, < 0.970 - mypy0.97: mypy >= 0.970, < 0.980 - mypy0.98: mypy >= 0.980, < 0.990 - mypy0.99: mypy >= 0.990, <= 0.999 - mypy0.9x: mypy >= 0.900, <= 0.999 + mypy0.x: mypy ~= 0.0 mypy1.0: mypy ~= 1.0.0 mypy1.x: mypy ~= 1.0 From 3b567bbd07fce2d079d99abb1975d162c7ebac02 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 7 May 2023 21:22:30 -0700 Subject: [PATCH 15/66] Mark the project as stable --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9ee67de..7af330c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ maintainers = [ {name = "David Tucker", email = "david@tucker.name"} ] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Framework :: Pytest", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", From 1ef88f9fc32328d7f30709fdb43a771194b2fb3e Mon Sep 17 00:00:00 2001 From: David Tucker Date: Thu, 8 Feb 2024 01:05:41 -0800 Subject: [PATCH 16/66] Prevent AttributeError in pytest_terminal_summary --- src/pytest_mypy.py | 29 +++++++++++++++-------------- tests/test_pytest_mypy.py | 2 ++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index faa02a7..82dbd2b 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -327,17 +327,18 @@ class MypyWarning(pytest.PytestWarning): def pytest_terminal_summary(terminalreporter, config): """Report stderr and unrecognized lines from stdout.""" - try: - with open(config._mypy_results_path, mode="r") as results_f: - results = MypyResults.load(results_f) - except FileNotFoundError: - # No MypyItems executed. - return - if results.unmatched_stdout or results.stderr: - terminalreporter.section("mypy") - if results.unmatched_stdout: - color = {"red": True} if results.status else {"green": True} - terminalreporter.write_line(results.unmatched_stdout, **color) - if results.stderr: - terminalreporter.write_line(results.stderr, yellow=True) - os.remove(config._mypy_results_path) + if _is_master(config): + try: + with open(config._mypy_results_path, mode="r") as results_f: + results = MypyResults.load(results_f) + except FileNotFoundError: + # No MypyItems executed. + return + if results.unmatched_stdout or results.stderr: + terminalreporter.section("mypy") + if results.unmatched_stdout: + color = {"red": True} if results.status else {"green": True} + terminalreporter.write_line(results.unmatched_stdout, **color) + if results.stderr: + terminalreporter.write_line(results.stderr, yellow=True) + os.remove(config._mypy_results_path) diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index 924cdd2..29bdf8f 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -89,6 +89,7 @@ def pyfunc(x: int) -> str: ) result = testdir.runpytest_subprocess(*xdist_args) result.assert_outcomes() + assert "_mypy_results_path" not in result.stderr.str() result = testdir.runpytest_subprocess("--mypy", *xdist_args) mypy_file_checks = 1 mypy_status_check = 1 @@ -96,6 +97,7 @@ def pyfunc(x: int) -> str: result.assert_outcomes(failed=mypy_checks) result.stdout.fnmatch_lines(["2: error: Incompatible return value*"]) assert result.ret != 0 + assert "_mypy_results_path" not in result.stderr.str() def test_mypy_annotation_unchecked(testdir, xdist_args): From 9608c7ea745162a7c1df1ac28b20d471de1c0abe Mon Sep 17 00:00:00 2001 From: David Tucker Date: Thu, 1 Feb 2024 22:15:36 -0800 Subject: [PATCH 17/66] Add support for Python 3.12 --- .github/workflows/validation.yml | 2 +- pyproject.toml | 1 + tests/test_pytest_mypy.py | 8 ++------ tox.ini | 3 +++ 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 3edcdb3..b9fe779 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 diff --git a/pyproject.toml b/pyproject.toml index 7af330c..19d1e41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Testing", ] diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index 29bdf8f..bf88847 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -116,13 +116,9 @@ def pyfunc(x): mypy_status_check = 1 mypy_checks = mypy_file_checks + mypy_status_check outcomes = {"passed": mypy_checks} - # mypy doesn't emit annotation-unchecked warnings until 0.990: - min_mypy_version = Version("0.990") - if MYPY_VERSION >= min_mypy_version and PYTEST_VERSION >= Version("7.0"): - # assert_outcomes does not support `warnings` until 7.x. - outcomes["warnings"] = 1 result.assert_outcomes(**outcomes) - if MYPY_VERSION >= min_mypy_version: + # mypy doesn't emit annotation-unchecked warnings until 0.990: + if MYPY_VERSION >= Version("0.990"): result.stdout.fnmatch_lines(["*MypyWarning*"]) assert result.ret == 0 diff --git a/tox.ini b/tox.ini index 4817b10..7d7f905 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ envlist = py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.x, 1.0, 1.x} py310-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.78, 0.x, 1.0, 1.x} py311-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.x, 1.0, 1.x} + py312-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.x, 1.0, 1.x} publish static @@ -18,6 +19,7 @@ python = 3.9: py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.x, 1.0, 1.x} 3.10: py310-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.78, 0.x, 1.0, 1.x} 3.11: py311-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.x, 1.0, 1.x} + 3.12: py312-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.x, 1.0, 1.x} [testenv] deps = @@ -25,6 +27,7 @@ deps = pytest5.0: pytest ~= 5.0.0 pytest5.x: pytest ~= 5.0 pytest6.0: pytest ~= 6.0.0 + pytest6.2: pytest ~= 6.2.0 pytest6.x: pytest ~= 6.0 pytest7.0: pytest ~= 7.0.0 pytest7.x: pytest ~= 7.0 From b14ba00e2e0dec8951d112a08183fd817c736b1b Mon Sep 17 00:00:00 2001 From: David Tucker Date: Thu, 1 Feb 2024 22:24:05 -0800 Subject: [PATCH 18/66] Add support for pytest 8 --- tox.ini | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tox.ini b/tox.ini index 7d7f905..9f772f3 100644 --- a/tox.ini +++ b/tox.ini @@ -4,22 +4,22 @@ minversion = 3.20 isolated_build = true envlist = py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.x, 1.0, 1.x} - py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.71, 0.x, 1.0, 1.x} - py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.x, 1.0, 1.x} - py310-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.78, 0.x, 1.0, 1.x} - py311-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.x, 1.0, 1.x} - py312-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.x, 1.0, 1.x} + py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.71, 0.x, 1.0, 1.x} + py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.78, 0.x, 1.0, 1.x} + py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.78, 0.x, 1.0, 1.x} + py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.90, 0.x, 1.0, 1.x} + py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.90, 0.x, 1.0, 1.x} publish static [gh-actions] python = 3.7: py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.x, 1.0, 1.x} - 3.8: py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.71, 0.x, 1.0, 1.x}, publish, static - 3.9: py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.78, 0.x, 1.0, 1.x} - 3.10: py310-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.78, 0.x, 1.0, 1.x} - 3.11: py311-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.x, 1.0, 1.x} - 3.12: py312-pytest{6.2, 6.x, 7.0, 7.x}-mypy{0.90, 0.x, 1.0, 1.x} + 3.8: py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.71, 0.x, 1.0, 1.x}, publish, static + 3.9: py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.78, 0.x, 1.0, 1.x} + 3.10: py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.78, 0.x, 1.0, 1.x} + 3.11: py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.90, 0.x, 1.0, 1.x} + 3.12: py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.90, 0.x, 1.0, 1.x} [testenv] deps = @@ -31,6 +31,8 @@ deps = pytest6.x: pytest ~= 6.0 pytest7.0: pytest ~= 7.0.0 pytest7.x: pytest ~= 7.0 + pytest8.0: pytest ~= 8.0.0 + pytest8.x: pytest ~= 8.0 mypy0.50: mypy >= 0.500, < 0.510 mypy0.71: mypy >= 0.710, < 0.720 mypy0.78: mypy >= 0.780, < 0.790 From ce33b5fda8c76bcacc82c02c48d5007b0b56d5c6 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Thu, 1 Feb 2024 23:01:33 -0800 Subject: [PATCH 19/66] Add a mypy version report header to the tests --- tests/conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 694d7d5..2306bcc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1 +1,7 @@ +import mypy.version + pytest_plugins = "pytester" + + +def pytest_report_header(): + return f"mypy: {mypy.version.__version__}" From 291da1f32143adea330fce3ade1a5d08eef4b33d Mon Sep 17 00:00:00 2001 From: David Tucker Date: Mon, 5 Feb 2024 01:19:44 -0800 Subject: [PATCH 20/66] Set constrain_package_deps in tox.ini --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9f772f3..2d59c92 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # For more information about tox, see https://tox.readthedocs.io/en/latest/ [tox] -minversion = 3.20 +minversion = 4.4 isolated_build = true envlist = py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.x, 1.0, 1.x} @@ -22,6 +22,7 @@ python = 3.12: py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.90, 0.x, 1.0, 1.x} [testenv] +constrain_package_deps = true deps = pytest4.6: pytest ~= 4.6.0 pytest5.0: pytest ~= 5.0.0 @@ -54,6 +55,7 @@ testpaths = tests [testenv:publish] passenv = TWINE_* +constrain_package_deps = false deps = build[virtualenv] ~= 0.9.0 twine ~= 4.0.0 From 3814c1249728a3e4366ede03160f93b5540a5483 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 4 Feb 2024 11:09:58 -0800 Subject: [PATCH 21/66] Upgrade static deps --- src/pytest_mypy.py | 5 ----- tox.ini | 6 +++--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 82dbd2b..7d8e1cb 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -138,7 +138,6 @@ def pytest_collect_file(path, parent): # type: ignore class MypyFile(pytest.File): - """A File that Mypy will run on.""" @classmethod @@ -161,7 +160,6 @@ def collect(self): class MypyItem(pytest.Item): - """A Mypy-related test Item.""" MARKER = "mypy" @@ -194,7 +192,6 @@ def repr_failure(self, excinfo): class MypyFileItem(MypyItem): - """A check for Mypy errors in a File.""" def runtest(self): @@ -221,7 +218,6 @@ def reportinfo(self): class MypyStatusItem(MypyItem): - """A check for a non-zero mypy exit status.""" def runtest(self): @@ -233,7 +229,6 @@ def runtest(self): @attr.s(frozen=True, kw_only=True) class MypyResults: - """Parsed results from Mypy.""" _abspath_errors_type = Dict[str, List[str]] diff --git a/tox.ini b/tox.ini index 2d59c92..de84457 100644 --- a/tox.ini +++ b/tox.ini @@ -66,9 +66,9 @@ commands = [testenv:static] deps = bandit ~= 1.7.0 - black ~= 22.3.0 - flake8 ~= 4.0.0 - mypy >= 0.900, < 0.910 + black ~= 24.2.0 + flake8 ~= 7.0.0 + mypy ~= 1.8.0 commands = black --check src tests flake8 src tests From 4300a1b066d37ceef0b245e65f999d5f718e4512 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 4 Feb 2024 11:21:59 -0800 Subject: [PATCH 22/66] Upgrade publish deps --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index de84457..ae6f422 100644 --- a/tox.ini +++ b/tox.ini @@ -57,8 +57,8 @@ testpaths = tests passenv = TWINE_* constrain_package_deps = false deps = - build[virtualenv] ~= 0.9.0 - twine ~= 4.0.0 + build[virtualenv] ~= 1.0.0 + twine ~= 5.0.0 commands = {envpython} -m build --outdir {envtmpdir} . twine {posargs:check} {envtmpdir}/* From 240cff76cfe72ea0d1e651073601534644dbffff Mon Sep 17 00:00:00 2001 From: David Tucker Date: Tue, 9 May 2023 00:52:53 -0700 Subject: [PATCH 23/66] Enable branch code coverage --- src/pytest_mypy.py | 40 ++++++++------- tests/test_pytest_mypy.py | 103 +++++++++++++++++++++++++++++++++++++- tox.ini | 4 +- 3 files changed, 126 insertions(+), 21 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 7d8e1cb..4005d39 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -16,6 +16,7 @@ PYTEST_MAJOR_VERSION = int(pytest.__version__.partition(".")[0]) mypy_argv = [] nodeid_name = "mypy" +terminal_summary_title = "mypy" def default_file_error_formatter(item, results, errors): @@ -205,8 +206,7 @@ def runtest(self): for error in errors ): raise MypyError(file_error_formatter(self, results, errors)) - # This line cannot be easily covered on mypy < 0.990: - warnings.warn("\n" + "\n".join(errors), MypyWarning) # pragma: no cover + warnings.warn("\n" + "\n".join(errors), MypyWarning) def reportinfo(self): """Produce a heading for the test report.""" @@ -258,7 +258,9 @@ def from_mypy( ) -> "MypyResults": """Generate results from mypy.""" - if opts is None: + # This is covered by test_mypy_results_from_mypy_with_opts; + # however, coverage is not recognized on py38-pytest4.6: + if opts is None: # pragma: no cover opts = mypy_argv[:] abspath_errors = { os.path.abspath(str(item.fspath)): [] for item in items @@ -322,18 +324,20 @@ class MypyWarning(pytest.PytestWarning): def pytest_terminal_summary(terminalreporter, config): """Report stderr and unrecognized lines from stdout.""" - if _is_master(config): - try: - with open(config._mypy_results_path, mode="r") as results_f: - results = MypyResults.load(results_f) - except FileNotFoundError: - # No MypyItems executed. - return - if results.unmatched_stdout or results.stderr: - terminalreporter.section("mypy") - if results.unmatched_stdout: - color = {"red": True} if results.status else {"green": True} - terminalreporter.write_line(results.unmatched_stdout, **color) - if results.stderr: - terminalreporter.write_line(results.stderr, yellow=True) - os.remove(config._mypy_results_path) + if not _is_master(config): + # This isn't hit in pytest 5.0 for some reason. + return # pragma: no cover + try: + with open(config._mypy_results_path, mode="r") as results_f: + results = MypyResults.load(results_f) + except FileNotFoundError: + # No MypyItems executed. + return + if results.unmatched_stdout or results.stderr: + terminalreporter.section(terminal_summary_title) + if results.unmatched_stdout: + color = {"red": True} if results.status else {"green": True} + terminalreporter.write_line(results.unmatched_stdout, **color) + if results.stderr: + terminalreporter.write_line(results.stderr, yellow=True) + os.remove(config._mypy_results_path) diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index bf88847..03d2b24 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -1,4 +1,5 @@ import signal +import sys import textwrap import mypy.version @@ -6,9 +7,21 @@ import pexpect import pytest +import pytest_mypy + MYPY_VERSION = Version(mypy.version.__version__) PYTEST_VERSION = Version(pytest.__version__) +PYTHON_VERSION = Version( + ".".join( + str(token) + for token in [ + sys.version_info.major, + sys.version_info.minor, + sys.version_info.micro, + ] + ) +) @pytest.fixture( @@ -100,7 +113,7 @@ def pyfunc(x: int) -> str: assert "_mypy_results_path" not in result.stderr.str() -def test_mypy_annotation_unchecked(testdir, xdist_args): +def test_mypy_annotation_unchecked(testdir, xdist_args, tmp_path, monkeypatch): """Verify that annotation-unchecked warnings do not manifest as an error.""" testdir.makepyfile( """ @@ -109,6 +122,29 @@ def pyfunc(x): return x * y """, ) + min_mypy_version = Version("0.990") + if MYPY_VERSION < min_mypy_version: + # mypy doesn't emit annotation-unchecked warnings until 0.990: + fake_mypy_path = tmp_path / "mypy" + fake_mypy_path.mkdir() + (fake_mypy_path / "__init__.py").touch() + (fake_mypy_path / "api.py").write_text( + textwrap.dedent( + """ + def run(*args, **kwargs): + return ( + "test_mypy_annotation_unchecked.py:2:" + " note: By default the bodies of untyped functions" + " are not checked, consider using --check-untyped-defs" + " [annotation-unchecked]\\nSuccess: no issues found in" + " 1 source file\\n", + "", + 0, + ) + """ + ) + ) + monkeypatch.setenv("PYTHONPATH", str(tmp_path)) result = testdir.runpytest_subprocess(*xdist_args) result.assert_outcomes() result = testdir.runpytest_subprocess("--mypy", *xdist_args) @@ -552,3 +588,68 @@ def test_mypy_item_collect(request): mypy_status_check = 1 result.assert_outcomes(passed=test_count + mypy_file_checks + mypy_status_check) assert result.ret == 0 + + +@pytest.mark.xfail( + MYPY_VERSION < Version("0.750"), + raises=AssertionError, + reason="https://github.com/python/mypy/issues/7800", +) +def test_mypy_results_from_mypy_with_opts(): + """MypyResults.from_mypy respects passed options.""" + mypy_results = pytest_mypy.MypyResults.from_mypy([], opts=["--version"]) + assert mypy_results.status == 0 + assert mypy_results.abspath_errors == {} + assert str(MYPY_VERSION) in mypy_results.stdout + + +@pytest.mark.xfail( + Version("3.7") < PYTHON_VERSION < Version("3.9") + and Version("0.710") <= MYPY_VERSION < Version("0.720"), + raises=AssertionError, + reason="Mypy crashes for some reason.", +) +def test_mypy_no_output(testdir, xdist_args): + """No terminal summary is shown if there is no output from mypy.""" + type_ignore = ( + "# type: ignore" + if ( + PYTEST_VERSION + < Version("6.0") # Pytest didn't add type annotations until 6.0. + or MYPY_VERSION < Version("0.710") + ) + else "" + ) + testdir.makepyfile( + # Mypy prints a success message to stderr by default: + # "Success: no issues found in 1 source file" + # Clear stderr and unmatched_stdout to simulate mypy having no output: + conftest=f""" + import pytest {type_ignore} + + @pytest.hookimpl(hookwrapper=True) + def pytest_terminal_summary(config): + mypy_results_path = getattr(config, "_mypy_results_path", None) + if not mypy_results_path: + # xdist worker + return + pytest_mypy = config.pluginmanager.getplugin("mypy") + with open(mypy_results_path, mode="w") as results_f: + pytest_mypy.MypyResults( + opts=[], + stdout="", + stderr="", + status=0, + abspath_errors={{}}, + unmatched_stdout="", + ).dump(results_f) + yield + """, + ) + result = testdir.runpytest_subprocess("--mypy", *xdist_args) + mypy_file_checks = 1 + mypy_status_check = 1 + mypy_checks = mypy_file_checks + mypy_status_check + result.assert_outcomes(passed=mypy_checks) + assert result.ret == 0 + assert f"= {pytest_mypy.terminal_summary_title} =" not in str(result.stdout) diff --git a/tox.ini b/tox.ini index ae6f422..090085e 100644 --- a/tox.ini +++ b/tox.ini @@ -44,11 +44,11 @@ deps = packaging ~= 21.3 pexpect ~= 4.8.0 - pytest-cov ~= 2.10 + pytest-cov ~= 4.1.0 pytest-randomly ~= 3.4 pytest-xdist ~= 1.34 -commands = pytest -p no:mypy {posargs:--cov pytest_mypy --cov-fail-under 100 --cov-report term-missing -n auto} +commands = pytest -p no:mypy {posargs:--cov pytest_mypy --cov-branch --cov-fail-under 100 --cov-report term-missing -n auto} [pytest] testpaths = tests From 5c63cb80fc12ba89ff77fe1213f5d72db1ef35a0 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Thu, 15 Feb 2024 08:43:13 -0800 Subject: [PATCH 24/66] Rename _is_master to _is_xdist_controller --- src/pytest_mypy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 4005d39..08b26e2 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -60,10 +60,10 @@ def _get_xdist_workerinput(config_node): return workerinput -def _is_master(config): +def _is_xdist_controller(config): """ True if the code running the given pytest.config object is running in - an xdist master node or not running xdist at all. + an xdist controller node or not running xdist at all. """ return _get_xdist_workerinput(config) is None @@ -74,7 +74,7 @@ def pytest_configure(config): register a custom marker for MypyItems, and configure the plugin based on the CLI. """ - if _is_master(config): + if _is_xdist_controller(config): # Get the path to a temporary file and delete it. # The first MypyItem to run will see the file does not exist, @@ -295,7 +295,7 @@ def from_session(cls, session) -> "MypyResults": """Load (or generate) cached mypy results for a pytest session.""" results_path = ( session.config._mypy_results_path - if _is_master(session.config) + if _is_xdist_controller(session.config) else _get_xdist_workerinput(session.config)["_mypy_results_path"] ) with FileLock(results_path + ".lock"): @@ -324,7 +324,7 @@ class MypyWarning(pytest.PytestWarning): def pytest_terminal_summary(terminalreporter, config): """Report stderr and unrecognized lines from stdout.""" - if not _is_master(config): + if not _is_xdist_controller(config): # This isn't hit in pytest 5.0 for some reason. return # pragma: no cover try: From 1c56fb5cfdee20b4c1eafebfc4bc8567820b8361 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Mon, 5 Feb 2024 01:20:31 -0800 Subject: [PATCH 25/66] Simplify project dependencies --- pyproject.toml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 19d1e41..983fe8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,12 +32,8 @@ requires-python = ">=3.7" dependencies = [ "attrs>=19.0", "filelock>=3.0", - "pytest>=4.6; python_version<'3.10'", - "pytest>=6.2; python_version>='3.10'", - "mypy>=0.500; python_version<'3.8'", - "mypy>=0.700; python_version>='3.8' and python_version<'3.9'", - "mypy>=0.780; python_version>='3.9' and python_version<'3.11'", - "mypy>=0.900; python_version>='3.11'", + "mypy>=0.50", + "pytest>=4.6", ] [project.entry-points.pytest11] From 0da2d13063d65e54004153286a424a29dd3c1d95 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 11 Aug 2024 11:56:14 -0700 Subject: [PATCH 26/66] Require mypy >= 1.0 --- pyproject.toml | 2 +- tests/test_pytest_mypy.py | 43 +-------------------------------------- tox.ini | 29 +++++++++++--------------- 3 files changed, 14 insertions(+), 60 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 983fe8e..3a5c72b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ requires-python = ">=3.7" dependencies = [ "attrs>=19.0", "filelock>=3.0", - "mypy>=0.50", + "mypy>=1.0", "pytest>=4.6", ] diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index 03d2b24..e5b9670 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -122,29 +122,6 @@ def pyfunc(x): return x * y """, ) - min_mypy_version = Version("0.990") - if MYPY_VERSION < min_mypy_version: - # mypy doesn't emit annotation-unchecked warnings until 0.990: - fake_mypy_path = tmp_path / "mypy" - fake_mypy_path.mkdir() - (fake_mypy_path / "__init__.py").touch() - (fake_mypy_path / "api.py").write_text( - textwrap.dedent( - """ - def run(*args, **kwargs): - return ( - "test_mypy_annotation_unchecked.py:2:" - " note: By default the bodies of untyped functions" - " are not checked, consider using --check-untyped-defs" - " [annotation-unchecked]\\nSuccess: no issues found in" - " 1 source file\\n", - "", - 0, - ) - """ - ) - ) - monkeypatch.setenv("PYTHONPATH", str(tmp_path)) result = testdir.runpytest_subprocess(*xdist_args) result.assert_outcomes() result = testdir.runpytest_subprocess("--mypy", *xdist_args) @@ -153,9 +130,7 @@ def run(*args, **kwargs): mypy_checks = mypy_file_checks + mypy_status_check outcomes = {"passed": mypy_checks} result.assert_outcomes(**outcomes) - # mypy doesn't emit annotation-unchecked warnings until 0.990: - if MYPY_VERSION >= Version("0.990"): - result.stdout.fnmatch_lines(["*MypyWarning*"]) + result.stdout.fnmatch_lines(["*MypyWarning*"]) assert result.ret == 0 @@ -409,10 +384,6 @@ def pytest_configure(config): assert result.ret != 0 -@pytest.mark.xfail( - Version("0.900") > MYPY_VERSION, - reason="Mypy added pyproject.toml configuration in 0.900.", -) def test_pyproject_toml(testdir, xdist_args): """Ensure that the plugin allows configuration with pyproject.toml.""" testdir.makefile( @@ -590,11 +561,6 @@ def test_mypy_item_collect(request): assert result.ret == 0 -@pytest.mark.xfail( - MYPY_VERSION < Version("0.750"), - raises=AssertionError, - reason="https://github.com/python/mypy/issues/7800", -) def test_mypy_results_from_mypy_with_opts(): """MypyResults.from_mypy respects passed options.""" mypy_results = pytest_mypy.MypyResults.from_mypy([], opts=["--version"]) @@ -603,12 +569,6 @@ def test_mypy_results_from_mypy_with_opts(): assert str(MYPY_VERSION) in mypy_results.stdout -@pytest.mark.xfail( - Version("3.7") < PYTHON_VERSION < Version("3.9") - and Version("0.710") <= MYPY_VERSION < Version("0.720"), - raises=AssertionError, - reason="Mypy crashes for some reason.", -) def test_mypy_no_output(testdir, xdist_args): """No terminal summary is shown if there is no output from mypy.""" type_ignore = ( @@ -616,7 +576,6 @@ def test_mypy_no_output(testdir, xdist_args): if ( PYTEST_VERSION < Version("6.0") # Pytest didn't add type annotations until 6.0. - or MYPY_VERSION < Version("0.710") ) else "" ) diff --git a/tox.ini b/tox.ini index 090085e..f8ebf12 100644 --- a/tox.ini +++ b/tox.ini @@ -3,23 +3,23 @@ minversion = 4.4 isolated_build = true envlist = - py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.x, 1.0, 1.x} - py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.71, 0.x, 1.0, 1.x} - py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.78, 0.x, 1.0, 1.x} - py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.78, 0.x, 1.0, 1.x} - py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.90, 0.x, 1.0, 1.x} - py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.90, 0.x, 1.0, 1.x} + py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x} + py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} publish static [gh-actions] python = - 3.7: py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{0.50, 0.x, 1.0, 1.x} - 3.8: py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.71, 0.x, 1.0, 1.x}, publish, static - 3.9: py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.78, 0.x, 1.0, 1.x} - 3.10: py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.78, 0.x, 1.0, 1.x} - 3.11: py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.90, 0.x, 1.0, 1.x} - 3.12: py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{0.90, 0.x, 1.0, 1.x} + 3.7: py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x} + 3.8: py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}, publish, static + 3.9: py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + 3.10: py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + 3.11: py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + 3.12: py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} [testenv] constrain_package_deps = true @@ -34,11 +34,6 @@ deps = pytest7.x: pytest ~= 7.0 pytest8.0: pytest ~= 8.0.0 pytest8.x: pytest ~= 8.0 - mypy0.50: mypy >= 0.500, < 0.510 - mypy0.71: mypy >= 0.710, < 0.720 - mypy0.78: mypy >= 0.780, < 0.790 - mypy0.90: mypy >= 0.900, < 0.910 - mypy0.x: mypy ~= 0.0 mypy1.0: mypy ~= 1.0.0 mypy1.x: mypy ~= 1.0 From 53d620162e08b05415375b161b931db4a663ebce Mon Sep 17 00:00:00 2001 From: David Tucker Date: Wed, 7 Aug 2024 23:01:03 -0700 Subject: [PATCH 27/66] Remove wheel as a build dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3a5c72b..eb58fef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 61.0", "setuptools-scm >= 7.1", "wheel >= 0.40"] +requires = ["setuptools >= 61.0", "setuptools-scm >= 7.1"] build-backend = "setuptools.build_meta" [project] From ed5ed3e23b1ff45098df50ec0b6da803a78289e2 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sat, 10 Aug 2024 23:51:44 -0700 Subject: [PATCH 28/66] Generalize MypyResults.from_mypy --- src/pytest_mypy.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 08b26e2..4a272ff 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -252,7 +252,7 @@ def load(cls, results_f: TextIO) -> "MypyResults": @classmethod def from_mypy( cls, - items: List[MypyFileItem], + paths: List[Path], *, opts: Optional[List[str]] = None, ) -> "MypyResults": @@ -263,7 +263,7 @@ def from_mypy( if opts is None: # pragma: no cover opts = mypy_argv[:] abspath_errors = { - os.path.abspath(str(item.fspath)): [] for item in items + str(path.absolute()): [] for path in paths } # type: MypyResults._abspath_errors_type stdout, stderr, status = mypy.api.run( @@ -304,7 +304,11 @@ def from_session(cls, session) -> "MypyResults": results = cls.load(results_f) except FileNotFoundError: results = cls.from_mypy( - [item for item in session.items if isinstance(item, MypyFileItem)], + [ + Path(item.fspath) + for item in session.items + if isinstance(item, MypyFileItem) + ], ) with open(results_path, mode="w") as results_f: results.dump(results_f) From 6afe15e4a254a203836b5cdfa67ad9cc35678ec3 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Mon, 10 Feb 2020 19:24:53 -0800 Subject: [PATCH 29/66] Require Pytest 5+ --- pyproject.toml | 2 +- tox.ini | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eb58fef..64aea48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "attrs>=19.0", "filelock>=3.0", "mypy>=1.0", - "pytest>=4.6", + "pytest>=5.0", ] [project.entry-points.pytest11] diff --git a/tox.ini b/tox.ini index f8ebf12..ecb81dd 100644 --- a/tox.ini +++ b/tox.ini @@ -3,9 +3,9 @@ minversion = 4.4 isolated_build = true envlist = - py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x} - py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py37-pytest{5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x} + py38-pytest{5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py39-pytest{5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} @@ -14,9 +14,9 @@ envlist = [gh-actions] python = - 3.7: py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x} - 3.8: py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}, publish, static - 3.9: py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + 3.7: py37-pytest{5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x} + 3.8: py38-pytest{5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}, publish, static + 3.9: py39-pytest{5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} 3.10: py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} 3.11: py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} 3.12: py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} @@ -24,7 +24,6 @@ python = [testenv] constrain_package_deps = true deps = - pytest4.6: pytest ~= 4.6.0 pytest5.0: pytest ~= 5.0.0 pytest5.x: pytest ~= 5.0 pytest6.0: pytest ~= 6.0.0 From f1aed2a0fa516eb06e518bc1d996852aa07b51da Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sat, 22 Jun 2019 23:56:17 -0700 Subject: [PATCH 30/66] Use pytest.ExitCode in tests --- tests/test_pytest_mypy.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index e5b9670..c3f9383 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -53,12 +53,13 @@ def pyfunc(x: int) -> int: ) result = testdir.runpytest_subprocess(*xdist_args) result.assert_outcomes() + assert result.ret == pytest.ExitCode.NO_TESTS_COLLECTED result = testdir.runpytest_subprocess("--mypy", *xdist_args) mypy_file_checks = pyfile_count mypy_status_check = 1 mypy_checks = mypy_file_checks + mypy_status_check result.assert_outcomes(passed=mypy_checks) - assert result.ret == 0 + assert result.ret == pytest.ExitCode.OK def test_mypy_pyi(testdir, xdist_args): @@ -89,7 +90,7 @@ def pyfunc(x: int) -> int: ... mypy_status_check = 1 mypy_checks = mypy_file_checks + mypy_status_check result.assert_outcomes(passed=mypy_checks) - assert result.ret == 0 + assert result.ret == pytest.ExitCode.OK def test_mypy_error(testdir, xdist_args): @@ -103,14 +104,15 @@ def pyfunc(x: int) -> str: result = testdir.runpytest_subprocess(*xdist_args) result.assert_outcomes() assert "_mypy_results_path" not in result.stderr.str() + assert result.ret == pytest.ExitCode.NO_TESTS_COLLECTED result = testdir.runpytest_subprocess("--mypy", *xdist_args) mypy_file_checks = 1 mypy_status_check = 1 mypy_checks = mypy_file_checks + mypy_status_check result.assert_outcomes(failed=mypy_checks) result.stdout.fnmatch_lines(["2: error: Incompatible return value*"]) - assert result.ret != 0 assert "_mypy_results_path" not in result.stderr.str() + assert result.ret == pytest.ExitCode.TESTS_FAILED def test_mypy_annotation_unchecked(testdir, xdist_args, tmp_path, monkeypatch): @@ -131,7 +133,7 @@ def pyfunc(x): outcomes = {"passed": mypy_checks} result.assert_outcomes(**outcomes) result.stdout.fnmatch_lines(["*MypyWarning*"]) - assert result.ret == 0 + assert result.ret == pytest.ExitCode.OK def test_mypy_ignore_missings_imports(testdir, xdist_args): @@ -162,10 +164,10 @@ def test_mypy_ignore_missings_imports(testdir, xdist_args): ), ], ) - assert result.ret != 0 + assert result.ret == pytest.ExitCode.TESTS_FAILED result = testdir.runpytest_subprocess("--mypy-ignore-missing-imports", *xdist_args) result.assert_outcomes(passed=mypy_checks) - assert result.ret == 0 + assert result.ret == pytest.ExitCode.OK def test_mypy_config_file(testdir, xdist_args): @@ -181,7 +183,7 @@ def pyfunc(x): mypy_status_check = 1 mypy_checks = mypy_file_checks + mypy_status_check result.assert_outcomes(passed=mypy_checks) - assert result.ret == 0 + assert result.ret == pytest.ExitCode.OK mypy_config_file = testdir.makeini( """ [mypy] @@ -210,10 +212,10 @@ def test_fails(): mypy_status_check = 1 mypy_checks = mypy_file_checks + mypy_status_check result.assert_outcomes(failed=test_count, passed=mypy_checks) - assert result.ret != 0 + assert result.ret == pytest.ExitCode.TESTS_FAILED result = testdir.runpytest_subprocess("--mypy", "-m", "mypy", *xdist_args) result.assert_outcomes(passed=mypy_checks) - assert result.ret == 0 + assert result.ret == pytest.ExitCode.OK def test_non_mypy_error(testdir, xdist_args): @@ -235,6 +237,7 @@ def runtest(self): ) result = testdir.runpytest_subprocess(*xdist_args) result.assert_outcomes() + assert result.ret == pytest.ExitCode.NO_TESTS_COLLECTED result = testdir.runpytest_subprocess("--mypy", *xdist_args) mypy_file_checks = 1 # conftest.py mypy_status_check = 1 @@ -243,7 +246,7 @@ def runtest(self): passed=mypy_status_check, # conftest.py has no type errors. ) result.stdout.fnmatch_lines(["*" + message]) - assert result.ret != 0 + assert result.ret == pytest.ExitCode.TESTS_FAILED def test_mypy_stderr(testdir, xdist_args): @@ -294,7 +297,7 @@ def pytest_configure(config): """, ) result = testdir.runpytest_subprocess("--mypy", *xdist_args) - assert result.ret == 0 + assert result.ret == pytest.ExitCode.OK def test_api_nodeid_name(testdir, xdist_args): @@ -311,7 +314,7 @@ def pytest_configure(config): ) result = testdir.runpytest_subprocess("--mypy", "--verbose", *xdist_args) result.stdout.fnmatch_lines(["*conftest.py::" + nodeid_name + "*"]) - assert result.ret == 0 + assert result.ret == pytest.ExitCode.OK @pytest.mark.xfail( @@ -352,7 +355,7 @@ def pyfunc(x: int) -> str: mypy_file_checks = 1 mypy_status_check = 1 result.assert_outcomes(passed=mypy_file_checks, failed=mypy_status_check) - assert result.ret != 0 + assert result.ret == pytest.ExitCode.TESTS_FAILED def test_api_error_formatter(testdir, xdist_args): @@ -381,7 +384,7 @@ def pytest_configure(config): ) result = testdir.runpytest_subprocess("--mypy", *xdist_args) result.stdout.fnmatch_lines(["*/bad.py:2: error: Incompatible return value*"]) - assert result.ret != 0 + assert result.ret == pytest.ExitCode.TESTS_FAILED def test_pyproject_toml(testdir, xdist_args): @@ -401,7 +404,7 @@ def pyfunc(x): ) result = testdir.runpytest_subprocess("--mypy", *xdist_args) result.stdout.fnmatch_lines(["1: error: Function is missing a type annotation*"]) - assert result.ret != 0 + assert result.ret == pytest.ExitCode.TESTS_FAILED def test_setup_cfg(testdir, xdist_args): @@ -421,7 +424,7 @@ def pyfunc(x): ) result = testdir.runpytest_subprocess("--mypy", *xdist_args) result.stdout.fnmatch_lines(["1: error: Function is missing a type annotation*"]) - assert result.ret != 0 + assert result.ret == pytest.ExitCode.TESTS_FAILED @pytest.mark.parametrize("module_name", ["__init__", "test_demo"]) @@ -558,7 +561,7 @@ def test_mypy_item_collect(request): mypy_file_checks = 1 mypy_status_check = 1 result.assert_outcomes(passed=test_count + mypy_file_checks + mypy_status_check) - assert result.ret == 0 + assert result.ret == pytest.ExitCode.OK def test_mypy_results_from_mypy_with_opts(): @@ -610,5 +613,5 @@ def pytest_terminal_summary(config): mypy_status_check = 1 mypy_checks = mypy_file_checks + mypy_status_check result.assert_outcomes(passed=mypy_checks) - assert result.ret == 0 + assert result.ret == pytest.ExitCode.OK assert f"= {pytest_mypy.terminal_summary_title} =" not in str(result.stdout) From af3b814b8496a05a7e835ddf8129dc446c2637ba Mon Sep 17 00:00:00 2001 From: David Tucker Date: Fri, 8 Mar 2024 00:08:40 -0800 Subject: [PATCH 31/66] Remove obsolete "no cover" --- src/pytest_mypy.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 4a272ff..b30b3a1 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -258,9 +258,7 @@ def from_mypy( ) -> "MypyResults": """Generate results from mypy.""" - # This is covered by test_mypy_results_from_mypy_with_opts; - # however, coverage is not recognized on py38-pytest4.6: - if opts is None: # pragma: no cover + if opts is None: opts = mypy_argv[:] abspath_errors = { str(path.absolute()): [] for path in paths From 00ad34171a6d3c55c5ce7096ee71b893e2d0dec3 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Wed, 5 Aug 2020 21:36:07 -0700 Subject: [PATCH 32/66] Require Pytest 6+ --- pyproject.toml | 2 +- src/pytest_mypy.py | 23 ++--------------------- tests/test_pytest_mypy.py | 24 ------------------------ tox.ini | 14 ++++++-------- 4 files changed, 9 insertions(+), 54 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 64aea48..93908e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "attrs>=19.0", "filelock>=3.0", "mypy>=1.0", - "pytest>=5.0", + "pytest>=6.0", ] [project.entry-points.pytest11] diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index b30b3a1..7618cfa 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -10,7 +10,7 @@ import attr from filelock import FileLock # type: ignore import mypy.api -import pytest # type: ignore +import pytest PYTEST_MAJOR_VERSION = int(pytest.__version__.partition(".")[0]) @@ -141,12 +141,6 @@ def pytest_collect_file(path, parent): # type: ignore class MypyFile(pytest.File): """A File that Mypy will run on.""" - @classmethod - def from_parent(cls, *args, **kwargs): - """Override from_parent for compatibility.""" - # pytest.File.from_parent did not exist before pytest 5.4. - return getattr(super(), "from_parent", cls)(*args, **kwargs) - def collect(self): """Create a MypyFileItem for the File.""" yield MypyFileItem.from_parent(parent=self, name=nodeid_name) @@ -169,19 +163,6 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.add_marker(self.MARKER) - def collect(self): - """ - Partially work around https://github.com/pytest-dev/pytest/issues/8016 - for pytest < 6.0 with --looponfail. - """ - yield self - - @classmethod - def from_parent(cls, *args, **kwargs): - """Override from_parent for compatibility.""" - # pytest.Item.from_parent did not exist before pytest 5.4. - return getattr(super(), "from_parent", cls)(*args, **kwargs) - def repr_failure(self, excinfo): """ Unwrap mypy errors so we get a clean error message without the @@ -213,7 +194,7 @@ def reportinfo(self): return ( self.fspath, None, - self.config.invocation_dir.bestrelpath(self.fspath), + str(Path(str(self.fspath)).relative_to(self.config.invocation_params.dir)), ) diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index c3f9383..1192600 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -540,30 +540,6 @@ def _break(): child.kill(signal.SIGTERM) -def test_mypy_item_collect(testdir, xdist_args): - """Ensure coverage for a 3.10<=pytest<6.0 workaround.""" - testdir.makepyfile( - """ - def test_mypy_item_collect(request): - plugin = request.config.pluginmanager.getplugin("mypy") - mypy_items = [ - item - for item in request.session.items - if isinstance(item, plugin.MypyItem) - ] - assert mypy_items - for mypy_item in mypy_items: - assert all(item is mypy_item for item in mypy_item.collect()) - """, - ) - result = testdir.runpytest_subprocess("--mypy", *xdist_args) - test_count = 1 - mypy_file_checks = 1 - mypy_status_check = 1 - result.assert_outcomes(passed=test_count + mypy_file_checks + mypy_status_check) - assert result.ret == pytest.ExitCode.OK - - def test_mypy_results_from_mypy_with_opts(): """MypyResults.from_mypy respects passed options.""" mypy_results = pytest_mypy.MypyResults.from_mypy([], opts=["--version"]) diff --git a/tox.ini b/tox.ini index ecb81dd..7782dcd 100644 --- a/tox.ini +++ b/tox.ini @@ -3,9 +3,9 @@ minversion = 4.4 isolated_build = true envlist = - py37-pytest{5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x} - py38-pytest{5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - py39-pytest{5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py37-pytest{6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x} + py38-pytest{6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py39-pytest{6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} @@ -14,9 +14,9 @@ envlist = [gh-actions] python = - 3.7: py37-pytest{5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x} - 3.8: py38-pytest{5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}, publish, static - 3.9: py39-pytest{5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + 3.7: py37-pytest{6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x} + 3.8: py38-pytest{6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}, publish, static + 3.9: py39-pytest{6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} 3.10: py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} 3.11: py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} 3.12: py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} @@ -24,8 +24,6 @@ python = [testenv] constrain_package_deps = true deps = - pytest5.0: pytest ~= 5.0.0 - pytest5.x: pytest ~= 5.0 pytest6.0: pytest ~= 6.0.0 pytest6.2: pytest ~= 6.2.0 pytest6.x: pytest ~= 6.0 From 9e3c6b4769e8ad470a2227699960fae562636053 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 6 Feb 2022 23:13:54 -0800 Subject: [PATCH 33/66] Require Pytest 7+ --- pyproject.toml | 2 +- src/pytest_mypy.py | 13 ------------- tox.ini | 27 ++++++++++++--------------- 3 files changed, 13 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 93908e3..7483076 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "attrs>=19.0", "filelock>=3.0", "mypy>=1.0", - "pytest>=6.0", + "pytest>=7.0", ] [project.entry-points.pytest11] diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 7618cfa..1dbe671 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -13,7 +13,6 @@ import pytest -PYTEST_MAJOR_VERSION = int(pytest.__version__.partition(".")[0]) mypy_argv = [] nodeid_name = "mypy" terminal_summary_title = "mypy" @@ -126,18 +125,6 @@ def pytest_collect_file(file_path, parent): return None -if PYTEST_MAJOR_VERSION < 7: # pragma: no cover - _pytest_collect_file = pytest_collect_file - - def pytest_collect_file(path, parent): # type: ignore - try: - # https://docs.pytest.org/en/7.0.x/deprecations.html#py-path-local-arguments-for-hooks-replaced-with-pathlib-path - return _pytest_collect_file(Path(str(path)), parent) - except TypeError: - # https://docs.pytest.org/en/7.0.x/deprecations.html#fspath-argument-for-node-constructors-replaced-with-pathlib-path - return MypyFile.from_parent(parent=parent, fspath=path) - - class MypyFile(pytest.File): """A File that Mypy will run on.""" diff --git a/tox.ini b/tox.ini index 7782dcd..0a6adcc 100644 --- a/tox.ini +++ b/tox.ini @@ -3,30 +3,27 @@ minversion = 4.4 isolated_build = true envlist = - py37-pytest{6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x} - py38-pytest{6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - py39-pytest{6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py37-pytest{7.0, 7.x}-mypy{1.0, 1.x} + py38-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py39-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} publish static [gh-actions] python = - 3.7: py37-pytest{6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x} - 3.8: py38-pytest{6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}, publish, static - 3.9: py39-pytest{6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - 3.10: py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - 3.11: py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - 3.12: py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + 3.7: py37-pytest{7.0, 7.x}-mypy{1.0, 1.x} + 3.8: py38-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}, publish, static + 3.9: py39-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + 3.10: py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + 3.11: py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + 3.12: py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} [testenv] constrain_package_deps = true deps = - pytest6.0: pytest ~= 6.0.0 - pytest6.2: pytest ~= 6.2.0 - pytest6.x: pytest ~= 6.0 pytest7.0: pytest ~= 7.0.0 pytest7.x: pytest ~= 7.0 pytest8.0: pytest ~= 8.0.0 From 7acfaeb4e838e2a984d1b8f9d2bc5872404c4eae Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 11 Aug 2024 15:00:34 -0700 Subject: [PATCH 34/66] Replace item.fspath with item.path --- src/pytest_mypy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 1dbe671..5487ab9 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -166,7 +166,7 @@ class MypyFileItem(MypyItem): def runtest(self): """Raise an exception if mypy found errors for this item.""" results = MypyResults.from_session(self.session) - abspath = os.path.abspath(str(self.fspath)) + abspath = str(self.path.absolute()) errors = results.abspath_errors.get(abspath) if errors: if not all( @@ -179,9 +179,9 @@ def runtest(self): def reportinfo(self): """Produce a heading for the test report.""" return ( - self.fspath, + self.path, None, - str(Path(str(self.fspath)).relative_to(self.config.invocation_params.dir)), + str(self.path.relative_to(self.config.invocation_params.dir)), ) @@ -271,7 +271,7 @@ def from_session(cls, session) -> "MypyResults": except FileNotFoundError: results = cls.from_mypy( [ - Path(item.fspath) + item.path for item in session.items if isinstance(item, MypyFileItem) ], From 0ac85af3a3c6ac7facaa289349f856757147cdf3 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 11 Aug 2024 15:07:18 -0700 Subject: [PATCH 35/66] Remove os dep --- src/pytest_mypy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 5487ab9..76eb0af 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -1,7 +1,6 @@ """Mypy static type checker plugin for Pytest""" import json -import os from pathlib import Path from tempfile import NamedTemporaryFile from typing import Dict, List, Optional, TextIO @@ -232,8 +231,9 @@ def from_mypy( str(path.absolute()): [] for path in paths } # type: MypyResults._abspath_errors_type + cwd = Path.cwd() stdout, stderr, status = mypy.api.run( - opts + [os.path.relpath(key) for key in abspath_errors.keys()] + opts + [str(Path(key).relative_to(cwd)) for key in abspath_errors.keys()] ) unmatched_lines = [] @@ -241,7 +241,7 @@ def from_mypy( if not line: continue path, _, error = line.partition(":") - abspath = os.path.abspath(path) + abspath = str(Path(path).absolute()) try: abspath_errors[abspath].append(error) except KeyError: @@ -310,4 +310,4 @@ def pytest_terminal_summary(terminalreporter, config): terminalreporter.write_line(results.unmatched_stdout, **color) if results.stderr: terminalreporter.write_line(results.stderr, yellow=True) - os.remove(config._mypy_results_path) + Path(config._mypy_results_path).unlink() From 826bccbddd30a5a71a993d4894b288a8db3acde6 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Mon, 12 Aug 2024 00:54:29 -0700 Subject: [PATCH 36/66] Remove outdated xfails --- src/pytest_mypy.py | 3 +- tests/test_pytest_mypy.py | 67 ++++++--------------------------------- tox.ini | 1 - 3 files changed, 11 insertions(+), 60 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 76eb0af..f9b06f3 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -295,8 +295,7 @@ class MypyWarning(pytest.PytestWarning): def pytest_terminal_summary(terminalreporter, config): """Report stderr and unrecognized lines from stdout.""" if not _is_xdist_controller(config): - # This isn't hit in pytest 5.0 for some reason. - return # pragma: no cover + return try: with open(config._mypy_results_path, mode="r") as results_f: results = MypyResults.load(results_f) diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index 1192600..1b1b7bf 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -4,14 +4,12 @@ import mypy.version from packaging.version import Version -import pexpect import pytest import pytest_mypy MYPY_VERSION = Version(mypy.version.__version__) -PYTEST_VERSION = Version(pytest.__version__) PYTHON_VERSION = Version( ".".join( str(token) @@ -325,14 +323,7 @@ def pytest_configure(config): @pytest.mark.parametrize( "module_name", [ - pytest.param( - "__init__", - marks=pytest.mark.xfail( - Version("3.10") <= PYTEST_VERSION < Version("6.2"), - raises=AssertionError, - reason="https://github.com/pytest-dev/pytest/issues/8016", - ), - ), + "__init__", "good", ], ) @@ -464,14 +455,6 @@ def pyfunc(x: int) -> str: expect_timeout=60.0, ) - num_tests = 2 - if module_name == "__init__" and Version("3.10") <= PYTEST_VERSION < Version("6.2"): - # https://github.com/pytest-dev/pytest/issues/8016 - # Pytest had a bug where it assumed only a Package would have a basename of - # __init__.py. In this test, Pytest mistakes MypyFile for a Package and - # returns after collecting only one object (the MypyFileItem). - num_tests = 1 - def _expect_session(): child.expect("==== test session starts ====") @@ -480,11 +463,9 @@ def _expect_failure(): child.expect("==== FAILURES ====") child.expect(pyfile.basename + " ____") child.expect("2: error: Incompatible return value") - # if num_tests == 2: - # # These only show with mypy>=0.730: - # child.expect("==== mypy ====") - # child.expect("Found 1 error in 1 file (checked 1 source file)") - child.expect(str(num_tests) + " failed") + child.expect("==== mypy ====") + child.expect("Found 1 error in 1 file (checked 1 source file)") + child.expect("2 failed") child.expect("#### LOOPONFAILING ####") _expect_waiting() @@ -503,29 +484,9 @@ def _expect_changed(): def _expect_success(): for _ in range(2): _expect_session() - # if num_tests == 2: - # # These only show with mypy>=0.730: - # child.expect("==== mypy ====") - # child.expect("Success: no issues found in 1 source file") - try: - child.expect(str(num_tests) + " passed") - except pexpect.exceptions.TIMEOUT: - if module_name == "__init__" and ( - Version("6.0") <= PYTEST_VERSION < Version("6.2") - ): - # MypyItems hit the __init__.py bug too when --looponfail - # re-collects them after the failing file is modified. - # Unlike MypyFile, MypyItem is not a Collector, so this used - # to cause an AttributeError until a workaround was added - # (MypyItem.collect was defined to yield itself). - # Mypy probably noticed the __init__.py problem during the - # development of Pytest 6.0, but the error was addressed - # with an isinstance assertion, which broke the workaround. - # Here, we hit that assertion: - child.expect("AssertionError") - child.expect("1 error") - pytest.xfail("https://github.com/pytest-dev/pytest/issues/8016") - raise + child.expect("==== mypy ====") + child.expect("Success: no issues found in 1 source file") + child.expect("2 passed") _expect_waiting() def _break(): @@ -550,20 +511,12 @@ def test_mypy_results_from_mypy_with_opts(): def test_mypy_no_output(testdir, xdist_args): """No terminal summary is shown if there is no output from mypy.""" - type_ignore = ( - "# type: ignore" - if ( - PYTEST_VERSION - < Version("6.0") # Pytest didn't add type annotations until 6.0. - ) - else "" - ) testdir.makepyfile( # Mypy prints a success message to stderr by default: # "Success: no issues found in 1 source file" # Clear stderr and unmatched_stdout to simulate mypy having no output: - conftest=f""" - import pytest {type_ignore} + conftest=""" + import pytest @pytest.hookimpl(hookwrapper=True) def pytest_terminal_summary(config): @@ -578,7 +531,7 @@ def pytest_terminal_summary(config): stdout="", stderr="", status=0, - abspath_errors={{}}, + abspath_errors={}, unmatched_stdout="", ).dump(results_f) yield diff --git a/tox.ini b/tox.ini index 0a6adcc..4e7f50e 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,6 @@ deps = mypy1.x: mypy ~= 1.0 packaging ~= 21.3 - pexpect ~= 4.8.0 pytest-cov ~= 4.1.0 pytest-randomly ~= 3.4 pytest-xdist ~= 1.34 From 90e1f321c9971fa0750f567a8314257e1c03c2ef Mon Sep 17 00:00:00 2001 From: David Tucker Date: Tue, 9 May 2023 01:00:52 -0700 Subject: [PATCH 37/66] Use config.stash to store the results path --- src/pytest_mypy.py | 28 ++++++++++++++++------------ tests/test_pytest_mypy.py | 8 +++++--- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index f9b06f3..e3fb2f8 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -14,6 +14,9 @@ mypy_argv = [] nodeid_name = "mypy" +stash_keys = { + "mypy_results_path": pytest.StashKey[Path](), +} terminal_summary_title = "mypy" @@ -80,7 +83,7 @@ def pytest_configure(config): # Subsequent MypyItems will see the file exists, # and they will read the parsed results. with NamedTemporaryFile(delete=True) as tmp_f: - config._mypy_results_path = tmp_f.name + config.stash[stash_keys["mypy_results_path"]] = Path(tmp_f.name) # If xdist is enabled, then the results path should be exposed to # the workers so that they know where to read parsed results from. @@ -88,10 +91,10 @@ def pytest_configure(config): class _MypyXdistPlugin: def pytest_configure_node(self, node): # xdist hook - """Pass config._mypy_results_path to workers.""" - _get_xdist_workerinput(node)[ - "_mypy_results_path" - ] = node.config._mypy_results_path + """Pass the mypy results path to workers.""" + _get_xdist_workerinput(node)["_mypy_results_path"] = str( + node.config.stash[stash_keys["mypy_results_path"]] + ) config.pluginmanager.register(_MypyXdistPlugin()) @@ -259,14 +262,14 @@ def from_mypy( @classmethod def from_session(cls, session) -> "MypyResults": """Load (or generate) cached mypy results for a pytest session.""" - results_path = ( - session.config._mypy_results_path + mypy_results_path = Path( + session.config.stash[stash_keys["mypy_results_path"]] if _is_xdist_controller(session.config) else _get_xdist_workerinput(session.config)["_mypy_results_path"] ) - with FileLock(results_path + ".lock"): + with FileLock(str(mypy_results_path) + ".lock"): try: - with open(results_path, mode="r") as results_f: + with open(mypy_results_path, mode="r") as results_f: results = cls.load(results_f) except FileNotFoundError: results = cls.from_mypy( @@ -276,7 +279,7 @@ def from_session(cls, session) -> "MypyResults": if isinstance(item, MypyFileItem) ], ) - with open(results_path, mode="w") as results_f: + with open(mypy_results_path, mode="w") as results_f: results.dump(results_f) return results @@ -296,8 +299,9 @@ def pytest_terminal_summary(terminalreporter, config): """Report stderr and unrecognized lines from stdout.""" if not _is_xdist_controller(config): return + mypy_results_path = config.stash[stash_keys["mypy_results_path"]] try: - with open(config._mypy_results_path, mode="r") as results_f: + with open(mypy_results_path, mode="r") as results_f: results = MypyResults.load(results_f) except FileNotFoundError: # No MypyItems executed. @@ -309,4 +313,4 @@ def pytest_terminal_summary(terminalreporter, config): terminalreporter.write_line(results.unmatched_stdout, **color) if results.stderr: terminalreporter.write_line(results.stderr, yellow=True) - Path(config._mypy_results_path).unlink() + mypy_results_path.unlink() diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index 1b1b7bf..e782561 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -520,11 +520,13 @@ def test_mypy_no_output(testdir, xdist_args): @pytest.hookimpl(hookwrapper=True) def pytest_terminal_summary(config): - mypy_results_path = getattr(config, "_mypy_results_path", None) - if not mypy_results_path: + pytest_mypy = config.pluginmanager.getplugin("mypy") + stash_key = pytest_mypy.stash_keys["mypy_results_path"] + try: + mypy_results_path = config.stash[stash_key] + except KeyError: # xdist worker return - pytest_mypy = config.pluginmanager.getplugin("mypy") with open(mypy_results_path, mode="w") as results_f: pytest_mypy.MypyResults( opts=[], From 63102ec8b897e15b0c07f40e74e753c81f7cc593 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 18 Aug 2024 13:51:14 -0700 Subject: [PATCH 38/66] Replace attrs with dataclasses --- pyproject.toml | 1 - src/pytest_mypy.py | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7483076..78965fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,6 @@ classifiers = [ ] requires-python = ">=3.7" dependencies = [ - "attrs>=19.0", "filelock>=3.0", "mypy>=1.0", "pytest>=7.0", diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index e3fb2f8..ac363b1 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -1,12 +1,12 @@ """Mypy static type checker plugin for Pytest""" +from dataclasses import dataclass import json from pathlib import Path from tempfile import NamedTemporaryFile from typing import Dict, List, Optional, TextIO import warnings -import attr from filelock import FileLock # type: ignore import mypy.api import pytest @@ -197,18 +197,18 @@ def runtest(self): raise MypyError(f"mypy exited with status {results.status}.") -@attr.s(frozen=True, kw_only=True) +@dataclass(frozen=True) # compat python < 3.10 (kw_only=True) class MypyResults: """Parsed results from Mypy.""" _abspath_errors_type = Dict[str, List[str]] - opts = attr.ib(type=List[str]) - stdout = attr.ib(type=str) - stderr = attr.ib(type=str) - status = attr.ib(type=int) - abspath_errors = attr.ib(type=_abspath_errors_type) - unmatched_stdout = attr.ib(type=str) + opts: List[str] + stdout: str + stderr: str + status: int + abspath_errors: _abspath_errors_type + unmatched_stdout: str def dump(self, results_f: TextIO) -> None: """Cache results in a format that can be parsed by load().""" From 9d03d95e82d2a10154b37ba9d10508e9cf1e3b96 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 18 Aug 2024 11:12:32 -0700 Subject: [PATCH 39/66] Create MypyConfigStash --- src/pytest_mypy.py | 40 ++++++++++++++++++++++++++++----------- tests/test_pytest_mypy.py | 5 ++--- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index ac363b1..f9249f6 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -12,10 +12,24 @@ import pytest +@dataclass(frozen=True) # compat python < 3.10 (kw_only=True) +class MypyConfigStash: + """Plugin data stored in the pytest.Config stash.""" + + mypy_results_path: Path + + @classmethod + def from_serialized(cls, serialized): + return cls(mypy_results_path=Path(serialized)) + + def serialized(self): + return str(self.mypy_results_path) + + mypy_argv = [] nodeid_name = "mypy" -stash_keys = { - "mypy_results_path": pytest.StashKey[Path](), +stash_key = { + "config": pytest.StashKey[MypyConfigStash](), } terminal_summary_title = "mypy" @@ -83,7 +97,9 @@ def pytest_configure(config): # Subsequent MypyItems will see the file exists, # and they will read the parsed results. with NamedTemporaryFile(delete=True) as tmp_f: - config.stash[stash_keys["mypy_results_path"]] = Path(tmp_f.name) + config.stash[stash_key["config"]] = MypyConfigStash( + mypy_results_path=Path(tmp_f.name), + ) # If xdist is enabled, then the results path should be exposed to # the workers so that they know where to read parsed results from. @@ -92,8 +108,8 @@ def pytest_configure(config): class _MypyXdistPlugin: def pytest_configure_node(self, node): # xdist hook """Pass the mypy results path to workers.""" - _get_xdist_workerinput(node)["_mypy_results_path"] = str( - node.config.stash[stash_keys["mypy_results_path"]] + _get_xdist_workerinput(node)["mypy_config_stash_serialized"] = ( + node.config.stash[stash_key["config"]].serialized() ) config.pluginmanager.register(_MypyXdistPlugin()) @@ -262,11 +278,13 @@ def from_mypy( @classmethod def from_session(cls, session) -> "MypyResults": """Load (or generate) cached mypy results for a pytest session.""" - mypy_results_path = Path( - session.config.stash[stash_keys["mypy_results_path"]] - if _is_xdist_controller(session.config) - else _get_xdist_workerinput(session.config)["_mypy_results_path"] - ) + if _is_xdist_controller(session.config): + mypy_config_stash = session.config.stash[stash_key["config"]] + else: + mypy_config_stash = MypyConfigStash.from_serialized( + _get_xdist_workerinput(session.config)["mypy_config_stash_serialized"] + ) + mypy_results_path = mypy_config_stash.mypy_results_path with FileLock(str(mypy_results_path) + ".lock"): try: with open(mypy_results_path, mode="r") as results_f: @@ -299,7 +317,7 @@ def pytest_terminal_summary(terminalreporter, config): """Report stderr and unrecognized lines from stdout.""" if not _is_xdist_controller(config): return - mypy_results_path = config.stash[stash_keys["mypy_results_path"]] + mypy_results_path = config.stash[stash_key["config"]].mypy_results_path try: with open(mypy_results_path, mode="r") as results_f: results = MypyResults.load(results_f) diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index e782561..2454e9b 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -521,13 +521,12 @@ def test_mypy_no_output(testdir, xdist_args): @pytest.hookimpl(hookwrapper=True) def pytest_terminal_summary(config): pytest_mypy = config.pluginmanager.getplugin("mypy") - stash_key = pytest_mypy.stash_keys["mypy_results_path"] try: - mypy_results_path = config.stash[stash_key] + mypy_config_stash = config.stash[pytest_mypy.stash_key["config"]] except KeyError: # xdist worker return - with open(mypy_results_path, mode="w") as results_f: + with open(mypy_config_stash.mypy_results_path, mode="w") as results_f: pytest_mypy.MypyResults( opts=[], stdout="", From 8008632564c49df40baf668c8b3c43f2f393daeb Mon Sep 17 00:00:00 2001 From: David Tucker Date: Mon, 19 Aug 2024 01:30:06 -0700 Subject: [PATCH 40/66] Create MypyReportingPlugin --- src/pytest_mypy.py | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index f9249f6..5572aaf 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -90,6 +90,7 @@ def pytest_configure(config): and configure the plugin based on the CLI. """ if _is_xdist_controller(config): + config.pluginmanager.register(MypyReportingPlugin()) # Get the path to a temporary file and delete it. # The first MypyItem to run will see the file does not exist, @@ -313,22 +314,23 @@ class MypyWarning(pytest.PytestWarning): """A non-failure message regarding the mypy run.""" -def pytest_terminal_summary(terminalreporter, config): - """Report stderr and unrecognized lines from stdout.""" - if not _is_xdist_controller(config): - return - mypy_results_path = config.stash[stash_key["config"]].mypy_results_path - try: - with open(mypy_results_path, mode="r") as results_f: - results = MypyResults.load(results_f) - except FileNotFoundError: - # No MypyItems executed. - return - if results.unmatched_stdout or results.stderr: - terminalreporter.section(terminal_summary_title) - if results.unmatched_stdout: - color = {"red": True} if results.status else {"green": True} - terminalreporter.write_line(results.unmatched_stdout, **color) - if results.stderr: - terminalreporter.write_line(results.stderr, yellow=True) - mypy_results_path.unlink() +class MypyReportingPlugin: + """A Pytest plugin that reports mypy results.""" + + def pytest_terminal_summary(self, terminalreporter, config): + """Report stderr and unrecognized lines from stdout.""" + mypy_results_path = config.stash[stash_key["config"]].mypy_results_path + try: + with open(mypy_results_path, mode="r") as results_f: + results = MypyResults.load(results_f) + except FileNotFoundError: + # No MypyItems executed. + return + if results.unmatched_stdout or results.stderr: + terminalreporter.section(terminal_summary_title) + if results.unmatched_stdout: + color = {"red": True} if results.status else {"green": True} + terminalreporter.write_line(results.unmatched_stdout, **color) + if results.stderr: + terminalreporter.write_line(results.stderr, yellow=True) + mypy_results_path.unlink() From 6264093c5be4e2675611e8cc6a9599a238ed964a Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sat, 24 Aug 2024 14:24:38 -0700 Subject: [PATCH 41/66] Create MypyXdistControllerPlugin --- src/pytest_mypy.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 5572aaf..9dd0592 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -83,6 +83,16 @@ def _is_xdist_controller(config): return _get_xdist_workerinput(config) is None +class MypyXdistControllerPlugin: + """A plugin that is only registered on xdist controller processes.""" + + def pytest_configure_node(self, node): + """Pass the config stash to workers.""" + _get_xdist_workerinput(node)["mypy_config_stash_serialized"] = ( + node.config.stash[stash_key["config"]].serialized() + ) + + def pytest_configure(config): """ Initialize the path used to cache mypy results, @@ -105,15 +115,7 @@ def pytest_configure(config): # If xdist is enabled, then the results path should be exposed to # the workers so that they know where to read parsed results from. if config.pluginmanager.getplugin("xdist"): - - class _MypyXdistPlugin: - def pytest_configure_node(self, node): # xdist hook - """Pass the mypy results path to workers.""" - _get_xdist_workerinput(node)["mypy_config_stash_serialized"] = ( - node.config.stash[stash_key["config"]].serialized() - ) - - config.pluginmanager.register(_MypyXdistPlugin()) + config.pluginmanager.register(MypyXdistControllerPlugin()) config.addinivalue_line( "markers", From b577d9dc525366ba1ad0e9602a382a4457f897f8 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Thu, 15 Aug 2024 01:43:18 -0700 Subject: [PATCH 42/66] Refactor xdist integration --- src/pytest_mypy.py | 43 +++++++++++++++++-------------------------- tox.ini | 30 +++++++++++++++++------------- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 9dd0592..47671d9 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -59,28 +59,18 @@ def pytest_addoption(parser): ) -XDIST_WORKERINPUT_ATTRIBUTE_NAMES = ( - "workerinput", - # xdist < 2.0.0: - "slaveinput", -) +def _xdist_worker(config): + try: + return {"input": _xdist_workerinput(config)} + except AttributeError: + return {} -def _get_xdist_workerinput(config_node): - workerinput = None - for attr_name in XDIST_WORKERINPUT_ATTRIBUTE_NAMES: - workerinput = getattr(config_node, attr_name, None) - if workerinput is not None: - break - return workerinput - - -def _is_xdist_controller(config): - """ - True if the code running the given pytest.config object is running in - an xdist controller node or not running xdist at all. - """ - return _get_xdist_workerinput(config) is None +def _xdist_workerinput(node): + try: + return node.workerinput + except AttributeError: # compat xdist < 2.0 + return node.slaveinput class MypyXdistControllerPlugin: @@ -88,9 +78,9 @@ class MypyXdistControllerPlugin: def pytest_configure_node(self, node): """Pass the config stash to workers.""" - _get_xdist_workerinput(node)["mypy_config_stash_serialized"] = ( - node.config.stash[stash_key["config"]].serialized() - ) + _xdist_workerinput(node)["mypy_config_stash_serialized"] = node.config.stash[ + stash_key["config"] + ].serialized() def pytest_configure(config): @@ -99,7 +89,7 @@ def pytest_configure(config): register a custom marker for MypyItems, and configure the plugin based on the CLI. """ - if _is_xdist_controller(config): + if not _xdist_worker(config): config.pluginmanager.register(MypyReportingPlugin()) # Get the path to a temporary file and delete it. @@ -281,11 +271,12 @@ def from_mypy( @classmethod def from_session(cls, session) -> "MypyResults": """Load (or generate) cached mypy results for a pytest session.""" - if _is_xdist_controller(session.config): + xdist_worker = _xdist_worker(session.config) + if not xdist_worker: mypy_config_stash = session.config.stash[stash_key["config"]] else: mypy_config_stash = MypyConfigStash.from_serialized( - _get_xdist_workerinput(session.config)["mypy_config_stash_serialized"] + xdist_worker["input"]["mypy_config_stash_serialized"] ) mypy_results_path = mypy_config_stash.mypy_results_path with FileLock(str(mypy_results_path) + ".lock"): diff --git a/tox.ini b/tox.ini index 4e7f50e..c208249 100644 --- a/tox.ini +++ b/tox.ini @@ -3,23 +3,23 @@ minversion = 4.4 isolated_build = true envlist = - py37-pytest{7.0, 7.x}-mypy{1.0, 1.x} - py38-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - py39-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + py37-pytest{7.0, 7.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} + py38-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} + py39-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} + py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} + py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} + py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} publish static [gh-actions] python = - 3.7: py37-pytest{7.0, 7.x}-mypy{1.0, 1.x} - 3.8: py38-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}, publish, static - 3.9: py39-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - 3.10: py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - 3.11: py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} - 3.12: py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x} + 3.7: py37-pytest{7.0, 7.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} + 3.8: py38-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x}, publish, static + 3.9: py39-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} + 3.10: py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} + 3.11: py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} + 3.12: py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} [testenv] constrain_package_deps = true @@ -30,11 +30,15 @@ deps = pytest8.x: pytest ~= 8.0 mypy1.0: mypy ~= 1.0.0 mypy1.x: mypy ~= 1.0 + xdist1.x: pytest-xdist ~= 1.0 + xdist2.0: pytest-xdist ~= 2.0.0 + xdist2.x: pytest-xdist ~= 2.0 + xdist3.0: pytest-xdist ~= 3.0.0 + xdist3.x: pytest-xdist ~= 3.0 packaging ~= 21.3 pytest-cov ~= 4.1.0 pytest-randomly ~= 3.4 - pytest-xdist ~= 1.34 commands = pytest -p no:mypy {posargs:--cov pytest_mypy --cov-branch --cov-fail-under 100 --cov-report term-missing -n auto} From 8a5fedcc2e05dcac346216014fc926d6e82d2c85 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sat, 24 Aug 2024 14:41:45 -0700 Subject: [PATCH 43/66] Populate the config stash on xdist workers --- src/pytest_mypy.py | 17 ++++++++--------- tests/test_pytest_mypy.py | 11 +++-------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 47671d9..98cff39 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -89,7 +89,8 @@ def pytest_configure(config): register a custom marker for MypyItems, and configure the plugin based on the CLI. """ - if not _xdist_worker(config): + xdist_worker = _xdist_worker(config) + if not xdist_worker: config.pluginmanager.register(MypyReportingPlugin()) # Get the path to a temporary file and delete it. @@ -106,6 +107,11 @@ def pytest_configure(config): # the workers so that they know where to read parsed results from. if config.pluginmanager.getplugin("xdist"): config.pluginmanager.register(MypyXdistControllerPlugin()) + else: + # xdist workers create the stash using input from the controller plugin. + config.stash[stash_key["config"]] = MypyConfigStash.from_serialized( + xdist_worker["input"]["mypy_config_stash_serialized"] + ) config.addinivalue_line( "markers", @@ -271,14 +277,7 @@ def from_mypy( @classmethod def from_session(cls, session) -> "MypyResults": """Load (or generate) cached mypy results for a pytest session.""" - xdist_worker = _xdist_worker(session.config) - if not xdist_worker: - mypy_config_stash = session.config.stash[stash_key["config"]] - else: - mypy_config_stash = MypyConfigStash.from_serialized( - xdist_worker["input"]["mypy_config_stash_serialized"] - ) - mypy_results_path = mypy_config_stash.mypy_results_path + mypy_results_path = session.config.stash[stash_key["config"]].mypy_results_path with FileLock(str(mypy_results_path) + ".lock"): try: with open(mypy_results_path, mode="r") as results_f: diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index 2454e9b..d933a9e 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -518,14 +518,10 @@ def test_mypy_no_output(testdir, xdist_args): conftest=""" import pytest - @pytest.hookimpl(hookwrapper=True) - def pytest_terminal_summary(config): + @pytest.hookimpl(trylast=True) + def pytest_configure(config): pytest_mypy = config.pluginmanager.getplugin("mypy") - try: - mypy_config_stash = config.stash[pytest_mypy.stash_key["config"]] - except KeyError: - # xdist worker - return + mypy_config_stash = config.stash[pytest_mypy.stash_key["config"]] with open(mypy_config_stash.mypy_results_path, mode="w") as results_f: pytest_mypy.MypyResults( opts=[], @@ -535,7 +531,6 @@ def pytest_terminal_summary(config): abspath_errors={}, unmatched_stdout="", ).dump(results_f) - yield """, ) result = testdir.runpytest_subprocess("--mypy", *xdist_args) From 9f8f03ce081275e2f94797c1bb79507b57f34e91 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sat, 10 Aug 2024 23:27:35 -0700 Subject: [PATCH 44/66] Add strict type-checking --- src/pytest_mypy.py | 94 +++++++++++++++++++++++++++++++++------------- tox.ini | 10 +++-- 2 files changed, 73 insertions(+), 31 deletions(-) diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index 98cff39..87f597c 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -1,16 +1,39 @@ """Mypy static type checker plugin for Pytest""" +from __future__ import annotations + from dataclasses import dataclass import json from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Dict, List, Optional, TextIO +import typing import warnings -from filelock import FileLock # type: ignore +from filelock import FileLock import mypy.api import pytest +if typing.TYPE_CHECKING: # pragma: no cover + from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + TextIO, + Tuple, + Union, + ) + + # https://github.com/pytest-dev/pytest/issues/7469 + from _pytest._code.code import TerminalRepr + + # https://github.com/pytest-dev/pytest/pull/12661 + from _pytest.terminal import TerminalReporter + + # https://github.com/pytest-dev/pytest-xdist/issues/1121 + from xdist.workermanage import WorkerController # type: ignore + @dataclass(frozen=True) # compat python < 3.10 (kw_only=True) class MypyConfigStash: @@ -19,14 +42,14 @@ class MypyConfigStash: mypy_results_path: Path @classmethod - def from_serialized(cls, serialized): + def from_serialized(cls, serialized: str) -> MypyConfigStash: return cls(mypy_results_path=Path(serialized)) - def serialized(self): + def serialized(self) -> str: return str(self.mypy_results_path) -mypy_argv = [] +mypy_argv: List[str] = [] nodeid_name = "mypy" stash_key = { "config": pytest.StashKey[MypyConfigStash](), @@ -34,7 +57,11 @@ def serialized(self): terminal_summary_title = "mypy" -def default_file_error_formatter(item, results, errors): +def default_file_error_formatter( + item: MypyItem, + results: MypyResults, + errors: List[str], +) -> str: """Create a string to be displayed when mypy finds errors in a file.""" return "\n".join(errors) @@ -42,7 +69,7 @@ def default_file_error_formatter(item, results, errors): file_error_formatter = default_file_error_formatter -def pytest_addoption(parser): +def pytest_addoption(parser: pytest.Parser) -> None: """Add options for enabling and running mypy.""" group = parser.getgroup("mypy") group.addoption("--mypy", action="store_true", help="run mypy on .py files") @@ -59,31 +86,33 @@ def pytest_addoption(parser): ) -def _xdist_worker(config): +def _xdist_worker(config: pytest.Config) -> Dict[str, Any]: try: return {"input": _xdist_workerinput(config)} except AttributeError: return {} -def _xdist_workerinput(node): +def _xdist_workerinput(node: Union[WorkerController, pytest.Config]) -> Any: try: - return node.workerinput + # mypy complains that pytest.Config does not have this attribute, + # but xdist.remote defines it in worker processes. + return node.workerinput # type: ignore[union-attr] except AttributeError: # compat xdist < 2.0 - return node.slaveinput + return node.slaveinput # type: ignore[union-attr] class MypyXdistControllerPlugin: """A plugin that is only registered on xdist controller processes.""" - def pytest_configure_node(self, node): + def pytest_configure_node(self, node: WorkerController) -> None: """Pass the config stash to workers.""" _xdist_workerinput(node)["mypy_config_stash_serialized"] = node.config.stash[ stash_key["config"] ].serialized() -def pytest_configure(config): +def pytest_configure(config: pytest.Config) -> None: """ Initialize the path used to cache mypy results, register a custom marker for MypyItems, @@ -125,7 +154,10 @@ def pytest_configure(config): mypy_argv.append(f"--config-file={mypy_config_file}") -def pytest_collect_file(file_path, parent): +def pytest_collect_file( + file_path: Path, + parent: pytest.Collector, +) -> Optional[MypyFile]: """Create a MypyFileItem for every file mypy should run on.""" if file_path.suffix in {".py", ".pyi"} and any( [ @@ -145,7 +177,7 @@ def pytest_collect_file(file_path, parent): class MypyFile(pytest.File): """A File that Mypy will run on.""" - def collect(self): + def collect(self) -> Iterator[MypyItem]: """Create a MypyFileItem for the File.""" yield MypyFileItem.from_parent(parent=self, name=nodeid_name) # Since mypy might check files that were not collected, @@ -163,24 +195,28 @@ class MypyItem(pytest.Item): MARKER = "mypy" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.add_marker(self.MARKER) - def repr_failure(self, excinfo): + def repr_failure( + self, + excinfo: pytest.ExceptionInfo[BaseException], + style: Optional[str] = None, + ) -> Union[str, TerminalRepr]: """ Unwrap mypy errors so we get a clean error message without the full exception repr. """ if excinfo.errisinstance(MypyError): - return excinfo.value.args[0] + return str(excinfo.value.args[0]) return super().repr_failure(excinfo) class MypyFileItem(MypyItem): """A check for Mypy errors in a File.""" - def runtest(self): + def runtest(self) -> None: """Raise an exception if mypy found errors for this item.""" results = MypyResults.from_session(self.session) abspath = str(self.path.absolute()) @@ -193,10 +229,10 @@ def runtest(self): raise MypyError(file_error_formatter(self, results, errors)) warnings.warn("\n" + "\n".join(errors), MypyWarning) - def reportinfo(self): + def reportinfo(self) -> Tuple[str, None, str]: """Produce a heading for the test report.""" return ( - self.path, + str(self.path), None, str(self.path.relative_to(self.config.invocation_params.dir)), ) @@ -205,7 +241,7 @@ def reportinfo(self): class MypyStatusItem(MypyItem): """A check for a non-zero mypy exit status.""" - def runtest(self): + def runtest(self) -> None: """Raise a MypyError if mypy exited with a non-zero status.""" results = MypyResults.from_session(self.session) if results.status: @@ -216,7 +252,7 @@ def runtest(self): class MypyResults: """Parsed results from Mypy.""" - _abspath_errors_type = Dict[str, List[str]] + _abspath_errors_type = typing.Dict[str, typing.List[str]] opts: List[str] stdout: str @@ -230,7 +266,7 @@ def dump(self, results_f: TextIO) -> None: return json.dump(vars(self), results_f) @classmethod - def load(cls, results_f: TextIO) -> "MypyResults": + def load(cls, results_f: TextIO) -> MypyResults: """Get results cached by dump().""" return cls(**json.load(results_f)) @@ -240,7 +276,7 @@ def from_mypy( paths: List[Path], *, opts: Optional[List[str]] = None, - ) -> "MypyResults": + ) -> MypyResults: """Generate results from mypy.""" if opts is None: @@ -275,7 +311,7 @@ def from_mypy( ) @classmethod - def from_session(cls, session) -> "MypyResults": + def from_session(cls, session: pytest.Session) -> MypyResults: """Load (or generate) cached mypy results for a pytest session.""" mypy_results_path = session.config.stash[stash_key["config"]].mypy_results_path with FileLock(str(mypy_results_path) + ".lock"): @@ -309,7 +345,11 @@ class MypyWarning(pytest.PytestWarning): class MypyReportingPlugin: """A Pytest plugin that reports mypy results.""" - def pytest_terminal_summary(self, terminalreporter, config): + def pytest_terminal_summary( + self, + terminalreporter: TerminalReporter, + config: pytest.Config, + ) -> None: """Report stderr and unrecognized lines from stdout.""" mypy_results_path = config.stash[stash_key["config"]].mypy_results_path try: diff --git a/tox.ini b/tox.ini index c208249..d80f6f6 100644 --- a/tox.ini +++ b/tox.ini @@ -15,11 +15,11 @@ envlist = [gh-actions] python = 3.7: py37-pytest{7.0, 7.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} - 3.8: py38-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x}, publish, static + 3.8: py38-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} 3.9: py39-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} 3.10: py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} 3.11: py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} - 3.12: py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} + 3.12: py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x}, publish, static [testenv] constrain_package_deps = true @@ -56,15 +56,17 @@ commands = twine {posargs:check} {envtmpdir}/* [testenv:static] +basepython = py312 # pytest.Node.from_parent uses typing.Self deps = bandit ~= 1.7.0 black ~= 24.2.0 flake8 ~= 7.0.0 - mypy ~= 1.8.0 + mypy ~= 1.11.0 + pytest-xdist >= 3.6.0 # needed for type-checking commands = black --check src tests flake8 src tests - mypy src + mypy --strict src bandit --recursive src [flake8] From f81c9eb6d18297d0540570d5b44f1698d26a40f0 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Fri, 16 Aug 2024 20:25:03 -0700 Subject: [PATCH 45/66] Make pytest_mypy a (typed) package --- src/{pytest_mypy.py => pytest_mypy/__init__.py} | 0 src/pytest_mypy/py.typed | 0 tests/test_pytest_mypy.py | 8 ++++++++ 3 files changed, 8 insertions(+) rename src/{pytest_mypy.py => pytest_mypy/__init__.py} (100%) create mode 100644 src/pytest_mypy/py.typed diff --git a/src/pytest_mypy.py b/src/pytest_mypy/__init__.py similarity index 100% rename from src/pytest_mypy.py rename to src/pytest_mypy/__init__.py diff --git a/src/pytest_mypy/py.typed b/src/pytest_mypy/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index d933a9e..d3d8948 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -540,3 +540,11 @@ def pytest_configure(config): result.assert_outcomes(passed=mypy_checks) assert result.ret == pytest.ExitCode.OK assert f"= {pytest_mypy.terminal_summary_title} =" not in str(result.stdout) + + +def test_py_typed(testdir): + """Mypy recognizes that pytest_mypy is typed.""" + name = "typed" + testdir.makepyfile(**{name: "import pytest_mypy"}) + result = testdir.run("mypy", f"{name}.py") + assert result.ret == 0 From 44826df2bffc5492fee261c66f29e867f4e1acdf Mon Sep 17 00:00:00 2001 From: David Tucker Date: Thu, 1 Feb 2024 23:00:48 -0800 Subject: [PATCH 46/66] Run static before publish in tox.ini --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index d80f6f6..5d18aaf 100644 --- a/tox.ini +++ b/tox.ini @@ -9,8 +9,8 @@ envlist = py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} - publish static + publish [gh-actions] python = @@ -19,7 +19,7 @@ python = 3.9: py39-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} 3.10: py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} 3.11: py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} - 3.12: py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x}, publish, static + 3.12: py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x}, static, publish [testenv] constrain_package_deps = true From fdc1116d76a1699feb68a106f13e3de9a89ec694 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Mon, 26 Aug 2024 21:31:29 -0700 Subject: [PATCH 47/66] Prevent coverage files from colliding in parallel --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5d18aaf..6a5f7c9 100644 --- a/tox.ini +++ b/tox.ini @@ -39,7 +39,8 @@ deps = packaging ~= 21.3 pytest-cov ~= 4.1.0 pytest-randomly ~= 3.4 - +setenv = + COVERAGE_FILE = .coverage.{envname} commands = pytest -p no:mypy {posargs:--cov pytest_mypy --cov-branch --cov-fail-under 100 --cov-report term-missing -n auto} [pytest] From 69aeb48145de1dffa7492e4c9a80d3ed3d098a74 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Wed, 28 Aug 2024 08:49:26 -0700 Subject: [PATCH 48/66] Add --mypy-no-status-check --- src/pytest_mypy/__init__.py | 10 +++++++++- tests/test_pytest_mypy.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index 87f597c..40d8c3d 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -84,6 +84,11 @@ def pytest_addoption(parser: pytest.Parser) -> None: type=str, help="adds custom mypy config file", ) + group.addoption( + "--mypy-no-status-check", + action="store_true", + help="ignore mypy's exit status", + ) def _xdist_worker(config: pytest.Config) -> Dict[str, Any]: @@ -164,6 +169,7 @@ def pytest_collect_file( parent.config.option.mypy, parent.config.option.mypy_config_file, parent.config.option.mypy_ignore_missing_imports, + parent.config.option.mypy_no_status_check, ], ): # Do not create MypyFile instance for a .py file if a @@ -183,7 +189,9 @@ def collect(self) -> Iterator[MypyItem]: # Since mypy might check files that were not collected, # pytest could pass even though mypy failed! # To prevent that, add an explicit check for the mypy exit status. - if not any(isinstance(item, MypyStatusItem) for item in self.session.items): + if not self.session.config.option.mypy_no_status_check and not any( + isinstance(item, MypyStatusItem) for item in self.session.items + ): yield MypyStatusItem.from_parent( parent=self, name=nodeid_name + "-status", diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index d3d8948..3143521 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -548,3 +548,16 @@ def test_py_typed(testdir): testdir.makepyfile(**{name: "import pytest_mypy"}) result = testdir.run("mypy", f"{name}.py") assert result.ret == 0 + + +def test_mypy_no_status_check(testdir, xdist_args): + """Verify that --mypy-no-status-check disables MypyStatusItem collection.""" + testdir.makepyfile(thon="one: int = 1") + result = testdir.runpytest_subprocess("--mypy", *xdist_args) + mypy_file_checks = 1 + mypy_status_check = 1 + result.assert_outcomes(passed=mypy_file_checks + mypy_status_check) + assert result.ret == pytest.ExitCode.OK + result = testdir.runpytest_subprocess("--mypy-no-status-check", *xdist_args) + result.assert_outcomes(passed=mypy_file_checks) + assert result.ret == pytest.ExitCode.OK From c7225b51af98b7d8a9e35e3a5ef283e2fed8460a Mon Sep 17 00:00:00 2001 From: David Tucker Date: Thu, 29 Aug 2024 08:53:42 -0700 Subject: [PATCH 49/66] Add --mypy-xfail --- src/pytest_mypy/__init__.py | 28 +++++++++++++++++-- tests/test_pytest_mypy.py | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index 40d8c3d..73bd12f 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -89,6 +89,11 @@ def pytest_addoption(parser: pytest.Parser) -> None: action="store_true", help="ignore mypy's exit status", ) + group.addoption( + "--mypy-xfail", + action="store_true", + help="xfail mypy errors", + ) def _xdist_worker(config: pytest.Config) -> Dict[str, Any]: @@ -170,6 +175,7 @@ def pytest_collect_file( parent.config.option.mypy_config_file, parent.config.option.mypy_ignore_missing_imports, parent.config.option.mypy_no_status_check, + parent.config.option.mypy_xfail, ], ): # Do not create MypyFile instance for a .py file if a @@ -234,6 +240,13 @@ def runtest(self) -> None: error.partition(":")[2].partition(":")[0].strip() == "note" for error in errors ): + if self.session.config.option.mypy_xfail: + self.add_marker( + pytest.mark.xfail( + raises=MypyError, + reason="mypy errors are expected by --mypy-xfail.", + ) + ) raise MypyError(file_error_formatter(self, results, errors)) warnings.warn("\n" + "\n".join(errors), MypyWarning) @@ -253,6 +266,15 @@ def runtest(self) -> None: """Raise a MypyError if mypy exited with a non-zero status.""" results = MypyResults.from_session(self.session) if results.status: + if self.session.config.option.mypy_xfail: + self.add_marker( + pytest.mark.xfail( + raises=MypyError, + reason=( + "A non-zero mypy exit status is expected by --mypy-xfail." + ), + ) + ) raise MypyError(f"mypy exited with status {results.status}.") @@ -366,9 +388,11 @@ def pytest_terminal_summary( except FileNotFoundError: # No MypyItems executed. return - if results.unmatched_stdout or results.stderr: + if config.option.mypy_xfail or results.unmatched_stdout or results.stderr: terminalreporter.section(terminal_summary_title) - if results.unmatched_stdout: + if config.option.mypy_xfail: + terminalreporter.write(results.stdout) + elif results.unmatched_stdout: color = {"red": True} if results.status else {"green": True} terminalreporter.write_line(results.unmatched_stdout, **color) if results.stderr: diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index 3143521..6ee40a0 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -561,3 +561,59 @@ def test_mypy_no_status_check(testdir, xdist_args): result = testdir.runpytest_subprocess("--mypy-no-status-check", *xdist_args) result.assert_outcomes(passed=mypy_file_checks) assert result.ret == pytest.ExitCode.OK + + +def test_mypy_xfail_passes(testdir, xdist_args): + """Verify that --mypy-xfail passes passes.""" + testdir.makepyfile(thon="one: int = 1") + result = testdir.runpytest_subprocess("--mypy", *xdist_args) + mypy_file_checks = 1 + mypy_status_check = 1 + result.assert_outcomes(passed=mypy_file_checks + mypy_status_check) + assert result.ret == pytest.ExitCode.OK + result = testdir.runpytest_subprocess("--mypy-xfail", *xdist_args) + result.assert_outcomes(passed=mypy_file_checks + mypy_status_check) + assert result.ret == pytest.ExitCode.OK + + +def test_mypy_xfail_xfails(testdir, xdist_args): + """Verify that --mypy-xfail xfails failures.""" + testdir.makepyfile(thon="one: str = 1") + result = testdir.runpytest_subprocess("--mypy", *xdist_args) + mypy_file_checks = 1 + mypy_status_check = 1 + result.assert_outcomes(failed=mypy_file_checks + mypy_status_check) + assert result.ret == pytest.ExitCode.TESTS_FAILED + result = testdir.runpytest_subprocess("--mypy-xfail", *xdist_args) + result.assert_outcomes(xfailed=mypy_file_checks + mypy_status_check) + assert result.ret == pytest.ExitCode.OK + + +def test_mypy_xfail_reports_stdout(testdir, xdist_args): + """Verify that --mypy-xfail reports stdout from mypy.""" + stdout = "a distinct string on stdout" + testdir.makepyfile( + conftest=f""" + import pytest + + @pytest.hookimpl(trylast=True) + def pytest_configure(config): + pytest_mypy = config.pluginmanager.getplugin("mypy") + mypy_config_stash = config.stash[pytest_mypy.stash_key["config"]] + with open(mypy_config_stash.mypy_results_path, mode="w") as results_f: + pytest_mypy.MypyResults( + opts=[], + stdout="{stdout}", + stderr="", + status=0, + abspath_errors={{}}, + unmatched_stdout="", + ).dump(results_f) + """, + ) + result = testdir.runpytest_subprocess("--mypy", *xdist_args) + assert result.ret == pytest.ExitCode.OK + assert stdout not in result.stdout.str() + result = testdir.runpytest_subprocess("--mypy-xfail", *xdist_args) + assert result.ret == pytest.ExitCode.OK + assert stdout in result.stdout.str() From c55a9ea8d93643affa11ea2ab1f4ca4cdd7c1ad7 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Fri, 6 Sep 2024 00:09:49 -0700 Subject: [PATCH 50/66] Replace MypyReportingPlugin with MypyControllerPlugin --- src/pytest_mypy/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index 73bd12f..fa016b5 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -130,7 +130,7 @@ def pytest_configure(config: pytest.Config) -> None: """ xdist_worker = _xdist_worker(config) if not xdist_worker: - config.pluginmanager.register(MypyReportingPlugin()) + config.pluginmanager.register(MypyControllerPlugin()) # Get the path to a temporary file and delete it. # The first MypyItem to run will see the file does not exist, @@ -372,15 +372,15 @@ class MypyWarning(pytest.PytestWarning): """A non-failure message regarding the mypy run.""" -class MypyReportingPlugin: - """A Pytest plugin that reports mypy results.""" +class MypyControllerPlugin: + """A plugin that is not registered on xdist worker processes.""" def pytest_terminal_summary( self, terminalreporter: TerminalReporter, config: pytest.Config, ) -> None: - """Report stderr and unrecognized lines from stdout.""" + """Report mypy results.""" mypy_results_path = config.stash[stash_key["config"]].mypy_results_path try: with open(mypy_results_path, mode="r") as results_f: From 97e8f4108ce4fe21f59f4cd00df2a8ef85c206de Mon Sep 17 00:00:00 2001 From: David Tucker Date: Fri, 6 Sep 2024 00:10:21 -0700 Subject: [PATCH 51/66] Move results path cleanup to pytest_unconfigure --- src/pytest_mypy/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index fa016b5..699a900 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -397,4 +397,11 @@ def pytest_terminal_summary( terminalreporter.write_line(results.unmatched_stdout, **color) if results.stderr: terminalreporter.write_line(results.stderr, yellow=True) - mypy_results_path.unlink() + + def pytest_unconfigure(self, config: pytest.Config) -> None: + """Clean up the mypy results path.""" + try: + config.stash[stash_key["config"]].mypy_results_path.unlink() + except FileNotFoundError: # compat python < 3.8 (missing_ok=True) + # No MypyItems executed. + return From 14c281a93a726bf6d53fef9126503f9bc833db5a Mon Sep 17 00:00:00 2001 From: David Tucker Date: Wed, 18 Sep 2024 08:37:16 -0700 Subject: [PATCH 52/66] Remove unnecessary kwargs from the tests --- tests/test_pytest_mypy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index 6ee40a0..fbfc361 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -552,7 +552,7 @@ def test_py_typed(testdir): def test_mypy_no_status_check(testdir, xdist_args): """Verify that --mypy-no-status-check disables MypyStatusItem collection.""" - testdir.makepyfile(thon="one: int = 1") + testdir.makepyfile("one: int = 1") result = testdir.runpytest_subprocess("--mypy", *xdist_args) mypy_file_checks = 1 mypy_status_check = 1 @@ -565,7 +565,7 @@ def test_mypy_no_status_check(testdir, xdist_args): def test_mypy_xfail_passes(testdir, xdist_args): """Verify that --mypy-xfail passes passes.""" - testdir.makepyfile(thon="one: int = 1") + testdir.makepyfile("one: int = 1") result = testdir.runpytest_subprocess("--mypy", *xdist_args) mypy_file_checks = 1 mypy_status_check = 1 @@ -578,7 +578,7 @@ def test_mypy_xfail_passes(testdir, xdist_args): def test_mypy_xfail_xfails(testdir, xdist_args): """Verify that --mypy-xfail xfails failures.""" - testdir.makepyfile(thon="one: str = 1") + testdir.makepyfile("one: str = 1") result = testdir.runpytest_subprocess("--mypy", *xdist_args) mypy_file_checks = 1 mypy_status_check = 1 From 0cb8090cf02d5e73e7751cfea7ee7af0f1c02def Mon Sep 17 00:00:00 2001 From: David Tucker Date: Tue, 17 Sep 2024 20:42:59 -0700 Subject: [PATCH 53/66] Use Path.resolve instead of Path.absolute --- src/pytest_mypy/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index 699a900..42845b9 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -233,7 +233,7 @@ class MypyFileItem(MypyItem): def runtest(self) -> None: """Raise an exception if mypy found errors for this item.""" results = MypyResults.from_session(self.session) - abspath = str(self.path.absolute()) + abspath = str(self.path.resolve()) errors = results.abspath_errors.get(abspath) if errors: if not all( @@ -312,7 +312,7 @@ def from_mypy( if opts is None: opts = mypy_argv[:] abspath_errors = { - str(path.absolute()): [] for path in paths + str(path.resolve()): [] for path in paths } # type: MypyResults._abspath_errors_type cwd = Path.cwd() @@ -325,7 +325,7 @@ def from_mypy( if not line: continue path, _, error = line.partition(":") - abspath = str(Path(path).absolute()) + abspath = str(Path(path).resolve()) try: abspath_errors[abspath].append(error) except KeyError: From 1aa7dbd97d862997a47565c80deb2d602d9d4755 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Tue, 17 Sep 2024 20:49:23 -0700 Subject: [PATCH 54/66] Preserve the whole line in abspath_errors --- src/pytest_mypy/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index 42845b9..065485b 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -234,7 +234,10 @@ def runtest(self) -> None: """Raise an exception if mypy found errors for this item.""" results = MypyResults.from_session(self.session) abspath = str(self.path.resolve()) - errors = results.abspath_errors.get(abspath) + errors = [ + error.partition(":")[2].strip() + for error in results.abspath_errors.get(abspath, []) + ] if errors: if not all( error.partition(":")[2].partition(":")[0].strip() == "note" @@ -327,7 +330,7 @@ def from_mypy( path, _, error = line.partition(":") abspath = str(Path(path).resolve()) try: - abspath_errors[abspath].append(error) + abspath_errors[abspath].append(line) except KeyError: unmatched_lines.append(line) From f974cb1b6598adade992d2c03b3c200e1747f035 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Tue, 17 Sep 2024 23:06:18 -0700 Subject: [PATCH 55/66] Refactor pytest_terminal_summary --- src/pytest_mypy/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index 065485b..25cbb49 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -391,15 +391,17 @@ def pytest_terminal_summary( except FileNotFoundError: # No MypyItems executed. return - if config.option.mypy_xfail or results.unmatched_stdout or results.stderr: - terminalreporter.section(terminal_summary_title) + if not results.stdout and not results.stderr: + return + terminalreporter.section(terminal_summary_title) + if results.stdout: if config.option.mypy_xfail: terminalreporter.write(results.stdout) elif results.unmatched_stdout: color = {"red": True} if results.status else {"green": True} terminalreporter.write_line(results.unmatched_stdout, **color) - if results.stderr: - terminalreporter.write_line(results.stderr, yellow=True) + if results.stderr: + terminalreporter.write_line(results.stderr, yellow=True) def pytest_unconfigure(self, config: pytest.Config) -> None: """Clean up the mypy results path.""" From 8c794f2e0d870c71d3ca0ff59a1ddbbd71308c30 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Tue, 17 Sep 2024 20:29:36 -0700 Subject: [PATCH 56/66] Remove MypyWarning --- src/pytest_mypy/__init__.py | 48 +++++++++++++++++++++---------------- tests/test_pytest_mypy.py | 4 +++- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index 25cbb49..d6a0172 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -7,7 +7,6 @@ from pathlib import Path from tempfile import NamedTemporaryFile import typing -import warnings from filelock import FileLock import mypy.api @@ -227,6 +226,14 @@ def repr_failure( return super().repr_failure(excinfo) +def _error_severity(error: str) -> str: + components = [component.strip() for component in error.split(":")] + # The second component is either the line or the severity: + # demo/note.py:2: note: By default the bodies of untyped functions are not checked + # demo/sub/conftest.py: error: Duplicate module named "conftest" + return components[2] if components[1].isdigit() else components[1] + + class MypyFileItem(MypyItem): """A check for Mypy errors in a File.""" @@ -238,20 +245,15 @@ def runtest(self) -> None: error.partition(":")[2].strip() for error in results.abspath_errors.get(abspath, []) ] - if errors: - if not all( - error.partition(":")[2].partition(":")[0].strip() == "note" - for error in errors - ): - if self.session.config.option.mypy_xfail: - self.add_marker( - pytest.mark.xfail( - raises=MypyError, - reason="mypy errors are expected by --mypy-xfail.", - ) + if errors and not all(_error_severity(error) == "note" for error in errors): + if self.session.config.option.mypy_xfail: + self.add_marker( + pytest.mark.xfail( + raises=MypyError, + reason="mypy errors are expected by --mypy-xfail.", ) - raise MypyError(file_error_formatter(self, results, errors)) - warnings.warn("\n" + "\n".join(errors), MypyWarning) + ) + raise MypyError(file_error_formatter(self, results, errors)) def reportinfo(self) -> Tuple[str, None, str]: """Produce a heading for the test report.""" @@ -371,10 +373,6 @@ class MypyError(Exception): """ -class MypyWarning(pytest.PytestWarning): - """A non-failure message regarding the mypy run.""" - - class MypyControllerPlugin: """A plugin that is not registered on xdist worker processes.""" @@ -397,9 +395,17 @@ def pytest_terminal_summary( if results.stdout: if config.option.mypy_xfail: terminalreporter.write(results.stdout) - elif results.unmatched_stdout: - color = {"red": True} if results.status else {"green": True} - terminalreporter.write_line(results.unmatched_stdout, **color) + else: + for note in ( + unreported_note + for errors in results.abspath_errors.values() + if all(_error_severity(error) == "note" for error in errors) + for unreported_note in errors + ): + terminalreporter.write_line(note) + if results.unmatched_stdout: + color = {"red": True} if results.status else {"green": True} + terminalreporter.write_line(results.unmatched_stdout, **color) if results.stderr: terminalreporter.write_line(results.stderr, yellow=True) diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index fbfc361..ed909c7 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -130,7 +130,9 @@ def pyfunc(x): mypy_checks = mypy_file_checks + mypy_status_check outcomes = {"passed": mypy_checks} result.assert_outcomes(**outcomes) - result.stdout.fnmatch_lines(["*MypyWarning*"]) + result.stdout.fnmatch_lines( + ["*:2: note: By default the bodies of untyped functions are not checked*"] + ) assert result.ret == pytest.ExitCode.OK From 506d91442403aedd0b3f541df5201104690ab403 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Thu, 19 Sep 2024 21:07:47 -0700 Subject: [PATCH 57/66] Replace MypyItem.MARKER with item_marker --- src/pytest_mypy/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index d6a0172..23f42f4 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -48,6 +48,7 @@ def serialized(self) -> str: return str(self.mypy_results_path) +item_marker = "mypy" mypy_argv: List[str] = [] nodeid_name = "mypy" stash_key = { @@ -153,7 +154,7 @@ def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line( "markers", - f"{MypyItem.MARKER}: mark tests to be checked by mypy.", + f"{item_marker}: mark tests to be checked by mypy.", ) if config.getoption("--mypy-ignore-missing-imports"): mypy_argv.append("--ignore-missing-imports") @@ -206,11 +207,9 @@ def collect(self) -> Iterator[MypyItem]: class MypyItem(pytest.Item): """A Mypy-related test Item.""" - MARKER = "mypy" - def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self.add_marker(self.MARKER) + self.add_marker(item_marker) def repr_failure( self, From 85c902495f73cac2c82f99bfa7e6f34db2121bd5 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sat, 19 Oct 2024 22:56:59 -0700 Subject: [PATCH 58/66] Create MypyCollectionPlugin --- src/pytest_mypy/__init__.py | 46 ++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index 23f42f4..0760256 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -163,27 +163,37 @@ def pytest_configure(config: pytest.Config) -> None: if mypy_config_file: mypy_argv.append(f"--config-file={mypy_config_file}") - -def pytest_collect_file( - file_path: Path, - parent: pytest.Collector, -) -> Optional[MypyFile]: - """Create a MypyFileItem for every file mypy should run on.""" - if file_path.suffix in {".py", ".pyi"} and any( + if any( [ - parent.config.option.mypy, - parent.config.option.mypy_config_file, - parent.config.option.mypy_ignore_missing_imports, - parent.config.option.mypy_no_status_check, - parent.config.option.mypy_xfail, + config.option.mypy, + config.option.mypy_config_file, + config.option.mypy_ignore_missing_imports, + config.option.mypy_no_status_check, + config.option.mypy_xfail, ], ): - # Do not create MypyFile instance for a .py file if a - # .pyi file with the same name already exists; - # pytest will complain about duplicate modules otherwise - if file_path.suffix == ".pyi" or not file_path.with_suffix(".pyi").is_file(): - return MypyFile.from_parent(parent=parent, path=file_path) - return None + config.pluginmanager.register(MypyCollectionPlugin()) + + +class MypyCollectionPlugin: + """A Pytest plugin that collects MypyFiles.""" + + def pytest_collect_file( + self, + file_path: Path, + parent: pytest.Collector, + ) -> Optional[MypyFile]: + """Create a MypyFileItem for every file mypy should run on.""" + if file_path.suffix in {".py", ".pyi"}: + # Do not create MypyFile instance for a .py file if a + # .pyi file with the same name already exists; + # pytest will complain about duplicate modules otherwise + if ( + file_path.suffix == ".pyi" + or not file_path.with_suffix(".pyi").is_file() + ): + return MypyFile.from_parent(parent=parent, path=file_path) + return None class MypyFile(pytest.File): From 05fc6527d64bd5c2c215dd9106667da38b4f7b85 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Sun, 20 Oct 2024 09:06:17 -0700 Subject: [PATCH 59/66] Add support for Python 3.13 --- .github/workflows/validation.yml | 2 +- pyproject.toml | 1 + tox.ini | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index b9fe779..f7ad205 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 diff --git a/pyproject.toml b/pyproject.toml index 78965fc..fb8d200 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Testing", ] diff --git a/tox.ini b/tox.ini index 6a5f7c9..edce1f0 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ envlist = py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} + py313-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} static publish @@ -20,6 +21,7 @@ python = 3.10: py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} 3.11: py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} 3.12: py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x}, static, publish + 3.13: py313-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} [testenv] constrain_package_deps = true From 5cc46c6c942815295dcf9795c0a75b35ad921655 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Wed, 13 Mar 2024 08:52:34 -0700 Subject: [PATCH 60/66] Drop support for Python 3.7 --- .github/workflows/validation.yml | 2 +- pyproject.toml | 3 +-- src/pytest_mypy/__init__.py | 6 +----- tox.ini | 2 -- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index f7ad205..dd36e5f 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 diff --git a/pyproject.toml b/pyproject.toml index fb8d200..be63658 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -29,7 +28,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Testing", ] -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = [ "filelock>=3.0", "mypy>=1.0", diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index 0760256..7b35ea8 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -420,8 +420,4 @@ def pytest_terminal_summary( def pytest_unconfigure(self, config: pytest.Config) -> None: """Clean up the mypy results path.""" - try: - config.stash[stash_key["config"]].mypy_results_path.unlink() - except FileNotFoundError: # compat python < 3.8 (missing_ok=True) - # No MypyItems executed. - return + config.stash[stash_key["config"]].mypy_results_path.unlink(missing_ok=True) diff --git a/tox.ini b/tox.ini index edce1f0..9deb574 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,6 @@ minversion = 4.4 isolated_build = true envlist = - py37-pytest{7.0, 7.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} py38-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} py39-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} @@ -15,7 +14,6 @@ envlist = [gh-actions] python = - 3.7: py37-pytest{7.0, 7.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} 3.8: py38-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} 3.9: py39-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} 3.10: py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}-xdist{1.x, 2.0, 2.x, 3.0, 3.x} From eb30be49c4dbef87194542da609dd3669ea706a0 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Mon, 6 Feb 2023 14:35:49 -0800 Subject: [PATCH 61/66] Resolve PYTHONWARNDEFAULTENCODING warnings --- src/pytest_mypy/__init__.py | 17 +++++++++-------- tests/test_pytest_mypy.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index 7b35ea8..0d661d1 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -16,10 +16,10 @@ from typing import ( Any, Dict, + IO, Iterator, List, Optional, - TextIO, Tuple, Union, ) @@ -297,6 +297,7 @@ class MypyResults: """Parsed results from Mypy.""" _abspath_errors_type = typing.Dict[str, typing.List[str]] + _encoding = "utf-8" opts: List[str] stdout: str @@ -305,14 +306,14 @@ class MypyResults: abspath_errors: _abspath_errors_type unmatched_stdout: str - def dump(self, results_f: TextIO) -> None: + def dump(self, results_f: IO[bytes]) -> None: """Cache results in a format that can be parsed by load().""" - return json.dump(vars(self), results_f) + results_f.write(json.dumps(vars(self)).encode(self._encoding)) @classmethod - def load(cls, results_f: TextIO) -> MypyResults: + def load(cls, results_f: IO[bytes]) -> MypyResults: """Get results cached by dump().""" - return cls(**json.load(results_f)) + return cls(**json.loads(results_f.read().decode(cls._encoding))) @classmethod def from_mypy( @@ -360,7 +361,7 @@ def from_session(cls, session: pytest.Session) -> MypyResults: mypy_results_path = session.config.stash[stash_key["config"]].mypy_results_path with FileLock(str(mypy_results_path) + ".lock"): try: - with open(mypy_results_path, mode="r") as results_f: + with open(mypy_results_path, mode="rb") as results_f: results = cls.load(results_f) except FileNotFoundError: results = cls.from_mypy( @@ -370,7 +371,7 @@ def from_session(cls, session: pytest.Session) -> MypyResults: if isinstance(item, MypyFileItem) ], ) - with open(mypy_results_path, mode="w") as results_f: + with open(mypy_results_path, mode="wb") as results_f: results.dump(results_f) return results @@ -393,7 +394,7 @@ def pytest_terminal_summary( """Report mypy results.""" mypy_results_path = config.stash[stash_key["config"]].mypy_results_path try: - with open(mypy_results_path, mode="r") as results_f: + with open(mypy_results_path, mode="rb") as results_f: results = MypyResults.load(results_f) except FileNotFoundError: # No MypyItems executed. diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index ed909c7..81c538d 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -10,6 +10,7 @@ MYPY_VERSION = Version(mypy.version.__version__) +PYTEST_VERSION = Version(pytest.__version__) PYTHON_VERSION = Version( ".".join( str(token) @@ -60,6 +61,30 @@ def pyfunc(x: int) -> int: assert result.ret == pytest.ExitCode.OK +@pytest.mark.skipif( + PYTEST_VERSION < Version("7.4"), + reason="https://github.com/pytest-dev/pytest/pull/10935", +) +@pytest.mark.skipif( + PYTHON_VERSION < Version("3.10"), + reason="PEP 597 was added in Python 3.10.", +) +@pytest.mark.skipif( + PYTHON_VERSION >= Version("3.12") and MYPY_VERSION < Version("1.5"), + reason="https://github.com/python/mypy/pull/15558", +) +def test_mypy_encoding_warnings(testdir, monkeypatch): + """Ensure no warnings are detected by PYTHONWARNDEFAULTENCODING.""" + testdir.makepyfile("") + monkeypatch.setenv("PYTHONWARNDEFAULTENCODING", "1") + result = testdir.runpytest_subprocess("--mypy") + mypy_file_checks = 1 + mypy_status_check = 1 + mypy_checks = mypy_file_checks + mypy_status_check + expected_warnings = 2 # https://github.com/python/mypy/issues/14603 + result.assert_outcomes(passed=mypy_checks, warnings=expected_warnings) + + def test_mypy_pyi(testdir, xdist_args): """ Verify that a .py file will be skipped if @@ -524,7 +549,7 @@ def test_mypy_no_output(testdir, xdist_args): def pytest_configure(config): pytest_mypy = config.pluginmanager.getplugin("mypy") mypy_config_stash = config.stash[pytest_mypy.stash_key["config"]] - with open(mypy_config_stash.mypy_results_path, mode="w") as results_f: + with open(mypy_config_stash.mypy_results_path, mode="wb") as results_f: pytest_mypy.MypyResults( opts=[], stdout="", @@ -602,7 +627,7 @@ def test_mypy_xfail_reports_stdout(testdir, xdist_args): def pytest_configure(config): pytest_mypy = config.pluginmanager.getplugin("mypy") mypy_config_stash = config.stash[pytest_mypy.stash_key["config"]] - with open(mypy_config_stash.mypy_results_path, mode="w") as results_f: + with open(mypy_config_stash.mypy_results_path, mode="wb") as results_f: pytest_mypy.MypyResults( opts=[], stdout="{stdout}", From 86aa89081113e97085b6e17d58488d86421fdb13 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Mon, 9 Dec 2024 22:06:25 -0800 Subject: [PATCH 62/66] Make MypyResults line-based --- src/pytest_mypy/__init__.py | 82 ++++++++++++++++++++++--------------- tests/test_pytest_mypy.py | 14 ++++--- 2 files changed, 57 insertions(+), 39 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index 0d661d1..af33ec1 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -235,8 +235,10 @@ def repr_failure( return super().repr_failure(excinfo) -def _error_severity(error: str) -> str: - components = [component.strip() for component in error.split(":")] +def _error_severity(line: str) -> Optional[str]: + components = [component.strip() for component in line.split(":", 3)] + if len(components) < 2: + return None # The second component is either the line or the severity: # demo/note.py:2: note: By default the bodies of untyped functions are not checked # demo/sub/conftest.py: error: Duplicate module named "conftest" @@ -249,12 +251,8 @@ class MypyFileItem(MypyItem): def runtest(self) -> None: """Raise an exception if mypy found errors for this item.""" results = MypyResults.from_session(self.session) - abspath = str(self.path.resolve()) - errors = [ - error.partition(":")[2].strip() - for error in results.abspath_errors.get(abspath, []) - ] - if errors and not all(_error_severity(error) == "note" for error in errors): + lines = results.path_lines.get(self.path.resolve(), []) + if lines and not all(_error_severity(line) == "note" for line in lines): if self.session.config.option.mypy_xfail: self.add_marker( pytest.mark.xfail( @@ -262,7 +260,13 @@ def runtest(self) -> None: reason="mypy errors are expected by --mypy-xfail.", ) ) - raise MypyError(file_error_formatter(self, results, errors)) + raise MypyError( + file_error_formatter( + self, + results, + errors=[line.partition(":")[2].strip() for line in lines], + ) + ) def reportinfo(self) -> Tuple[str, None, str]: """Produce a heading for the test report.""" @@ -296,24 +300,32 @@ def runtest(self) -> None: class MypyResults: """Parsed results from Mypy.""" - _abspath_errors_type = typing.Dict[str, typing.List[str]] _encoding = "utf-8" opts: List[str] + args: List[str] stdout: str stderr: str status: int - abspath_errors: _abspath_errors_type - unmatched_stdout: str + path_lines: Dict[Optional[Path], List[str]] def dump(self, results_f: IO[bytes]) -> None: """Cache results in a format that can be parsed by load().""" - results_f.write(json.dumps(vars(self)).encode(self._encoding)) + prepared = vars(self).copy() + prepared["path_lines"] = { + str(path or ""): lines for path, lines in prepared["path_lines"].items() + } + results_f.write(json.dumps(prepared).encode(self._encoding)) @classmethod def load(cls, results_f: IO[bytes]) -> MypyResults: """Get results cached by dump().""" - return cls(**json.loads(results_f.read().decode(cls._encoding))) + prepared = json.loads(results_f.read().decode(cls._encoding)) + prepared["path_lines"] = { + Path(path) if path else None: lines + for path, lines in prepared["path_lines"].items() + } + return cls(**prepared) @classmethod def from_mypy( @@ -326,33 +338,31 @@ def from_mypy( if opts is None: opts = mypy_argv[:] - abspath_errors = { - str(path.resolve()): [] for path in paths - } # type: MypyResults._abspath_errors_type + args = [str(path) for path in paths] - cwd = Path.cwd() - stdout, stderr, status = mypy.api.run( - opts + [str(Path(key).relative_to(cwd)) for key in abspath_errors.keys()] - ) + stdout, stderr, status = mypy.api.run(opts + args) - unmatched_lines = [] + path_lines: Dict[Optional[Path], List[str]] = { + path.resolve(): [] for path in paths + } + path_lines[None] = [] for line in stdout.split("\n"): if not line: continue - path, _, error = line.partition(":") - abspath = str(Path(path).resolve()) + path = Path(line.partition(":")[0]).resolve() try: - abspath_errors[abspath].append(line) + lines = path_lines[path] except KeyError: - unmatched_lines.append(line) + lines = path_lines[None] + lines.append(line) return cls( opts=opts, + args=args, stdout=stdout, stderr=stderr, status=status, - abspath_errors=abspath_errors, - unmatched_stdout="\n".join(unmatched_lines), + path_lines=path_lines, ) @classmethod @@ -364,9 +374,10 @@ def from_session(cls, session: pytest.Session) -> MypyResults: with open(mypy_results_path, mode="rb") as results_f: results = cls.load(results_f) except FileNotFoundError: + cwd = Path.cwd() results = cls.from_mypy( [ - item.path + item.path.relative_to(cwd) for item in session.items if isinstance(item, MypyFileItem) ], @@ -408,14 +419,17 @@ def pytest_terminal_summary( else: for note in ( unreported_note - for errors in results.abspath_errors.values() - if all(_error_severity(error) == "note" for error in errors) - for unreported_note in errors + for path, lines in results.path_lines.items() + if path is not None + if all(_error_severity(line) == "note" for line in lines) + for unreported_note in lines ): terminalreporter.write_line(note) - if results.unmatched_stdout: + if results.path_lines.get(None): color = {"red": True} if results.status else {"green": True} - terminalreporter.write_line(results.unmatched_stdout, **color) + terminalreporter.write_line( + "\n".join(results.path_lines[None]), **color + ) if results.stderr: terminalreporter.write_line(results.stderr, yellow=True) diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index 81c538d..d4dd6a9 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -532,7 +532,6 @@ def test_mypy_results_from_mypy_with_opts(): """MypyResults.from_mypy respects passed options.""" mypy_results = pytest_mypy.MypyResults.from_mypy([], opts=["--version"]) assert mypy_results.status == 0 - assert mypy_results.abspath_errors == {} assert str(MYPY_VERSION) in mypy_results.stdout @@ -552,11 +551,11 @@ def pytest_configure(config): with open(mypy_config_stash.mypy_results_path, mode="wb") as results_f: pytest_mypy.MypyResults( opts=[], + args=[], stdout="", stderr="", status=0, - abspath_errors={}, - unmatched_stdout="", + path_lines={}, ).dump(results_f) """, ) @@ -630,11 +629,11 @@ def pytest_configure(config): with open(mypy_config_stash.mypy_results_path, mode="wb") as results_f: pytest_mypy.MypyResults( opts=[], + args=[], stdout="{stdout}", stderr="", status=0, - abspath_errors={{}}, - unmatched_stdout="", + path_lines={{}}, ).dump(results_f) """, ) @@ -644,3 +643,8 @@ def pytest_configure(config): result = testdir.runpytest_subprocess("--mypy-xfail", *xdist_args) assert result.ret == pytest.ExitCode.OK assert stdout in result.stdout.str() + + +def test_error_severity(): + """Verify that non-error lines produce no severity.""" + assert pytest_mypy._error_severity("arbitrary line with no error") is None From 1adb680fdfe0272cabfb11f6ee84ac58fb2e4107 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Wed, 12 Mar 2025 00:07:34 -0700 Subject: [PATCH 63/66] Add test_name_formatter --- src/pytest_mypy/__init__.py | 16 ++++++++++------ tests/test_pytest_mypy.py | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index af33ec1..4163b71 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -57,6 +57,14 @@ def serialized(self) -> str: terminal_summary_title = "mypy" +def default_test_name_formatter(*, item: MypyFileItem) -> str: + path = item.path.relative_to(item.config.invocation_params.dir) + return f"[{terminal_summary_title}] {path}" + + +test_name_formatter = default_test_name_formatter + + def default_file_error_formatter( item: MypyItem, results: MypyResults, @@ -268,13 +276,9 @@ def runtest(self) -> None: ) ) - def reportinfo(self) -> Tuple[str, None, str]: + def reportinfo(self) -> Tuple[Path, None, str]: """Produce a heading for the test report.""" - return ( - str(self.path), - None, - str(self.path.relative_to(self.config.invocation_params.dir)), - ) + return (self.path, None, test_name_formatter(item=self)) class MypyStatusItem(MypyItem): diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index d4dd6a9..e217f2d 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -342,6 +342,29 @@ def pytest_configure(config): assert result.ret == pytest.ExitCode.OK +def test_api_test_name_formatter(testdir, xdist_args): + """Ensure that the test_name_formatter can be replaced in a conftest.py.""" + test_name = "UnmistakableTestName" + testdir.makepyfile( + conftest=f""" + cause_a_mypy_error: str = 5 + + def custom_test_name_formatter(item): + return "{test_name}" + + def pytest_configure(config): + plugin = config.pluginmanager.getplugin('mypy') + plugin.test_name_formatter = custom_test_name_formatter + """, + ) + result = testdir.runpytest_subprocess("--mypy", *xdist_args) + result.stdout.fnmatch_lines([f"*{test_name}*"]) + mypy_file_check = 1 + mypy_status_check = 1 + result.assert_outcomes(failed=mypy_file_check + mypy_status_check) + assert result.ret == pytest.ExitCode.TESTS_FAILED + + @pytest.mark.xfail( Version("0.971") <= MYPY_VERSION, raises=AssertionError, From e236fdb95e73c3819addb3838426995a65595756 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Thu, 13 Mar 2025 22:52:17 -0700 Subject: [PATCH 64/66] Improve test_api_error_formatter --- tests/test_pytest_mypy.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index e217f2d..a799b88 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -399,24 +399,19 @@ def pyfunc(x: int) -> str: assert result.ret == pytest.ExitCode.TESTS_FAILED -def test_api_error_formatter(testdir, xdist_args): - """Ensure that the plugin can be configured in a conftest.py.""" +def test_api_file_error_formatter(testdir, xdist_args): + """Ensure that the file_error_formatter can be replaced in a conftest.py.""" testdir.makepyfile( bad=""" def pyfunc(x: int) -> str: return x * 2 """, ) + file_error = "UnmistakableFileError" testdir.makepyfile( - conftest=""" + conftest=f""" def custom_file_error_formatter(item, results, errors): - return '\\n'.join( - '{path}:{error}'.format( - path=item.fspath, - error=error, - ) - for error in errors - ) + return '{file_error}' def pytest_configure(config): plugin = config.pluginmanager.getplugin('mypy') @@ -424,7 +419,7 @@ def pytest_configure(config): """, ) result = testdir.runpytest_subprocess("--mypy", *xdist_args) - result.stdout.fnmatch_lines(["*/bad.py:2: error: Incompatible return value*"]) + result.stdout.fnmatch_lines([f"*{file_error}*"]) assert result.ret == pytest.ExitCode.TESTS_FAILED From 9824747b4898d4fc3d7ed239927be17c65a48c6f Mon Sep 17 00:00:00 2001 From: David Tucker Date: Thu, 13 Mar 2025 22:58:55 -0700 Subject: [PATCH 65/66] Add --mypy-report-style --- src/pytest_mypy/__init__.py | 25 ++++++++++++++++--------- tests/test_pytest_mypy.py | 28 +++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index 4163b71..dd80bf3 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -68,10 +68,12 @@ def default_test_name_formatter(*, item: MypyFileItem) -> str: def default_file_error_formatter( item: MypyItem, results: MypyResults, - errors: List[str], + lines: List[str], ) -> str: """Create a string to be displayed when mypy finds errors in a file.""" - return "\n".join(errors) + if item.config.option.mypy_report_style == "mypy": + return "\n".join(lines) + return "\n".join(line.partition(":")[2].strip() for line in lines) file_error_formatter = default_file_error_formatter @@ -92,6 +94,16 @@ def pytest_addoption(parser: pytest.Parser) -> None: type=str, help="adds custom mypy config file", ) + styles = { + "mypy": "modify the original mypy output as little as possible", + "no-path": "(default) strip the path prefix from mypy errors", + } + group.addoption( + "--mypy-report-style", + choices=list(styles), + help="change the way mypy output is reported:\n" + + "\n".join(f"- {name}: {desc}" for name, desc in styles.items()), + ) group.addoption( "--mypy-no-status-check", action="store_true", @@ -175,6 +187,7 @@ def pytest_configure(config: pytest.Config) -> None: [ config.option.mypy, config.option.mypy_config_file, + config.option.mypy_report_style, config.option.mypy_ignore_missing_imports, config.option.mypy_no_status_check, config.option.mypy_xfail, @@ -268,13 +281,7 @@ def runtest(self) -> None: reason="mypy errors are expected by --mypy-xfail.", ) ) - raise MypyError( - file_error_formatter( - self, - results, - errors=[line.partition(":")[2].strip() for line in lines], - ) - ) + raise MypyError(file_error_formatter(self, results, lines)) def reportinfo(self) -> Tuple[Path, None, str]: """Produce a heading for the test report.""" diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index a799b88..498b5f4 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -410,7 +410,7 @@ def pyfunc(x: int) -> str: file_error = "UnmistakableFileError" testdir.makepyfile( conftest=f""" - def custom_file_error_formatter(item, results, errors): + def custom_file_error_formatter(item, results, lines): return '{file_error}' def pytest_configure(config): @@ -666,3 +666,29 @@ def pytest_configure(config): def test_error_severity(): """Verify that non-error lines produce no severity.""" assert pytest_mypy._error_severity("arbitrary line with no error") is None + + +def test_mypy_report_style(testdir, xdist_args): + """Verify that --mypy-report-style functions correctly.""" + module_name = "unmistakable_module_name" + testdir.makepyfile( + **{ + module_name: """ + def pyfunc(x: int) -> str: + return x * 2 + """ + }, + ) + result = testdir.runpytest_subprocess("--mypy-report-style", "no-path", *xdist_args) + mypy_file_checks = 1 + mypy_status_check = 1 + mypy_checks = mypy_file_checks + mypy_status_check + result.assert_outcomes(failed=mypy_checks) + result.stdout.fnmatch_lines(["2: error: Incompatible return value*"]) + assert result.ret == pytest.ExitCode.TESTS_FAILED + result = testdir.runpytest_subprocess("--mypy-report-style", "mypy", *xdist_args) + result.assert_outcomes(failed=mypy_checks) + result.stdout.fnmatch_lines( + [f"{module_name}.py:2: error: Incompatible return value*"] + ) + assert result.ret == pytest.ExitCode.TESTS_FAILED From 93aa86df33cb144152de9f9af5cf449ae3cef3e7 Mon Sep 17 00:00:00 2001 From: David Tucker Date: Wed, 2 Apr 2025 00:30:38 -0700 Subject: [PATCH 66/66] Catch OSErrors when parsing mypy error paths --- src/pytest_mypy/__init__.py | 5 ++++- tests/test_pytest_mypy.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/pytest_mypy/__init__.py b/src/pytest_mypy/__init__.py index dd80bf3..11e6ebd 100644 --- a/src/pytest_mypy/__init__.py +++ b/src/pytest_mypy/__init__.py @@ -360,7 +360,10 @@ def from_mypy( for line in stdout.split("\n"): if not line: continue - path = Path(line.partition(":")[0]).resolve() + try: + path = Path(line.partition(":")[0]).resolve() + except OSError: + path = None try: lines = path_lines[path] except KeyError: diff --git a/tests/test_pytest_mypy.py b/tests/test_pytest_mypy.py index 498b5f4..1ad7153 100644 --- a/tests/test_pytest_mypy.py +++ b/tests/test_pytest_mypy.py @@ -138,7 +138,34 @@ def pyfunc(x: int) -> str: assert result.ret == pytest.ExitCode.TESTS_FAILED -def test_mypy_annotation_unchecked(testdir, xdist_args, tmp_path, monkeypatch): +def test_mypy_path_error(testdir, xdist_args): + """Verify that runs are not affected by path errors.""" + testdir.makepyfile( + conftest=""" + def pytest_configure(config): + plugin = config.pluginmanager.getplugin('mypy') + + class FakePath: + def __init__(self, _): + pass + def resolve(self): + raise OSError + + Path = plugin.Path + plugin.Path = FakePath + plugin.MypyResults.from_mypy([], opts=['--version']) + plugin.Path = Path + """, + ) + result = testdir.runpytest_subprocess("--mypy", *xdist_args) + mypy_file_checks = 1 + mypy_status_check = 1 + mypy_checks = mypy_file_checks + mypy_status_check + result.assert_outcomes(passed=mypy_checks) + assert result.ret == pytest.ExitCode.OK + + +def test_mypy_annotation_unchecked(testdir, xdist_args, tmp_path): """Verify that annotation-unchecked warnings do not manifest as an error.""" testdir.makepyfile( """