refactor(bottles): BottlePlatform becomes ABC; DockerBottlePlatform in docker.py
test / run tests/run_tests.py (pull_request) Successful in 18s
test / run tests/run_tests.py (pull_request) Successful in 18s
Mirror the BottlePlan -> DockerBottlePlan hierarchy at the platform layer. BottlePlatform is now an abstract base with abstract `prepare` and `launch` methods; DockerBottlePlatform lives alongside the rest of the Docker code in bottles/docker.py and supplies the concrete impls. The registry in bottles/__init__.py now holds an instance of each concrete platform class. Future per-platform state (region, api token, cleanup primitives) has a natural home on the subclass rather than being stitched onto a dataclass struct. No behavior change. Tests pass; dry-run output unchanged.
This commit is contained in:
@@ -24,7 +24,7 @@ from abc import ABC, abstractmethod
|
||||
from contextlib import AbstractContextManager
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable, Protocol
|
||||
from typing import Protocol
|
||||
|
||||
from ..log import die
|
||||
from ..manifest import Manifest
|
||||
@@ -58,12 +58,6 @@ class BottlePlan(ABC):
|
||||
"""Render the y/N preflight summary to stderr."""
|
||||
|
||||
|
||||
# Import concrete platform factories AFTER the base types are defined,
|
||||
# so each platform module can pull BottleSpec / BottlePlan via
|
||||
# `from . import ...` without hitting a partially-initialized module.
|
||||
from .docker import launch_docker_bottle, prepare_docker_bottle # noqa: E402
|
||||
|
||||
|
||||
class Bottle(Protocol):
|
||||
"""Handle to a running bottle. Yielded by a platform's launch step.
|
||||
|
||||
@@ -79,21 +73,31 @@ class Bottle(Protocol):
|
||||
def close(self) -> None: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BottlePlatform:
|
||||
"""Bundles a platform's two-phase factory under one selectable name."""
|
||||
class BottlePlatform(ABC):
|
||||
"""Abstract base for selectable bottle platforms. Concrete subclasses
|
||||
(e.g. DockerBottlePlatform) own their own prepare/launch impls.
|
||||
Symmetric with the BottlePlan → DockerBottlePlan hierarchy."""
|
||||
|
||||
name: str
|
||||
prepare: Callable[..., BottlePlan]
|
||||
launch: Callable[..., AbstractContextManager[Bottle]]
|
||||
|
||||
@abstractmethod
|
||||
def prepare(self, spec: BottleSpec, *, stage_dir: Path) -> BottlePlan:
|
||||
"""Resolve names, validate host-side prerequisites, write
|
||||
scratch files. No remote/runtime resources created yet."""
|
||||
|
||||
@abstractmethod
|
||||
def launch(self, plan: BottlePlan) -> AbstractContextManager[Bottle]:
|
||||
"""Build/run the bottle and yield a handle; tear down on exit."""
|
||||
|
||||
|
||||
# Import concrete platform classes AFTER the base types are defined, so
|
||||
# each platform module can pull BottleSpec / BottlePlan / BottlePlatform
|
||||
# via `from . import ...` without hitting a partially-initialized module.
|
||||
from .docker import DockerBottlePlatform # noqa: E402
|
||||
|
||||
|
||||
_PLATFORMS: dict[str, BottlePlatform] = {
|
||||
"docker": BottlePlatform(
|
||||
name="docker",
|
||||
prepare=prepare_docker_bottle,
|
||||
launch=launch_docker_bottle,
|
||||
),
|
||||
"docker": DockerBottlePlatform(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from contextlib import AbstractContextManager, contextmanager
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
@@ -34,7 +34,7 @@ from .. import skills as skills_mod
|
||||
from .. import ssh as ssh_mod
|
||||
from ..env_resolve import env_resolve
|
||||
from ..log import die, info
|
||||
from . import BottlePlan, BottleSpec
|
||||
from . import BottlePlan, BottlePlatform, BottleSpec
|
||||
|
||||
|
||||
# --- Runtime detection -----------------------------------------------------
|
||||
@@ -434,3 +434,24 @@ def _provision_container(plan: DockerBottlePlan, container: str) -> str | None:
|
||||
)
|
||||
|
||||
return in_container_prompt_path if agent.prompt else None
|
||||
|
||||
|
||||
# --- Platform --------------------------------------------------------------
|
||||
|
||||
|
||||
class DockerBottlePlatform(BottlePlatform):
|
||||
"""Docker platform implementation. Selected by CLAUDE_BOTTLE_PLATFORM
|
||||
(default). The methods delegate to the module-level prepare/launch
|
||||
functions so the platform class itself stays a thin dispatch layer."""
|
||||
|
||||
name = "docker"
|
||||
|
||||
def prepare(self, spec: BottleSpec, *, stage_dir: Path) -> BottlePlan:
|
||||
return prepare_docker_bottle(spec, stage_dir=stage_dir)
|
||||
|
||||
def launch(self, plan: BottlePlan) -> AbstractContextManager[_DockerBottle]:
|
||||
assert isinstance(plan, DockerBottlePlan), (
|
||||
f"DockerBottlePlatform.launch expects DockerBottlePlan, "
|
||||
f"got {type(plan).__name__}"
|
||||
)
|
||||
return launch_docker_bottle(plan)
|
||||
|
||||
Reference in New Issue
Block a user