From aaed390953a2cd2555bfe0713f842c02dd24f573 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 10 May 2026 23:32:33 -0400 Subject: [PATCH] refactor(bottles): Bottle becomes an ABC; DockerBottle inherits Bottle was the only Protocol in an otherwise-ABC family (BottlePlan, BottleCleanupPlan, BottlePlatform are all ABCs). Convert to an ABC with abstract exec_claude / cp_in / close, matching the rest of the hierarchy. Rename _DockerBottle -> DockerBottle: the underscore was a default-Python-private instinct that doesn't match the sibling plan classes (DockerBottlePlan, DockerBottleCleanupPlan), all of which are equally "only constructed by the platform" and yet public-by-name. Re-export DockerBottle from claude_bottle.bottles.docker. --- claude_bottle/bottles/__init__.py | 8 ++++++-- claude_bottle/bottles/docker/__init__.py | 6 ++++-- claude_bottle/bottles/docker/bottle.py | 6 ++++-- claude_bottle/bottles/docker/platform.py | 8 ++++---- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/claude_bottle/bottles/__init__.py b/claude_bottle/bottles/__init__.py index 740a3f2..893eb5b 100644 --- a/claude_bottle/bottles/__init__.py +++ b/claude_bottle/bottles/__init__.py @@ -31,7 +31,6 @@ from abc import ABC, abstractmethod from contextlib import AbstractContextManager from dataclasses import dataclass from pathlib import Path -from typing import Protocol from ..log import die from ..manifest import Manifest @@ -82,7 +81,7 @@ class BottleCleanupPlan(ABC): short-circuit before showing the y/N.""" -class Bottle(Protocol): +class Bottle(ABC): """Handle to a running bottle. Yielded by a platform's launch step. `exec_claude` runs `claude` inside the bottle and blocks until the @@ -92,8 +91,13 @@ class Bottle(Protocol): name: str + @abstractmethod def exec_claude(self, argv: list[str], *, tty: bool = True) -> int: ... + + @abstractmethod def cp_in(self, host_path: str, container_path: str) -> None: ... + + @abstractmethod def close(self) -> None: ... diff --git a/claude_bottle/bottles/docker/__init__.py b/claude_bottle/bottles/docker/__init__.py index 53d5783..3a34d7d 100644 --- a/claude_bottle/bottles/docker/__init__.py +++ b/claude_bottle/bottles/docker/__init__.py @@ -5,21 +5,23 @@ The bulk of the implementation lives in sibling modules: - util: thin Docker subprocess wrappers - bottle_plan: DockerBottlePlan - bottle_cleanup_plan: DockerBottleCleanupPlan - - bottle: _DockerBottle handle + - bottle: DockerBottle handle - platform: DockerBottlePlatform -This file only re-exports the platform class so +This file only re-exports the public names so `from claude_bottle.bottles.docker import DockerBottlePlatform` keeps working. """ from __future__ import annotations +from .bottle import DockerBottle from .bottle_cleanup_plan import DockerBottleCleanupPlan from .bottle_plan import DockerBottlePlan from .platform import DockerBottlePlatform __all__ = [ + "DockerBottle", "DockerBottleCleanupPlan", "DockerBottlePlan", "DockerBottlePlatform", diff --git a/claude_bottle/bottles/docker/bottle.py b/claude_bottle/bottles/docker/bottle.py index 45c8361..32a3770 100644 --- a/claude_bottle/bottles/docker/bottle.py +++ b/claude_bottle/bottles/docker/bottle.py @@ -1,4 +1,4 @@ -"""_DockerBottle — concrete Bottle handle yielded by +"""DockerBottle — concrete Bottle handle yielded by DockerBottlePlatform.launch. Holds the container name plus the in-container prompt path so @@ -10,8 +10,10 @@ from __future__ import annotations import subprocess +from .. import Bottle -class _DockerBottle: + +class DockerBottle(Bottle): """Concrete Bottle for Docker.""" def __init__(self, container: str, teardown, prompt_path_in_container: str | None): diff --git a/claude_bottle/bottles/docker/platform.py b/claude_bottle/bottles/docker/platform.py index 3a0db74..f4952b6 100644 --- a/claude_bottle/bottles/docker/platform.py +++ b/claude_bottle/bottles/docker/platform.py @@ -2,7 +2,7 @@ Methods: .prepare(spec, stage_dir=...) -> DockerBottlePlan - .launch(plan) -> ContextManager[_DockerBottle] + .launch(plan) -> ContextManager[DockerBottle] .prepare_cleanup() -> DockerBottleCleanupPlan .cleanup(plan) -> None .list_active() -> None @@ -25,7 +25,7 @@ from ...env_resolve import env_resolve from ...log import die, info from .. import BottleCleanupPlan, BottlePlan, BottlePlatform, BottleSpec from . import util as docker_mod -from .bottle import _DockerBottle +from .bottle import DockerBottle from .bottle_cleanup_plan import DockerBottleCleanupPlan from .bottle_plan import DockerBottlePlan @@ -128,7 +128,7 @@ class DockerBottlePlatform(BottlePlatform): ) @contextmanager - def launch(self, plan: BottlePlan) -> Iterator[_DockerBottle]: + def launch(self, plan: BottlePlan) -> Iterator[DockerBottle]: """Build, launch, and provision a Docker bottle. Teardown on exit.""" assert isinstance(plan, DockerBottlePlan), ( f"DockerBottlePlatform.launch expects DockerBottlePlan, " @@ -187,7 +187,7 @@ class DockerBottlePlatform(BottlePlatform): prompt_path = self._provision_container(plan, container) - bottle = _DockerBottle(container, teardown, prompt_path) + bottle = DockerBottle(container, teardown, prompt_path) yield bottle finally: teardown()