From 8008632564c49df40baf668c8b3c43f2f393daeb Mon Sep 17 00:00:00 2001 From: David Tucker Date: Mon, 19 Aug 2024 01:30:06 -0700 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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)