diff --git a/furtka/api.py b/furtka/api.py index 371191a..7af5209 100644 --- a/furtka/api.py +++ b/furtka/api.py @@ -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 diff --git a/furtka/cli.py b/furtka/cli.py index 232f906..87091f4 100644 --- a/furtka/cli.py +++ b/furtka/cli.py @@ -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 diff --git a/furtka/deps.py b/furtka/deps.py index 0ec5418..c907fdb 100644 --- a/furtka/deps.py +++ b/furtka/deps.py @@ -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)) diff --git a/furtka/dockerops.py b/furtka/dockerops.py index 05be219..0db9eb0 100644 --- a/furtka/dockerops.py +++ b/furtka/dockerops.py @@ -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") diff --git a/furtka/install_runner.py b/furtka/install_runner.py index 77b5751..f339fb2 100644 --- a/furtka/install_runner.py +++ b/furtka/install_runner.py @@ -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( diff --git a/furtka/manifest.py b/furtka/manifest.py index e84afe9..03a4c73 100644 --- a/furtka/manifest.py +++ b/furtka/manifest.py @@ -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) diff --git a/furtka/reconciler.py b/furtka/reconciler.py index 4d97099..2f8278a 100644 --- a/furtka/reconciler.py +++ b/furtka/reconciler.py @@ -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, diff --git a/tests/test_api.py b/tests/test_api.py index 1d5e8f9..244f0a2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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. diff --git a/tests/test_deps.py b/tests/test_deps.py index 4bada7e..fa3f9e3 100644 --- a/tests/test_deps.py +++ b/tests/test_deps.py @@ -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). diff --git a/tests/test_dockerops.py b/tests/test_dockerops.py index 41197d3..5c1bd33 100644 --- a/tests/test_dockerops.py +++ b/tests/test_dockerops.py @@ -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") diff --git a/tests/test_install_runner.py b/tests/test_install_runner.py index 9f16403..8a82b7f 100644 --- a/tests/test_install_runner.py +++ b/tests/test_install_runner.py @@ -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"} diff --git a/tests/test_reconciler.py b/tests/test_reconciler.py index 717782c..d5575cf 100644 --- a/tests/test_reconciler.py +++ b/tests/test_reconciler.py @@ -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"]