refactor(bottles): BottlePlatform becomes ABC; DockerBottlePlatform in docker.py
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:
2026-05-10 22:56:47 -04:00
parent 2827d9b899
commit e22a96e511
2 changed files with 44 additions and 19 deletions
+21 -17
View File
@@ -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(),
}