3e62f31d8b
tracker-policy-pr / check-pr (pull_request) Successful in 12s
test / integration-docker (pull_request) Successful in 38s
test / unit (pull_request) Successful in 52s
lint / lint (push) Successful in 1m1s
test / integration-firecracker (pull_request) Successful in 3m33s
test / coverage (pull_request) Successful in 17s
test / publish-infra (pull_request) Has been skipped
backend/__init__.py eagerly imported the whole framework (manifest, egress,
git-gate, env, workspace, agent-provider), so importing ANY backend.* submodule
paid ~32 modules just to run the package init. Split it:
- backend/base.py — the abstract contract (BottleSpec, BottlePlan,
BottleCleanupPlan, ExecResult, ActiveAgent, Bottle,
BottleImages, BottleBackend). Carries the framework
imports; loaded only when a caller needs the contract.
- backend/selection.py — backend registry / selection / enumeration
(get_bottle_backend, _get_backends, _auto_select_backend,
has_backend, known_backend_names, enumerate_active_agents).
Concrete backends still imported lazily inside.
- backend/__init__.py — thin: a __getattr__ that resolves the public names
from those submodules on first access (+ a TYPE_CHECKING
block for checkers). Existing `from bot_bottle.backend
import X` and patch.object call-sites keep working.
`import bot_bottle.backend` drops from 32 -> 2 bot_bottle modules. Repointed the
backend-selection test's patch targets to the `selection` module (the functions
reference each other there now; the FirecrackerBottleBackend class-method
patches are unchanged — same class object).
Note: backend.docker.util is still heavy because backend/docker/__init__ eagerly
imports DockerBottleBackend — slimming the three sub-package inits is the
follow-up that makes the leaves cheap.
Full unit suite green (2243).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
107 lines
3.3 KiB
Python
107 lines
3.3 KiB
Python
"""The bottle-backend package: abstract contract + backend selection.
|
|
|
|
Thin by design — nothing framework-heavy is imported at package init, so
|
|
importing any `backend.*` submodule (e.g. `backend.docker.util`) doesn't drag
|
|
the manifest / egress / git-gate framework into memory. The public names are
|
|
re-exported lazily:
|
|
|
|
* the abstract contract (`BottleBackend`, `BottleSpec`, `Bottle`, …) lives in
|
|
`backend.base`;
|
|
* backend selection / enumeration (`get_bottle_backend`,
|
|
`enumerate_active_agents`, …) in `backend.selection`;
|
|
* the concrete backends and the freeze helpers in their own submodules.
|
|
|
|
`from bot_bottle.backend import X` resolves X on first access via `__getattr__`
|
|
and caches it at package level, so existing call-sites (and
|
|
`patch.object(backend_mod, X, …)`) keep working.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
if TYPE_CHECKING:
|
|
from .base import (
|
|
ActiveAgent,
|
|
Bottle,
|
|
BottleBackend,
|
|
BottleCleanupPlan,
|
|
BottleImages,
|
|
BottlePlan,
|
|
BottleSpec,
|
|
ExecResult,
|
|
)
|
|
from .selection import (
|
|
enumerate_active_agents,
|
|
get_bottle_backend,
|
|
has_backend,
|
|
known_backend_names,
|
|
)
|
|
from .docker import DockerBottleBackend
|
|
from .firecracker import FirecrackerBottleBackend
|
|
from .macos_container import MacosContainerBottleBackend
|
|
from .freeze import CommitCancelled, Freezer, get_freezer
|
|
|
|
|
|
# Public name -> submodule that defines it. Contract types resolve from `base`,
|
|
# selection/enumeration from `selection`, the concrete backends + freeze helpers
|
|
# from their own subpackages.
|
|
_LAZY_MODULES: dict[str, str] = {
|
|
"BottleSpec": "base",
|
|
"BottlePlan": "base",
|
|
"BottleCleanupPlan": "base",
|
|
"ExecResult": "base",
|
|
"ActiveAgent": "base",
|
|
"Bottle": "base",
|
|
"BottleImages": "base",
|
|
"BottleBackend": "base",
|
|
"get_bottle_backend": "selection",
|
|
"known_backend_names": "selection",
|
|
"has_backend": "selection",
|
|
"enumerate_active_agents": "selection",
|
|
"_print_vm_install_instructions": "selection",
|
|
"DockerBottleBackend": "docker",
|
|
"FirecrackerBottleBackend": "firecracker",
|
|
"MacosContainerBottleBackend": "macos_container",
|
|
"CommitCancelled": "freeze",
|
|
"Freezer": "freeze",
|
|
"get_freezer": "freeze",
|
|
}
|
|
|
|
|
|
def __getattr__(name: str) -> Any:
|
|
"""Lazily surface the package's public names from their submodules and
|
|
cache them at package level — so `from bot_bottle.backend import X` and
|
|
`patch.object(backend_mod, X, …)` keep working without importing the
|
|
framework (or every backend) at package-init time."""
|
|
mod = _LAZY_MODULES.get(name)
|
|
if mod is None:
|
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
from importlib import import_module
|
|
|
|
value = getattr(import_module(f"{__name__}.{mod}"), name)
|
|
globals()[name] = value
|
|
return value
|
|
|
|
|
|
__all__ = [
|
|
"ActiveAgent",
|
|
"Bottle",
|
|
"BottleBackend",
|
|
"BottleCleanupPlan",
|
|
"BottleImages",
|
|
"BottlePlan",
|
|
"BottleSpec",
|
|
"ExecResult",
|
|
"CommitCancelled",
|
|
"Freezer",
|
|
"get_freezer",
|
|
"DockerBottleBackend",
|
|
"FirecrackerBottleBackend",
|
|
"MacosContainerBottleBackend",
|
|
"enumerate_active_agents",
|
|
"get_bottle_backend",
|
|
"has_backend",
|
|
"known_backend_names",
|
|
]
|