chore: ruff format
All checks were successful
Build ISO / build-iso (push) Successful in 18m3s
CI / lint (push) Successful in 27s
CI / test (push) Successful in 1m22s
CI / validate-json (push) Successful in 25s
CI / markdown-links (push) Successful in 13s
Release / release (push) Successful in 12m13s
All checks were successful
Build ISO / build-iso (push) Successful in 18m3s
CI / lint (push) Successful in 27s
CI / test (push) Successful in 1m22s
CI / validate-json (push) Successful in 25s
CI / markdown-links (push) Successful in 13s
Release / release (push) Successful in 12m13s
Whitespace-only — `ruff check` was green when 26.17-alpha shipped but I forgot to run `ruff format`, so the CI format-check job went red on the release commit. Runtime artifacts are unaffected (release.yml doesn't gate on lint); this just re-greens the main baseline going forward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8e1f817d85
commit
b725bf1773
12 changed files with 25 additions and 72 deletions
|
|
@ -945,10 +945,7 @@ def _do_remove(name):
|
|||
dependents = deps.dependents_of(name)
|
||||
if dependents:
|
||||
return 409, {
|
||||
"error": (
|
||||
f"{name!r} is required by: {', '.join(dependents)}. "
|
||||
"Remove those first."
|
||||
),
|
||||
"error": (f"{name!r} is required by: {', '.join(dependents)}. Remove those first."),
|
||||
"dependents": list(dependents),
|
||||
}
|
||||
compose_warning = None
|
||||
|
|
|
|||
|
|
@ -128,8 +128,7 @@ def _cmd_app_remove(args: argparse.Namespace) -> int:
|
|||
dependents = deps.dependents_of(args.name)
|
||||
if dependents:
|
||||
print(
|
||||
f"error: {args.name!r} is required by: {', '.join(dependents)}. "
|
||||
"Remove those first.",
|
||||
f"error: {args.name!r} is required by: {', '.join(dependents)}. Remove those first.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
|
|
|||
|
|
@ -90,8 +90,7 @@ def plan_install(name: str) -> DepPlan:
|
|||
m = _load_any(start)
|
||||
if m is None:
|
||||
raise DependencyError(
|
||||
f"required app {start!r} not found in installed apps, "
|
||||
"catalog, or bundled apps"
|
||||
f"required app {start!r} not found in installed apps, catalog, or bundled apps"
|
||||
)
|
||||
# Sort requires alphabetically for deterministic install order.
|
||||
children = iter(sorted(r.app for r in m.requires))
|
||||
|
|
|
|||
|
|
@ -137,8 +137,7 @@ def compose_exec_script(
|
|||
if proc.returncode != 0:
|
||||
err = (proc.stderr or proc.stdout or b"").decode("utf-8", "replace").strip()
|
||||
raise DockerError(
|
||||
f"compose exec {service} hook {script_path.name} exited "
|
||||
f"{proc.returncode}: {err}"
|
||||
f"compose exec {service} hook {script_path.name} exited {proc.returncode}: {err}"
|
||||
)
|
||||
return proc.stdout.decode("utf-8", "replace")
|
||||
|
||||
|
|
|
|||
|
|
@ -152,9 +152,7 @@ def _parse_hook_output(text: str) -> dict[str, str]:
|
|||
out: dict[str, str] = {}
|
||||
|
||||
# First pass: skip FURTKA_JSON lines for KEY=VALUE extraction.
|
||||
kv_lines = [
|
||||
line for line in text.splitlines() if not _FURTKA_JSON_RE.match(line.strip())
|
||||
]
|
||||
kv_lines = [line for line in text.splitlines() if not _FURTKA_JSON_RE.match(line.strip())]
|
||||
kv = installer.parse_env_text("\n".join(kv_lines))
|
||||
for key, value in kv.items():
|
||||
if not SETTING_NAME_RE.match(key):
|
||||
|
|
@ -172,22 +170,16 @@ def _parse_hook_output(text: str) -> dict[str, str]:
|
|||
try:
|
||||
payload = json.loads(m.group(1))
|
||||
except json.JSONDecodeError as e:
|
||||
raise InstallRunnerError(
|
||||
f"hook returned invalid FURTKA_JSON payload: {e}"
|
||||
) from e
|
||||
raise InstallRunnerError(f"hook returned invalid FURTKA_JSON payload: {e}") from e
|
||||
if not isinstance(payload, dict):
|
||||
raise InstallRunnerError(
|
||||
"hook FURTKA_JSON payload must be an object of KEY=VALUE strings"
|
||||
)
|
||||
for key, value in payload.items():
|
||||
if not isinstance(key, str) or not SETTING_NAME_RE.match(key):
|
||||
raise InstallRunnerError(
|
||||
f"hook FURTKA_JSON key {key!r} must be UPPER_SNAKE_CASE"
|
||||
)
|
||||
raise InstallRunnerError(f"hook FURTKA_JSON key {key!r} must be UPPER_SNAKE_CASE")
|
||||
if not isinstance(value, str):
|
||||
raise InstallRunnerError(
|
||||
f"hook FURTKA_JSON value for {key!r} must be a string"
|
||||
)
|
||||
raise InstallRunnerError(f"hook FURTKA_JSON value for {key!r} must be a string")
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
|
@ -228,9 +220,7 @@ def _fire_install_hooks(consumer: Manifest, consumer_dir: Path) -> None:
|
|||
provider_dir = apps_dir() / req.app
|
||||
provider_manifest_path = provider_dir / "manifest.json"
|
||||
if not provider_manifest_path.is_file():
|
||||
raise InstallRunnerError(
|
||||
f"{consumer.name}: required app {req.app!r} is not installed"
|
||||
)
|
||||
raise InstallRunnerError(f"{consumer.name}: required app {req.app!r} is not installed")
|
||||
# Validate provider manifest loads (matches the contract the rest of
|
||||
# the system relies on — never trust a provider folder with a busted
|
||||
# manifest).
|
||||
|
|
@ -238,8 +228,7 @@ def _fire_install_hooks(consumer: Manifest, consumer_dir: Path) -> None:
|
|||
hook_abs = provider_dir / req.on_install
|
||||
if not hook_abs.is_file():
|
||||
raise InstallRunnerError(
|
||||
f"{consumer.name}: on_install hook "
|
||||
f"{req.on_install!r} missing in provider {req.app}"
|
||||
f"{consumer.name}: on_install hook {req.on_install!r} missing in provider {req.app}"
|
||||
)
|
||||
service = deps.provider_exec_service(provider_dir, req.app)
|
||||
stdout = dockerops.compose_exec_script(
|
||||
|
|
|
|||
|
|
@ -125,9 +125,7 @@ def _validate_hook_path(value: object, manifest_path: Path, where: str) -> str |
|
|||
return value
|
||||
|
||||
|
||||
def _parse_requires(
|
||||
raw: object, manifest_path: Path, self_name: str
|
||||
) -> tuple[Requirement, ...]:
|
||||
def _parse_requires(raw: object, manifest_path: Path, self_name: str) -> tuple[Requirement, ...]:
|
||||
if raw is None:
|
||||
return ()
|
||||
if not isinstance(raw, list):
|
||||
|
|
@ -143,9 +141,7 @@ def _parse_requires(
|
|||
f"{manifest_path}: requires[{i}].app must be a non-empty lowercase app name"
|
||||
)
|
||||
if app == self_name:
|
||||
raise ManifestError(
|
||||
f"{manifest_path}: requires[{i}].app {app!r} is a self-reference"
|
||||
)
|
||||
raise ManifestError(f"{manifest_path}: requires[{i}].app {app!r} is a self-reference")
|
||||
if app in seen:
|
||||
raise ManifestError(f"{manifest_path}: requires has duplicate app {app!r}")
|
||||
seen.add(app)
|
||||
|
|
|
|||
|
|
@ -63,9 +63,7 @@ def reconcile(apps_root: Path, dry_run: bool = False) -> list[Action]:
|
|||
OSError,
|
||||
ManifestError,
|
||||
) as e:
|
||||
actions.append(
|
||||
Action("error", m.name, f"on_start({req.app}): {e}")
|
||||
)
|
||||
actions.append(Action("error", m.name, f"on_start({req.app}): {e}"))
|
||||
hook_failed = True
|
||||
break
|
||||
if hook_failed:
|
||||
|
|
@ -95,17 +93,13 @@ def _fire_on_start_hook(consumer, req, apps_root: Path) -> None:
|
|||
provider_dir = apps_root / req.app
|
||||
provider_manifest_path = provider_dir / "manifest.json"
|
||||
if not provider_manifest_path.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"required app {req.app!r} is not installed"
|
||||
)
|
||||
raise FileNotFoundError(f"required app {req.app!r} is not installed")
|
||||
# Validate provider manifest loads (otherwise scanner would have skipped
|
||||
# it and we'd still try to exec — fail loud here instead).
|
||||
load_manifest(provider_manifest_path, expected_name=req.app)
|
||||
hook_abs = provider_dir / req.on_start
|
||||
if not hook_abs.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"on_start hook {req.on_start!r} missing in provider {req.app}"
|
||||
)
|
||||
raise FileNotFoundError(f"on_start hook {req.on_start!r} missing in provider {req.app}")
|
||||
service = deps.provider_exec_service(provider_dir, req.app)
|
||||
dockerops.compose_exec_script(
|
||||
provider_dir,
|
||||
|
|
|
|||
|
|
@ -311,9 +311,7 @@ def test_install_endpoint_without_confirm_returns_409_for_transitive(fake_dirs):
|
|||
manifest=dict(VALID_MANIFEST, name="mosquitto"),
|
||||
env_example="A=real",
|
||||
)
|
||||
consumer = dict(
|
||||
VALID_MANIFEST, name="zigbee2mqtt", requires=[{"app": "mosquitto"}]
|
||||
)
|
||||
consumer = dict(VALID_MANIFEST, name="zigbee2mqtt", requires=[{"app": "mosquitto"}])
|
||||
_write_bundled(bundled, "zigbee2mqtt", manifest=consumer, env_example="A=real")
|
||||
status, body = api._do_install("zigbee2mqtt")
|
||||
assert status == 409
|
||||
|
|
@ -329,9 +327,7 @@ def test_install_endpoint_with_confirm_dispatches_plan(fake_dirs, no_docker, no_
|
|||
manifest=dict(VALID_MANIFEST, name="mosquitto"),
|
||||
env_example="A=real",
|
||||
)
|
||||
consumer = dict(
|
||||
VALID_MANIFEST, name="zigbee2mqtt", requires=[{"app": "mosquitto"}]
|
||||
)
|
||||
consumer = dict(VALID_MANIFEST, name="zigbee2mqtt", requires=[{"app": "mosquitto"}])
|
||||
_write_bundled(bundled, "zigbee2mqtt", manifest=consumer, env_example="A=real")
|
||||
status, body = api._do_install("zigbee2mqtt", confirm_dependencies=True)
|
||||
assert status == 202
|
||||
|
|
@ -362,9 +358,7 @@ def test_remove_blocked_when_other_app_depends(fake_dirs, no_docker, no_systemd_
|
|||
manifest=dict(VALID_MANIFEST, name="mosquitto"),
|
||||
env_example="A=real",
|
||||
)
|
||||
consumer = dict(
|
||||
VALID_MANIFEST, name="zigbee2mqtt", requires=[{"app": "mosquitto"}]
|
||||
)
|
||||
consumer = dict(VALID_MANIFEST, name="zigbee2mqtt", requires=[{"app": "mosquitto"}])
|
||||
_write_bundled(bundled, "zigbee2mqtt", manifest=consumer, env_example="A=real")
|
||||
api._do_install("zigbee2mqtt", confirm_dependencies=True)
|
||||
status, body = api._do_remove("mosquitto")
|
||||
|
|
@ -383,9 +377,7 @@ def test_remove_succeeds_when_dependent_first_removed(fake_dirs, no_docker, no_s
|
|||
manifest=dict(VALID_MANIFEST, name="mosquitto"),
|
||||
env_example="A=real",
|
||||
)
|
||||
consumer = dict(
|
||||
VALID_MANIFEST, name="zigbee2mqtt", requires=[{"app": "mosquitto"}]
|
||||
)
|
||||
consumer = dict(VALID_MANIFEST, name="zigbee2mqtt", requires=[{"app": "mosquitto"}])
|
||||
_write_bundled(bundled, "zigbee2mqtt", manifest=consumer, env_example="A=real")
|
||||
api._do_install("zigbee2mqtt", confirm_dependencies=True)
|
||||
# Remove consumer first — should succeed.
|
||||
|
|
|
|||
|
|
@ -61,9 +61,7 @@ def test_plan_install_diamond(apps_root):
|
|||
_write_manifest(apps_root["catalog"], "d")
|
||||
_write_manifest(apps_root["catalog"], "b", requires=[{"app": "d"}])
|
||||
_write_manifest(apps_root["catalog"], "c", requires=[{"app": "d"}])
|
||||
_write_manifest(
|
||||
apps_root["catalog"], "a", requires=[{"app": "b"}, {"app": "c"}]
|
||||
)
|
||||
_write_manifest(apps_root["catalog"], "a", requires=[{"app": "b"}, {"app": "c"}])
|
||||
plan = deps.plan_install("a")
|
||||
order = plan.install_order
|
||||
# D first, A last, B and C in between (deterministically alphabetical).
|
||||
|
|
|
|||
|
|
@ -44,9 +44,7 @@ def test_compose_exec_propagates_env(tmp_path, monkeypatch):
|
|||
return FakeProc()
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
dockerops.compose_exec(
|
||||
tmp_path, "p", "s", ["true"], env={"A": "1", "B": "two"}
|
||||
)
|
||||
dockerops.compose_exec(tmp_path, "p", "s", ["true"], env={"A": "1", "B": "two"})
|
||||
cmd = recorded["cmd"]
|
||||
# `--env A=1 --env B=two` should appear before the service name.
|
||||
s_idx = cmd.index("s")
|
||||
|
|
|
|||
|
|
@ -342,9 +342,7 @@ def test_run_install_hook_rejects_bad_key_name(runner, monkeypatch):
|
|||
calls: list = []
|
||||
_stub_docker_ops(monkeypatch, calls)
|
||||
monkeypatch.setattr(dockerops, "compose_image_tags", lambda a, p: {"mosquitto": "img"})
|
||||
monkeypatch.setattr(
|
||||
dockerops, "compose_exec_script", lambda *a, **k: "lowercase_key=oops\n"
|
||||
)
|
||||
monkeypatch.setattr(dockerops, "compose_exec_script", lambda *a, **k: "lowercase_key=oops\n")
|
||||
|
||||
with pytest.raises(runner.InstallRunnerError, match="UPPER_SNAKE_CASE"):
|
||||
runner.run_install("z2m")
|
||||
|
|
@ -373,9 +371,7 @@ def test_run_install_hook_rejects_placeholder_value(runner, monkeypatch):
|
|||
calls: list = []
|
||||
_stub_docker_ops(monkeypatch, calls)
|
||||
monkeypatch.setattr(dockerops, "compose_image_tags", lambda a, p: {"mosquitto": "img"})
|
||||
monkeypatch.setattr(
|
||||
dockerops, "compose_exec_script", lambda *a, **k: "MQTT_PASS=changeme\n"
|
||||
)
|
||||
monkeypatch.setattr(dockerops, "compose_exec_script", lambda *a, **k: "MQTT_PASS=changeme\n")
|
||||
|
||||
with pytest.raises(runner.InstallRunnerError, match="placeholder"):
|
||||
runner.run_install("z2m")
|
||||
|
|
@ -465,9 +461,7 @@ def test_parse_hook_output_rejects_lowercase_key(runner):
|
|||
|
||||
|
||||
def test_parse_hook_output_furtka_json(runner):
|
||||
out = runner._parse_hook_output(
|
||||
'FURTKA_JSON: {"FOO": "bar", "BAZ": "qux"}\n'
|
||||
)
|
||||
out = runner._parse_hook_output('FURTKA_JSON: {"FOO": "bar", "BAZ": "qux"}\n')
|
||||
assert out == {"FOO": "bar", "BAZ": "qux"}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -232,9 +232,7 @@ def test_reconcile_dry_run_emits_hook_action_without_executing(tmp_path, fake_do
|
|||
_make_app(tmp_path, "zigbee2mqtt", CONSUMER_MANIFEST)
|
||||
|
||||
called = []
|
||||
monkeypatch.setattr(
|
||||
dockerops, "compose_exec_script", lambda *a, **k: called.append(1) or ""
|
||||
)
|
||||
monkeypatch.setattr(dockerops, "compose_exec_script", lambda *a, **k: called.append(1) or "")
|
||||
actions = reconciler.reconcile(tmp_path, dry_run=True)
|
||||
assert called == []
|
||||
hook_actions = [a for a in actions if a.kind == "hook"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue