exclude docs from wheel; harden bundle item handling; single-source version
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
include pyproject.toml
|
include pyproject.toml
|
||||||
include setup.py
|
include setup.py
|
||||||
include README.md
|
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 config *.json
|
||||||
recursive-include sensors *
|
recursive-include sensors *
|
||||||
recursive-include build/common *.py
|
recursive-include build/common *.py
|
||||||
|
|||||||
+8
-7
@@ -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.
|
passed. See `docs/publishing.md` for the full flow.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python build/run.py release preflight --version 0.5.2
|
python build/run.py release preflight --version <version>
|
||||||
python build/run.py release preflight --all-wheel-pythons --check-pypi
|
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\<version>\orisys-<version>-cp312-cp312-win_amd64.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
`preflight` runs: repo state → version consistency → duplicate guard (fails only
|
`preflight` runs: repo state → version consistency → version-string scan →
|
||||||
when the version is already released: a local git tag, or PyPI with
|
duplicate guard (fails only when the version is already released: a local git
|
||||||
`--check-pypi`) → pytest → bundle precheck → per-wheel static content/metadata
|
tag, or PyPI with `--check-pypi`) → pytest → bundle precheck → per-wheel static
|
||||||
assertions → isolated runtime verification (bundled assets + real frames, no
|
content/metadata assertions → isolated runtime verification in one venv per wheel
|
||||||
camera). A populated `SDK/<version>/` is the expected input, so build first:
|
(imports + bundled assets + real frames, no camera). A populated `SDK/<version>/`
|
||||||
|
is the expected input, so build first:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python build/run.py wheel-win
|
python build/run.py wheel-win
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from common.verify.gui import GUI_PRODUCTS, verify_gui_dist
|
from common.verify.gui import GUI_PRODUCTS, verify_gui_dist
|
||||||
from common.verify.wheel import (
|
from common.verify.wheel import (
|
||||||
verify_built_wheel,
|
verify_built_wheel,
|
||||||
|
verify_wheel,
|
||||||
verify_wheel_assets,
|
verify_wheel_assets,
|
||||||
verify_wheel_runtime,
|
verify_wheel_runtime,
|
||||||
)
|
)
|
||||||
@@ -14,6 +15,7 @@ __all__ = [
|
|||||||
"inspect_wheel",
|
"inspect_wheel",
|
||||||
"verify_built_wheel",
|
"verify_built_wheel",
|
||||||
"verify_gui_dist",
|
"verify_gui_dist",
|
||||||
|
"verify_wheel",
|
||||||
"verify_wheel_assets",
|
"verify_wheel_assets",
|
||||||
"verify_wheel_runtime",
|
"verify_wheel_runtime",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from common.verify import ( # noqa: E402
|
|||||||
inspect_wheel,
|
inspect_wheel,
|
||||||
verify_built_wheel,
|
verify_built_wheel,
|
||||||
verify_gui_dist,
|
verify_gui_dist,
|
||||||
|
verify_wheel,
|
||||||
verify_wheel_assets,
|
verify_wheel_assets,
|
||||||
verify_wheel_runtime,
|
verify_wheel_runtime,
|
||||||
)
|
)
|
||||||
@@ -79,7 +80,28 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
if not result.ok:
|
if not result.ok:
|
||||||
exit_code = 1
|
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:
|
try:
|
||||||
print(verify_wheel_assets(args.wheel))
|
print(verify_wheel_assets(args.wheel))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import venv
|
import venv
|
||||||
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Public SDK surface that must import from a released wheel.
|
# Public SDK surface that must import from a released wheel.
|
||||||
@@ -302,25 +303,10 @@ class WheelVerificationError(RuntimeError):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _run_in_fresh_venv(
|
def _run_script(
|
||||||
wheel_path: Path,
|
python: Path, script: str, args: list[str] | None = None, *, env: dict
|
||||||
script: str,
|
|
||||||
args: list[str] | None = None,
|
|
||||||
*,
|
|
||||||
python_executable: Path | None = None,
|
|
||||||
offline: bool = False,
|
|
||||||
prefix: str = "orisys-wheel-verify-",
|
|
||||||
) -> str:
|
) -> str:
|
||||||
wheel_path = wheel_path.resolve()
|
"""Run a verification script in an already-prepared venv."""
|
||||||
env = _hermetic_env()
|
|
||||||
with tempfile.TemporaryDirectory(prefix=prefix) as tmp:
|
|
||||||
venv_dir = Path(tmp) / "venv"
|
|
||||||
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(
|
completed = subprocess.run(
|
||||||
[str(python), "-c", script, *(args or [])],
|
[str(python), "-c", script, *(args or [])],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -341,21 +327,51 @@ def _run_in_fresh_venv(
|
|||||||
return completed.stdout.strip()
|
return completed.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _installed_venv(
|
||||||
|
wheel_path: Path,
|
||||||
|
*,
|
||||||
|
python_executable: Path | None = None,
|
||||||
|
offline: bool = False,
|
||||||
|
prefix: str = "orisys-wheel-verify-",
|
||||||
|
):
|
||||||
|
"""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)
|
||||||
|
yield python, env
|
||||||
|
|
||||||
|
|
||||||
def verify_built_wheel(wheel_path: Path, *, python_executable: Path | None = None) -> None:
|
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."""
|
"""Install the wheel in a temporary venv and smoke-test public imports."""
|
||||||
_run_in_fresh_venv(
|
with _installed_venv(wheel_path, python_executable=python_executable) as (python, env):
|
||||||
wheel_path, _SMOKE_IMPORT_SCRIPT, python_executable=python_executable
|
_run_script(python, _SMOKE_IMPORT_SCRIPT, env=env)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def verify_wheel_assets(wheel_path: Path, *, python_executable: Path | None = None) -> str:
|
def verify_wheel_assets(wheel_path: Path, *, python_executable: Path | None = None) -> str:
|
||||||
"""Assert the wheel carries loadable bundled configs and sensor assets."""
|
"""Assert the wheel carries loadable bundled configs and sensor assets."""
|
||||||
return _run_in_fresh_venv(
|
with _installed_venv(wheel_path, python_executable=python_executable) as (python, env):
|
||||||
wheel_path,
|
return _run_script(python, _ASSET_SCRIPT, env=env)
|
||||||
_ASSET_SCRIPT,
|
|
||||||
python_executable=python_executable,
|
|
||||||
prefix="orisys-wheel-assets-",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def verify_wheel_runtime(
|
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
|
This is the only check that proves the compiled extension plus the bundled
|
||||||
sensor assets actually produce finite flow/force output end to end.
|
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()
|
video = Path(video).resolve()
|
||||||
if not video.is_file():
|
if not video.is_file():
|
||||||
raise FileNotFoundError(f"verification video not found: {video}")
|
raise FileNotFoundError(f"verification video not found: {video}")
|
||||||
if frames < 1:
|
if frames < 1:
|
||||||
raise ValueError("frames must be >= 1")
|
raise ValueError("frames must be >= 1")
|
||||||
|
|
||||||
output = _run_in_fresh_venv(
|
print(f" processing {frames} frame(s) with config {config_name!r} ...", flush=True)
|
||||||
wheel_path,
|
output = _run_script(
|
||||||
_RUNTIME_SCRIPT,
|
python, _RUNTIME_SCRIPT, [str(video), config_name, str(frames)], env=env
|
||||||
[str(video), config_name, str(frames)],
|
|
||||||
python_executable=python_executable,
|
|
||||||
offline=offline,
|
|
||||||
prefix="orisys-wheel-runtime-",
|
|
||||||
)
|
)
|
||||||
import json
|
import json
|
||||||
|
|
||||||
return json.loads(output.splitlines()[-1])
|
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
|
||||||
|
|||||||
+26
-1
@@ -14,6 +14,7 @@ from common.paths import REPO_ROOT # noqa: E402
|
|||||||
from common.verify.wheel import ( # noqa: E402
|
from common.verify.wheel import ( # noqa: E402
|
||||||
DEFAULT_RUNTIME_CONFIG,
|
DEFAULT_RUNTIME_CONFIG,
|
||||||
default_verification_video,
|
default_verification_video,
|
||||||
|
verify_wheel,
|
||||||
verify_wheel_assets,
|
verify_wheel_assets,
|
||||||
verify_wheel_runtime,
|
verify_wheel_runtime,
|
||||||
)
|
)
|
||||||
@@ -116,7 +117,31 @@ def _cmd_audit(argv: list[str]) -> int:
|
|||||||
if not result.ok:
|
if not result.ok:
|
||||||
exit_code = 1
|
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:
|
try:
|
||||||
print(verify_wheel_assets(wheel, python_executable=args.python))
|
print(verify_wheel_assets(wheel, python_executable=args.python))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
+49
-16
@@ -8,12 +8,13 @@ Steps, in order:
|
|||||||
|
|
||||||
1. ``repo-state`` - git repository, clean working tree, recorded commit
|
1. ``repo-state`` - git repository, clean working tree, recorded commit
|
||||||
2. ``version-consistency`` - __version__ == pyproject dynamic attr == wheel names
|
2. ``version-consistency`` - __version__ == pyproject dynamic attr == wheel names
|
||||||
3. ``duplicate-guard`` - SDK/<version>/ already populated; optionally PyPI
|
3. ``version-strings`` - no stale version literal left in tracked prose
|
||||||
4. ``tests`` - pytest (slow/hardware markers excluded)
|
4. ``duplicate-guard`` - already-released version (git tag, optionally PyPI)
|
||||||
5. ``bundle-precheck`` - every bundle item resolves; required assets present
|
5. ``tests`` - pytest (slow/hardware markers excluded)
|
||||||
6. ``wheel-inspection`` - static metadata/content assertions per wheel
|
6. ``bundle-precheck`` - every bundle item resolves; required assets present
|
||||||
7. ``wheel-runtime`` - isolated venv: bundled assets load and real frames
|
7. ``wheel-inspection`` - static metadata/content assertions per wheel
|
||||||
flow through the public pipeline
|
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/<version>/preflight.{json,txt}``.
|
Output lands in ``build/release/reports/<version>/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
|
from common.verify.wheel import ( # noqa: E402
|
||||||
DEFAULT_RUNTIME_CONFIG,
|
DEFAULT_RUNTIME_CONFIG,
|
||||||
default_verification_video,
|
default_verification_video,
|
||||||
verify_wheel_assets,
|
verify_wheel,
|
||||||
verify_wheel_runtime,
|
|
||||||
)
|
)
|
||||||
from common.verify.wheel_contents import inspect_wheel # noqa: E402
|
from common.verify.wheel_contents import inspect_wheel # noqa: E402
|
||||||
from release import report as report_mod # 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 = get_version(self.repo_root)
|
||||||
self.version_source = "pyproject.toml dynamic attr"
|
self.version_source = "pyproject.toml dynamic attr"
|
||||||
else:
|
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.version_source = "unresolved"
|
||||||
|
|
||||||
self.report_dir = report_mod.reports_root(self.repo_root, options.report_dir) / self.version
|
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)
|
detail += "\n" + "\n".join(problems)
|
||||||
return Step("duplicate-guard", not problems, detail)
|
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:
|
def _tests(self) -> Step:
|
||||||
if self.options.skip_tests:
|
if self.options.skip_tests:
|
||||||
return Step(
|
return Step(
|
||||||
@@ -439,17 +469,16 @@ class Preflight:
|
|||||||
for tag, wheel, executable in targets:
|
for tag, wheel, executable in targets:
|
||||||
label = f"{tag} {wheel.name}"
|
label = f"{tag} {wheel.name}"
|
||||||
try:
|
try:
|
||||||
if self.options.no_assets:
|
# One venv per wheel: it runs the import smoke test, the bundled
|
||||||
assets_out = "asset check skipped (--no-assets)"
|
# asset check and the frame pipeline together.
|
||||||
else:
|
record = verify_wheel(
|
||||||
assets_out = verify_wheel_assets(wheel, python_executable=executable)
|
|
||||||
runtime_out = verify_wheel_runtime(
|
|
||||||
wheel,
|
wheel,
|
||||||
video=video,
|
video=video,
|
||||||
config_name=self.options.config_name,
|
config_name=self.options.config_name,
|
||||||
frames=self.options.frames,
|
frames=self.options.frames,
|
||||||
python_executable=executable,
|
python_executable=executable,
|
||||||
offline=self.options.offline,
|
offline=self.options.offline,
|
||||||
|
check_assets=not self.options.no_assets,
|
||||||
)
|
)
|
||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
problems.append(f"{label}: command failed ({exc})")
|
problems.append(f"{label}: command failed ({exc})")
|
||||||
@@ -458,8 +487,11 @@ class Preflight:
|
|||||||
problems.append(f"{label}: {type(exc).__name__}: {exc}")
|
problems.append(f"{label}: {type(exc).__name__}: {exc}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
lines.append(f"{label}: {assets_out} | frames={runtime_out['frames']} flow={runtime_out['flow_shape']}")
|
lines.append(
|
||||||
self.runtime_records.append({"tag": tag, "wheel": wheel.name, **runtime_out})
|
f"{label}: {record['assets']} | frames={record['frames']} "
|
||||||
|
f"flow={record['flow_shape']}"
|
||||||
|
)
|
||||||
|
self.runtime_records.append({"tag": tag, **record})
|
||||||
|
|
||||||
detail = "\n".join(lines)
|
detail = "\n".join(lines)
|
||||||
if problems:
|
if problems:
|
||||||
@@ -480,6 +512,7 @@ class Preflight:
|
|||||||
for step_fn in (
|
for step_fn in (
|
||||||
self._repo_state,
|
self._repo_state,
|
||||||
self._version_consistency,
|
self._version_consistency,
|
||||||
|
self._version_strings,
|
||||||
self._duplicate_guard,
|
self._duplicate_guard,
|
||||||
self._tests,
|
self._tests,
|
||||||
self._bundle_precheck,
|
self._bundle_precheck,
|
||||||
|
|||||||
@@ -143,6 +143,97 @@ def released_tags_for(version: str, tags: list[str]) -> list[str]:
|
|||||||
return sorted({version, f"v{version}"} & set(tags))
|
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:
|
def conda_envs_root() -> Path | None:
|
||||||
"""Locate the conda envs directory the same way build/wheel/win.bat does."""
|
"""Locate the conda envs directory the same way build/wheel/win.bat does."""
|
||||||
override = os.getenv("CONDA_ENVS_ROOT")
|
override = os.getenv("CONDA_ENVS_ROOT")
|
||||||
|
|||||||
+2
-2
@@ -91,8 +91,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
print(" python build/run.py wheel --help")
|
print(" python build/run.py wheel --help")
|
||||||
print(" python build/run.py gui finger")
|
print(" python build/run.py gui finger")
|
||||||
print(" python build/run.py manylinux -PythonVersions 3.10")
|
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 preflight --version <version>")
|
||||||
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 audit SDK\\<version>\\orisys-<version>-cp312-cp312-win_amd64.whl")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
command, rest = argv[0], argv[1:]
|
command, rest = argv[0], argv[1:]
|
||||||
|
|||||||
@@ -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 rules live in pyproject.toml → [[tool.orisys.bundle.items]]
|
||||||
bundle = bundle_items(REPO_ROOT)
|
bundle = bundle_items(REPO_ROOT)
|
||||||
copied_counts = copy_bundle_items(REPO_ROOT, pkg_dir, bundle)
|
copied_counts = copy_bundle_items(REPO_ROOT, pkg_dir, bundle)
|
||||||
print(f"Copied {copied_counts['configs']} JSON config files")
|
for item in bundle:
|
||||||
print(f"Copied {copied_counts['examples']} example files to orisys/examples/")
|
destination = f"orisys/{item['target']}/"
|
||||||
print(f"Copied {copied_counts['docs']} doc files to orisys/docs/")
|
print(
|
||||||
print(f"Copied {copied_counts['sensors']} sensor bundle files to orisys/sensors/")
|
f"Copied {copied_counts.get(item['name'], 0)} file(s) "
|
||||||
|
f"for bundle item {item['name']!r} to {destination}"
|
||||||
|
)
|
||||||
|
|
||||||
write_wheel_setup(
|
write_wheel_setup(
|
||||||
NUITKA_WHEEL_DIR,
|
NUITKA_WHEEL_DIR,
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ def metadata_kwargs(project: dict) -> dict:
|
|||||||
|
|
||||||
The Nuitka wheel is built by a generated ``setup.py`` outside the normal
|
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
|
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
|
explicitly. Omitting them is what left the early PyPI releases with no
|
||||||
``Requires-Python``, classifiers, project URLs or license on PyPI.
|
``Requires-Python``, classifiers, project URLs or license on the project page.
|
||||||
"""
|
"""
|
||||||
authors = _person_strings(project.get("authors"))
|
authors = _person_strings(project.get("authors"))
|
||||||
maintainers = _person_strings(project.get("maintainers"))
|
maintainers = _person_strings(project.get("maintainers"))
|
||||||
|
|||||||
+4
-6
@@ -5,8 +5,6 @@ description: 安装并运行 Orisys SDK 的基础步骤
|
|||||||
|
|
||||||
# 快速开始
|
# 快速开始
|
||||||
|
|
||||||
版本:0.5.2
|
|
||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
本文件介绍如何上手和运行叠动视触觉传感器配套的开发套件。本套件是配合叠动视触觉传感器进行图像处理,计算形变、力以及接触信息。它可以从摄像头或视频文件中读取图像,处理后打印和展示处理后信息。
|
本文件介绍如何上手和运行叠动视触觉传感器配套的开发套件。本套件是配合叠动视触觉传感器进行图像处理,计算形变、力以及接触信息。它可以从摄像头或视频文件中读取图像,处理后打印和展示处理后信息。
|
||||||
支持操作系统:Windows、Ubuntu
|
支持操作系统:Windows、Ubuntu
|
||||||
@@ -84,9 +82,9 @@ pip install --upgrade orisys
|
|||||||
### 使用 .whl 文件安装(离线或指定版本)
|
### 使用 .whl 文件安装(离线或指定版本)
|
||||||
从官方渠道获取 .whl 文件,文件名格式通常为:
|
从官方渠道获取 .whl 文件,文件名格式通常为:
|
||||||
orisys-{版本}-{Python版本}-{Python版本}-{操作系统}_{硬件架构}.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)
|
Python 版本:3.10(cp310)
|
||||||
操作系统:Windows(win)
|
操作系统:Windows(win)
|
||||||
硬件架构:64位(amd64)
|
硬件架构:64位(amd64)
|
||||||
@@ -97,7 +95,7 @@ Python 版本:3.10(cp310)
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 请将文件名替换为您实际下载的文件名
|
# 请将文件名替换为您实际下载的文件名
|
||||||
pip install orisys-0.5.2-cp310-cp310-win_amd64.whl
|
pip install orisys-<版本>-cp310-cp310-win_amd64.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
### 安装 CuPy(可选)
|
### 安装 CuPy(可选)
|
||||||
@@ -139,7 +137,7 @@ python
|
|||||||
import orisys
|
import orisys
|
||||||
print(orisys.__version__)
|
print(orisys.__version__)
|
||||||
```
|
```
|
||||||
如果输出版本号(例如 0.5.2),则说明安装成功。
|
如果打印出版本号,则说明安装成功。
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
### 运行示例程序
|
### 运行示例程序
|
||||||
|
|||||||
+49
-9
@@ -46,20 +46,22 @@ python build/run.py wheel-manylinux -PythonVersions 3.10
|
|||||||
## 2. 发布前门禁:`release preflight`
|
## 2. 发布前门禁:`release preflight`
|
||||||
|
|
||||||
```powershell
|
```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` 可豁免,用于提交前预检) |
|
| `repo-state` | 是 git 仓库、工作树干净、记录 HEAD(`--allow-dirty` 可豁免,用于提交前预检) |
|
||||||
| `version-consistency` | `src/orisys/__init__.py::__version__` == pyproject 的 dynamic attr == `SDK/<version>/` 中 wheel 文件名里的版本 |
|
| `version-consistency` | `src/orisys/__init__.py::__version__` == pyproject 的 dynamic attr == `SDK/<version>/` 中 wheel 文件名里的版本 |
|
||||||
|
| `version-strings` | 追踪的文本文件里没有与当前版本不符的版本字面量(见 §5.3) |
|
||||||
| `duplicate-guard` | 该版本是否**已经发布过**:本地 git tag(`v<version>` / `<version>`,含 sibling `orisys-sdk-release` 仓库)命中即拒绝;`--check-pypi` 时再查 PyPI。`SDK/<version>/` 里已有 wheel 只作信息展示——那是 preflight 的正常输入,不是错误 |
|
| `duplicate-guard` | 该版本是否**已经发布过**:本地 git tag(`v<version>` / `<version>`,含 sibling `orisys-sdk-release` 仓库)命中即拒绝;`--check-pypi` 时再查 PyPI。`SDK/<version>/` 里已有 wheel 只作信息展示——那是 preflight 的正常输入,不是错误 |
|
||||||
| `tests` | `pytest -m "not slow and not hardware"`(`--longrun` 时包含 slow) |
|
| `tests` | `pytest -m "not slow and not hardware"`(`--longrun` 时包含 slow) |
|
||||||
| `bundle-precheck` | 每个 `[[tool.orisys.bundle.items]]` 至少解析出 1 个文件;finger 必需资产存在 |
|
| `bundle-precheck` | 每个 `[[tool.orisys.bundle.items]]` 至少解析出 1 个文件;finger 必需资产存在 |
|
||||||
| `wheel-inspection` | 逐个 wheel 做静态断言:文件名/版本/METADATA 字段/RECORD 哈希/压缩包路径/体积上限/无 pyc 与缓存/无 `orisys/**/*.py` 源码泄漏/与源码树逐文件字节一致 |
|
| `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**,不需要额外开关:
|
正常顺序是**先构建、再 preflight**,不需要额外开关:
|
||||||
|
|
||||||
@@ -98,10 +100,10 @@ python build/run.py release preflight --all-wheel-pythons --check-pypi
|
|||||||
对单个 wheel 做静态断言(+ 可选运行时校验),不需要版本上下文。适合体检历史产物:
|
对单个 wheel 做静态断言(+ 可选运行时校验),不需要版本上下文。适合体检历史产物:
|
||||||
|
|
||||||
```powershell
|
```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 SDK\<version>\orisys-<version>-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 dist\repaired\orisys-<version>-cp310-cp310-win_amd64.whl --strict-bundle
|
||||||
|
|
||||||
# 追加运行时校验
|
# 追加运行时校验(--assets 与 --runtime 同时给时共用一个 venv,只装一次依赖)
|
||||||
python build/run.py release audit <wheel> --assets --runtime --video data/sample/s1_480p.mp4
|
python build/run.py release audit <wheel> --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 再正式仓库。
|
4. PyPI:先 TestPyPI 再正式仓库。
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python -m twine upload --repository testpypi SDK/0.5.2/*.whl
|
python -m twine upload --repository testpypi SDK/<version>/*.whl
|
||||||
python -m twine upload SDK/0.5.2/*.whl
|
python -m twine upload SDK/<version>/*.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
**不要把 API token 写进仓库**。`release preflight --check-pypi` 只做只读查询,不需要 token。
|
**不要把 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__` 是假的
|
### 5.1 Nuitka 编译包的 `__file__` 是假的
|
||||||
|
|
||||||
@@ -194,3 +196,41 @@ root = distribution("orisys").locate_file("orisys") # site-packages/orisys
|
|||||||
|
|
||||||
处置:在构建环境里执行 `pip uninstall -y orisys`,或改用不带 `--offline` 的完全隔离 venv(会联网安装依赖)。
|
处置:在构建环境里执行 `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/<version>/` 目录名 | 同上 |
|
||||||
|
| `importlib.metadata.version("orisys")` | 从 wheel METADATA 读出 |
|
||||||
|
|
||||||
|
因此**不要**再往文档、示例、测试里写具体版本号:
|
||||||
|
|
||||||
|
- 用法示例写 `SDK/<version>/...`、`--version <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/` 都塞进去。
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
叠动科技视触觉传感器配套的 Python SDK,提供图像采集、拼接、稠密光流、Helmholtz-Hodge 分解、接触与深度估计,以及可运行的示例。
|
叠动科技视触觉传感器配套的 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
|
```bash
|
||||||
python -m pip install orisys
|
python -m pip install orisys
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ ORY-BOX:单传感器基础流程
|
|||||||
3. 按需计算接触区域
|
3. 按需计算接触区域
|
||||||
4. 读取并可视化结果
|
4. 读取并可视化结果
|
||||||
|
|
||||||
SDK version: 0.5.2
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ ORY-MINI:单传感器基础流程
|
|||||||
3. 按需计算接触区域
|
3. 按需计算接触区域
|
||||||
4. 读取并可视化结果
|
4. 读取并可视化结果
|
||||||
|
|
||||||
SDK version: 0.5.2
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
- Windows 下必须在 if __name__ == '__main__' 中调用 mp.freeze_support()
|
- Windows 下必须在 if __name__ == '__main__' 中调用 mp.freeze_support()
|
||||||
- 每个传感器进程都会占用独立的 GPU 显存
|
- 每个传感器进程都会占用独立的 GPU 显存
|
||||||
|
|
||||||
SDK version: 0.5.2
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||||
|
|||||||
+8
-5
@@ -99,9 +99,12 @@ target = "examples"
|
|||||||
patterns = ["*.py", "README.md", "viz/**/*.py", "pipeline/**/*.py", "gui/**/*.py"]
|
patterns = ["*.py", "README.md", "viz/**/*.py", "pipeline/**/*.py", "gui/**/*.py"]
|
||||||
preserve_tree = true
|
preserve_tree = true
|
||||||
|
|
||||||
[[tool.orisys.bundle.items]]
|
# NOTE: docs/ is deliberately NOT bundled into the wheel.
|
||||||
name = "docs"
|
# Documentation ships with the GitHub repository / PyPI project page only:
|
||||||
source = "docs"
|
# - docs/publishing.md is an internal process document
|
||||||
target = "docs"
|
# - docs/pypi_readme.md is read from the checkout at build time as the PyPI
|
||||||
patterns = ["*.md", "*.pdf"]
|
# 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.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ Flow:
|
|||||||
4. Feed the rectified frame into Sensor.get_img(..., already_processed=True)
|
4. Feed the rectified frame into Sensor.get_img(..., already_processed=True)
|
||||||
5. Compute deformation/contact and visualize
|
5. Compute deformation/contact and visualize
|
||||||
|
|
||||||
SDK version: 0.5.2
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -28,14 +28,25 @@ def test_pyproject_dependencies(packaging_utils):
|
|||||||
|
|
||||||
|
|
||||||
def test_pyproject_dynamic_version(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
|
import sys
|
||||||
|
|
||||||
build_dir = REPO_ROOT / "build"
|
build_dir = REPO_ROOT / "build"
|
||||||
if str(build_dir) not in sys.path:
|
if str(build_dir) not in sys.path:
|
||||||
sys.path.insert(0, str(build_dir))
|
sys.path.insert(0, str(build_dir))
|
||||||
from common.pyproject_utils import get_version
|
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):
|
def test_pyproject_bundle_manifest(packaging_utils):
|
||||||
@@ -48,7 +59,9 @@ def test_pyproject_bundle_manifest(packaging_utils):
|
|||||||
|
|
||||||
items = bundle_items(REPO_ROOT)
|
items = bundle_items(REPO_ROOT)
|
||||||
names = {item["name"] for item in items}
|
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))
|
data_files = dict(bundle_data_files(REPO_ROOT))
|
||||||
assert "orisys/configs" in data_files
|
assert "orisys/configs" in data_files
|
||||||
|
|||||||
+115
-47
@@ -30,7 +30,7 @@ version = {attr = "orisys.__version__"}
|
|||||||
""".lstrip()
|
""".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")
|
(tmp_path / "pyproject.toml").write_text(PYPROJECT, encoding="utf-8")
|
||||||
package = tmp_path / "src" / "orisys"
|
package = tmp_path / "src" / "orisys"
|
||||||
package.mkdir(parents=True)
|
package.mkdir(parents=True)
|
||||||
@@ -63,16 +63,16 @@ class _FakeResponse:
|
|||||||
|
|
||||||
|
|
||||||
def test_wheel_version_parsing():
|
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("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
|
assert version_mod.wheel_version(Path("something-else.whl")) is None
|
||||||
|
|
||||||
|
|
||||||
def test_sdk_versions_are_numerically_sorted(tmp_path):
|
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" / name).mkdir(parents=True)
|
||||||
(tmp_path / "SDK" / "not-a-version").mkdir()
|
(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):
|
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):
|
def test_sdk_wheels_lists_only_that_version(tmp_path):
|
||||||
repo = _make_repo(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")
|
_stage_wheel(repo, "0.4.3")
|
||||||
wheels = version_mod.sdk_wheels(repo, "0.5.2")
|
wheels = version_mod.sdk_wheels(repo, "1.2.3")
|
||||||
assert [w.name for w in wheels] == ["orisys-0.5.2-cp310-cp310-win_amd64.whl"]
|
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):
|
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):
|
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)
|
monkeypatch.setattr(version_mod.urllib.request, "urlopen", lambda *a, **k: fake)
|
||||||
versions, error = version_mod.pypi_released_versions()
|
versions, error = version_mod.pypi_released_versions()
|
||||||
assert error is None
|
assert error is None
|
||||||
# An empty file list means nothing was ever uploaded for that version.
|
# 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):
|
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):
|
def test_write_report_round_trips(tmp_path):
|
||||||
payload = {
|
payload = {
|
||||||
"version": "0.5.2",
|
"version": "1.2.3",
|
||||||
"ok": False,
|
"ok": False,
|
||||||
"git": {"commit": "abc", "branch": "dev", "dirty": True},
|
"git": {"commit": "abc", "branch": "dev", "dirty": True},
|
||||||
"interpreter": {"version": "3.10.19", "executable": "python"},
|
"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):
|
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()
|
step = Preflight(PreflightOptions(version="9.9.9"), repo_root=repo)._version_consistency()
|
||||||
assert not step.ok
|
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):
|
def test_version_consistency_passes_on_match(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"), repo_root=repo)._version_consistency()
|
step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._version_consistency()
|
||||||
assert step.ok, step.detail
|
assert step.ok, step.detail
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate_guard_allows_staged_sdk_dir(tmp_path, monkeypatch):
|
def test_duplicate_guard_allows_staged_sdk_dir(tmp_path, monkeypatch):
|
||||||
"""A populated SDK/<version>/ is the normal preflight input, not a hazard."""
|
"""A populated SDK/<version>/ is the normal preflight input, not a hazard."""
|
||||||
repo = _make_repo(tmp_path, version="0.5.2")
|
repo = _make_repo(tmp_path, version="1.2.3")
|
||||||
_stage_wheel(repo, "0.5.2")
|
_stage_wheel(repo, "1.2.3")
|
||||||
monkeypatch.setattr(version_mod, "git_tags", lambda root: [])
|
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 step.ok, step.detail
|
||||||
assert "staged wheel(s) to verify" in step.detail
|
assert "staged wheel(s) to verify" in step.detail
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate_guard_blocks_released_tag(tmp_path, monkeypatch):
|
def test_duplicate_guard_blocks_released_tag(tmp_path, monkeypatch):
|
||||||
repo = _make_repo(tmp_path, version="0.5.2")
|
repo = _make_repo(tmp_path, version="1.2.3")
|
||||||
_stage_wheel(repo, "0.5.2")
|
_stage_wheel(repo, "1.2.3")
|
||||||
monkeypatch.setattr(version_mod, "git_tags", lambda root: ["v0.5.2", "v0.5.1"])
|
monkeypatch.setattr(version_mod, "git_tags", lambda root: ["v1.2.3", "v0.5.1"])
|
||||||
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 not step.ok
|
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
|
assert "--force" in step.detail
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate_guard_force_accepts_released_tag(tmp_path, monkeypatch):
|
def test_duplicate_guard_force_accepts_released_tag(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: ["0.5.2"])
|
monkeypatch.setattr(version_mod, "git_tags", lambda root: ["1.2.3"])
|
||||||
step = Preflight(
|
step = Preflight(
|
||||||
PreflightOptions(version="0.5.2", force=True), repo_root=repo
|
PreflightOptions(version="1.2.3", force=True), repo_root=repo
|
||||||
)._duplicate_guard()
|
)._duplicate_guard()
|
||||||
assert step.ok, step.detail
|
assert step.ok, step.detail
|
||||||
assert "accepted via --force" in 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):
|
def test_duplicate_guard_reports_unreleased_version(tmp_path, monkeypatch):
|
||||||
repo = _make_repo(tmp_path, version="0.5.3")
|
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()
|
step = Preflight(PreflightOptions(version="0.5.3"), repo_root=repo)._duplicate_guard()
|
||||||
assert step.ok, step.detail
|
assert step.ok, step.detail
|
||||||
assert "no local git tag marks 0.5.3" in step.detail
|
assert "no local git tag marks 0.5.3" in step.detail
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate_guard_flags_published_version(tmp_path, monkeypatch):
|
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, "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(
|
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()
|
)._duplicate_guard()
|
||||||
assert not step.ok
|
assert not step.ok
|
||||||
assert "already published on PyPI" in step.detail
|
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():
|
def test_released_tags_for_matches_version_spellings():
|
||||||
tags = ["v0.5.1", "0.5.1", "v0.4.3", "0.50"]
|
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.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):
|
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")
|
(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", "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", "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):
|
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(
|
(repo / "pyproject.toml").write_text(
|
||||||
PYPROJECT
|
PYPROJECT
|
||||||
+ """
|
+ """
|
||||||
@@ -279,23 +347,23 @@ patterns = ["*.json"]
|
|||||||
""",
|
""",
|
||||||
encoding="utf-8",
|
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 not step.ok
|
||||||
assert "resolved 0 files" in step.detail
|
assert "resolved 0 files" in step.detail
|
||||||
|
|
||||||
|
|
||||||
def test_wheel_inspection_fails_without_wheels(tmp_path):
|
def test_wheel_inspection_fails_without_wheels(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"), repo_root=repo)._wheel_inspection()
|
step = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)._wheel_inspection()
|
||||||
assert not step.ok
|
assert not step.ok
|
||||||
assert "does not exist" in step.detail
|
assert "does not exist" in step.detail
|
||||||
|
|
||||||
|
|
||||||
def test_wheel_runtime_fails_without_video(tmp_path, monkeypatch):
|
def test_wheel_runtime_fails_without_video(tmp_path, monkeypatch):
|
||||||
repo = _make_repo(tmp_path, version="0.5.2")
|
repo = _make_repo(tmp_path, version="1.2.3")
|
||||||
_stage_wheel(repo, "0.5.2")
|
_stage_wheel(repo, "1.2.3")
|
||||||
monkeypatch.setattr("release.preflight.default_verification_video", lambda root: None)
|
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()
|
preflight._discover_wheels()
|
||||||
step = preflight._wheel_runtime()
|
step = preflight._wheel_runtime()
|
||||||
assert not step.ok
|
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):
|
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(
|
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()
|
)._wheel_runtime()
|
||||||
assert step.ok
|
assert step.ok
|
||||||
assert "SKIPPED" in step.detail
|
assert "SKIPPED" in step.detail
|
||||||
|
|
||||||
|
|
||||||
def test_tests_skip_is_loud_and_recorded(tmp_path):
|
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(
|
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()
|
)._tests()
|
||||||
assert step.ok
|
assert step.ok
|
||||||
assert "SKIPPED" in step.detail
|
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):
|
def test_execute_marks_failure_and_aborts(tmp_path, capsys):
|
||||||
repo = _make_repo(tmp_path, version="0.5.2")
|
repo = _make_repo(tmp_path, version="1.2.3")
|
||||||
preflight = Preflight(PreflightOptions(version="0.5.2"), repo_root=repo)
|
preflight = Preflight(PreflightOptions(version="1.2.3"), repo_root=repo)
|
||||||
preflight._execute(preflight._repo_state)
|
preflight._execute(preflight._repo_state)
|
||||||
assert preflight.aborted
|
assert preflight.aborted
|
||||||
assert preflight.steps[-1].name == "repo-state"
|
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):
|
def test_execute_keep_going_does_not_abort(tmp_path):
|
||||||
repo = _make_repo(tmp_path, version="0.5.2")
|
repo = _make_repo(tmp_path, version="1.2.3")
|
||||||
preflight = Preflight(PreflightOptions(version="0.5.2", keep_going=True), repo_root=repo)
|
preflight = Preflight(PreflightOptions(version="1.2.3", keep_going=True), repo_root=repo)
|
||||||
preflight._execute(preflight._repo_state)
|
preflight._execute(preflight._repo_state)
|
||||||
assert not preflight.aborted
|
assert not preflight.aborted
|
||||||
|
|||||||
@@ -19,25 +19,25 @@ def wheel_package():
|
|||||||
|
|
||||||
def test_compliant_manylinux_tags(wheel_package):
|
def test_compliant_manylinux_tags(wheel_package):
|
||||||
assert wheel_package.is_compliant_manylinux_wheel(
|
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(
|
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(
|
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):
|
def test_rejects_illegal_manylinux_tags(wheel_package):
|
||||||
assert not wheel_package.is_compliant_manylinux_wheel(
|
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(
|
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(
|
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")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ if str(BUILD_ROOT) not in sys.path:
|
|||||||
from common.verify import wheel_contents # noqa: E402
|
from common.verify import wheel_contents # noqa: E402
|
||||||
from common.verify.wheel_contents import inspect_wheel # 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"
|
DIST_INFO = f"orisys-{VERSION}.dist-info"
|
||||||
|
|
||||||
CORE_DEPS = ("numpy>=1.24.4", "opencv-python>=4.10.0.84")
|
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):
|
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 not result.ok
|
||||||
assert "wheel-exists" in _failed_names(result)
|
assert "wheel-exists" in _failed_names(result)
|
||||||
|
|
||||||
|
|
||||||
def test_unparsable_filename_fails(tmp_path):
|
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)
|
result = inspect_wheel(wheel, repo_root=repo)
|
||||||
assert "filename" in _failed_names(result)
|
assert "filename" in _failed_names(result)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Tests for build/wheel/package.py metadata plumbing.
|
"""Tests for build/wheel/package.py metadata plumbing.
|
||||||
|
|
||||||
The Nuitka wheel is built from a generated ``setup.py``, which is exactly where
|
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
|
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.
|
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)
|
_stub_wheel_root(wheel_dir)
|
||||||
write_wheel_setup(
|
write_wheel_setup(
|
||||||
wheel_dir,
|
wheel_dir,
|
||||||
version="0.5.2",
|
version="1.2.3",
|
||||||
package_data={"orisys": ["*.pyd"]},
|
package_data={"orisys": ["*.pyd"]},
|
||||||
install_requires=["numpy>=1.24.4"],
|
install_requires=["numpy>=1.24.4"],
|
||||||
extras_require={"gui": ["open3d>=0.17.0"]},
|
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")
|
text = metadata_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "Name: orisys" in text
|
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 f"Summary: {PROJECT['description']}" in text
|
||||||
assert "Requires-Python: >=3.10" in text
|
assert "Requires-Python: >=3.10" in text
|
||||||
assert "License: Proprietary" in text
|
assert "License: Proprietary" in text
|
||||||
|
|||||||
Reference in New Issue
Block a user