fix(config): harden pack version fallback

This commit is contained in:
rookiestar28
2026-04-20 05:47:29 +08:00
parent 71199efa07
commit 75d560719e
4 changed files with 114 additions and 30 deletions
+53 -24
View File
@@ -42,22 +42,31 @@ PACK_NAME = "ComfyUI-OpenClaw"
PACK_START_TIME = time.time()
def _read_pyproject_version() -> Optional[str]:
"""
Read version from pyproject.toml ([project].version) as the single source of truth.
def _extract_toml_section(text: str, header: str) -> Optional[str]:
pattern = re.compile(
rf"(?ms)^\ufeff?\[{re.escape(header)}\]\s*$\n(?P<body>.*?)(?=^\[|\Z)"
)
match = pattern.search(text)
if not match:
return None
return match.group("body")
Uses a lightweight regex parse to avoid non-stdlib TOML dependencies.
"""
try:
pack_dir = os.path.dirname(os.path.abspath(__file__))
pyproject_path = os.path.join(pack_dir, "pyproject.toml")
if not os.path.exists(pyproject_path):
return None
text = ""
with open(pyproject_path, "r", encoding="utf-8") as f:
text = f.read()
# Prefer stdlib TOML parser if available (Python 3.11+), then fallback to regex.
def _extract_toml_string_assignment(section_text: str, key: str) -> Optional[str]:
match = re.search(
rf"(?m)^\s*{re.escape(key)}\s*=\s*['\"]([^'\"]+)['\"]\s*$",
section_text,
)
if not match:
return None
value = match.group(1).strip()
return value or None
def _parse_pyproject_version_text(
text: str, *, prefer_tomllib: bool = True
) -> Optional[str]:
if prefer_tomllib:
try:
from tomllib import loads as _toml_loads # type: ignore
except Exception:
@@ -72,21 +81,41 @@ def _read_pyproject_version() -> Optional[str]:
except Exception:
pass
# Find the [project] section and parse `version = "..."` within it.
# IMPORTANT: tolerate BOM/CRLF so the UI version does not silently fall back to 0.1.0.
# This is intentionally conservative to avoid false matches in other sections.
m = re.search(
r"(?ms)^\ufeff?\\[project\\]\\s*(?:[^\\[]*?)^version\\s*=\\s*[\"']([^\"']+)[\"']\\s*$",
text,
)
if not m:
# IMPORTANT: keep this fallback section-bounded.
# Matching any `version = ...` outside `[project]` silently reports the wrong build.
project_section = _extract_toml_section(text, "project")
if project_section is None:
return None
return _extract_toml_string_assignment(project_section, "version")
def _read_pyproject_version_from_path(
pyproject_path: os.PathLike[str] | str, *, prefer_tomllib: bool = True
) -> Optional[str]:
"""
Read version from pyproject.toml ([project].version) as the single source of truth.
Uses a lightweight regex parse to avoid non-stdlib TOML dependencies.
"""
try:
pyproject_path = os.fspath(pyproject_path)
if not os.path.exists(pyproject_path):
return None
ver = (m.group(1) or "").strip()
return ver or None
with open(pyproject_path, "r", encoding="utf-8") as f:
text = f.read()
return _parse_pyproject_version_text(
text,
prefer_tomllib=prefer_tomllib,
)
except Exception:
return None
def _read_pyproject_version() -> Optional[str]:
pack_dir = os.path.dirname(os.path.abspath(__file__))
return _read_pyproject_version_from_path(os.path.join(pack_dir, "pyproject.toml"))
# Version: single source of truth is pyproject.toml (line 4 in this repo).
PACK_VERSION = _read_pyproject_version() or "0.1.0"
+16 -5
View File
@@ -60,7 +60,9 @@ def _validate_skip_policy(repo_root: Path, path: Path) -> List[str]:
if not isinstance(modules, list) or any(
not isinstance(item, str) or not item.strip() for item in modules
):
failures.append("skip policy: no_skip_modules must be a list of non-empty strings")
failures.append(
"skip policy: no_skip_modules must be a list of non-empty strings"
)
return failures
seen = set()
@@ -90,7 +92,9 @@ def _validate_skip_policy(repo_root: Path, path: Path) -> List[str]:
return failures
metadata_keys = {str(key).strip() for key in metadata.keys()}
missing_metadata = [module for module in normalized_modules if module not in metadata_keys]
missing_metadata = [
module for module in normalized_modules if module not in metadata_keys
]
extra_metadata = sorted(
key for key in metadata_keys if key and key not in set(normalized_modules)
)
@@ -153,7 +157,10 @@ def _validate_mutation_allowlist(repo_root: Path, path: Path) -> List[str]:
if duplicates:
failures.append(
"mutation allowlist: duplicate (file, mutation_index) entries: "
+ ", ".join(f"{file}@{mutation_index}" for file, mutation_index in sorted(duplicates))
+ ", ".join(
f"{file}@{mutation_index}"
for file, mutation_index in sorted(duplicates)
)
)
return failures
@@ -171,10 +178,14 @@ def verify_test_debt_governance(
try:
failures.extend(_validate_skip_policy(repo_root, skip_policy_path))
except Exception as exc:
failures.append(f"skip policy: failed to validate {skip_policy_path}: {exc}")
failures.append(
f"skip policy: failed to validate {skip_policy_path}: {exc}"
)
if not mutation_allowlist_path.is_file():
failures.append(f"missing mutation survivor allowlist: {mutation_allowlist_path}")
failures.append(
f"missing mutation survivor allowlist: {mutation_allowlist_path}"
)
else:
try:
failures.extend(
-1
View File
@@ -3,7 +3,6 @@ import textwrap
from pathlib import Path
from typing import Any, Dict, Iterable, Optional
DEFAULT_REQUIRED_HOTSPOT_FAMILIES = [
"safe_io",
"security_boundary",
+45
View File
@@ -0,0 +1,45 @@
import tempfile
import unittest
from pathlib import Path
import config
class TestR175PackVersionFallback(unittest.TestCase):
def test_fallback_parser_handles_bom_crlf_and_double_quotes(self):
text = '\ufeff[project]\r\nname = "ComfyUI-OpenClaw"\r\nversion = "9.9.9"\r\n'
self.assertEqual(
config._parse_pyproject_version_text(text, prefer_tomllib=False),
"9.9.9",
)
def test_fallback_parser_handles_single_quotes_and_spacing(self):
text = "[project]\nname='ComfyUI-OpenClaw'\nversion = '1.2.3'\n"
self.assertEqual(
config._parse_pyproject_version_text(text, prefer_tomllib=False),
"1.2.3",
)
def test_fallback_parser_returns_none_when_project_section_missing(self):
text = '[tool.black]\nline-length = 88\nversion = "7.7.7"\n'
self.assertIsNone(
config._parse_pyproject_version_text(text, prefer_tomllib=False)
)
def test_read_version_from_path_returns_none_for_missing_file(self):
with tempfile.TemporaryDirectory() as tmpdir:
missing = Path(tmpdir) / "missing.toml"
self.assertIsNone(
config._read_pyproject_version_from_path(
missing, prefer_tomllib=False
)
)
def test_repo_pack_version_matches_pyproject_source_of_truth(self):
pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml"
expected = config._read_pyproject_version_from_path(pyproject)
self.assertEqual(config.PACK_VERSION, expected)
if __name__ == "__main__":
unittest.main()