From 9693158c090f7532e78348321cc093c45732f30b Mon Sep 17 00:00:00 2001 From: Scaxlibur <51772892+Scaxlibur@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:01:20 +0800 Subject: [PATCH 1/8] build: use uv for plugin editable environments --- scripts/dev_env.py | 30 +++++++++++++++++------------- tests/test_dev_env.py | 25 ++++++++++++++++++++----- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/scripts/dev_env.py b/scripts/dev_env.py index 73b5c31..00933e8 100644 --- a/scripts/dev_env.py +++ b/scripts/dev_env.py @@ -147,19 +147,17 @@ def build_expected_state( } -def build_sync_command(venv_python: Path, state: dict[str, object]) -> list[str]: +def build_sync_command(uv: str, venv_python: Path, state: dict[str, object]) -> list[str]: core = state["wavebench"] assert isinstance(core, dict) plugins = state["plugins"] assert isinstance(plugins, list) command = [ - str(venv_python), - "-m", + uv, "pip", "install", - "--disable-pip-version-check", - "--upgrade-strategy", - "only-if-needed", + "--python", + str(venv_python), ] requirements = state["build_requirements"] assert isinstance(requirements, list) @@ -228,7 +226,8 @@ def _probe_environment(venv_python: Path, state: dict[str, object]) -> dict[str, from importlib import metadata from pathlib import Path from packaging.requirements import Requirement -from urllib.parse import unquote, urlparse +from urllib.parse import urlparse +from urllib.request import url2pathname from wavebench.instruments.registry import build_instrument_registry @@ -262,7 +261,9 @@ def normalize(value): parsed = urlparse(direct_url.get("url", "")) if parsed.scheme != "file": raise RuntimeError(f"{project['distribution']} has a non-file editable source") - installed_path = Path(unquote(parsed.path)).resolve() + installed_path = Path(url2pathname( + f"//{parsed.netloc}{parsed.path}" if parsed.netloc else parsed.path + )).resolve() expected_path = Path(project["path"]).resolve() if installed_path != expected_path: raise RuntimeError( @@ -334,9 +335,12 @@ def sync_environment(args: argparse.Namespace) -> None: wavebench_root = args.wavebench_root.resolve() venv = args.venv.resolve() expected = build_expected_state(repository_root, wavebench_root) + uv = shutil.which("uv") + if uv is None: + raise DevEnvironmentError("uv is required on PATH / PATH 中未找到 uv") created = False if not venv.exists(): - subprocess.run([str(args.python), "-m", "venv", str(venv)], check=True) + subprocess.run([uv, "venv", "--python", str(args.python), str(venv)], check=True) created = True elif not (venv / MARKER_FILE).is_file() and not args.adopt_existing: raise DevEnvironmentError( @@ -349,8 +353,8 @@ def sync_environment(args: argparse.Namespace) -> None: if not venv_python.is_file(): raise DevEnvironmentError(f"virtualenv Python not found / 未找到虚拟环境 Python: {venv}") print( - "Syncing editable WaveBench and plugin packages; pip may access configured indexes.\n" - "正在同步 editable 核心与插件;pip 可能访问已配置的软件源。" + "Syncing editable WaveBench and plugin packages; uv may access configured indexes.\n" + "正在同步 editable 核心与插件;uv 可能访问已配置的软件源。" ) recorded = None if (venv / STATE_FILE).is_file(): @@ -358,10 +362,10 @@ def sync_environment(args: argparse.Namespace) -> None: removed = removed_plugin_distributions(recorded, expected) if removed: subprocess.run( - [str(venv_python), "-m", "pip", "uninstall", "--yes", *removed], + [uv, "pip", "uninstall", "--python", str(venv_python), *removed], check=True, ) - subprocess.run(build_sync_command(venv_python, expected), check=True) + subprocess.run(build_sync_command(uv, venv_python, expected), check=True) probe = _probe_environment(venv_python, expected) (venv / MARKER_FILE).write_text("wavebench-plugin-dev-env-v1\n", encoding="utf-8") _write_json_atomic(venv / STATE_FILE, expected) diff --git a/tests/test_dev_env.py b/tests/test_dev_env.py index 2b3b3dd..ecb8bd5 100644 --- a/tests/test_dev_env.py +++ b/tests/test_dev_env.py @@ -4,6 +4,7 @@ import os from pathlib import Path import sys +import tomllib import pytest @@ -56,7 +57,9 @@ def test_expected_state_tracks_core_and_plugin_metadata(): assert state["schema_version"] == 1 assert state["build_requirements"] == ["hatchling>=1.25"] assert state["wavebench"]["distribution"] == "wavebench" - assert state["wavebench"]["version"] == "0.8.26" + assert state["wavebench"]["version"] == tomllib.loads( + (WAVEBENCH_ROOT / "pyproject.toml").read_text(encoding="utf-8") + )["project"]["version"] assert len(state["wavebench"]["pyproject_sha256"]) == 64 assert len(state["plugins"]) == 11 assert all(len(plugin["pyproject_sha256"]) == 64 for plugin in state["plugins"]) @@ -64,9 +67,9 @@ def test_expected_state_tracks_core_and_plugin_metadata(): def test_sync_command_uses_standard_editable_installs(): state = DEV_ENV.build_expected_state(ROOT, WAVEBENCH_ROOT) - command = DEV_ENV.build_sync_command(Path("/tmp/dev/bin/python"), state) + command = DEV_ENV.build_sync_command("uv", Path("/tmp/dev/bin/python"), state) - assert command[:4] == ["/tmp/dev/bin/python", "-m", "pip", "install"] + assert command[:5] == ["uv", "pip", "install", "--python", "/tmp/dev/bin/python"] assert "hatchling>=1.25" in command editable_targets = [ command[index + 1] for index, item in enumerate(command) if item == "--editable" @@ -89,15 +92,27 @@ def test_sync_command_uses_standard_editable_installs(): assert "--no-index" not in command -def test_rejects_managed_plugin_state(tmp_path): +@pytest.mark.parametrize("state_name", DEV_ENV.MANAGED_STATE_NAMES) +def test_rejects_managed_plugin_state(tmp_path, state_name): state_root = tmp_path / ".wavebench" state_root.mkdir() - (state_root / "plugin-installs-v1.json").write_text("{}\n", encoding="utf-8") + (state_root / state_name).write_text("{}\n", encoding="utf-8") with pytest.raises(DEV_ENV.DevEnvironmentError, match="受管插件账本"): DEV_ENV._assert_not_managed_environment(tmp_path) +def test_missing_uv_does_not_create_environment(tmp_path, monkeypatch): + target = tmp_path / "new environment" + args = DEV_ENV.build_parser().parse_args(["--venv", str(target), "sync"]) + monkeypatch.setattr(DEV_ENV.shutil, "which", lambda _name: None) + + with pytest.raises(DEV_ENV.DevEnvironmentError, match="uv is required"): + DEV_ENV.sync_environment(args) + + assert not target.exists() + + def test_identifies_only_plugins_removed_from_recorded_state(): expected = DEV_ENV.build_expected_state(ROOT, WAVEBENCH_ROOT) recorded = { From d098e1964482945dc148396ca157fc176ab198bf Mon Sep 17 00:00:00 2001 From: Scaxlibur <51772892+Scaxlibur@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:07:58 +0800 Subject: [PATCH 2/8] test: validate uv builds and pip-free plugin lifecycle in CI --- .github/workflows/ci.yml | 56 +++++++++++++++++++ .github/workflows/docs.yml | 12 ++-- .../tests/test_wheel.py | 53 +++++++++++------- .../tests/_rigol_dm3000_wheel_helpers.py | 10 +++- .../tests/test_lifecycle.py | 24 +++++--- .../tests/test_wheel.py | 17 +++--- .../tests/test_lifecycle.py | 33 ++++++++--- .../wavebench-rigol-dp800/tests/test_wheel.py | 15 +++-- .../tests/test_wheel.py | 25 ++++++--- .../tests/test_wheel.py | 25 ++++++--- .../tests/test_wheel.py | 25 ++++++--- .../tests/test_lifecycle.py | 33 ++++++++--- .../tests/test_wheel.py | 15 +++-- .../tests/_shengpu_sp3000a_wheel_helpers.py | 10 +++- .../tests/test_lifecycle.py | 24 +++++--- .../tests/test_wheel.py | 17 +++--- .../tests/test_wheel.py | 25 ++++++--- .../tests/test_wheel.py | 25 ++++++--- .../tests/test_wheel.py | 25 ++++++--- 19 files changed, 335 insertions(+), 134 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7c5de55 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: Plugin lifecycle + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + inputs: + core_ref: + description: WaveBench Core branch or commit with uv lifecycle support + default: Dev + type: string + +permissions: + contents: read + +jobs: + lifecycle: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + python: ["3.11", "3.12"] + env: + PYTHONUTF8: "1" + steps: + - uses: actions/checkout@v4 + with: + path: wavebench-instrument-plugins + - uses: actions/checkout@v4 + with: + repository: Scaxlibur/wavebench + ref: ${{ inputs.core_ref || 'Dev' }} + path: wavebench + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - uses: astral-sh/setup-uv@v9 + with: + version: "0.12.15" + - name: Install editable development environment + working-directory: wavebench-instrument-plugins + run: python scripts/dev_env.py sync + - name: Verify development environment + working-directory: wavebench-instrument-plugins + run: python scripts/dev_env.py check + - name: Check code + working-directory: wavebench-instrument-plugins + run: uv run --no-project --python .venv python -m ruff check . + - name: Test development tools + working-directory: wavebench-instrument-plugins + run: uv run --no-project --python .venv python -m pytest -q tests/test_dev_env.py + - name: Test wheel compatibility and pip-free plugin lifecycle + working-directory: wavebench-instrument-plugins + run: uv run --no-project --python .venv python -m pytest -q packages -k "wheel or lifecycle" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 030d295..0d196d3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -35,8 +35,12 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" - - run: python -m pip install "pytest>=8" - - run: python scripts/generate_plugin_catalog.py --check + - uses: astral-sh/setup-uv@v9 + with: + version: "0.12.15" + - run: uv venv + - run: uv pip install --python .venv "pytest>=8" + - run: uv run --no-project --python .venv python scripts/generate_plugin_catalog.py --check # Errors fail the pilot gate; known migration warnings remain visible. - - run: python scripts/audit_docs.py - - run: python -m pytest -q tests/test_docs_audit.py tests/test_plugin_catalog.py + - run: uv run --no-project --python .venv python scripts/audit_docs.py + - run: uv run --no-project --python .venv python -m pytest -q tests/test_docs_audit.py tests/test_plugin_catalog.py diff --git a/packages/wavebench-rigol-dg4000/tests/test_wheel.py b/packages/wavebench-rigol-dg4000/tests/test_wheel.py index f695b91..cbd3efb 100644 --- a/packages/wavebench-rigol-dg4000/tests/test_wheel.py +++ b/packages/wavebench-rigol-dg4000/tests/test_wheel.py @@ -5,6 +5,7 @@ from pathlib import Path import subprocess import sys +import shutil import sysconfig import tarfile from zipfile import ZipFile @@ -28,15 +29,17 @@ def test_wheel_contains_license_and_expected_entry_points(tmp_path: Path) -> Non wheelhouse.mkdir() _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -77,15 +80,17 @@ def test_wheel_contains_verified_source_conformance_manifests(tmp_path: Path) -> wheelhouse.mkdir() _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -170,7 +175,13 @@ def _write_isolated_runtime_bridge(*, purelib: Path, workspace: Path) -> None: or name.endswith((".dist-info", ".egg-info", ".pth", ".egg")) ): continue - os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + try: + os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + except OSError: + if source.is_dir(): + shutil.copytree(source, bridge / name) + else: + shutil.copy2(source, bridge / name) Path(purelib, "wavebench-test-runtime.pth").write_text( str(Path(wavebench.__file__).resolve().parents[1]) + "\n" + str(bridge) + "\n", encoding="utf-8", @@ -182,15 +193,17 @@ def test_wheel_install_migration_doctor_routing_and_uninstall_fallback(tmp_path: wheelhouse.mkdir() _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -201,7 +214,7 @@ def test_wheel_install_migration_doctor_routing_and_uninstall_fallback(tmp_path: ) venv_dir = tmp_path / "venv" _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, diff --git a/packages/wavebench-rigol-dm3000/tests/_rigol_dm3000_wheel_helpers.py b/packages/wavebench-rigol-dm3000/tests/_rigol_dm3000_wheel_helpers.py index 46623a3..9e66555 100644 --- a/packages/wavebench-rigol-dm3000/tests/_rigol_dm3000_wheel_helpers.py +++ b/packages/wavebench-rigol-dm3000/tests/_rigol_dm3000_wheel_helpers.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import shutil from pathlib import Path import subprocess import sysconfig @@ -19,10 +20,17 @@ def _write_isolated_runtime_bridge(*, purelib: Path, workspace: Path) -> None: name = source.name if ( name.startswith("wavebench") + or name == "pip" or name.endswith((".dist-info", ".egg-info", ".pth", ".egg")) ): continue - os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + try: + os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + except OSError: + if source.is_dir(): + shutil.copytree(source, bridge / name) + else: + shutil.copy2(source, bridge / name) Path(purelib, "wavebench-test-runtime.pth").write_text( str(Path(wavebench.__file__).resolve().parents[1]) + "\n" + str(bridge) + "\n", encoding="utf-8", diff --git a/packages/wavebench-rigol-dm3000/tests/test_lifecycle.py b/packages/wavebench-rigol-dm3000/tests/test_lifecycle.py index 6da5ead..da4b1bd 100644 --- a/packages/wavebench-rigol-dm3000/tests/test_lifecycle.py +++ b/packages/wavebench-rigol-dm3000/tests/test_lifecycle.py @@ -2,6 +2,7 @@ from pathlib import Path import sys +import shutil from _rigol_dm3000_wheel_helpers import _run, _write_isolated_runtime_bridge from wavebench.plugins.lifecycle import PluginLifecycle @@ -14,15 +15,17 @@ def test_managed_install_routes_canonical_and_remove_restores_builtin(tmp_path: wheelhouse.mkdir() _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -30,14 +33,19 @@ def test_managed_install_routes_canonical_and_remove_restores_builtin(tmp_path: ) wheel = next(wheelhouse.glob("wavebench_rigol_dm3000-0.5.0-*.whl")) venv_dir = tmp_path / "venv" - _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + _run([sys.executable, "-m", "venv", "--without-pip", str(venv_dir)], cwd=tmp_path) + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, ).stdout.strip() _write_isolated_runtime_bridge(purelib=Path(purelib), workspace=tmp_path) + _run( + [str(python), "-I", "-c", + "import importlib.util; assert importlib.util.find_spec('pip') is None"], + cwd=tmp_path, + ) lifecycle = PluginLifecycle(python_executable=python) result = lifecycle.install(wheel) installed = lifecycle.info("rigol.dm3000") diff --git a/packages/wavebench-rigol-dm3000/tests/test_wheel.py b/packages/wavebench-rigol-dm3000/tests/test_wheel.py index 378767b..deea6c6 100644 --- a/packages/wavebench-rigol-dm3000/tests/test_wheel.py +++ b/packages/wavebench-rigol-dm3000/tests/test_wheel.py @@ -2,6 +2,7 @@ from pathlib import Path import sys +import shutil from _rigol_dm3000_wheel_helpers import _run, _write_isolated_runtime_bridge @@ -13,15 +14,17 @@ def test_wheel_install_routes_canonical_and_uninstall_restores_builtin(tmp_path: wheelhouse.mkdir() _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -30,7 +33,7 @@ def test_wheel_install_routes_canonical_and_uninstall_restores_builtin(tmp_path: wheel = next(wheelhouse.glob("wavebench_rigol_dm3000-0.5.0-*.whl")) venv_dir = tmp_path / "venv" _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, diff --git a/packages/wavebench-rigol-dp800/tests/test_lifecycle.py b/packages/wavebench-rigol-dp800/tests/test_lifecycle.py index 4b3e97c..6f2d7e8 100644 --- a/packages/wavebench-rigol-dp800/tests/test_lifecycle.py +++ b/packages/wavebench-rigol-dp800/tests/test_lifecycle.py @@ -4,6 +4,7 @@ from pathlib import Path import subprocess import sys +import shutil import sysconfig import wavebench @@ -24,10 +25,17 @@ def _write_isolated_runtime_bridge(*, purelib: Path, workspace: Path) -> None: name = source.name if ( name.startswith("wavebench") + or name == "pip" or name.endswith((".dist-info", ".egg-info", ".pth", ".egg")) ): continue - os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + try: + os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + except OSError: + if source.is_dir(): + shutil.copytree(source, bridge / name) + else: + shutil.copy2(source, bridge / name) Path(purelib, "wavebench-test-runtime.pth").write_text( str(Path(wavebench.__file__).resolve().parents[1]) + "\n" + str(bridge) + "\n", encoding="utf-8", @@ -39,15 +47,17 @@ def test_managed_install_routes_canonical_and_remove_restores_builtin(tmp_path: wheelhouse.mkdir() _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -55,14 +65,19 @@ def test_managed_install_routes_canonical_and_remove_restores_builtin(tmp_path: ) wheel = next(wheelhouse.glob("wavebench_rigol_dp800-0.3.0-*.whl")) venv_dir = tmp_path / "venv" - _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + _run([sys.executable, "-m", "venv", "--without-pip", str(venv_dir)], cwd=tmp_path) + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, ).stdout.strip() _write_isolated_runtime_bridge(purelib=Path(purelib), workspace=tmp_path) + _run( + [str(python), "-I", "-c", + "import importlib.util; assert importlib.util.find_spec('pip') is None"], + cwd=tmp_path, + ) lifecycle = PluginLifecycle(python_executable=python) result = lifecycle.install(wheel) installed = lifecycle.info("rigol.dp800") diff --git a/packages/wavebench-rigol-dp800/tests/test_wheel.py b/packages/wavebench-rigol-dp800/tests/test_wheel.py index 1176e9a..e677e30 100644 --- a/packages/wavebench-rigol-dp800/tests/test_wheel.py +++ b/packages/wavebench-rigol-dp800/tests/test_wheel.py @@ -4,6 +4,7 @@ from pathlib import Path import subprocess import sys +import shutil import tarfile from zipfile import ZipFile @@ -16,15 +17,17 @@ def test_wheel_contains_license_and_single_entry_point(tmp_path: Path) -> None: wheelhouse.mkdir() subprocess.run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], diff --git a/packages/wavebench-rigol-ds1000z/tests/test_wheel.py b/packages/wavebench-rigol-ds1000z/tests/test_wheel.py index 2a60dab..4740d4e 100644 --- a/packages/wavebench-rigol-ds1000z/tests/test_wheel.py +++ b/packages/wavebench-rigol-ds1000z/tests/test_wheel.py @@ -4,6 +4,7 @@ from pathlib import Path import subprocess import sys +import shutil import sysconfig import wavebench @@ -32,7 +33,13 @@ def _write_isolated_runtime_bridge(*, purelib: Path, workspace: Path) -> None: or name.endswith((".dist-info", ".egg-info", ".pth", ".egg")) ): continue - os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + try: + os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + except OSError: + if source.is_dir(): + shutil.copytree(source, bridge / name) + else: + shutil.copy2(source, bridge / name) Path(purelib, "wavebench-test-runtime.pth").write_text( str(Path(wavebench.__file__).resolve().parents[1]) + "\n" + str(bridge) + "\n", encoding="utf-8", @@ -44,15 +51,17 @@ def test_wheel_install_discovery_and_uninstall_without_instrument_io(tmp_path): wheelhouse.mkdir() _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -61,7 +70,7 @@ def test_wheel_install_discovery_and_uninstall_without_instrument_io(tmp_path): plugin_wheel = next(wheelhouse.glob("wavebench_rigol_ds1000z-0.1.0-*.whl")) venv_dir = tmp_path / "venv" _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, diff --git a/packages/wavebench-rigol-dsg830/tests/test_wheel.py b/packages/wavebench-rigol-dsg830/tests/test_wheel.py index 70fe025..0a412fa 100644 --- a/packages/wavebench-rigol-dsg830/tests/test_wheel.py +++ b/packages/wavebench-rigol-dsg830/tests/test_wheel.py @@ -5,6 +5,7 @@ from pathlib import Path import subprocess import sys +import shutil import sysconfig import tarfile from zipfile import ZipFile @@ -23,15 +24,17 @@ def _run(command: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]: def _build_wheel(*, wheelhouse: Path, workspace: Path) -> Path: _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -50,7 +53,13 @@ def _write_isolated_runtime_bridge(*, purelib: Path, workspace: Path) -> None: or name.endswith((".dist-info", ".egg-info", ".pth", ".egg")) ): continue - os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + try: + os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + except OSError: + if source.is_dir(): + shutil.copytree(source, bridge / name) + else: + shutil.copy2(source, bridge / name) Path(purelib, "wavebench-test-runtime.pth").write_text( str(Path(wavebench.__file__).resolve().parents[1]) + "\n" + str(bridge) + "\n", encoding="utf-8", @@ -115,7 +124,7 @@ def test_wheel_install_discovers_descriptor_without_instrument_io(tmp_path: Path plugin_wheel = _build_wheel(wheelhouse=wheelhouse, workspace=tmp_path) venv_dir = tmp_path / "venv" _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, diff --git a/packages/wavebench-rigol-mso8000/tests/test_wheel.py b/packages/wavebench-rigol-mso8000/tests/test_wheel.py index f32b177..3012d48 100644 --- a/packages/wavebench-rigol-mso8000/tests/test_wheel.py +++ b/packages/wavebench-rigol-mso8000/tests/test_wheel.py @@ -5,6 +5,7 @@ from pathlib import Path import subprocess import sys +import shutil import sysconfig import tarfile from zipfile import ZipFile @@ -29,15 +30,17 @@ def _run(command: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]: def _build_wheel(wheelhouse: Path) -> Path: _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -56,7 +59,13 @@ def _write_isolated_runtime_bridge(*, purelib: Path, workspace: Path) -> None: or name.endswith((".dist-info", ".egg-info", ".pth", ".egg")) ): continue - os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + try: + os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + except OSError: + if source.is_dir(): + shutil.copytree(source, bridge / name) + else: + shutil.copy2(source, bridge / name) Path(purelib, "wavebench-test-runtime.pth").write_text( str(Path(wavebench.__file__).resolve().parents[1]) + "\n" + str(bridge) + "\n", encoding="utf-8", @@ -128,7 +137,7 @@ def test_wheel_install_discovery_and_uninstall_without_instrument_io(tmp_path: P wheel = _build_wheel(wheelhouse) venv_dir = tmp_path / "venv" _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, diff --git a/packages/wavebench-rohde-schwarz-rtm2000/tests/test_lifecycle.py b/packages/wavebench-rohde-schwarz-rtm2000/tests/test_lifecycle.py index 504596c..37cb328 100644 --- a/packages/wavebench-rohde-schwarz-rtm2000/tests/test_lifecycle.py +++ b/packages/wavebench-rohde-schwarz-rtm2000/tests/test_lifecycle.py @@ -4,6 +4,7 @@ from pathlib import Path import subprocess import sys +import shutil import sysconfig import wavebench @@ -24,10 +25,17 @@ def _write_isolated_runtime_bridge(*, purelib: Path, workspace: Path) -> None: name = source.name if ( name.startswith("wavebench") + or name == "pip" or name.endswith((".dist-info", ".egg-info", ".pth", ".egg")) ): continue - os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + try: + os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + except OSError: + if source.is_dir(): + shutil.copytree(source, bridge / name) + else: + shutil.copy2(source, bridge / name) Path(purelib, "wavebench-test-runtime.pth").write_text( str(Path(wavebench.__file__).resolve().parents[1]) + "\n" + str(bridge) + "\n", encoding="utf-8", @@ -39,15 +47,17 @@ def test_managed_install_routes_canonical_and_remove_restores_builtin(tmp_path: wheelhouse.mkdir() _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -55,14 +65,19 @@ def test_managed_install_routes_canonical_and_remove_restores_builtin(tmp_path: ) wheel = next(wheelhouse.glob("wavebench_rohde_schwarz_rtm2000-0.15.0-*.whl")) venv_dir = tmp_path / "venv" - _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + _run([sys.executable, "-m", "venv", "--without-pip", str(venv_dir)], cwd=tmp_path) + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, ).stdout.strip() _write_isolated_runtime_bridge(purelib=Path(purelib), workspace=tmp_path) + _run( + [str(python), "-I", "-c", + "import importlib.util; assert importlib.util.find_spec('pip') is None"], + cwd=tmp_path, + ) lifecycle = PluginLifecycle(python_executable=python) assert lifecycle.install(wheel).status == "installed" assert lifecycle.info("rohde-schwarz.rtm2032").status == "healthy" diff --git a/packages/wavebench-rohde-schwarz-rtm2000/tests/test_wheel.py b/packages/wavebench-rohde-schwarz-rtm2000/tests/test_wheel.py index d65d465..a27b033 100644 --- a/packages/wavebench-rohde-schwarz-rtm2000/tests/test_wheel.py +++ b/packages/wavebench-rohde-schwarz-rtm2000/tests/test_wheel.py @@ -4,6 +4,7 @@ from pathlib import Path import subprocess import sys +import shutil import tarfile from zipfile import ZipFile @@ -40,15 +41,17 @@ def test_wheel_contains_license_and_single_entry_point(tmp_path: Path) -> None: wheelhouse.mkdir() subprocess.run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], diff --git a/packages/wavebench-shengpu-sp3000a/tests/_shengpu_sp3000a_wheel_helpers.py b/packages/wavebench-shengpu-sp3000a/tests/_shengpu_sp3000a_wheel_helpers.py index 46623a3..9e66555 100644 --- a/packages/wavebench-shengpu-sp3000a/tests/_shengpu_sp3000a_wheel_helpers.py +++ b/packages/wavebench-shengpu-sp3000a/tests/_shengpu_sp3000a_wheel_helpers.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import shutil from pathlib import Path import subprocess import sysconfig @@ -19,10 +20,17 @@ def _write_isolated_runtime_bridge(*, purelib: Path, workspace: Path) -> None: name = source.name if ( name.startswith("wavebench") + or name == "pip" or name.endswith((".dist-info", ".egg-info", ".pth", ".egg")) ): continue - os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + try: + os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + except OSError: + if source.is_dir(): + shutil.copytree(source, bridge / name) + else: + shutil.copy2(source, bridge / name) Path(purelib, "wavebench-test-runtime.pth").write_text( str(Path(wavebench.__file__).resolve().parents[1]) + "\n" + str(bridge) + "\n", encoding="utf-8", diff --git a/packages/wavebench-shengpu-sp3000a/tests/test_lifecycle.py b/packages/wavebench-shengpu-sp3000a/tests/test_lifecycle.py index e0601b3..3b95cf8 100644 --- a/packages/wavebench-shengpu-sp3000a/tests/test_lifecycle.py +++ b/packages/wavebench-shengpu-sp3000a/tests/test_lifecycle.py @@ -2,6 +2,7 @@ from pathlib import Path import sys +import shutil from _shengpu_sp3000a_wheel_helpers import _run, _write_isolated_runtime_bridge from wavebench.plugins.lifecycle import PluginLifecycle @@ -14,15 +15,17 @@ def test_managed_install_healthy_load_and_remove_round_trip(tmp_path: Path) -> N wheelhouse.mkdir() _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -30,14 +33,19 @@ def test_managed_install_healthy_load_and_remove_round_trip(tmp_path: Path) -> N ) wheel = next(wheelhouse.glob("wavebench_shengpu_sp3000a-0.2.0-*.whl")) venv_dir = tmp_path / "venv" - _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + _run([sys.executable, "-m", "venv", "--without-pip", str(venv_dir)], cwd=tmp_path) + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, ).stdout.strip() _write_isolated_runtime_bridge(purelib=Path(purelib), workspace=tmp_path) + _run( + [str(python), "-I", "-c", + "import importlib.util; assert importlib.util.find_spec('pip') is None"], + cwd=tmp_path, + ) lifecycle = PluginLifecycle(python_executable=python) result = lifecycle.install(wheel) installed = lifecycle.info("shengpu.sp30120") diff --git a/packages/wavebench-shengpu-sp3000a/tests/test_wheel.py b/packages/wavebench-shengpu-sp3000a/tests/test_wheel.py index 2dcc456..13db833 100644 --- a/packages/wavebench-shengpu-sp3000a/tests/test_wheel.py +++ b/packages/wavebench-shengpu-sp3000a/tests/test_wheel.py @@ -2,6 +2,7 @@ from pathlib import Path import sys +import shutil import tarfile from _shengpu_sp3000a_wheel_helpers import _run, _write_isolated_runtime_bridge @@ -35,15 +36,17 @@ def test_wheel_install_discovery_and_uninstall_without_instrument_io(tmp_path: P wheelhouse.mkdir() _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -52,7 +55,7 @@ def test_wheel_install_discovery_and_uninstall_without_instrument_io(tmp_path: P plugin_wheel = next(wheelhouse.glob("wavebench_shengpu_sp3000a-0.2.0-*.whl")) venv_dir = tmp_path / "venv" _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, diff --git a/packages/wavebench-siglent-sdg2000x/tests/test_wheel.py b/packages/wavebench-siglent-sdg2000x/tests/test_wheel.py index e6d2d14..059da8e 100644 --- a/packages/wavebench-siglent-sdg2000x/tests/test_wheel.py +++ b/packages/wavebench-siglent-sdg2000x/tests/test_wheel.py @@ -5,6 +5,7 @@ from pathlib import Path import subprocess import sys +import shutil import sysconfig import tarfile import tomllib @@ -29,15 +30,17 @@ def _run(command: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]: def _build_wheel(wheelhouse: Path) -> Path: _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -56,7 +59,13 @@ def _write_isolated_runtime_bridge(*, purelib: Path, workspace: Path) -> None: or name.endswith((".dist-info", ".egg-info", ".pth", ".egg")) ): continue - os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + try: + os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + except OSError: + if source.is_dir(): + shutil.copytree(source, bridge / name) + else: + shutil.copy2(source, bridge / name) Path(purelib, "wavebench-test-runtime.pth").write_text( str(Path(wavebench.__file__).resolve().parents[1]) + "\n" + str(bridge) + "\n", encoding="utf-8", @@ -191,7 +200,7 @@ def test_wheel_install_discovery_and_uninstall_without_instrument_io(tmp_path: P plugin_wheel = _build_wheel(wheelhouse) venv_dir = tmp_path / "venv" _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, diff --git a/packages/wavebench-siglent-sds3000/tests/test_wheel.py b/packages/wavebench-siglent-sds3000/tests/test_wheel.py index b1aac9b..6250c35 100644 --- a/packages/wavebench-siglent-sds3000/tests/test_wheel.py +++ b/packages/wavebench-siglent-sds3000/tests/test_wheel.py @@ -5,6 +5,7 @@ from pathlib import Path import subprocess import sys +import shutil import sysconfig import tarfile from zipfile import ZipFile @@ -35,7 +36,13 @@ def _write_isolated_runtime_bridge(*, purelib: Path, workspace: Path) -> None: (".dist-info", ".egg-info", ".pth", ".egg") ): continue - os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + try: + os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + except OSError: + if source.is_dir(): + shutil.copytree(source, bridge / name) + else: + shutil.copy2(source, bridge / name) Path(purelib, "wavebench-test-runtime.pth").write_text( str(Path(wavebench.__file__).resolve().parents[1]) + "\n" + str(bridge) + "\n", encoding="utf-8", @@ -46,15 +53,17 @@ def _build_wheel(destination: Path) -> Path: destination.mkdir() _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(destination), str(PACKAGE_ROOT), ], @@ -142,7 +151,7 @@ def test_install_discovery_reinstall_and_uninstall_without_io(tmp_path: Path) -> wheel = _build_wheel(tmp_path / "wheelhouse") venv_dir = tmp_path / "venv" _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, diff --git a/packages/wavebench-siglent-sds800x-hd/tests/test_wheel.py b/packages/wavebench-siglent-sds800x-hd/tests/test_wheel.py index 3935984..804af61 100644 --- a/packages/wavebench-siglent-sds800x-hd/tests/test_wheel.py +++ b/packages/wavebench-siglent-sds800x-hd/tests/test_wheel.py @@ -4,6 +4,7 @@ from pathlib import Path import subprocess import sys +import shutil import sysconfig import wavebench @@ -32,7 +33,13 @@ def _write_isolated_runtime_bridge(*, purelib: Path, workspace: Path) -> None: or name.endswith((".dist-info", ".egg-info", ".pth", ".egg")) ): continue - os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + try: + os.symlink(source, bridge / name, target_is_directory=source.is_dir()) + except OSError: + if source.is_dir(): + shutil.copytree(source, bridge / name) + else: + shutil.copy2(source, bridge / name) Path(purelib, "wavebench-test-runtime.pth").write_text( str(Path(wavebench.__file__).resolve().parents[1]) + "\n" + str(bridge) + "\n", encoding="utf-8", @@ -46,15 +53,17 @@ def test_wheel_install_discovery_reinstall_and_uninstall_without_instrument_io( wheelhouse.mkdir() _run( [ + shutil.which("uv"), + "--no-config", + "--offline", + "--no-python-downloads", + "build", + "--wheel", + "--python", sys.executable, - "-m", - "pip", - "wheel", "--no-build-isolation", - "--no-deps", "--no-index", - "--disable-pip-version-check", - "--wheel-dir", + "--out-dir", str(wheelhouse), str(PACKAGE_ROOT), ], @@ -63,7 +72,7 @@ def test_wheel_install_discovery_reinstall_and_uninstall_without_instrument_io( plugin_wheel = next(wheelhouse.glob("wavebench_siglent_sds800x_hd-0.6.1-*.whl")) venv_dir = tmp_path / "venv" _run([sys.executable, "-m", "venv", str(venv_dir)], cwd=tmp_path) - python = venv_dir / "bin" / "python" + python = venv_dir / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") purelib = _run( [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], cwd=tmp_path, From 6c7900825391e03e7b77fd028407795604330b15 Mon Sep 17 00:00:00 2001 From: Scaxlibur <51772892+Scaxlibur@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:08:38 +0800 Subject: [PATCH 3/8] docs: explain uv plugin development and lifecycle validation --- .../references/packaging-and-lifecycle.md | 2 +- doc/DEVELOPMENT.md | 19 ++++++++++++++++--- doc/DEVELOPMENT_EN.md | 14 +++++++++++--- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.agents/skills/wavebench-plugin-development/references/packaging-and-lifecycle.md b/.agents/skills/wavebench-plugin-development/references/packaging-and-lifecycle.md index 6c7a8dd..6a4e142 100644 --- a/.agents/skills/wavebench-plugin-development/references/packaging-and-lifecycle.md +++ b/.agents/skills/wavebench-plugin-development/references/packaging-and-lifecycle.md @@ -16,7 +16,7 @@ python3 scripts/dev_env.py check python3 scripts/dev_env.py sync ``` -`sync` 会创建或修改 `.venv`,以 editable 方式安装相邻 Core 与所有正式包,且 pip 可能访问已配置的软件源。运行前必须说明这些影响。已有但未标记的环境只能在审阅后显式接管;不要绕过脚本对 WaveBench 受管插件账本或未完成事务的拒绝。 +`sync` 会创建或修改 `.venv`,以 editable 方式安装相邻 Core 与所有正式包,通过 PATH 中的 uv 创建环境并安装,可能访问已配置的软件源。运行前必须说明这些影响。已有但未标记的环境只能在审阅后显式接管;不要绕过脚本对 WaveBench 受管插件账本或未完成事务的拒绝。 开发环境只提供快速反馈,不是发行验收环境,也不能与受管 install/remove 流程混用。 diff --git a/doc/DEVELOPMENT.md b/doc/DEVELOPMENT.md index 2a106e3..51d9ec7 100644 --- a/doc/DEVELOPMENT.md +++ b/doc/DEVELOPMENT.md @@ -6,6 +6,8 @@ ## 首次同步 +需要 Python 3.11+ 和 PATH 中可用的 [uv](https://docs.astral.sh/uv/getting-started/installation/)。 + 默认目录布局为相邻的 `wavebench/` 和 `wavebench-instrument-plugins/`: ```bash @@ -13,7 +15,7 @@ cd wavebench-instrument-plugins python3 scripts/dev_env.py sync ``` -脚本会创建仓库内被忽略的 `.venv/`,汇总并安装各项目声明的 build-system 依赖,安装 `wavebench[dev]` 和所有正式插件的 editable metadata,并通过真实 `wavebench.instruments` entry point 与 registry 解析验证环境。`sync` 会调用 pip,首次运行可能访问配置的软件源并安装依赖;它不是离线发布门禁。 +脚本会创建仓库内被忽略的 `.venv/`,汇总并安装各项目声明的 build-system 依赖,安装 `wavebench[dev]` 和所有正式插件的 editable metadata,并通过真实 `wavebench.instruments` entry point 与 registry 解析验证环境。`sync` 使用 `uv venv` 和 `uv pip install`,首次运行可能访问配置的软件源并安装依赖;它不是离线发布门禁。 若核心仓库不在默认相邻位置,应显式指定: @@ -24,8 +26,8 @@ python3 scripts/dev_env.py --wavebench-root sync ## 日常开发 ```bash -.venv/bin/python -m pytest -.venv/bin/python -m ruff check . +uv run --no-project --python .venv python -m pytest +uv run --no-project --python .venv python -m ruff check . python3 scripts/dev_env.py check ``` @@ -38,8 +40,19 @@ python3 scripts/dev_env.py check `check` 不安装软件、不访问仪器,也不联网;它会比较已记录的项目元数据,确认核心和插件仍为 editable 安装,要求仪器 entry point 集合与当前正式插件集合精确一致,并通过 WaveBench registry 解析每个 canonical driver ID。再次运行 `sync` 时,脚本会依据这个专用环境自己的旧状态记录卸载已经从仓库移除的正式插件。只有文档、没有 `pyproject.toml` 的孵化目录会被明确跳过。 +根目录不维护独立的 uv project 或 lock;脚本按 Core 与各插件的声明解析 editable 开发依赖, +不等同于 Core 的锁定 CI 环境。不要在此环境执行 Core 的 `uv sync`,否则可能移除插件。 +脚本仅卸载自身旧状态中已从仓库移除的插件,并保留对受管账本的拒绝检查。 + ## 与正式验收的边界 editable 环境仅用于开发反馈,不代表可发布验收。提交或发布前仍需使用 WaveBench 的 `plugin package check`、真实 wheel、一次性虚拟环境和受管 install/remove 生命周期门禁。不要在同一虚拟环境中混用 editable 安装与 WaveBench 受管插件账本;脚本检测到账本或未完成事务时会拒绝操作。 插件 descriptor 导入仍不得打开 transport。默认测试不得扫描端口、连接仪器或发送 SCPI;实机测试必须另行获得明确授权并使用脱敏配置。 + +## 跨仓 CI + +`Plugin lifecycle` workflow 在 Linux/Windows 和 Python 3.11/3.12 上检查开发环境与 +四类插件的无 pip 生命周期。默认使用相邻 Core 的 `Dev` 分支,该分支须已包含 uv 安装后端; +迁移阶段可通过手动运行的 `core_ref` 输入指定 Core 分支或提交。先合入 Core 后端,再启用插件仓的对应变更。 +wheel 安装兼容测试继续使用 pip;构建步骤使用 uv,并保留既有 wheel 内容和证据摘要检查。 diff --git a/doc/DEVELOPMENT_EN.md b/doc/DEVELOPMENT_EN.md index edf96b7..bf586b6 100644 --- a/doc/DEVELOPMENT_EN.md +++ b/doc/DEVELOPMENT_EN.md @@ -6,6 +6,8 @@ This repository uses one long-lived, dedicated development virtual environment. ## First synchronization +Python 3.11+ and [uv](https://docs.astral.sh/uv/getting-started/installation/) on PATH are required. + The default layout expects sibling `wavebench/` and `wavebench-instrument-plugins/` directories: ```bash @@ -13,7 +15,7 @@ cd wavebench-instrument-plugins python3 scripts/dev_env.py sync ``` -The script creates the ignored repository-local `.venv/`, collects and installs each project's declared build-system requirements, installs `wavebench[dev]` and every maintained plugin as editable distributions, then verifies their real `wavebench.instruments` entry points and registry resolution. `sync` invokes pip and may access configured package indexes to install dependencies on its first run; it is not an offline release gate. +The script creates the ignored repository-local `.venv/`, collects and installs each project's declared build-system requirements, installs `wavebench[dev]` and every maintained plugin as editable distributions, then verifies their real `wavebench.instruments` entry points and registry resolution. `sync` uses `uv venv` and `uv pip install` and may access configured package indexes to install dependencies on its first run; it is not an offline release gate. Specify a non-sibling core checkout explicitly: @@ -24,8 +26,8 @@ python3 scripts/dev_env.py --wavebench-root sync ## Daily development ```bash -.venv/bin/python -m pytest -.venv/bin/python -m ruff check . +uv run --no-project --python .venv python -m pytest +uv run --no-project --python .venv python -m ruff check . python3 scripts/dev_env.py check ``` @@ -38,8 +40,14 @@ Source edits do not require another `sync`. Synchronize again only when: `check` does not install software, access instruments, or use the network. It compares recorded project metadata, verifies editable core and plugin distributions, requires the instrument entry-point set to match the current maintained-plugin set exactly, and resolves every canonical driver ID through the WaveBench registry. On a later `sync`, the script uses this dedicated environment's own previous state record to uninstall maintained plugins that were removed from the repository. Documentation-only incubation directories without `pyproject.toml` are deliberately skipped. +The repository root is not a uv project and has no separate lockfile. The script resolves the declared Core and plugin requirements for editable development; this is not the Core locked CI environment. Do not run Core `uv sync` against this environment: it may remove plugins. The script only removes plugins listed in its previous state that no longer exist in the repository, and still rejects managed runtime ledgers. + ## Boundary with release acceptance The editable environment is a development feedback loop, not release acceptance. Before committing or publishing a plugin, continue to use WaveBench `plugin package check`, a real wheel, a disposable virtual environment, and the managed install/remove lifecycle gate. Do not mix editable installs and the WaveBench managed-plugin ledger in one environment; the script refuses to operate when it detects a ledger or unfinished transaction. Importing a plugin descriptor must still perform no transport I/O. Default tests must not scan ports, connect to instruments, or send SCPI. Hardware tests require separate explicit authorization and sanitized configuration. + +## Cross-repository CI + +The `Plugin lifecycle` workflow checks development setup and four plugin types without pip on Linux/Windows with Python 3.11/3.12. It uses Core `Dev` by default, which must contain the uv lifecycle backend. During migration, use the manual `core_ref` input to select a Core branch or commit. Land the Core backend before the corresponding plugin repository changes. Wheel installation compatibility tests keep pip; build steps use uv and retain wheel-content and evidence-digest checks. From f0abd898424922085698d29ac688cc1bf44a2626 Mon Sep 17 00:00:00 2001 From: Scaxlibur <51772892+Scaxlibur@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:24:21 +0800 Subject: [PATCH 4/8] docs: clarify uv setup and cross-platform development --- .../skills/wavebench-plugin-development/SKILL.md | 4 +++- .../references/development-validation.md | 2 +- .../references/packaging-and-lifecycle.md | 4 ++-- doc/DEVELOPMENT.md | 14 ++++++++++---- doc/DEVELOPMENT_EN.md | 14 ++++++++++---- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.agents/skills/wavebench-plugin-development/SKILL.md b/.agents/skills/wavebench-plugin-development/SKILL.md index 0af6f8a..00bcd00 100644 --- a/.agents/skills/wavebench-plugin-development/SKILL.md +++ b/.agents/skills/wavebench-plugin-development/SKILL.md @@ -26,6 +26,8 @@ metadata: 本流程面向 Codex 或兼容 Agent Skills 的宿主,仓库要求 Python 3.11+。验证使用已同步的 WaveBench 开发环境;实时仪器工作还需要明确授权、已确认接线和 Core 安全流程。 +命令中的 `python` 指 Python 3.11+;仅提供 `python3` 的系统可替换命令名。虚拟环境示例使用 POSIX 的 `.venv/bin/python`,原生 Windows 替换为 `.venv\Scripts\python.exe`。 + ## Invocation boundary 仅在插件仓的生产交付物是主要任务时使用本 Skill,例如修改 `packages/wavebench-*` 下的 driver、descriptor、parser、profile、测试或打包配置,或者修改仓库级开发、目录生成、conformance 与实机证据工具。 @@ -92,7 +94,7 @@ metadata: 按改动风险逐层增加验证,不把全仓测试当作每次修改的起点: ```bash -python3 scripts/dev_env.py check +python scripts/dev_env.py check .venv/bin/python -m pytest -q packages//tests/.py .venv/bin/python -m ruff check python scripts/generate_plugin_catalog.py --check diff --git a/.agents/skills/wavebench-plugin-development/references/development-validation.md b/.agents/skills/wavebench-plugin-development/references/development-validation.md index 3cc4ed7..7aed011 100644 --- a/.agents/skills/wavebench-plugin-development/references/development-validation.md +++ b/.agents/skills/wavebench-plugin-development/references/development-validation.md @@ -57,7 +57,7 @@ 根据变更逐级运行,前一级失败时先停止并处理根因: ```bash -python3 scripts/dev_env.py check +python scripts/dev_env.py check .venv/bin/python -m pytest -q packages//tests/.py .venv/bin/python -m pytest -q packages//tests .venv/bin/python -m ruff check diff --git a/.agents/skills/wavebench-plugin-development/references/packaging-and-lifecycle.md b/.agents/skills/wavebench-plugin-development/references/packaging-and-lifecycle.md index 6a4e142..186eefc 100644 --- a/.agents/skills/wavebench-plugin-development/references/packaging-and-lifecycle.md +++ b/.agents/skills/wavebench-plugin-development/references/packaging-and-lifecycle.md @@ -7,13 +7,13 @@ 先检查现有标准环境: ```bash -python3 scripts/dev_env.py check +python scripts/dev_env.py check ``` `check` 验证 Core 与所有正式插件的 metadata 快照、editable 安装和 entry point,不访问软件源,也不连接仪器。失败信息如果说明包集合或 metadata 漂移,再考虑: ```bash -python3 scripts/dev_env.py sync +python scripts/dev_env.py sync ``` `sync` 会创建或修改 `.venv`,以 editable 方式安装相邻 Core 与所有正式包,通过 PATH 中的 uv 创建环境并安装,可能访问已配置的软件源。运行前必须说明这些影响。已有但未标记的环境只能在审阅后显式接管;不要绕过脚本对 WaveBench 受管插件账本或未完成事务的拒绝。 diff --git a/doc/DEVELOPMENT.md b/doc/DEVELOPMENT.md index 51d9ec7..4f3b390 100644 --- a/doc/DEVELOPMENT.md +++ b/doc/DEVELOPMENT.md @@ -8,19 +8,21 @@ 需要 Python 3.11+ 和 PATH 中可用的 [uv](https://docs.astral.sh/uv/getting-started/installation/)。 +下文的 `python` 指 Python 3.11+,Bash 与 PowerShell 使用相同命令;若系统仅提供 `python3`,替换该命令名即可。uv 不读取 `pip.conf` 或 `PIP_INDEX_URL`;使用镜像或私有软件源时,先配置 [uv 软件源](https://docs.astral.sh/uv/concepts/indexes/)。 + 默认目录布局为相邻的 `wavebench/` 和 `wavebench-instrument-plugins/`: ```bash cd wavebench-instrument-plugins -python3 scripts/dev_env.py sync +python scripts/dev_env.py sync ``` -脚本会创建仓库内被忽略的 `.venv/`,汇总并安装各项目声明的 build-system 依赖,安装 `wavebench[dev]` 和所有正式插件的 editable metadata,并通过真实 `wavebench.instruments` entry point 与 registry 解析验证环境。`sync` 使用 `uv venv` 和 `uv pip install`,首次运行可能访问配置的软件源并安装依赖;它不是离线发布门禁。 +脚本会创建仓库内被忽略的 `.venv/`,汇总并安装各项目声明的 build-system 依赖,以 editable 方式安装 Core 和所有正式插件及各自的 `[dev]` extras,并通过真实 `wavebench.instruments` entry point 与 registry 解析验证环境。`sync` 使用 `uv venv` 和 `uv pip install`,首次运行可能访问配置的软件源并安装依赖;它不是离线发布门禁。 若核心仓库不在默认相邻位置,应显式指定: ```bash -python3 scripts/dev_env.py --wavebench-root sync +python scripts/dev_env.py --wavebench-root sync ``` ## 日常开发 @@ -28,7 +30,7 @@ python3 scripts/dev_env.py --wavebench-root sync ```bash uv run --no-project --python .venv python -m pytest uv run --no-project --python .venv python -m ruff check . -python3 scripts/dev_env.py check +python scripts/dev_env.py check ``` 源码修改不需要重新运行 `sync`。只有下列内容变化时才需再次同步: @@ -44,10 +46,14 @@ python3 scripts/dev_env.py check 不等同于 Core 的锁定 CI 环境。不要在此环境执行 Core 的 `uv sync`,否则可能移除插件。 脚本仅卸载自身旧状态中已从仓库移除的插件,并保留对受管账本的拒绝检查。 +已有的标记开发环境可继续同步;未标记环境须审阅后显式传入 `sync --adopt-existing`。接管不会清理任意旧包;需要干净环境时,通过 `--venv <新目录>` 创建独立环境,不覆盖原环境。 + ## 与正式验收的边界 editable 环境仅用于开发反馈,不代表可发布验收。提交或发布前仍需使用 WaveBench 的 `plugin package check`、真实 wheel、一次性虚拟环境和受管 install/remove 生命周期门禁。不要在同一虚拟环境中混用 editable 安装与 WaveBench 受管插件账本;脚本检测到账本或未完成事务时会拒绝操作。 +uv 插件管理后端要求包含该变更的 Core `Dev`,操作与运行环境边界见 [Core 插件管理说明](https://github.com/Scaxlibur/wavebench/blob/Dev/docs/how-to/manage-plugins.md)。 + 插件 descriptor 导入仍不得打开 transport。默认测试不得扫描端口、连接仪器或发送 SCPI;实机测试必须另行获得明确授权并使用脱敏配置。 ## 跨仓 CI diff --git a/doc/DEVELOPMENT_EN.md b/doc/DEVELOPMENT_EN.md index bf586b6..7c24b3e 100644 --- a/doc/DEVELOPMENT_EN.md +++ b/doc/DEVELOPMENT_EN.md @@ -8,19 +8,21 @@ This repository uses one long-lived, dedicated development virtual environment. Python 3.11+ and [uv](https://docs.astral.sh/uv/getting-started/installation/) on PATH are required. +Here, `python` means Python 3.11+; the commands work in Bash and PowerShell. Use `python3` instead if that is the only command available on the system. uv does not read `pip.conf` or `PIP_INDEX_URL`; configure [uv indexes](https://docs.astral.sh/uv/concepts/indexes/) first when a mirror or private index is required. + The default layout expects sibling `wavebench/` and `wavebench-instrument-plugins/` directories: ```bash cd wavebench-instrument-plugins -python3 scripts/dev_env.py sync +python scripts/dev_env.py sync ``` -The script creates the ignored repository-local `.venv/`, collects and installs each project's declared build-system requirements, installs `wavebench[dev]` and every maintained plugin as editable distributions, then verifies their real `wavebench.instruments` entry points and registry resolution. `sync` uses `uv venv` and `uv pip install` and may access configured package indexes to install dependencies on its first run; it is not an offline release gate. +The script creates the ignored repository-local `.venv/`, collects and installs each project's declared build-system requirements, installs Core and every maintained plugin as editable distributions with their respective `[dev]` extras, then verifies their real `wavebench.instruments` entry points and registry resolution. `sync` uses `uv venv` and `uv pip install` and may access configured package indexes to install dependencies on its first run; it is not an offline release gate. Specify a non-sibling core checkout explicitly: ```bash -python3 scripts/dev_env.py --wavebench-root sync +python scripts/dev_env.py --wavebench-root sync ``` ## Daily development @@ -28,7 +30,7 @@ python3 scripts/dev_env.py --wavebench-root sync ```bash uv run --no-project --python .venv python -m pytest uv run --no-project --python .venv python -m ruff check . -python3 scripts/dev_env.py check +python scripts/dev_env.py check ``` Source edits do not require another `sync`. Synchronize again only when: @@ -42,10 +44,14 @@ Source edits do not require another `sync`. Synchronize again only when: The repository root is not a uv project and has no separate lockfile. The script resolves the declared Core and plugin requirements for editable development; this is not the Core locked CI environment. Do not run Core `uv sync` against this environment: it may remove plugins. The script only removes plugins listed in its previous state that no longer exist in the repository, and still rejects managed runtime ledgers. +An existing marked development environment can be synchronized again. An unmarked environment requires review and explicit `sync --adopt-existing`. Adoption does not remove arbitrary old packages. Use `--venv ` for a clean, separate environment without replacing the original. + ## Boundary with release acceptance The editable environment is a development feedback loop, not release acceptance. Before committing or publishing a plugin, continue to use WaveBench `plugin package check`, a real wheel, a disposable virtual environment, and the managed install/remove lifecycle gate. Do not mix editable installs and the WaveBench managed-plugin ledger in one environment; the script refuses to operate when it detects a ledger or unfinished transaction. +The uv plugin backend requires Core `Dev` containing that change; see the [Core plugin management guide](https://github.com/Scaxlibur/wavebench/blob/Dev/docs/how-to/manage-plugins.md) for runtime setup and operation boundaries. + Importing a plugin descriptor must still perform no transport I/O. Default tests must not scan ports, connect to instruments, or send SCPI. Hardware tests require separate explicit authorization and sanitized configuration. ## Cross-repository CI From 00a702f1630dfbf1f087a35e271bac26dd17bdf2 Mon Sep 17 00:00:00 2001 From: Scaxlibur <51772892+Scaxlibur@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:38:44 +0800 Subject: [PATCH 5/8] ci: use published setup-uv release tag --- .github/workflows/ci.yml | 2 +- .github/workflows/docs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c5de55..2dbc884 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - - uses: astral-sh/setup-uv@v9 + - uses: astral-sh/setup-uv@v9.0.0 with: version: "0.12.15" - name: Install editable development environment diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0d196d3..97d0023 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -35,7 +35,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" - - uses: astral-sh/setup-uv@v9 + - uses: astral-sh/setup-uv@v9.0.0 with: version: "0.12.15" - run: uv venv From 09159685679f599f94c7001272dcd72930e62151 Mon Sep 17 00:00:00 2001 From: Scaxlibur <51772892+Scaxlibur@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:46:16 +0800 Subject: [PATCH 6/8] test: use native paths in development environment assertions --- tests/test_dev_env.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_dev_env.py b/tests/test_dev_env.py index ecb8bd5..c6db548 100644 --- a/tests/test_dev_env.py +++ b/tests/test_dev_env.py @@ -65,16 +65,17 @@ def test_expected_state_tracks_core_and_plugin_metadata(): assert all(len(plugin["pyproject_sha256"]) == 64 for plugin in state["plugins"]) -def test_sync_command_uses_standard_editable_installs(): +def test_sync_command_uses_standard_editable_installs(tmp_path): state = DEV_ENV.build_expected_state(ROOT, WAVEBENCH_ROOT) - command = DEV_ENV.build_sync_command("uv", Path("/tmp/dev/bin/python"), state) + python = tmp_path / "python" + command = DEV_ENV.build_sync_command("uv", python, state) - assert command[:5] == ["uv", "pip", "install", "--python", "/tmp/dev/bin/python"] + assert command[:5] == ["uv", "pip", "install", "--python", str(python)] assert "hatchling>=1.25" in command editable_targets = [ command[index + 1] for index, item in enumerate(command) if item == "--editable" ] - assert editable_targets[0].endswith("/wavebench[dev]") + assert editable_targets[0] == f"{WAVEBENCH_ROOT}[dev]" assert editable_targets[1:] == [ f"{ROOT / 'packages/wavebench-rigol-dg4000'}[dev]", f"{ROOT / 'packages/wavebench-rigol-dm3000'}[dev]", From 91f58fe3af3719f6a912d8dd10860d3f17801ba7 Mon Sep 17 00:00:00 2001 From: Scaxlibur <51772892+Scaxlibur@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:51:43 +0800 Subject: [PATCH 7/8] build: preserve wheel evidence across platform line endings --- .gitattributes | 1 + doc/DEVELOPMENT.md | 2 ++ doc/DEVELOPMENT_EN.md | 2 ++ 3 files changed, 5 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/doc/DEVELOPMENT.md b/doc/DEVELOPMENT.md index 4f3b390..bcfce59 100644 --- a/doc/DEVELOPMENT.md +++ b/doc/DEVELOPMENT.md @@ -54,6 +54,8 @@ editable 环境仅用于开发反馈,不代表可发布验收。提交或发 uv 插件管理后端要求包含该变更的 Core `Dev`,操作与运行环境边界见 [Core 插件管理说明](https://github.com/Scaxlibur/wavebench/blob/Dev/docs/how-to/manage-plugins.md)。 +`.gitattributes` 将文本文件的检出换行固定为 LF,保证 Windows 与 Linux 构建的源码和许可证字节一致。wheel 证据摘要绑定这些字节,构建前不应另外转换换行。 + 插件 descriptor 导入仍不得打开 transport。默认测试不得扫描端口、连接仪器或发送 SCPI;实机测试必须另行获得明确授权并使用脱敏配置。 ## 跨仓 CI diff --git a/doc/DEVELOPMENT_EN.md b/doc/DEVELOPMENT_EN.md index 7c24b3e..793a88d 100644 --- a/doc/DEVELOPMENT_EN.md +++ b/doc/DEVELOPMENT_EN.md @@ -52,6 +52,8 @@ The editable environment is a development feedback loop, not release acceptance. The uv plugin backend requires Core `Dev` containing that change; see the [Core plugin management guide](https://github.com/Scaxlibur/wavebench/blob/Dev/docs/how-to/manage-plugins.md) for runtime setup and operation boundaries. +`.gitattributes` fixes text checkout line endings to LF so Windows and Linux builds preserve identical source and license bytes. Wheel evidence digests bind those bytes; do not convert line endings separately before building. + Importing a plugin descriptor must still perform no transport I/O. Default tests must not scan ports, connect to instruments, or send SCPI. Hardware tests require separate explicit authorization and sanitized configuration. ## Cross-repository CI From 0e07022e263f8fc1f248da8bea3a46904f2a821a Mon Sep 17 00:00:00 2001 From: Scaxlibur <51772892+Scaxlibur@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:58:53 +0800 Subject: [PATCH 8/8] test: enable UTF-8 for isolated Windows CLI checks --- packages/wavebench-rigol-dg4000/tests/test_wheel.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/wavebench-rigol-dg4000/tests/test_wheel.py b/packages/wavebench-rigol-dg4000/tests/test_wheel.py index cbd3efb..769bb13 100644 --- a/packages/wavebench-rigol-dg4000/tests/test_wheel.py +++ b/packages/wavebench-rigol-dg4000/tests/test_wheel.py @@ -156,13 +156,15 @@ def test_sdist_excludes_vendor_manuals_and_contains_public_docs(tmp_path: Path) def _run(command: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]: - return subprocess.run( + completed = subprocess.run( command, cwd=cwd, text=True, capture_output=True, - check=True, + check=False, ) + assert completed.returncode == 0, completed.stdout + completed.stderr + return completed def _write_isolated_runtime_bridge(*, purelib: Path, workspace: Path) -> None: @@ -264,7 +266,7 @@ def forbidden(*args, **kwargs): """ _run([str(python), "-I", "-c", discovery_script], cwd=tmp_path) _run( - [str(python), "-I", "-m", "wavebench", "plugin", "doctor", "--load"], + [str(python), "-I", "-X", "utf8", "-m", "wavebench", "plugin", "doctor", "--load"], cwd=tmp_path, ) assert lifecycle.remove("rigol.dg4202-v2").status == "removed"