diff --git a/src/pytest_mypy.py b/src/pytest_mypy.py index f9249f6..98cff39 100644 --- a/src/pytest_mypy.py +++ b/src/pytest_mypy.py @@ -59,28 +59,28 @@ 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 _xdist_workerinput(node): + try: + return node.workerinput + except AttributeError: # compat xdist < 2.0 + return node.slaveinput -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 +class MypyXdistControllerPlugin: + """A plugin that is only registered on xdist controller processes.""" + + def pytest_configure_node(self, node): + """Pass the config stash to workers.""" + _xdist_workerinput(node)["mypy_config_stash_serialized"] = node.config.stash[ + stash_key["config"] + ].serialized() def pytest_configure(config): @@ -89,7 +89,9 @@ def pytest_configure(config): register a custom marker for MypyItems, and configure the plugin based on the CLI. """ - if _is_xdist_controller(config): + xdist_worker = _xdist_worker(config) + if not xdist_worker: + 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, @@ -104,15 +106,12 @@ 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()) + 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", @@ -278,13 +277,7 @@ 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): - 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 + 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: @@ -313,22 +306,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() 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) 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}