diff --git a/MANIFEST.in b/MANIFEST.in index 2a0ae0d..c7546a7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,8 @@ include pyproject.toml include setup.py include README.md +# Build-time input for the PyPI long description (read from the checkout). +include docs/pypi_readme.md recursive-include config *.json recursive-include sensors * recursive-include build/common *.py diff --git a/build/README.md b/build/README.md index 2f988a7..8ae20d4 100644 --- a/build/README.md +++ b/build/README.md @@ -48,16 +48,17 @@ Local only: never publishes, and makes no network call unless `--check-pypi` is passed. See `docs/publishing.md` for the full flow. ```bash -python build/run.py release preflight --version 0.5.2 +python build/run.py release preflight --version python build/run.py release preflight --all-wheel-pythons --check-pypi -python build/run.py release audit SDK\0.5.2\orisys-0.5.2-cp312-cp312-win_amd64.whl +python build/run.py release audit SDK\\orisys--cp312-cp312-win_amd64.whl ``` -`preflight` runs: repo state → version consistency → duplicate guard (fails only -when the version is already released: a local git tag, or PyPI with -`--check-pypi`) → pytest → bundle precheck → per-wheel static content/metadata -assertions → isolated runtime verification (bundled assets + real frames, no -camera). A populated `SDK//` is the expected input, so build first: +`preflight` runs: repo state → version consistency → version-string scan → +duplicate guard (fails only when the version is already released: a local git +tag, or PyPI with `--check-pypi`) → pytest → bundle precheck → per-wheel static +content/metadata assertions → isolated runtime verification in one venv per wheel +(imports + bundled assets + real frames, no camera). A populated `SDK//` +is the expected input, so build first: ```bash python build/run.py wheel-win diff --git a/build/common/verify/__init__.py b/build/common/verify/__init__.py index d866817..3d5dc47 100644 --- a/build/common/verify/__init__.py +++ b/build/common/verify/__init__.py @@ -3,6 +3,7 @@ from common.verify.gui import GUI_PRODUCTS, verify_gui_dist from common.verify.wheel import ( verify_built_wheel, + verify_wheel, verify_wheel_assets, verify_wheel_runtime, ) @@ -14,6 +15,7 @@ __all__ = [ "inspect_wheel", "verify_built_wheel", "verify_gui_dist", + "verify_wheel", "verify_wheel_assets", "verify_wheel_runtime", ] diff --git a/build/common/verify/cli.py b/build/common/verify/cli.py index fa948fa..ec9c56b 100644 --- a/build/common/verify/cli.py +++ b/build/common/verify/cli.py @@ -16,6 +16,7 @@ from common.verify import ( # noqa: E402 inspect_wheel, verify_built_wheel, verify_gui_dist, + verify_wheel, verify_wheel_assets, verify_wheel_runtime, ) @@ -79,7 +80,28 @@ def main(argv: list[str] | None = None) -> int: if not result.ok: exit_code = 1 - if args.assets or args.runtime: + if args.assets and args.runtime: + video = args.video or default_verification_video(REPO_ROOT) + if video is None: + print("[FAIL] runtime: no clip found; pass --video or set ORISYS_VERIFY_VIDEO") + return 1 + try: + record = verify_wheel( + args.wheel, + video=video, + config_name=args.config, + frames=args.frames, + offline=args.offline, + ) + print(f"[ok ] {record['imports']}") + print(f"[ok ] {record['assets']}") + print(f"[ok ] runtime: frames={record['frames']} flow={record['flow_shape']}") + except Exception as exc: + print(f"[FAIL] isolated verification: {type(exc).__name__}: {exc}") + exit_code = 1 + return exit_code + + if args.assets: try: print(verify_wheel_assets(args.wheel)) except Exception as exc: diff --git a/build/common/verify/wheel.py b/build/common/verify/wheel.py index afeaef5..cf5a761 100644 --- a/build/common/verify/wheel.py +++ b/build/common/verify/wheel.py @@ -20,6 +20,7 @@ import subprocess import sys import tempfile import venv +from contextlib import contextmanager from pathlib import Path # Public SDK surface that must import from a released wheel. @@ -302,60 +303,75 @@ class WheelVerificationError(RuntimeError): """ -def _run_in_fresh_venv( +def _run_script( + python: Path, script: str, args: list[str] | None = None, *, env: dict +) -> str: + """Run a verification script in an already-prepared venv.""" + completed = subprocess.run( + [str(python), "-c", script, *(args or [])], + capture_output=True, + text=True, + env=env, + check=False, + ) + if completed.returncode != 0: + command = " ".join(str(part) for part in (args or [])) + raise WheelVerificationError( + f"verification script failed in the venv (exit {completed.returncode}" + + (f", args: {command}" if command else "") + + ").\n--- stdout ---\n" + + (completed.stdout.strip() or "") + + "\n--- stderr ---\n" + + (completed.stderr.strip() or "") + ) + return completed.stdout.strip() + + +@contextmanager +def _installed_venv( wheel_path: Path, - script: str, - args: list[str] | None = None, *, python_executable: Path | None = None, offline: bool = False, prefix: str = "orisys-wheel-verify-", -) -> str: +): + """Create a venv with the wheel installed, for one or more checks. + + Creating a venv and installing the runtime dependencies is by far the most + expensive part of verification (tens of seconds plus a large download when + not using ``--offline``), so callers that run several checks must share one + venv instead of building one per check. + """ wheel_path = wheel_path.resolve() env = _hermetic_env() with tempfile.TemporaryDirectory(prefix=prefix) as tmp: venv_dir = Path(tmp) / "venv" + interpreter = python_executable or Path(sys.executable) + print( + f" [{wheel_path.name}] creating venv from {interpreter.name}" + + ("" if offline else " and installing dependencies (first run downloads packages)") + + " ...", + flush=True, + ) python = _create_venv( venv_dir, python_executable=python_executable, system_site_packages=offline, ) _install_wheel(python, wheel_path, offline=offline, env=env) - completed = subprocess.run( - [str(python), "-c", script, *(args or [])], - capture_output=True, - text=True, - env=env, - check=False, - ) - if completed.returncode != 0: - command = " ".join(str(part) for part in (args or [])) - raise WheelVerificationError( - f"verification script failed in the venv (exit {completed.returncode}" - + (f", args: {command}" if command else "") - + ").\n--- stdout ---\n" - + (completed.stdout.strip() or "") - + "\n--- stderr ---\n" - + (completed.stderr.strip() or "") - ) - return completed.stdout.strip() + yield python, env def verify_built_wheel(wheel_path: Path, *, python_executable: Path | None = None) -> None: """Install the wheel in a temporary venv and smoke-test public imports.""" - _run_in_fresh_venv( - wheel_path, _SMOKE_IMPORT_SCRIPT, python_executable=python_executable - ) + with _installed_venv(wheel_path, python_executable=python_executable) as (python, env): + _run_script(python, _SMOKE_IMPORT_SCRIPT, env=env) def verify_wheel_assets(wheel_path: Path, *, python_executable: Path | None = None) -> str: """Assert the wheel carries loadable bundled configs and sensor assets.""" - return _run_in_fresh_venv( - wheel_path, - _ASSET_SCRIPT, - python_executable=python_executable, - prefix="orisys-wheel-assets-", - ) + with _installed_venv(wheel_path, python_executable=python_executable) as (python, env): + return _run_script(python, _ASSET_SCRIPT, env=env) def verify_wheel_runtime( @@ -372,20 +388,59 @@ def verify_wheel_runtime( This is the only check that proves the compiled extension plus the bundled sensor assets actually produce finite flow/force output end to end. """ + with _installed_venv( + wheel_path, python_executable=python_executable, offline=offline + ) as (python, env): + return _runtime_check(python, env, video=video, config_name=config_name, frames=frames) + + +def _runtime_check( + python: Path, env: dict, *, video: Path, config_name: str, frames: int +) -> dict: video = Path(video).resolve() if not video.is_file(): raise FileNotFoundError(f"verification video not found: {video}") if frames < 1: raise ValueError("frames must be >= 1") - output = _run_in_fresh_venv( - wheel_path, - _RUNTIME_SCRIPT, - [str(video), config_name, str(frames)], - python_executable=python_executable, - offline=offline, - prefix="orisys-wheel-runtime-", + print(f" processing {frames} frame(s) with config {config_name!r} ...", flush=True) + output = _run_script( + python, _RUNTIME_SCRIPT, [str(video), config_name, str(frames)], env=env ) import json return json.loads(output.splitlines()[-1]) + + +def verify_wheel( + wheel_path: Path, + *, + video: Path | None = None, + config_name: str = DEFAULT_RUNTIME_CONFIG, + frames: int = 40, + python_executable: Path | None = None, + offline: bool = False, + check_assets: bool = True, +) -> dict: + """Run every isolated-check level for one wheel inside a single venv. + + Use this instead of calling the individual ``verify_wheel_*`` helpers: it + pays the venv/dependency cost once and returns the runtime record plus the + asset and import summaries. + """ + with _installed_venv( + wheel_path, python_executable=python_executable, offline=offline + ) as (python, env): + imports = _run_script(python, _SMOKE_IMPORT_SCRIPT, env=env) + record: dict = {"wheel": Path(wheel_path).name, "imports": imports} + if check_assets: + record["assets"] = _run_script(python, _ASSET_SCRIPT, env=env) + else: + record["assets"] = "asset check skipped (--no-assets)" + if video is not None: + record.update( + _runtime_check( + python, env, video=video, config_name=config_name, frames=frames + ) + ) + return record diff --git a/build/release/cli.py b/build/release/cli.py index 553bc81..ca8652d 100644 --- a/build/release/cli.py +++ b/build/release/cli.py @@ -14,6 +14,7 @@ from common.paths import REPO_ROOT # noqa: E402 from common.verify.wheel import ( # noqa: E402 DEFAULT_RUNTIME_CONFIG, default_verification_video, + verify_wheel, verify_wheel_assets, verify_wheel_runtime, ) @@ -116,7 +117,31 @@ def _cmd_audit(argv: list[str]) -> int: if not result.ok: exit_code = 1 - if args.assets or args.runtime: + if args.assets and args.runtime: + # One venv for both checks: building it (and downloading dependencies in + # online mode) is the expensive part. + video = args.video or default_verification_video(REPO_ROOT) + if video is None: + print("[FAIL] runtime: no verification clip; pass --video or set ORISYS_VERIFY_VIDEO") + return 1 + try: + record = verify_wheel( + wheel, + video=video, + config_name=args.config, + frames=args.frames, + python_executable=args.python, + offline=args.offline, + ) + print(f"[ok ] {record['imports']}") + print(f"[ok ] {record['assets']}") + print(f"[ok ] runtime: frames={record['frames']} flow={record['flow_shape']}") + except Exception as exc: + print(f"[FAIL] isolated verification: {type(exc).__name__}: {exc}") + exit_code = 1 + return exit_code + + if args.assets: try: print(verify_wheel_assets(wheel, python_executable=args.python)) except Exception as exc: diff --git a/build/release/preflight.py b/build/release/preflight.py index 3228155..81b00fc 100644 --- a/build/release/preflight.py +++ b/build/release/preflight.py @@ -8,12 +8,13 @@ Steps, in order: 1. ``repo-state`` - git repository, clean working tree, recorded commit 2. ``version-consistency`` - __version__ == pyproject dynamic attr == wheel names -3. ``duplicate-guard`` - SDK// already populated; optionally PyPI -4. ``tests`` - pytest (slow/hardware markers excluded) -5. ``bundle-precheck`` - every bundle item resolves; required assets present -6. ``wheel-inspection`` - static metadata/content assertions per wheel -7. ``wheel-runtime`` - isolated venv: bundled assets load and real frames - flow through the public pipeline +3. ``version-strings`` - no stale version literal left in tracked prose +4. ``duplicate-guard`` - already-released version (git tag, optionally PyPI) +5. ``tests`` - pytest (slow/hardware markers excluded) +6. ``bundle-precheck`` - every bundle item resolves; required assets present +7. ``wheel-inspection`` - static metadata/content assertions per wheel +8. ``wheel-runtime`` - one isolated venv per wheel: bundled assets load and + real frames flow through the public pipeline Output lands in ``build/release/reports//preflight.{json,txt}``. """ @@ -39,8 +40,7 @@ from common.pyproject_utils import bundle_items, get_version # noqa: E402 from common.verify.wheel import ( # noqa: E402 DEFAULT_RUNTIME_CONFIG, default_verification_video, - verify_wheel_assets, - verify_wheel_runtime, + verify_wheel, ) from common.verify.wheel_contents import inspect_wheel # noqa: E402 from release import report as report_mod # noqa: E402 @@ -108,7 +108,9 @@ class Preflight: self.version = get_version(self.repo_root) self.version_source = "pyproject.toml dynamic attr" else: - self.version = "0.0.0" + # Not version-shaped on purpose: this is a sentinel for "unresolved", + # and it must not look like a release version to the version-string scan. + self.version = "unknown" self.version_source = "unresolved" self.report_dir = report_mod.reports_root(self.repo_root, options.report_dir) / self.version @@ -270,6 +272,34 @@ class Preflight: detail += "\n" + "\n".join(problems) return Step("duplicate-guard", not problems, detail) + def _version_strings(self) -> Step: + """Keep the version out of prose: one source, everything else derived. + + Scans tracked text for version-looking literals that disagree with the + release version. Dependency constraints are ignored by design; deliberate + exceptions live in ``version.VERSION_STRING_ALLOWLIST``. + """ + findings = version_mod.find_version_literals(self.repo_root, self.version) + if not findings: + scanned = len(version_mod.tracked_text_files(self.repo_root)) + return Step( + "version-strings", + True, + f"no stale version literal in {scanned} scanned file(s); " + f"version {self.version} lives only where it is derived from", + ) + + by_file: dict[str, list[str]] = {} + for path, lineno, literal in findings: + by_file.setdefault(path, []).append(f"{path}:{lineno} -> {literal}") + detail = ( + f"{len(findings)} version literal(s) disagree with {self.version}. " + "Update them, or make the file version-agnostic, or add a documented " + "entry to build/release/version.py::VERSION_STRING_ALLOWLIST:\n" + + "\n".join(line for lines in by_file.values() for line in lines) + ) + return Step("version-strings", False, detail, data={"count": len(findings)}) + def _tests(self) -> Step: if self.options.skip_tests: return Step( @@ -439,17 +469,16 @@ class Preflight: for tag, wheel, executable in targets: label = f"{tag} {wheel.name}" try: - if self.options.no_assets: - assets_out = "asset check skipped (--no-assets)" - else: - assets_out = verify_wheel_assets(wheel, python_executable=executable) - runtime_out = verify_wheel_runtime( + # One venv per wheel: it runs the import smoke test, the bundled + # asset check and the frame pipeline together. + record = verify_wheel( wheel, video=video, config_name=self.options.config_name, frames=self.options.frames, python_executable=executable, offline=self.options.offline, + check_assets=not self.options.no_assets, ) except subprocess.CalledProcessError as exc: problems.append(f"{label}: command failed ({exc})") @@ -458,8 +487,11 @@ class Preflight: problems.append(f"{label}: {type(exc).__name__}: {exc}") continue - lines.append(f"{label}: {assets_out} | frames={runtime_out['frames']} flow={runtime_out['flow_shape']}") - self.runtime_records.append({"tag": tag, "wheel": wheel.name, **runtime_out}) + lines.append( + f"{label}: {record['assets']} | frames={record['frames']} " + f"flow={record['flow_shape']}" + ) + self.runtime_records.append({"tag": tag, **record}) detail = "\n".join(lines) if problems: @@ -480,6 +512,7 @@ class Preflight: for step_fn in ( self._repo_state, self._version_consistency, + self._version_strings, self._duplicate_guard, self._tests, self._bundle_precheck, diff --git a/build/release/version.py b/build/release/version.py index 51a226e..f278282 100644 --- a/build/release/version.py +++ b/build/release/version.py @@ -143,6 +143,97 @@ def released_tags_for(version: str, tags: list[str]) -> list[str]: return sorted({version, f"v{version}"} & set(tags)) +# Version literals that are deliberately not the package version. Keyed by +# repo-relative path; each entry documents *why* the literal may stay. +VERSION_STRING_ALLOWLIST: dict[str, frozenset[str]] = { + # Historical release records: rewriting these would destroy the audit trail. + "docs/publishing.md": frozenset({"0.4.2", "0.4.3", "0.4.4", "0.5.0", "0.5.1", "0.5.2"}), + # Unrelated third-party/CUDA versions quoted in prose. + "docs/get_started.md": frozenset({"12.9.0"}), +} + +# Paths never scanned: tests deliberately use synthetic versions, reports are +# generated output, and this module's own allowlist/sentinel literals would +# otherwise flag themselves. +VERSION_SCAN_EXCLUDED_PREFIXES = ( + "tests/", + "build/release/reports/", + "build/nuitka/", + "build/lib/", + "dist/", + "SDK/", + "data/", +) +VERSION_SCAN_EXCLUDED_FILES = ("build/release/version.py",) + +_VERSION_LITERAL_RE = re.compile( + r"(?=!~\w.])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.]+)?)(?![0-9A-Za-z.])" +) +_SCANNED_SUFFIXES = (".md", ".py", ".txt", ".toml", ".cfg", ".ini", ".rst") + + +def tracked_text_files(repo_root: Path) -> list[Path]: + """Tracked files worth scanning for version literals.""" + repo_root = Path(repo_root) + try: + completed = subprocess.run( + ["git", "-C", str(repo_root), "ls-files", "-z"], + capture_output=True, + text=True, + check=False, + ) + names = [n for n in (completed.stdout or "").split("\0") if n] + except OSError: + names = [] + + if not names: + names = [ + str(path.relative_to(repo_root)).replace("\\", "/") + for path in repo_root.rglob("*") + if path.is_file() + ] + + selected = [] + for name in names: + if name.startswith(VERSION_SCAN_EXCLUDED_PREFIXES): + continue + if name in VERSION_SCAN_EXCLUDED_FILES: + continue + if not name.endswith(_SCANNED_SUFFIXES): + continue + selected.append(Path(name)) + return selected + + +def find_version_literals( + repo_root: Path, version: str +) -> list[tuple[str, int, str]]: + """``(path, lineno, literal)`` for every version-looking string != ``version``. + + Dependency constraints are skipped by the regex lookbehind (``numpy>=1.24.4`` + is not a version reference), and historical/third-party literals require an + explicit entry in :data:`VERSION_STRING_ALLOWLIST`. + """ + repo_root = Path(repo_root) + findings: list[tuple[str, int, str]] = [] + for relative in tracked_text_files(repo_root): + absolute = repo_root / relative + try: + text = absolute.read_text(encoding="utf-8", errors="ignore") + except OSError: + continue + if "\0" in text: + continue + allowed = VERSION_STRING_ALLOWLIST.get(relative.as_posix(), frozenset()) + for lineno, line in enumerate(text.splitlines(), start=1): + for match in _VERSION_LITERAL_RE.finditer(line): + literal = match.group(1) + if literal == version or literal in allowed: + continue + findings.append((relative.as_posix(), lineno, literal)) + return findings + + def conda_envs_root() -> Path | None: """Locate the conda envs directory the same way build/wheel/win.bat does.""" override = os.getenv("CONDA_ENVS_ROOT") diff --git a/build/run.py b/build/run.py index 6fdd20d..5296e12 100644 --- a/build/run.py +++ b/build/run.py @@ -91,8 +91,8 @@ def main(argv: list[str] | None = None) -> int: print(" python build/run.py wheel --help") print(" python build/run.py gui finger") print(" python build/run.py manylinux -PythonVersions 3.10") - print(" python build/run.py release preflight --version 0.5.2") - print(" python build/run.py release audit SDK\\0.5.2\\orisys-0.5.2-cp312-cp312-win_amd64.whl") + print(" python build/run.py release preflight --version ") + print(" python build/run.py release audit SDK\\\\orisys--cp312-cp312-win_amd64.whl") return 0 command, rest = argv[0], argv[1:] diff --git a/build/wheel/build.py b/build/wheel/build.py index fba9669..42ed659 100644 --- a/build/wheel/build.py +++ b/build/wheel/build.py @@ -118,10 +118,12 @@ def build(*, target: str = "internal", verify: bool | None = None) -> Path: # Bundle rules live in pyproject.toml → [[tool.orisys.bundle.items]] bundle = bundle_items(REPO_ROOT) copied_counts = copy_bundle_items(REPO_ROOT, pkg_dir, bundle) - print(f"Copied {copied_counts['configs']} JSON config files") - print(f"Copied {copied_counts['examples']} example files to orisys/examples/") - print(f"Copied {copied_counts['docs']} doc files to orisys/docs/") - print(f"Copied {copied_counts['sensors']} sensor bundle files to orisys/sensors/") + for item in bundle: + destination = f"orisys/{item['target']}/" + print( + f"Copied {copied_counts.get(item['name'], 0)} file(s) " + f"for bundle item {item['name']!r} to {destination}" + ) write_wheel_setup( NUITKA_WHEEL_DIR, diff --git a/build/wheel/package.py b/build/wheel/package.py index 848eaee..416d46c 100644 --- a/build/wheel/package.py +++ b/build/wheel/package.py @@ -55,8 +55,8 @@ def metadata_kwargs(project: dict) -> dict: The Nuitka wheel is built by a generated ``setup.py`` outside the normal setuptools pipeline, so every PyPI-visible field has to be passed through - explicitly. Omitting them is what left the 0.5.2 release with no - ``Requires-Python``, classifiers, project URLs or license on PyPI. + explicitly. Omitting them is what left the early PyPI releases with no + ``Requires-Python``, classifiers, project URLs or license on the project page. """ authors = _person_strings(project.get("authors")) maintainers = _person_strings(project.get("maintainers")) diff --git a/docs/get_started.md b/docs/get_started.md index b760513..b6019be 100644 --- a/docs/get_started.md +++ b/docs/get_started.md @@ -5,8 +5,6 @@ description: 安装并运行 Orisys SDK 的基础步骤 # 快速开始 -版本:0.5.2 - ## 简介 本文件介绍如何上手和运行叠动视触觉传感器配套的开发套件。本套件是配合叠动视触觉传感器进行图像处理,计算形变、力以及接触信息。它可以从摄像头或视频文件中读取图像,处理后打印和展示处理后信息。 支持操作系统:Windows、Ubuntu @@ -84,9 +82,9 @@ pip install --upgrade orisys ### 使用 .whl 文件安装(离线或指定版本) 从官方渠道获取 .whl 文件,文件名格式通常为: orisys-{版本}-{Python版本}-{Python版本}-{操作系统}_{硬件架构}.whl -例如:orisys-0.5.2-cp310-cp310-win_amd64.whl 表示: +例如 `orisys-<版本>-cp310-cp310-win_amd64.whl` 表示: -版本:0.5.2 +版本:`<版本>`(以实际下载的文件名为准,可用 `python -c "import orisys; print(orisys.__version__)"` 确认) Python 版本:3.10(cp310) 操作系统:Windows(win) 硬件架构:64位(amd64) @@ -97,7 +95,7 @@ Python 版本:3.10(cp310) ```bash # 请将文件名替换为您实际下载的文件名 -pip install orisys-0.5.2-cp310-cp310-win_amd64.whl +pip install orisys-<版本>-cp310-cp310-win_amd64.whl ``` ### 安装 CuPy(可选) @@ -139,7 +137,7 @@ python import orisys print(orisys.__version__) ``` -如果输出版本号(例如 0.5.2),则说明安装成功。 +如果打印出版本号,则说明安装成功。 ## 使用方法 ### 运行示例程序 diff --git a/docs/publishing.md b/docs/publishing.md index 35d3d70..714c069 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -46,20 +46,22 @@ python build/run.py wheel-manylinux -PythonVersions 3.10 ## 2. 发布前门禁:`release preflight` ```powershell -python build/run.py release preflight --version 0.5.2 +# --version 可省略:默认取 pyproject 的 dynamic version(即 src/orisys/__init__.py 里的 __version__) +python build/run.py release preflight ``` -按顺序执行 7 个步骤,任一步失败即整体失败(`--keep-going` 可改为收集全部失败): +按顺序执行 8 个步骤,任一步失败即整体失败(`--keep-going` 可改为收集全部失败): | 步骤 | 检查内容 | | --- | --- | | `repo-state` | 是 git 仓库、工作树干净、记录 HEAD(`--allow-dirty` 可豁免,用于提交前预检) | | `version-consistency` | `src/orisys/__init__.py::__version__` == pyproject 的 dynamic attr == `SDK//` 中 wheel 文件名里的版本 | +| `version-strings` | 追踪的文本文件里没有与当前版本不符的版本字面量(见 §5.3) | | `duplicate-guard` | 该版本是否**已经发布过**:本地 git tag(`v` / ``,含 sibling `orisys-sdk-release` 仓库)命中即拒绝;`--check-pypi` 时再查 PyPI。`SDK//` 里已有 wheel 只作信息展示——那是 preflight 的正常输入,不是错误 | | `tests` | `pytest -m "not slow and not hardware"`(`--longrun` 时包含 slow) | | `bundle-precheck` | 每个 `[[tool.orisys.bundle.items]]` 至少解析出 1 个文件;finger 必需资产存在 | | `wheel-inspection` | 逐个 wheel 做静态断言:文件名/版本/METADATA 字段/RECORD 哈希/压缩包路径/体积上限/无 pyc 与缓存/无 `orisys/**/*.py` 源码泄漏/与源码树逐文件字节一致 | -| `wheel-runtime` | 在一次性 venv 中安装该 wheel,断言内置 configs/sensors 资产可加载,并对真实视频跑 40 帧公开流水线(无需相机、无 GUI) | +| `wheel-runtime` | 每个 wheel **一个** venv,在里面依次做:导入冒烟 → 内置 configs/sensors 资产可加载 → 对真实视频跑 40 帧公开流水线(无需相机、无 GUI) | 正常顺序是**先构建、再 preflight**,不需要额外开关: @@ -98,10 +100,10 @@ python build/run.py release preflight --all-wheel-pythons --check-pypi 对单个 wheel 做静态断言(+ 可选运行时校验),不需要版本上下文。适合体检历史产物: ```powershell -python build/run.py release audit SDK\0.5.2\orisys-0.5.2-cp312-cp312-win_amd64.whl -python build/run.py release audit dist\repaired\orisys-0.5.2-cp310-cp310-win_amd64.whl --strict-bundle +python build/run.py release audit SDK\\orisys--cp312-cp312-win_amd64.whl +python build/run.py release audit dist\repaired\orisys--cp310-cp310-win_amd64.whl --strict-bundle -# 追加运行时校验 +# 追加运行时校验(--assets 与 --runtime 同时给时共用一个 venv,只装一次依赖) python build/run.py release audit --assets --runtime --video data/sample/s1_480p.mp4 ``` @@ -129,8 +131,8 @@ python build/run.py verify --gui-dist --product ory_finger 4. PyPI:先 TestPyPI 再正式仓库。 ```powershell -python -m twine upload --repository testpypi SDK/0.5.2/*.whl -python -m twine upload SDK/0.5.2/*.whl +python -m twine upload --repository testpypi SDK//*.whl +python -m twine upload SDK//*.whl ``` **不要把 API token 写进仓库**。`release preflight --check-pypi` 只做只读查询,不需要 token。 @@ -163,7 +165,7 @@ python -m twine upload SDK/0.5.2/*.whl --- -## 5. 两个必须记住的坑 +## 5. 三个必须记住的坑 ### 5.1 Nuitka 编译包的 `__file__` 是假的 @@ -194,3 +196,41 @@ root = distribution("orisys").locate_file("orisys") # site-packages/orisys 处置:在构建环境里执行 `pip uninstall -y orisys`,或改用不带 `--offline` 的完全隔离 venv(会联网安装依赖)。 +### 5.3 版本号只有一个地方,其他都是推导出来的 + +版本号的唯一来源是 **`src/orisys/__init__.py::__version__`**。以下全部由它推导,不需要手改: + +| 产物 | 推导方式 | +| --- | --- | +| pyproject 的 `[project] version` | `[tool.setuptools.dynamic] version = {attr = "orisys.__version__"}` | +| wheel 文件名里的版本 | `build/wheel/build.py` 调 `get_version()` | +| wheel `METADATA` 的 `Version:` | 生成的 `setup.py` 里写入同一个 `version` | +| `SDK//` 目录名 | 同上 | +| `importlib.metadata.version("orisys")` | 从 wheel METADATA 读出 | + +因此**不要**再往文档、示例、测试里写具体版本号: + +- 用法示例写 `SDK//...`、`--version `,不要写具体版本 +- 文档里不要写「版本:x.y.z」;需要时让用户用 `python -c "import orisys; print(orisys.__version__)"` 自己看 +- 示例脚本的 docstring 不要再写 `SDK version: x.y.z` +- 测试里需要「一个版本」时用明显是假的版本号(如 `1.x.y` 这种不会与真实版本混淆的值),不要用当前版本 —— 否则每次发版都要改测试,而且一次全局替换就会悄悄改坏断言 + +`release preflight` 的 `version-strings` 步骤强制这条规则:扫描所有被追踪的文本文件,任何与当前版本不符的版本字面量都会导致失败(`tests/` 与生成物除外;依赖约束如 `numpy>=1.24.4` 由正则排除)。 + +历史记录属于例外,需显式登记在 `build/release/version.py::VERSION_STRING_ALLOWLIST` 里——登记本身就是「该字面量故意不是包版本」的文档。**不要用全局替换修改含历史版本的文件**:本文件的发布记录表就曾被一次全局替换改错(`0.50` typo tag 被挂到错误的版本上,`0.5.1` 那一行被覆盖丢失)。 + +### 5.4 wheel 里有什么、没有什么 + +打包内容由 `pyproject.toml` 的 `[[tool.orisys.bundle.items]]` 单点定义: + +| 内容 | 是否进 wheel | 说明 | +| --- | --- | --- | +| `config/*.json` | 是 | `orisys/configs/`,运行时配置解析要用 | +| `sensors/**` | 是 | `orisys/sensors/`,含 finger 的 npz/npy 资产,运行时与 GUI 都要用 | +| `examples/**` | 是 | `orisys/examples/`,用户装完就能跑示例 | +| `docs/**` | **否** | 只随 GitHub 仓库与 PyPI 项目页提供。`docs/publishing.md` 是内部流程文档;`docs/pypi_readme.md` 仅在构建时被读取作为 long description | + +因为 docs 不进 wheel,**改文档不会让已构建的 wheel 失效**,不需要重新构建。(改 `examples/`、`config/`、`sensors/` 会——它们确实在 wheel 里,`wheel-inspection` 的 `bundle-fidelity` 会报出来。) + +如果以后需要离线 API 参考,加一个只包含 `docs/api.md` 的窄条目即可,不要整个 `docs/` 都塞进去。 + diff --git a/docs/pypi_readme.md b/docs/pypi_readme.md index 52aa170..e7aa721 100644 --- a/docs/pypi_readme.md +++ b/docs/pypi_readme.md @@ -2,7 +2,7 @@ 叠动科技视触觉传感器配套的 Python SDK,提供图像采集、拼接、稠密光流、Helmholtz-Hodge 分解、接触与深度估计,以及可运行的示例。 -本文件是 PyPI 项目描述(`long_description`)的源文件。它按构建配置 `PYPI_README_RELPATH` 打包进 wheel,因此**不要**在这里写内部仓库布局、构建流水线或发布步骤——那些内容属于仓库内的 `README.md` 与 `docs/publishing.md`。 +本文件是 PyPI 项目描述(`long_description`)的源文件:构建时由 `docs/publishing.md` 所述流程从**检出目录**读取(`build/wheel/package.py::PYPI_README_RELPATH`),**不会**被打进 wheel。因此不要在这里写内部仓库布局、构建流水线或发布步骤——那些内容属于仓库内的 `README.md` 与 `docs/publishing.md`。 ```bash python -m pip install orisys diff --git a/examples/basic_pipeline.py b/examples/basic_pipeline.py index bc6feeb..0c16a7d 100644 --- a/examples/basic_pipeline.py +++ b/examples/basic_pipeline.py @@ -7,7 +7,6 @@ ORY-BOX:单传感器基础流程 3. 按需计算接触区域 4. 读取并可视化结果 -SDK version: 0.5.2 """ import os os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0" diff --git a/examples/mini_pipeline.py b/examples/mini_pipeline.py index da963d5..ea9bc22 100644 --- a/examples/mini_pipeline.py +++ b/examples/mini_pipeline.py @@ -7,7 +7,6 @@ ORY-MINI:单传感器基础流程 3. 按需计算接触区域 4. 读取并可视化结果 -SDK version: 0.5.2 """ import os os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0" diff --git a/examples/two_sensor_pipeline.py b/examples/two_sensor_pipeline.py index 30c0464..613eac3 100644 --- a/examples/two_sensor_pipeline.py +++ b/examples/two_sensor_pipeline.py @@ -18,7 +18,6 @@ - Windows 下必须在 if __name__ == '__main__' 中调用 mp.freeze_support() - 每个传感器进程都会占用独立的 GPU 显存 -SDK version: 0.5.2 """ import os os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0" diff --git a/pyproject.toml b/pyproject.toml index acbd128..4c7e627 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,9 +99,12 @@ target = "examples" patterns = ["*.py", "README.md", "viz/**/*.py", "pipeline/**/*.py", "gui/**/*.py"] preserve_tree = true -[[tool.orisys.bundle.items]] -name = "docs" -source = "docs" -target = "docs" -patterns = ["*.md", "*.pdf"] +# NOTE: docs/ is deliberately NOT bundled into the wheel. +# Documentation ships with the GitHub repository / PyPI project page only: +# - docs/publishing.md is an internal process document +# - docs/pypi_readme.md is read from the checkout at build time as the PyPI +# long description (build/wheel/package.py::PYPI_README_RELPATH), never shipped +# - keeping docs out means editing a doc no longer invalidates built wheels +# Should an offline API reference be needed later, add a narrower item that ships +# only docs/api.md. diff --git a/tests/dd06.py b/tests/dd06.py index 04465be..5a37bef 100644 --- a/tests/dd06.py +++ b/tests/dd06.py @@ -8,7 +8,6 @@ Flow: 4. Feed the rectified frame into Sensor.get_img(..., already_processed=True) 5. Compute deformation/contact and visualize -SDK version: 0.5.2 """ from __future__ import annotations diff --git a/tests/test_pyproject.py b/tests/test_pyproject.py index 5f8cc47..7fd64a8 100644 --- a/tests/test_pyproject.py +++ b/tests/test_pyproject.py @@ -28,14 +28,25 @@ def test_pyproject_dependencies(packaging_utils): def test_pyproject_dynamic_version(packaging_utils): + """The dynamic version must resolve to a valid version from one source. + + Deliberately does NOT assert a literal version: pinning it here means every + release has to edit this file too. ``src/orisys/__init__.py::__version__`` is + the single source of truth, and build/release/version.py checks that the + wheels agree with it. + """ + import re import sys build_dir = REPO_ROOT / "build" if str(build_dir) not in sys.path: sys.path.insert(0, str(build_dir)) from common.pyproject_utils import get_version + from release.version import dynamic_version_attr - assert get_version(REPO_ROOT) == "0.5.2" + version = get_version(REPO_ROOT) + assert re.fullmatch(r"\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.]+)?", version), version + assert dynamic_version_attr(REPO_ROOT) == "orisys.__version__" def test_pyproject_bundle_manifest(packaging_utils): @@ -48,7 +59,9 @@ def test_pyproject_bundle_manifest(packaging_utils): items = bundle_items(REPO_ROOT) names = {item["name"] for item in items} - assert names == {"configs", "examples", "docs", "sensors"} + # docs/ is intentionally not bundled: it ships with the GitHub repo and the + # PyPI project page only, so doc edits do not invalidate built wheels. + assert names == {"configs", "examples", "sensors"} data_files = dict(bundle_data_files(REPO_ROOT)) assert "orisys/configs" in data_files diff --git a/tests/test_release_version.py b/tests/test_release_version.py index 66b0204..cde73ee 100644 --- a/tests/test_release_version.py +++ b/tests/test_release_version.py @@ -30,7 +30,7 @@ version = {attr = "orisys.__version__"} """.lstrip() -def _make_repo(tmp_path: Path, *, version: str = "0.5.2", with_git: bool = False) -> Path: +def _make_repo(tmp_path: Path, *, version: str = "1.2.3", with_git: bool = False) -> Path: (tmp_path / "pyproject.toml").write_text(PYPROJECT, encoding="utf-8") package = tmp_path / "src" / "orisys" package.mkdir(parents=True) @@ -63,16 +63,16 @@ class _FakeResponse: def test_wheel_version_parsing(): - assert version_mod.wheel_version(Path("orisys-0.5.2-cp310-cp310-win_amd64.whl")) == "0.5.2" + assert version_mod.wheel_version(Path("orisys-1.2.3-cp310-cp310-win_amd64.whl")) == "1.2.3" assert version_mod.wheel_version(Path("orisys-1.0.0-1-cp312-abi3-manylinux_2_28_x86_64.whl")) == "1.0.0" assert version_mod.wheel_version(Path("something-else.whl")) is None def test_sdk_versions_are_numerically_sorted(tmp_path): - for name in ("0.9.0", "0.4.10", "0.5.2"): + for name in ("0.9.0", "0.4.10", "1.2.3"): (tmp_path / "SDK" / name).mkdir(parents=True) (tmp_path / "SDK" / "not-a-version").mkdir() - assert version_mod.sdk_versions(tmp_path)[:3] == ["0.4.10", "0.5.2", "0.9.0"] + assert version_mod.sdk_versions(tmp_path)[:3] == ["0.4.10", "0.9.0", "1.2.3"] def test_sdk_versions_empty_without_sdk_dir(tmp_path): @@ -81,10 +81,10 @@ def test_sdk_versions_empty_without_sdk_dir(tmp_path): def test_sdk_wheels_lists_only_that_version(tmp_path): repo = _make_repo(tmp_path) - _stage_wheel(repo, "0.5.2") + _stage_wheel(repo, "1.2.3") _stage_wheel(repo, "0.4.3") - wheels = version_mod.sdk_wheels(repo, "0.5.2") - assert [w.name for w in wheels] == ["orisys-0.5.2-cp310-cp310-win_amd64.whl"] + wheels = version_mod.sdk_wheels(repo, "1.2.3") + assert [w.name for w in wheels] == ["orisys-1.2.3-cp310-cp310-win_amd64.whl"] def test_dynamic_version_attr_read_from_pyproject(tmp_path): @@ -98,12 +98,12 @@ def test_dynamic_version_attr_none_when_absent(tmp_path): def test_pypi_released_versions_parses_response(tmp_path, monkeypatch): - fake = _FakeResponse({"releases": {"0.4.2": [{"filename": "a"}], "0.5.2": [{"filename": "b"}], "0.0.0": []}}) + fake = _FakeResponse({"releases": {"0.4.2": [{"filename": "a"}], "1.2.3": [{"filename": "b"}], "0.0.0": []}}) monkeypatch.setattr(version_mod.urllib.request, "urlopen", lambda *a, **k: fake) versions, error = version_mod.pypi_released_versions() assert error is None # An empty file list means nothing was ever uploaded for that version. - assert versions == ["0.4.2", "0.5.2"] + assert versions == ["0.4.2", "1.2.3"] def test_pypi_released_versions_reports_http_404_as_empty(monkeypatch): @@ -137,7 +137,7 @@ def test_python_for_tag_uses_envs_root(tmp_path): def test_write_report_round_trips(tmp_path): payload = { - "version": "0.5.2", + "version": "1.2.3", "ok": False, "git": {"commit": "abc", "branch": "dev", "dirty": True}, "interpreter": {"version": "3.10.19", "executable": "python"}, @@ -182,43 +182,43 @@ def test_repo_state_fails_outside_git(tmp_path, capsys): def test_version_consistency_detects_mismatch(tmp_path): - repo = _make_repo(tmp_path, version="0.5.2") + repo = _make_repo(tmp_path, version="1.2.3") step = Preflight(PreflightOptions(version="9.9.9"), repo_root=repo)._version_consistency() assert not step.ok - assert "0.5.2" in step.detail and "9.9.9" in step.detail + assert "1.2.3" in step.detail and "9.9.9" in step.detail def test_version_consistency_passes_on_match(tmp_path): - repo = _make_repo(tmp_path, version="0.5.2") - step = Preflight(PreflightOptions(version="0.5.2"), repo_root=repo)._version_consistency() + repo = _make_repo(tmp_path, version="1.2.3") + step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._version_consistency() assert step.ok, step.detail def test_duplicate_guard_allows_staged_sdk_dir(tmp_path, monkeypatch): """A populated SDK// is the normal preflight input, not a hazard.""" - repo = _make_repo(tmp_path, version="0.5.2") - _stage_wheel(repo, "0.5.2") + repo = _make_repo(tmp_path, version="1.2.3") + _stage_wheel(repo, "1.2.3") monkeypatch.setattr(version_mod, "git_tags", lambda root: []) - step = Preflight(PreflightOptions(version="0.5.2"), repo_root=repo)._duplicate_guard() + step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._duplicate_guard() assert step.ok, step.detail assert "staged wheel(s) to verify" in step.detail def test_duplicate_guard_blocks_released_tag(tmp_path, monkeypatch): - repo = _make_repo(tmp_path, version="0.5.2") - _stage_wheel(repo, "0.5.2") - monkeypatch.setattr(version_mod, "git_tags", lambda root: ["v0.5.2", "v0.5.1"]) - step = Preflight(PreflightOptions(version="0.5.2"), repo_root=repo)._duplicate_guard() + repo = _make_repo(tmp_path, version="1.2.3") + _stage_wheel(repo, "1.2.3") + monkeypatch.setattr(version_mod, "git_tags", lambda root: ["v1.2.3", "v0.5.1"]) + step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._duplicate_guard() assert not step.ok - assert "already mark 0.5.2 as released" in step.detail + assert "already mark 1.2.3 as released" in step.detail assert "--force" in step.detail def test_duplicate_guard_force_accepts_released_tag(tmp_path, monkeypatch): - repo = _make_repo(tmp_path, version="0.5.2") - monkeypatch.setattr(version_mod, "git_tags", lambda root: ["0.5.2"]) + repo = _make_repo(tmp_path, version="1.2.3") + monkeypatch.setattr(version_mod, "git_tags", lambda root: ["1.2.3"]) step = Preflight( - PreflightOptions(version="0.5.2", force=True), repo_root=repo + PreflightOptions(version="1.2.3", force=True), repo_root=repo )._duplicate_guard() assert step.ok, step.detail assert "accepted via --force" in step.detail @@ -226,18 +226,18 @@ def test_duplicate_guard_force_accepts_released_tag(tmp_path, monkeypatch): def test_duplicate_guard_reports_unreleased_version(tmp_path, monkeypatch): repo = _make_repo(tmp_path, version="0.5.3") - monkeypatch.setattr(version_mod, "git_tags", lambda root: ["v0.5.2"]) + monkeypatch.setattr(version_mod, "git_tags", lambda root: ["v1.2.3"]) step = Preflight(PreflightOptions(version="0.5.3"), repo_root=repo)._duplicate_guard() assert step.ok, step.detail assert "no local git tag marks 0.5.3" in step.detail def test_duplicate_guard_flags_published_version(tmp_path, monkeypatch): - repo = _make_repo(tmp_path, version="0.5.2") + repo = _make_repo(tmp_path, version="1.2.3") monkeypatch.setattr(version_mod, "git_tags", lambda root: []) - monkeypatch.setattr(version_mod, "pypi_released_versions", lambda *a, **k: (["0.5.2"], None)) + monkeypatch.setattr(version_mod, "pypi_released_versions", lambda *a, **k: (["1.2.3"], None)) step = Preflight( - PreflightOptions(version="0.5.2", check_pypi=True), repo_root=repo + PreflightOptions(version="1.2.3", check_pypi=True), repo_root=repo )._duplicate_guard() assert not step.ok assert "already published on PyPI" in step.detail @@ -246,7 +246,7 @@ def test_duplicate_guard_flags_published_version(tmp_path, monkeypatch): def test_released_tags_for_matches_version_spellings(): tags = ["v0.5.1", "0.5.1", "v0.4.3", "0.50"] assert version_mod.released_tags_for("0.5.1", tags) == ["0.5.1", "v0.5.1"] - assert version_mod.released_tags_for("0.5.2", tags) == [] + assert version_mod.released_tags_for("1.2.3", tags) == [] def test_git_tags_reads_repo_tags(tmp_path): @@ -261,13 +261,81 @@ def test_git_tags_reads_repo_tags(tmp_path): (repo / "f.txt").write_text("x", encoding="utf-8") subprocess.run(["git", "add", "f.txt"], cwd=repo, check=True, capture_output=True) subprocess.run(["git", "commit", "-qm", "init"], cwd=repo, check=True, capture_output=True) - subprocess.run(["git", "tag", "v0.5.2"], cwd=repo, check=True, capture_output=True) + subprocess.run(["git", "tag", "v1.2.3"], cwd=repo, check=True, capture_output=True) - assert "v0.5.2" in version_mod.git_tags(repo) + assert "v1.2.3" in version_mod.git_tags(repo) + + +def test_version_string_scan_flags_stale_literal(tmp_path): + repo = tmp_path / "repo" + (repo / "docs").mkdir(parents=True) + (repo / "docs" / "a.md").write_text("版本:0.4.0\n", encoding="utf-8") + (repo / "README.md").write_text("release 1.2.3\n", encoding="utf-8") + hits = version_mod.find_version_literals(repo, "1.2.3") + assert [(path, line) for path, line, _ in hits] == [("docs/a.md", 1)] + + +def test_version_string_scan_ignores_dependency_constraints(tmp_path): + repo = tmp_path / "repo" + repo.mkdir() + (repo / "docs.md").write_text( + "pip install numpy>=1.24.4 scipy==1.13 opencv-python>=4.10.0.84\n", + encoding="utf-8", + ) + assert version_mod.find_version_literals(repo, "1.2.3") == [] + + +def test_version_string_scan_honours_allowlist(tmp_path, monkeypatch): + monkeypatch.setitem( + version_mod.VERSION_STRING_ALLOWLIST, "docs.md", frozenset({"0.4.0"}) + ) + repo = tmp_path / "repo" + repo.mkdir() + (repo / "docs.md").write_text("历史版本:0.4.0\n", encoding="utf-8") + assert version_mod.find_version_literals(repo, "1.2.3") == [] + + +def test_version_string_scan_skips_tests_directory(tmp_path): + repo = tmp_path / "repo" + (repo / "tests").mkdir(parents=True) + (repo / "tests" / "test_x.py").write_text('v = "0.1.2"\n', encoding="utf-8") + assert version_mod.find_version_literals(repo, "1.2.3") == [] + + +def test_version_string_scan_accepts_current_version_and_v_prefix(tmp_path): + repo = tmp_path / "repo" + repo.mkdir() + (repo / "README.md").write_text("current 1.2.3 and tag v1.2.3\n", encoding="utf-8") + assert version_mod.find_version_literals(repo, "1.2.3") == [] + + +def test_preflight_version_strings_step_fails_on_leftover(tmp_path): + repo = _make_repo(tmp_path, version="1.2.3") + (repo / "docs").mkdir() + (repo / "docs" / "guide.md").write_text("版本:0.4.0\n", encoding="utf-8") + step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._version_strings() + assert not step.ok + assert "0.4.0" in step.detail + assert "VERSION_STRING_ALLOWLIST" in step.detail + + +def test_preflight_version_strings_step_passes_on_clean_tree(tmp_path): + repo = _make_repo(tmp_path, version="1.2.3") + (repo / "README.md").write_text("no version literal here\n", encoding="utf-8") + step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._version_strings() + assert step.ok, step.detail + + +def test_repo_has_no_stale_version_literals(): + """The real repository must satisfy its own version-string gate.""" + from common.pyproject_utils import get_version + + version = get_version(REPO_ROOT) + assert version_mod.find_version_literals(REPO_ROOT, version) == [] def test_bundle_precheck_fails_when_pattern_resolves_nothing(tmp_path): - repo = _make_repo(tmp_path, version="0.5.2") + repo = _make_repo(tmp_path, version="1.2.3") (repo / "pyproject.toml").write_text( PYPROJECT + """ @@ -279,23 +347,23 @@ patterns = ["*.json"] """, encoding="utf-8", ) - step = Preflight(PreflightOptions(version="0.5.2"), repo_root=repo)._bundle_precheck() + step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._bundle_precheck() assert not step.ok assert "resolved 0 files" in step.detail def test_wheel_inspection_fails_without_wheels(tmp_path): - repo = _make_repo(tmp_path, version="0.5.2") - step = Preflight(PreflightOptions(version="0.5.2"), repo_root=repo)._wheel_inspection() + repo = _make_repo(tmp_path, version="1.2.3") + step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._wheel_inspection() assert not step.ok assert "does not exist" in step.detail def test_wheel_runtime_fails_without_video(tmp_path, monkeypatch): - repo = _make_repo(tmp_path, version="0.5.2") - _stage_wheel(repo, "0.5.2") + repo = _make_repo(tmp_path, version="1.2.3") + _stage_wheel(repo, "1.2.3") monkeypatch.setattr("release.preflight.default_verification_video", lambda root: None) - preflight = Preflight(PreflightOptions(version="0.5.2"), repo_root=repo) + preflight = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo) preflight._discover_wheels() step = preflight._wheel_runtime() assert not step.ok @@ -303,18 +371,18 @@ def test_wheel_runtime_fails_without_video(tmp_path, monkeypatch): def test_wheel_runtime_skip_is_recorded(tmp_path): - repo = _make_repo(tmp_path, version="0.5.2") + repo = _make_repo(tmp_path, version="1.2.3") step = Preflight( - PreflightOptions(version="0.5.2", no_runtime=True), repo_root=repo + PreflightOptions(version="1.2.3", no_runtime=True), repo_root=repo )._wheel_runtime() assert step.ok assert "SKIPPED" in step.detail def test_tests_skip_is_loud_and_recorded(tmp_path): - repo = _make_repo(tmp_path, version="0.5.2") + repo = _make_repo(tmp_path, version="1.2.3") step = Preflight( - PreflightOptions(version="0.5.2", skip_tests=True), repo_root=repo + PreflightOptions(version="1.2.3", skip_tests=True), repo_root=repo )._tests() assert step.ok assert "SKIPPED" in step.detail @@ -322,8 +390,8 @@ def test_tests_skip_is_loud_and_recorded(tmp_path): def test_execute_marks_failure_and_aborts(tmp_path, capsys): - repo = _make_repo(tmp_path, version="0.5.2") - preflight = Preflight(PreflightOptions(version="0.5.2"), repo_root=repo) + repo = _make_repo(tmp_path, version="1.2.3") + preflight = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo) preflight._execute(preflight._repo_state) assert preflight.aborted assert preflight.steps[-1].name == "repo-state" @@ -331,7 +399,7 @@ def test_execute_marks_failure_and_aborts(tmp_path, capsys): def test_execute_keep_going_does_not_abort(tmp_path): - repo = _make_repo(tmp_path, version="0.5.2") - preflight = Preflight(PreflightOptions(version="0.5.2", keep_going=True), repo_root=repo) + repo = _make_repo(tmp_path, version="1.2.3") + preflight = Preflight(PreflightOptions(version="1.2.3", keep_going=True), repo_root=repo) preflight._execute(preflight._repo_state) assert not preflight.aborted diff --git a/tests/test_wheel_build.py b/tests/test_wheel_build.py index fec6590..60cc150 100644 --- a/tests/test_wheel_build.py +++ b/tests/test_wheel_build.py @@ -19,25 +19,25 @@ def wheel_package(): def test_compliant_manylinux_tags(wheel_package): assert wheel_package.is_compliant_manylinux_wheel( - Path("orisys-0.5.2-cp310-cp310-manylinux_2_17_x86_64.whl") + Path("orisys-1.2.3-cp310-cp310-manylinux_2_17_x86_64.whl") ) assert wheel_package.is_compliant_manylinux_wheel( - Path("orisys-0.5.2-cp310-cp310-manylinux2014_x86_64.whl") + Path("orisys-1.2.3-cp310-cp310-manylinux2014_x86_64.whl") ) assert wheel_package.is_compliant_manylinux_wheel( - Path("orisys-0.5.2-cp310-cp310-manylinux_2_28_x86_64.whl") + Path("orisys-1.2.3-cp310-cp310-manylinux_2_28_x86_64.whl") ) def test_rejects_illegal_manylinux_tags(wheel_package): assert not wheel_package.is_compliant_manylinux_wheel( - Path("orisys-0.5.2-cp310-cp310-manylinux_x86_64.whl") + Path("orisys-1.2.3-cp310-cp310-manylinux_x86_64.whl") ) assert not wheel_package.is_compliant_manylinux_wheel( - Path("orisys-0.5.2-cp310-cp310-linux_x86_64.whl") + Path("orisys-1.2.3-cp310-cp310-linux_x86_64.whl") ) assert not wheel_package.is_compliant_manylinux_wheel( - Path("orisys-0.5.2-cp310-cp310-win_amd64.whl") + Path("orisys-1.2.3-cp310-cp310-win_amd64.whl") ) diff --git a/tests/test_wheel_contents.py b/tests/test_wheel_contents.py index 473185c..5181c79 100644 --- a/tests/test_wheel_contents.py +++ b/tests/test_wheel_contents.py @@ -24,7 +24,7 @@ if str(BUILD_ROOT) not in sys.path: from common.verify import wheel_contents # noqa: E402 from common.verify.wheel_contents import inspect_wheel # noqa: E402 -VERSION = "0.5.2" +VERSION = "1.2.3" DIST_INFO = f"orisys-{VERSION}.dist-info" CORE_DEPS = ("numpy>=1.24.4", "opencv-python>=4.10.0.84") @@ -201,13 +201,13 @@ def test_good_wheel_passes(tmp_path): def test_missing_wheel_file_fails(tmp_path): - result = inspect_wheel(tmp_path / "nope-0.5.2-cp312-cp312-win_amd64.whl") + result = inspect_wheel(tmp_path / "nope-1.2.3-cp312-cp312-win_amd64.whl") assert not result.ok assert "wheel-exists" in _failed_names(result) def test_unparsable_filename_fails(tmp_path): - wheel, repo = _make_wheel(tmp_path, filename="orisys-0.5.2-cp312-cp312.whl") + wheel, repo = _make_wheel(tmp_path, filename="orisys-1.2.3-cp312-cp312.whl") result = inspect_wheel(wheel, repo_root=repo) assert "filename" in _failed_names(result) diff --git a/tests/test_wheel_setup_metadata.py b/tests/test_wheel_setup_metadata.py index ab341e5..a2b9e94 100644 --- a/tests/test_wheel_setup_metadata.py +++ b/tests/test_wheel_setup_metadata.py @@ -1,7 +1,7 @@ """Tests for build/wheel/package.py metadata plumbing. The Nuitka wheel is built from a generated ``setup.py``, which is exactly where -the 0.5.2 release silently lost ``Requires-Python``, classifiers, project URLs +the early 0.5.x releases silently lost ``Requires-Python``, classifiers, project URLs and the license. These tests run setuptools for real (in a temp dir, on a stub package tree) and assert on the METADATA it produces. """ @@ -57,7 +57,7 @@ def _generate(wheel_dir: Path, tmp_path: Path) -> Path: _stub_wheel_root(wheel_dir) write_wheel_setup( wheel_dir, - version="0.5.2", + version="1.2.3", package_data={"orisys": ["*.pyd"]}, install_requires=["numpy>=1.24.4"], extras_require={"gui": ["open3d>=0.17.0"]}, @@ -117,7 +117,7 @@ def test_generated_wheel_metadata_is_complete(tmp_path): text = metadata_path.read_text(encoding="utf-8") assert "Name: orisys" in text - assert "Version: 0.5.2" in text + assert "Version: 1.2.3" in text assert f"Summary: {PROJECT['description']}" in text assert "Requires-Python: >=3.10" in text assert "License: Proprietary" in text