From e41434744e6c1d09f2904d50f354767557976c70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 21:58:44 +0000 Subject: [PATCH 01/12] Initial plan From c44d10b93ed72f087fcfc70d90e292c90ae3fac2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:00:50 +0000 Subject: [PATCH 02/12] Reorder pr-check.yml so specific pytest version installs after test-requirements --- .github/workflows/pr-check.yml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index c58844bbf110..846e461b5e46 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -172,18 +172,6 @@ jobs: with: python-version: ${{ matrix.python }} - - name: Install specific pytest version - if: matrix.pytest-version == 'pytest@pre-release' - run: | - python -m pip install --pre pytest - - - name: Install specific pytest version - if: matrix.pytest-version != 'pytest@pre-release' - run: | - python -m pip install "${{ matrix.pytest-version }}" - - - name: Install specific pytest version - run: python -m pytest --version - name: Install base Python requirements uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: @@ -193,6 +181,19 @@ jobs: - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt + # Install the specific pytest version AFTER test-requirements.txt so it is not + # overridden by the `pytest<8.1` pin (needed for pytest-black compatibility). + - name: Install specific pytest version (pre-release) + if: matrix.pytest-version == 'pytest@pre-release' + run: python -m pip install --upgrade --pre pytest + + - name: Install specific pytest version + if: matrix.pytest-version != 'pytest@pre-release' + run: python -m pip install --upgrade "${{ matrix.pytest-version }}" + + - name: Print pytest version + run: python -m pytest --version + - name: Run Python unit tests run: python python_files/tests/run_all.py From a903338a1a7077be8df01b0be409de49e9d472f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:24:41 +0000 Subject: [PATCH 03/12] Fix failing Python Tests CI job by preserving pytest<8.1 pin for pytest-black compatibility --- .github/workflows/pr-check.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 846e461b5e46..74bf826b4633 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -183,12 +183,14 @@ jobs: # Install the specific pytest version AFTER test-requirements.txt so it is not # overridden by the `pytest<8.1` pin (needed for pytest-black compatibility). + # When pytest-version is 'pytest' (latest stable), keep the pinned version from + # test-requirements.txt to maintain pytest-black compatibility. - name: Install specific pytest version (pre-release) if: matrix.pytest-version == 'pytest@pre-release' run: python -m pip install --upgrade --pre pytest - name: Install specific pytest version - if: matrix.pytest-version != 'pytest@pre-release' + if: matrix.pytest-version != 'pytest@pre-release' && matrix.pytest-version != 'pytest' run: python -m pip install --upgrade "${{ matrix.pytest-version }}" - name: Print pytest version From cdb5ddf167cadb11411ddfc20c10c62c07388e35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:34:48 +0000 Subject: [PATCH 04/12] Replace pytest-black with local conftest; remove pytest<8.1 pin --- .github/workflows/pr-check.yml | 6 +- build/test-requirements.txt | 4 - .../.data/2496-black-formatter/conftest.py | 149 ++++++++++++++++++ 3 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 74bf826b4633..af92b255ae5b 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -181,16 +181,12 @@ jobs: - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt - # Install the specific pytest version AFTER test-requirements.txt so it is not - # overridden by the `pytest<8.1` pin (needed for pytest-black compatibility). - # When pytest-version is 'pytest' (latest stable), keep the pinned version from - # test-requirements.txt to maintain pytest-black compatibility. - name: Install specific pytest version (pre-release) if: matrix.pytest-version == 'pytest@pre-release' run: python -m pip install --upgrade --pre pytest - name: Install specific pytest version - if: matrix.pytest-version != 'pytest@pre-release' && matrix.pytest-version != 'pytest' + if: matrix.pytest-version != 'pytest@pre-release' run: python -m pip install --upgrade "${{ matrix.pytest-version }}" - name: Print pytest version diff --git a/build/test-requirements.txt b/build/test-requirements.txt index d1acb9a33d84..f19194c2c772 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -7,9 +7,6 @@ pylint pycodestyle pydocstyle prospector -# pytest-black 0.6.0 uses the deprecated `path` arg in pytest_collect_file, -# which was removed in pytest 8.1. Pin to <8.1 to maintain compatibility. -pytest<8.1 flask fastapi uvicorn @@ -41,4 +38,3 @@ pytest-describe # for pytest-ruff related tests pytest-ruff -pytest-black diff --git a/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py b/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py new file mode 100644 index 000000000000..30af91f89fd5 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +# Local replacement for the pytest-black plugin that supports pytest 7+. +# pytest-black 0.6.0 uses the deprecated `path` argument in pytest_collect_file, +# which was removed in pytest 8.1. This conftest provides a compatible implementation. + +import re +import subprocess +import sys + +import pytest + +PYTEST_VER = tuple(int(x) for x in pytest.__version__.split(".")[:2]) + +try: + import tomllib +except ImportError: + try: + import tomli as tomllib # type: ignore[no-redef] + except ImportError: + tomllib = None # type: ignore[assignment] + +HISTKEY = "black/mtimes" + + +def pytest_addoption(parser): + group = parser.getgroup("general") + group.addoption( + "--black", action="store_true", help="enable format checking with black" + ) + + +def pytest_configure(config): + # load cached mtimes at session startup + if config.option.black and hasattr(config, "cache"): + config._blackmtimes = config.cache.get(HISTKEY, {}) + config.addinivalue_line("markers", "black: enable format checking with black") + + +def pytest_unconfigure(config): + # save cached mtimes at end of session + if hasattr(config, "_blackmtimes"): + config.cache.set(HISTKEY, config._blackmtimes) + + +if PYTEST_VER >= (8, 1): + + def pytest_collect_file(file_path, parent): + config = parent.config + if ( + config.option.black + and file_path.suffix in (".py", ".pyi") + and file_path.name != "conftest.py" + ): + return BlackFile.from_parent(parent, path=file_path) + +elif PYTEST_VER >= (7, 0): + + def pytest_collect_file(file_path, path, parent): # type: ignore[misc] + config = parent.config + if ( + config.option.black + and file_path.suffix in (".py", ".pyi") + and file_path.name != "conftest.py" + ): + return BlackFile.from_parent(parent, path=file_path) + + +class BlackFile(pytest.File): + def collect(self): + """Return a list of children (items and collectors) for this collection node.""" + yield BlackItem.from_parent(self, name="black") + + +class BlackItem(pytest.Item): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.add_marker("black") + try: + if tomllib is not None: + with open("pyproject.toml", "rb") as toml_file: + settings = tomllib.load(toml_file)["tool"]["black"] + if "include" in settings: + settings["include"] = self._re_fix_verbose(settings["include"]) + if "exclude" in settings: + settings["exclude"] = self._re_fix_verbose(settings["exclude"]) + self.pyproject = settings + else: + self.pyproject = {} + except Exception: + self.pyproject = {} + + def setup(self): + pytest.importorskip("black") + mtimes = getattr(self.config, "_blackmtimes", {}) + self._blackmtime = self.path.stat().st_mtime + old = mtimes.get(str(self.path), 0) + if self._blackmtime == old: + pytest.skip("file(s) previously passed black format checks") + + if self._skip_test(): + pytest.skip("file(s) excluded by pyproject.toml") + + def runtest(self): + cmd = [ + sys.executable, + "-m", + "black", + "--check", + "--diff", + "--quiet", + str(self.path), + ] + try: + subprocess.run(cmd, check=True, stdout=subprocess.PIPE, universal_newlines=True) + except subprocess.CalledProcessError as e: + raise BlackError(e) + + mtimes = getattr(self.config, "_blackmtimes", {}) + mtimes[str(self.path)] = self._blackmtime + + def repr_failure(self, excinfo): + if excinfo.errisinstance(BlackError): + return excinfo.value.args[0].stdout + return super().repr_failure(excinfo) + + def reportinfo(self): + return (self.path, -1, "Black format check") + + def _skip_test(self): + return self._excluded() or (not self._included()) + + def _included(self): + if "include" not in self.pyproject: + return True + return re.search(self.pyproject["include"], str(self.path)) + + def _excluded(self): + if "exclude" not in self.pyproject: + return False + return re.search(self.pyproject["exclude"], str(self.path)) + + def _re_fix_verbose(self, regex): + if "\n" in regex: + regex = "(?x)" + regex + return re.compile(regex) + + +class BlackError(Exception): + pass From 43a5c240d0f756a23b3b57f877a37877e4380b7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:37:48 +0000 Subject: [PATCH 05/12] Use text=True in subprocess.run; document why path param must match hookspec --- .../pytestadapter/.data/2496-black-formatter/conftest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py b/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py index 30af91f89fd5..ec7c872d690e 100644 --- a/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py +++ b/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py @@ -55,7 +55,8 @@ def pytest_collect_file(file_path, parent): elif PYTEST_VER >= (7, 0): - def pytest_collect_file(file_path, path, parent): # type: ignore[misc] + def pytest_collect_file(file_path, path, parent): # type: ignore[misc] # noqa: ARG001 + # `path` must match the pytest 7.x hookspec; use file_path (pathlib.Path) in body. config = parent.config if ( config.option.black @@ -111,7 +112,7 @@ def runtest(self): str(self.path), ] try: - subprocess.run(cmd, check=True, stdout=subprocess.PIPE, universal_newlines=True) + subprocess.run(cmd, check=True, stdout=subprocess.PIPE, text=True) except subprocess.CalledProcessError as e: raise BlackError(e) From 12f414526d662224b021ea0039718bd5f859790c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:19:07 +0000 Subject: [PATCH 06/12] Fix pytest 6.2 CI lane plugin compatibility installs --- .github/workflows/pr-check.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index af92b255ae5b..7e215cdd813b 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -189,6 +189,11 @@ jobs: if: matrix.pytest-version != 'pytest@pre-release' run: python -m pip install --upgrade "${{ matrix.pytest-version }}" + - name: Install pytest plugin compatibility packages + if: matrix.pytest-version == 'pytest==6.2.0' + run: | + python -m pip install --upgrade "pytest-cov<6.2.1" "pytest-describe<3.2" "pytest-timeout<2.3.1" + - name: Print pytest version run: python -m pytest --version From 50a2522b8c15ff55ca208d5306f90caa622f40b3 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:25:22 -0700 Subject: [PATCH 07/12] Fix pytest CI matrix and prevent pipe hangs Use pytest 7.0.1 as the oldest supported lane on Python 3.10 and install matrix selections after shared requirements. Keep non-matrix builds on stable pytest, make manual plugin loading compatible across pytest versions, and fail bounded pipe harness waits instead of hanging CI indefinitely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 3 + .github/workflows/pr-check.yml | 14 +++-- python_files/tests/pytestadapter/helpers.py | 59 +++++++++++-------- .../tests/pytestadapter/test_coverage.py | 6 +- .../tests/pytestadapter/test_helpers.py | 54 +++++++++++++++++ 5 files changed, 107 insertions(+), 29 deletions(-) create mode 100644 python_files/tests/pytestadapter/test_helpers.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 78b19331116d..515d84416eb9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -197,6 +197,9 @@ jobs: - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt + - name: Install pytest + run: python -m pip install --upgrade pytest + - name: Run Python unit tests run: python python_files/tests/run_all.py diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 7e215cdd813b..680ae91f3da0 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -157,8 +157,14 @@ jobs: # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. - python: ['3.10', '3.x', '3.13'] # run for 3 pytest versions, most recent stable, oldest version supported and pre-release - pytest-version: ['pytest', 'pytest@pre-release', 'pytest==6.2.0'] + python: ['3.10', '3.13', '3.x'] + pytest-version: ['pytest', 'pytest@pre-release', 'pytest==7.0.1'] + exclude: + # pytest 7.0.1 is the oldest release supported by the adapter, but it predates Python 3.13. + - python: '3.13' + pytest-version: 'pytest==7.0.1' + - python: '3.x' + pytest-version: 'pytest==7.0.1' steps: - name: Checkout @@ -190,9 +196,9 @@ jobs: run: python -m pip install --upgrade "${{ matrix.pytest-version }}" - name: Install pytest plugin compatibility packages - if: matrix.pytest-version == 'pytest==6.2.0' + if: matrix.pytest-version == 'pytest==7.0.1' run: | - python -m pip install --upgrade "pytest-cov<6.2.1" "pytest-describe<3.2" "pytest-timeout<2.3.1" + python -m pip install --upgrade "anyio<4" "pytest-cov<6" "pytest-describe<3.2" "pytest-timeout<2.3.1" - name: Print pytest version run: python -m pytest --version diff --git a/python_files/tests/pytestadapter/helpers.py b/python_files/tests/pytestadapter/helpers.py index 7cc5e678bd61..d58ae56d8849 100644 --- a/python_files/tests/pytestadapter/helpers.py +++ b/python_files/tests/pytestadapter/helpers.py @@ -28,6 +28,8 @@ TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" CONTENT_LENGTH: str = "Content-Length:" CONTENT_TYPE: str = "Content-Type:" +PIPE_RESULT_TIMEOUT_SECONDS = 10 +TEST_SUBPROCESS_TIMEOUT_SECONDS = 300 @contextlib.contextmanager @@ -242,10 +244,33 @@ def _listen_on_pipe_new(listener, result: List[str], completed: threading.Event) result.append("".join(all_data)) -def _run_test_code(proc_args: List[str], proc_env, proc_cwd: str, completed: threading.Event): - result = subprocess.run(proc_args, env=proc_env, cwd=proc_cwd, check=False) - completed.set() - return result +def _run_test_code( + proc_args: List[str], proc_env, proc_cwd: str, completed: threading.Event +) -> subprocess.CompletedProcess: + try: + return subprocess.run( + proc_args, + env=proc_env, + cwd=proc_cwd, + check=False, + timeout=TEST_SUBPROCESS_TIMEOUT_SECONDS, + ) + finally: + completed.set() + + +def _wait_for_pipe_result( + listener_thread: threading.Thread, + process_result: subprocess.CompletedProcess, + result: List[str], +) -> None: + listener_thread.join(timeout=PIPE_RESULT_TIMEOUT_SECONDS) + if listener_thread.is_alive(): + if process_result.returncode: + raise subprocess.CalledProcessError(process_result.returncode, process_result.args) + raise TimeoutError("Timed out waiting for the test subprocess pipe result") + if process_result.returncode and not result: + raise subprocess.CalledProcessError(process_result.returncode, process_result.args) def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: @@ -315,7 +340,6 @@ def runner_with_cwd_env( sys.executable, "-m", "pytest", - "--disable-plugin-autoload", "-p", "pytest_cov.plugin", "-p", @@ -359,18 +383,12 @@ def runner_with_cwd_env( result = [] # result is a string array to store the data during threading t1: threading.Thread = threading.Thread( - target=_listen_on_pipe_new, args=(pipe, result, completed) + target=_listen_on_pipe_new, args=(pipe, result, completed), daemon=True ) t1.start() - t2 = threading.Thread( - target=_run_test_code, - args=(process_args, env, path, completed), - ) - t2.start() - - t1.join() - t2.join() + process_result = _run_test_code(process_args, env, path, completed) + _wait_for_pipe_result(t1, process_result, result) return process_data_received(result[0]) if result else None else: # Unix design @@ -397,19 +415,12 @@ def runner_with_cwd_env( result = [] # result is a string array to store the data during threading t1: threading.Thread = threading.Thread( - target=_listen_on_fifo, args=(pipe_name, result, completed) + target=_listen_on_fifo, args=(pipe_name, result, completed), daemon=True ) t1.start() - t2: threading.Thread = threading.Thread( - target=_run_test_code, - args=(process_args, env, path, completed), - ) - - t2.start() - - t1.join() - t2.join() + process_result = _run_test_code(process_args, env, path, completed) + _wait_for_pipe_result(t1, process_result, result) return process_data_received(result[0]) if result else None diff --git a/python_files/tests/pytestadapter/test_coverage.py b/python_files/tests/pytestadapter/test_coverage.py index f2387527698f..a8f4cb29aab2 100644 --- a/python_files/tests/pytestadapter/test_coverage.py +++ b/python_files/tests/pytestadapter/test_coverage.py @@ -154,7 +154,11 @@ def test_pytest_cov_manual_plugin_loading(): (pytest_cov) in this scenario. """ args = ["--collect-only"] - env_add = {"COVERAGE_ENABLED": "True", "_PYTEST_MANUAL_PLUGIN_LOAD": "True"} + env_add = { + "COVERAGE_ENABLED": "True", + "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", + "_PYTEST_MANUAL_PLUGIN_LOAD": "True", + } cov_folder_path = TEST_DATA_PATH / "coverage_gen" # Should NOT raise VSCodePytestError about pytest-cov not being installed diff --git a/python_files/tests/pytestadapter/test_helpers.py b/python_files/tests/pytestadapter/test_helpers.py new file mode 100644 index 000000000000..552c571d51f8 --- /dev/null +++ b/python_files/tests/pytestadapter/test_helpers.py @@ -0,0 +1,54 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import subprocess +import sys +import threading + +import pytest + +from . import helpers + + +def test_run_test_code_times_out(tmp_path, monkeypatch): + monkeypatch.setattr(helpers, "TEST_SUBPROCESS_TIMEOUT_SECONDS", 0.01) + completed = threading.Event() + + with pytest.raises(subprocess.TimeoutExpired): + helpers._run_test_code( # noqa: SLF001 + [sys.executable, "-c", "import time; time.sleep(1)"], + os.environ.copy(), + str(tmp_path), + completed, + ) + + assert completed.is_set() + + +def test_wait_for_pipe_result_surfaces_subprocess_failure(): + listener_thread = threading.Thread(target=lambda: None) + listener_thread.start() + process_result = subprocess.CompletedProcess(["pytest"], returncode=2) + + with pytest.raises(subprocess.CalledProcessError): + helpers._wait_for_pipe_result( # noqa: SLF001 + listener_thread, process_result, [] + ) + + +def test_wait_for_pipe_result_times_out(monkeypatch): + monkeypatch.setattr(helpers, "PIPE_RESULT_TIMEOUT_SECONDS", 0.01) + release_listener = threading.Event() + listener_thread = threading.Thread(target=release_listener.wait, daemon=True) + listener_thread.start() + process_result = subprocess.CompletedProcess(["pytest"], returncode=0) + + try: + with pytest.raises(TimeoutError, match="Timed out waiting"): + helpers._wait_for_pipe_result( # noqa: SLF001 + listener_thread, process_result, [] + ) + finally: + release_listener.set() + listener_thread.join() From b9fe1a4a64d04e1c8bb052466beea277a95def39 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:32:43 -0700 Subject: [PATCH 08/12] Test supported Python versions against recent pytest Cover Python 3.10 through 3.14 with the latest pytest 8.4 patch and the pytest pre-release channel. This keeps approximately one year of pytest compatibility coverage while detecting upcoming breaking changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-check.yml | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 680ae91f3da0..655e0de23ee5 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -156,15 +156,9 @@ jobs: # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. os: [ubuntu-latest, windows-latest] - # Run the tests on the oldest and most recent versions of Python. - python: ['3.10', '3.13', '3.x'] - pytest-version: ['pytest', 'pytest@pre-release', 'pytest==7.0.1'] - exclude: - # pytest 7.0.1 is the oldest release supported by the adapter, but it predates Python 3.13. - - python: '3.13' - pytest-version: 'pytest==7.0.1' - - python: '3.x' - pytest-version: 'pytest==7.0.1' + python: ['3.10', '3.11', '3.12', '3.13', '3.14'] + # Test approximately one year of pytest releases and upcoming pytest changes. + pytest-version: ['pytest==8.4.*', 'pytest@pre-release'] steps: - name: Checkout @@ -195,11 +189,6 @@ jobs: if: matrix.pytest-version != 'pytest@pre-release' run: python -m pip install --upgrade "${{ matrix.pytest-version }}" - - name: Install pytest plugin compatibility packages - if: matrix.pytest-version == 'pytest==7.0.1' - run: | - python -m pip install --upgrade "anyio<4" "pytest-cov<6" "pytest-describe<3.2" "pytest-timeout<2.3.1" - - name: Print pytest version run: python -m pytest --version From c15488a4abb8db8e77fb15e212b2ce2c6ba03380 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:42:24 -0700 Subject: [PATCH 09/12] Fix pytest test harness type errors Keep the subprocess cwd annotation compatible with pathlib callers and simplify the local Black fixture for the supported pytest 8.4+ range so the CI Pyright version can analyze it cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.data/2496-black-formatter/conftest.py | 45 ++++++------------- python_files/tests/pytestadapter/helpers.py | 7 ++- 2 files changed, 18 insertions(+), 34 deletions(-) diff --git a/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py b/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py index ec7c872d690e..e6f2a1e60ecf 100644 --- a/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py +++ b/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Local replacement for the pytest-black plugin that supports pytest 7+. +# Local replacement for the pytest-black plugin that supports pytest 8.4+. # pytest-black 0.6.0 uses the deprecated `path` argument in pytest_collect_file, # which was removed in pytest 8.1. This conftest provides a compatible implementation. @@ -9,15 +9,10 @@ import pytest -PYTEST_VER = tuple(int(x) for x in pytest.__version__.split(".")[:2]) - try: - import tomllib + import tomli except ImportError: - try: - import tomli as tomllib # type: ignore[no-redef] - except ImportError: - tomllib = None # type: ignore[assignment] + tomli = None # type: ignore[assignment] HISTKEY = "black/mtimes" @@ -42,28 +37,14 @@ def pytest_unconfigure(config): config.cache.set(HISTKEY, config._blackmtimes) -if PYTEST_VER >= (8, 1): - - def pytest_collect_file(file_path, parent): - config = parent.config - if ( - config.option.black - and file_path.suffix in (".py", ".pyi") - and file_path.name != "conftest.py" - ): - return BlackFile.from_parent(parent, path=file_path) - -elif PYTEST_VER >= (7, 0): - - def pytest_collect_file(file_path, path, parent): # type: ignore[misc] # noqa: ARG001 - # `path` must match the pytest 7.x hookspec; use file_path (pathlib.Path) in body. - config = parent.config - if ( - config.option.black - and file_path.suffix in (".py", ".pyi") - and file_path.name != "conftest.py" - ): - return BlackFile.from_parent(parent, path=file_path) +def pytest_collect_file(file_path, parent): + config = parent.config + if ( + config.option.black + and file_path.suffix in (".py", ".pyi") + and file_path.name != "conftest.py" + ): + return BlackFile.from_parent(parent, path=file_path) class BlackFile(pytest.File): @@ -77,9 +58,9 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self.add_marker("black") try: - if tomllib is not None: + if tomli is not None: with open("pyproject.toml", "rb") as toml_file: - settings = tomllib.load(toml_file)["tool"]["black"] + settings = tomli.load(toml_file)["tool"]["black"] if "include" in settings: settings["include"] = self._re_fix_verbose(settings["include"]) if "exclude" in settings: diff --git a/python_files/tests/pytestadapter/helpers.py b/python_files/tests/pytestadapter/helpers.py index d58ae56d8849..a923d4771557 100644 --- a/python_files/tests/pytestadapter/helpers.py +++ b/python_files/tests/pytestadapter/helpers.py @@ -12,7 +12,7 @@ import tempfile import threading import uuid -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union if sys.platform == "win32": from namedpipe import NPopen # pylint: disable=import-error # cspell: disable-line @@ -245,7 +245,10 @@ def _listen_on_pipe_new(listener, result: List[str], completed: threading.Event) def _run_test_code( - proc_args: List[str], proc_env, proc_cwd: str, completed: threading.Event + proc_args: List[str], + proc_env, + proc_cwd: Union[str, os.PathLike[str]], + completed: threading.Event, ) -> subprocess.CompletedProcess: try: return subprocess.run( From 367dd0ecb041ff238d2d243997aa84909b9a2799 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:31:43 -0700 Subject: [PATCH 10/12] Skip obsolete pytest-black discovery case pytest-black does not support pytest 8.1 or newer, and pytest-ruff already covers discovery of plugin-generated test items. Skip the obsolete Black-specific parameter instead of maintaining a local replacement for the third-party plugin. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.data/2496-black-formatter/conftest.py | 131 ------------------ .../tests/pytestadapter/test_discovery.py | 5 +- 2 files changed, 4 insertions(+), 132 deletions(-) delete mode 100644 python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py diff --git a/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py b/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py deleted file mode 100644 index e6f2a1e60ecf..000000000000 --- a/python_files/tests/pytestadapter/.data/2496-black-formatter/conftest.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -# Local replacement for the pytest-black plugin that supports pytest 8.4+. -# pytest-black 0.6.0 uses the deprecated `path` argument in pytest_collect_file, -# which was removed in pytest 8.1. This conftest provides a compatible implementation. - -import re -import subprocess -import sys - -import pytest - -try: - import tomli -except ImportError: - tomli = None # type: ignore[assignment] - -HISTKEY = "black/mtimes" - - -def pytest_addoption(parser): - group = parser.getgroup("general") - group.addoption( - "--black", action="store_true", help="enable format checking with black" - ) - - -def pytest_configure(config): - # load cached mtimes at session startup - if config.option.black and hasattr(config, "cache"): - config._blackmtimes = config.cache.get(HISTKEY, {}) - config.addinivalue_line("markers", "black: enable format checking with black") - - -def pytest_unconfigure(config): - # save cached mtimes at end of session - if hasattr(config, "_blackmtimes"): - config.cache.set(HISTKEY, config._blackmtimes) - - -def pytest_collect_file(file_path, parent): - config = parent.config - if ( - config.option.black - and file_path.suffix in (".py", ".pyi") - and file_path.name != "conftest.py" - ): - return BlackFile.from_parent(parent, path=file_path) - - -class BlackFile(pytest.File): - def collect(self): - """Return a list of children (items and collectors) for this collection node.""" - yield BlackItem.from_parent(self, name="black") - - -class BlackItem(pytest.Item): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.add_marker("black") - try: - if tomli is not None: - with open("pyproject.toml", "rb") as toml_file: - settings = tomli.load(toml_file)["tool"]["black"] - if "include" in settings: - settings["include"] = self._re_fix_verbose(settings["include"]) - if "exclude" in settings: - settings["exclude"] = self._re_fix_verbose(settings["exclude"]) - self.pyproject = settings - else: - self.pyproject = {} - except Exception: - self.pyproject = {} - - def setup(self): - pytest.importorskip("black") - mtimes = getattr(self.config, "_blackmtimes", {}) - self._blackmtime = self.path.stat().st_mtime - old = mtimes.get(str(self.path), 0) - if self._blackmtime == old: - pytest.skip("file(s) previously passed black format checks") - - if self._skip_test(): - pytest.skip("file(s) excluded by pyproject.toml") - - def runtest(self): - cmd = [ - sys.executable, - "-m", - "black", - "--check", - "--diff", - "--quiet", - str(self.path), - ] - try: - subprocess.run(cmd, check=True, stdout=subprocess.PIPE, text=True) - except subprocess.CalledProcessError as e: - raise BlackError(e) - - mtimes = getattr(self.config, "_blackmtimes", {}) - mtimes[str(self.path)] = self._blackmtime - - def repr_failure(self, excinfo): - if excinfo.errisinstance(BlackError): - return excinfo.value.args[0].stdout - return super().repr_failure(excinfo) - - def reportinfo(self): - return (self.path, -1, "Black format check") - - def _skip_test(self): - return self._excluded() or (not self._included()) - - def _included(self): - if "include" not in self.pyproject: - return True - return re.search(self.pyproject["include"], str(self.path)) - - def _excluded(self): - if "exclude" not in self.pyproject: - return False - return re.search(self.pyproject["exclude"], str(self.path)) - - def _re_fix_verbose(self, regex): - if "\n" in regex: - regex = "(?x)" + regex - return re.compile(regex) - - -class BlackError(Exception): - pass diff --git a/python_files/tests/pytestadapter/test_discovery.py b/python_files/tests/pytestadapter/test_discovery.py index c2bb5a039850..7ecc891770c7 100644 --- a/python_files/tests/pytestadapter/test_discovery.py +++ b/python_files/tests/pytestadapter/test_discovery.py @@ -476,10 +476,13 @@ def test_config_sub_folder(): expected_discovery_test_output.ruff_test_expected_output, "--ruff", ), - ( + pytest.param( "2496-black-formatter", expected_discovery_test_output.black_formatter_expected_output, "--black", + marks=pytest.mark.skip( + reason="pytest-black does not support pytest 8.1 or newer" + ), ), ], ) From c458145346dba82d5bb75a378971380c66e8eab0 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:10:54 -0700 Subject: [PATCH 11/12] Format pytest discovery test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python_files/tests/pytestadapter/test_discovery.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python_files/tests/pytestadapter/test_discovery.py b/python_files/tests/pytestadapter/test_discovery.py index 7ecc891770c7..7e7336b87c65 100644 --- a/python_files/tests/pytestadapter/test_discovery.py +++ b/python_files/tests/pytestadapter/test_discovery.py @@ -480,9 +480,7 @@ def test_config_sub_folder(): "2496-black-formatter", expected_discovery_test_output.black_formatter_expected_output, "--black", - marks=pytest.mark.skip( - reason="pytest-black does not support pytest 8.1 or newer" - ), + marks=pytest.mark.skip(reason="pytest-black does not support pytest 8.1 or newer"), ), ], ) From 18922ea9158be9d447455f18128627c7828a66ee Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:31:14 -0700 Subject: [PATCH 12/12] Restore pytest plugin autoload CLI option The supported pytest 8.4+ range provides --disable-plugin-autoload, so use the option exercised by the regression test instead of the compatibility environment variable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python_files/tests/pytestadapter/helpers.py | 1 + python_files/tests/pytestadapter/test_coverage.py | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/python_files/tests/pytestadapter/helpers.py b/python_files/tests/pytestadapter/helpers.py index a923d4771557..86f3b127b5fb 100644 --- a/python_files/tests/pytestadapter/helpers.py +++ b/python_files/tests/pytestadapter/helpers.py @@ -343,6 +343,7 @@ def runner_with_cwd_env( sys.executable, "-m", "pytest", + "--disable-plugin-autoload", "-p", "pytest_cov.plugin", "-p", diff --git a/python_files/tests/pytestadapter/test_coverage.py b/python_files/tests/pytestadapter/test_coverage.py index a8f4cb29aab2..f2387527698f 100644 --- a/python_files/tests/pytestadapter/test_coverage.py +++ b/python_files/tests/pytestadapter/test_coverage.py @@ -154,11 +154,7 @@ def test_pytest_cov_manual_plugin_loading(): (pytest_cov) in this scenario. """ args = ["--collect-only"] - env_add = { - "COVERAGE_ENABLED": "True", - "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", - "_PYTEST_MANUAL_PLUGIN_LOAD": "True", - } + env_add = {"COVERAGE_ENABLED": "True", "_PYTEST_MANUAL_PLUGIN_LOAD": "True"} cov_folder_path = TEST_DATA_PATH / "coverage_gen" # Should NOT raise VSCodePytestError about pytest-cov not being installed