diff --git a/autoform_cli/project/_lake_metadata.py b/autoform_cli/project/_lake_metadata.py index 87cac6df..767fe1a0 100644 --- a/autoform_cli/project/_lake_metadata.py +++ b/autoform_cli/project/_lake_metadata.py @@ -15,8 +15,7 @@ _TARGET_KINDS = ("lean_lib", "lean_exe", "input_file", "input_dir") # the kinds lakefile.toml declares _MATHLIB_NAME = (("str", "mathlib"),) -_LAKE_VERSION = re.compile(r"[0-9]+\.[0-9]+\.[0-9]+(?:-[^ \t\r\n]+)?") # Lake's StdVer -_MANIFEST_VERSION = re.compile(r"([0-9]+)\.([0-9]+)\.([0-9]+)(?:-[^ \t\r\n]+)?") +_LAKE_VERSION = re.compile(r"([0-9]+)\.([0-9]+)\.([0-9]+)(?:-[^ \t\r\n]+)?") # Lake's StdVer _URL_CREDENTIALS = re.compile(r"^([A-Za-z][A-Za-z0-9+.-]*://)[^/@]*@") _LEAN_ID_BEGIN_ESCAPE = "«" _LEAN_ID_END_ESCAPE = "»" @@ -274,7 +273,7 @@ def _validate_manifest_root(payload: dict[str, object]) -> None: raise ValueError("name") _json_default(payload, "lakeDir", ".lake", str) _json_default(payload, "fixedToolchain", False, bool) - _json_optional(payload, "packagesDir", str) + _json_default(payload, "packagesDir", None, str) def _decode_package_entry(entry: object, source: str) -> tuple[tuple[tuple[str, str], ...], MathlibLock]: @@ -303,9 +302,9 @@ def _decode_package_entry(entry: object, source: str) -> tuple[tuple[tuple[str, return name, MathlibLock( "git", url=_redact(_json_required(entry, "url", str)), - input_rev=_json_optional(entry, "inputRev", str), + input_rev=_json_default(entry, "inputRev", None, str), rev=_json_required(entry, "rev", str), - sub_dir=_json_optional(entry, "subDir", str), + sub_dir=_json_default(entry, "subDir", None, str), **common, ) @@ -325,6 +324,7 @@ def _json_default(mapping: dict[str, object], key: str, default, expected: type) return value +# Unused here; the library locator (#204) imports it. def _json_optional(mapping: dict[str, object], key: str, expected: type): value = mapping.get(key) if value is None: @@ -342,7 +342,7 @@ def _manifest_layout(version: object) -> str | None: if numeric_version is None or _decimal_less_than(numeric_version, "5"): return None return "legacy" if _decimal_less_than(numeric_version, "7") else "current" - if type(version) is not str or (match := _MANIFEST_VERSION.fullmatch(version)) is None: + if type(version) is not str or (match := _LAKE_VERSION.fullmatch(version)) is None: return None major, minor, _patch = (_normalize_decimal(part) for part in match.groups()) if major == "1": @@ -428,22 +428,17 @@ def _canonical_manifest_name(value: str) -> tuple[tuple[str, str], ...] | None: if value == "[anonymous]": return () - parts = _split_lean_name(value) - if parts is None: - return None - return tuple((kind, _normalize_decimal(text) if kind == "num" else text) for kind, text in parts) + return _split_lean_name(value) def _canonical_toml_name(value: str) -> tuple[tuple[str, str], ...]: """Return Lake's Name, including TOML's simple-name fallback.""" parts = _split_lean_name(value) - if parts is None: - return (("str", value),) - return tuple((kind, _normalize_decimal(text) if kind == "num" else text) for kind, text in parts) + return (("str", value),) if parts is None else parts -def _split_lean_name(value: str) -> list[tuple[str, str]] | None: +def _split_lean_name(value: str) -> tuple[tuple[str, str], ...] | None: parts: list[tuple[str, str]] = [] index = 0 while index < len(value): @@ -464,11 +459,11 @@ def _split_lean_name(value: str) -> list[tuple[str, str]] | None: start = index while index < len(value) and "0" <= value[index] <= "9": index += 1 - parts.append(("num", value[start:index])) + parts.append(("num", _normalize_decimal(value[start:index]))) else: return None if index == len(value): - return parts + return tuple(parts) if value[index] != ".": return None index += 1 diff --git a/autoform_cli/project/inspect.py b/autoform_cli/project/inspect.py index 07ac391b..cee929db 100644 --- a/autoform_cli/project/inspect.py +++ b/autoform_cli/project/inspect.py @@ -49,7 +49,7 @@ # Rust's ``char::is_whitespace`` set, which ``str::trim`` uses in elan. # Python additionally treats U+001C..U+001F as whitespace; accepting those # would disagree with elan because they remain control characters there. -_ELAN_WHITESPACE = frozenset( +_ELAN_WHITESPACE = ( "\u0009\u000a\u000b\u000c\u000d\u0020\u0085\u00a0\u1680" "\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a" "\u2028\u2029\u202f\u205f\u3000" @@ -125,14 +125,7 @@ def inspect_project(target: str | Path, *, catalog: ReleaseCatalog | None = None project_root = "/".join([".."] * (len(start.parts) - len(root.parts))) or "." autoform_paths = _inspect_autoform_paths(root) snapshot = _snapshot._capture_decision_snapshot(root) - attempt_diagnostics: list[ProjectDiagnostic] = [] - result = _inspect_snapshot( - catalog, - snapshot, - attempt_diagnostics, - project_root=project_root, - autoform_paths=autoform_paths, - ) + result = _inspect_snapshot(catalog, snapshot, project_root=project_root, autoform_paths=autoform_paths) verified = _snapshot._capture_decision_snapshot(root) if ( snapshot.stable @@ -176,11 +169,11 @@ def _inspect_autoform_paths(root: Path) -> tuple[str, ...]: def _inspect_snapshot( catalog: ReleaseCatalog, snapshot: _DecisionSnapshot, - diagnostics: list[ProjectDiagnostic], *, project_root: str, autoform_paths: tuple[str, ...], ) -> ProjectInspection: + diagnostics: list[ProjectDiagnostic] = [] lake, requirements = _inspect_lake(snapshot, diagnostics) requirement = requirements.mathlib if requirements is not None else None toolchain = _inspect_toolchain(snapshot, diagnostics) @@ -414,6 +407,7 @@ def _unused_mathlib(requirements: _Requirements) -> str | None: ) +# Unused here; the library locator (#204) imports it. def _trim_elan_whitespace(value: str) -> str: """Mirror Rust's Unicode whitespace trim without Python's extra C0 separators.""" @@ -435,7 +429,7 @@ def _inspect_toolchain(snapshot: _DecisionSnapshot, diagnostics: list[ProjectDia return None # elan reads only the trimmed first line and rejects an existing file when # that line is empty or malformed. - toolchain = _trim_elan_whitespace(text.split("\n", 1)[0]) + toolchain = text.split("\n", 1)[0].strip(_ELAN_WHITESPACE) if not toolchain or not toolchain.isprintable() or any(character.isspace() for character in toolchain): diagnostics.append( ProjectDiagnostic( @@ -467,23 +461,16 @@ def _locked_mathlib( layout = _manifest_layout(version) if layout is None: raise ValueError(relative) - except (AttributeError, RecursionError, ValueError): - kind = "Lake manifest" if relative == _MANIFEST else "Lake package-overrides file" - diagnostics.append( - ProjectDiagnostic("error", "invalid-lake-manifest", f"{relative} is not a {kind} Autoform reads.", relative) - ) - return False, None - if layout == "legacy": - if relative == _MANIFEST: - message = ( - f"Lake still reads the legacy layout of {relative}, but Autoform does not; " - "`lake update` rewrites it." - ) - else: - message = f"Lake still reads the legacy layout of {relative}, but Autoform does not decode it." - diagnostics.append(ProjectDiagnostic("warning", "unsupported-lake-manifest", message, relative)) - return False, None - try: + if layout == "legacy": + if relative == _MANIFEST: + message = ( + f"Lake still reads the legacy layout of {relative}, but Autoform does not; " + "`lake update` rewrites it." + ) + else: + message = f"Lake still reads the legacy layout of {relative}, but Autoform does not decode it." + diagnostics.append(ProjectDiagnostic("warning", "unsupported-lake-manifest", message, relative)) + return False, None if relative == _MANIFEST: _validate_manifest_root(payload) packages = payload.get("packages")