diff --git a/config.py b/config.py index fe752df..26c81fb 100644 --- a/config.py +++ b/config.py @@ -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.*?)(?=^\[|\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" diff --git a/scripts/verify_test_debt_governance.py b/scripts/verify_test_debt_governance.py index c483502..f4bc128 100644 --- a/scripts/verify_test_debt_governance.py +++ b/scripts/verify_test_debt_governance.py @@ -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( diff --git a/tests/quality_governance_test_utils.py b/tests/quality_governance_test_utils.py index 9f3c9d0..08e77fb 100644 --- a/tests/quality_governance_test_utils.py +++ b/tests/quality_governance_test_utils.py @@ -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", diff --git a/tests/test_r175_pack_version_fallback.py b/tests/test_r175_pack_version_fallback.py new file mode 100644 index 0000000..4aef3ca --- /dev/null +++ b/tests/test_r175_pack_version_fallback.py @@ -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()