chore(types): add pyright strict config and fix resulting errors
Adds pyrightconfig.json (strict, Python 3.11) covering cli.py, claude_bottle/, and tests/. Fixes the 49 strict-mode errors: - Type DockerBottle.teardown as Callable[[], None]. - ResolvedEnv default_factory uses parameterized list[str] / dict[str, str]. - Erase BottleBackend generics at the registry boundary (BottleBackend[Any, Any]) since selection is runtime-driven and callers use the unparameterized interface. - DockerBottleBackend.launch returns Generator[DockerBottle, None, None]; @contextmanager now flags Iterator returns as deprecated. - Sidestep cli.list submodule shadowing builtins.list in main()'s argv annotation via an aliased re-import in cli/__init__.py. - Cast cfg[...] results in test_pipelock_yaml at the dict[str, object] boundary. - Annotate write_fixture's fn parameter and _manifest_with_runtime's return type.
This commit is contained in:
@@ -34,7 +34,7 @@ from abc import ABC, abstractmethod
|
|||||||
from contextlib import AbstractContextManager
|
from contextlib import AbstractContextManager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Generic, TypeVar
|
from typing import Any, Generic, TypeVar
|
||||||
|
|
||||||
from ..log import die
|
from ..log import die
|
||||||
from ..manifest import Manifest
|
from ..manifest import Manifest
|
||||||
@@ -199,14 +199,16 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
|||||||
from .docker import DockerBottleBackend # noqa: E402
|
from .docker import DockerBottleBackend # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
# The dict carries heterogeneous BottleBackend specializations; callers
|
# The dict is heterogeneous: each value is a BottleBackend specialized
|
||||||
# use it through the unparameterized BottleBackend interface.
|
# over its own plan type. Concrete plan types are erased here because
|
||||||
_BACKENDS: dict[str, BottleBackend] = {
|
# the registry is selected at runtime and the CLI only needs the
|
||||||
|
# unparameterized methods (prepare → plan → launch(plan), cleanup, etc.).
|
||||||
|
_BACKENDS: dict[str, BottleBackend[Any, Any]] = {
|
||||||
"docker": DockerBottleBackend(),
|
"docker": DockerBottleBackend(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_bottle_backend() -> BottleBackend:
|
def get_bottle_backend() -> BottleBackend[Any, Any]:
|
||||||
"""Resolve the bottle backend for the active environment. Dies with
|
"""Resolve the bottle backend for the active environment. Dies with
|
||||||
a pointer at the known backends if CLAUDE_BOTTLE_BACKEND names an
|
a pointer at the known backends if CLAUDE_BOTTLE_BACKEND names an
|
||||||
unimplemented one."""
|
unimplemented one."""
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from contextlib import ExitStack, contextmanager
|
from contextlib import ExitStack, contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterator, Sequence
|
from typing import Generator, Sequence
|
||||||
|
|
||||||
from ... import pipelock
|
from ... import pipelock
|
||||||
from ...env import ResolvedEnv, resolve_env
|
from ...env import ResolvedEnv, resolve_env
|
||||||
@@ -165,7 +165,7 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
|
|||||||
env_file.chmod(0o600)
|
env_file.chmod(0o600)
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def launch(self, plan: DockerBottlePlan) -> Iterator[DockerBottle]:
|
def launch(self, plan: DockerBottlePlan) -> Generator[DockerBottle, None, None]:
|
||||||
"""Build, launch, and provision a Docker bottle. Teardown on exit."""
|
"""Build, launch, and provision a Docker bottle. Teardown on exit."""
|
||||||
stack = ExitStack()
|
stack = ExitStack()
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ prompt was provisioned.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from .. import Bottle
|
from .. import Bottle
|
||||||
|
|
||||||
@@ -16,7 +17,12 @@ from .. import Bottle
|
|||||||
class DockerBottle(Bottle):
|
class DockerBottle(Bottle):
|
||||||
"""Concrete Bottle for Docker."""
|
"""Concrete Bottle for Docker."""
|
||||||
|
|
||||||
def __init__(self, container: str, teardown, prompt_path_in_container: str | None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
container: str,
|
||||||
|
teardown: Callable[[], None],
|
||||||
|
prompt_path_in_container: str | None,
|
||||||
|
):
|
||||||
self.name = container
|
self.name = container
|
||||||
self._teardown = teardown
|
self._teardown = teardown
|
||||||
self._prompt_path = prompt_path_in_container
|
self._prompt_path = prompt_path_in_container
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ import sys
|
|||||||
|
|
||||||
from ..log import Die, die
|
from ..log import Die, die
|
||||||
from ._common import PROG
|
from ._common import PROG
|
||||||
|
from . import list as _list_mod
|
||||||
from .cleanup import cmd_cleanup
|
from .cleanup import cmd_cleanup
|
||||||
from .edit import cmd_edit
|
from .edit import cmd_edit
|
||||||
from .info import cmd_info
|
from .info import cmd_info
|
||||||
from .init import cmd_init
|
from .init import cmd_init
|
||||||
from .list import cmd_list
|
|
||||||
from .start import cmd_start
|
from .start import cmd_start
|
||||||
|
|
||||||
|
cmd_list = _list_mod.cmd_list
|
||||||
|
|
||||||
COMMANDS = {
|
COMMANDS = {
|
||||||
"cleanup": cmd_cleanup,
|
"cleanup": cmd_cleanup,
|
||||||
"edit": cmd_edit,
|
"edit": cmd_edit,
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ class ResolvedEnv:
|
|||||||
resolve_env; the backend forwards by-name. `literals` carry their
|
resolve_env; the backend forwards by-name. `literals` carry their
|
||||||
values verbatim and are serialized by the backend."""
|
values verbatim and are serialized by the backend."""
|
||||||
|
|
||||||
forwarded: list[str] = field(default_factory=list)
|
forwarded: list[str] = field(default_factory=list[str])
|
||||||
literals: dict[str, str] = field(default_factory=dict)
|
literals: dict[str, str] = field(default_factory=dict[str, str])
|
||||||
|
|
||||||
|
|
||||||
def env_entry_kind(raw: str) -> str:
|
def env_entry_kind(raw: str) -> str:
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"cli.py",
|
||||||
|
"claude_bottle",
|
||||||
|
"tests"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/__pycache__",
|
||||||
|
"**/.venv",
|
||||||
|
"**/venv"
|
||||||
|
],
|
||||||
|
"pythonVersion": "3.11",
|
||||||
|
"typeCheckingMode": "strict",
|
||||||
|
"reportMissingTypeStubs": "none"
|
||||||
|
}
|
||||||
+2
-2
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any, Callable
|
||||||
|
|
||||||
from claude_bottle.manifest import Manifest
|
from claude_bottle.manifest import Manifest
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ def fixture_with_ssh() -> Manifest:
|
|||||||
return Manifest.from_json_obj(fixture_with_ssh_dict())
|
return Manifest.from_json_obj(fixture_with_ssh_dict())
|
||||||
|
|
||||||
|
|
||||||
def write_fixture(fn) -> Path:
|
def write_fixture(fn: Callable[[], dict[str, Any]]) -> Path:
|
||||||
"""Write fixture JSON to a temp file; return the path. Caller must rm.
|
"""Write fixture JSON to a temp file; return the path. Caller must rm.
|
||||||
Accepts a function returning either a dict (JSON shape) or a Manifest;
|
Accepts a function returning either a dict (JSON shape) or a Manifest;
|
||||||
only the dict form is supported here since we need to serialize."""
|
only the dict form is supported here since we need to serialize."""
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ the legacy 'runtime' field must fail, regardless of value, rather than
|
|||||||
silently ignoring."""
|
silently ignoring."""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from claude_bottle.log import Die
|
from claude_bottle.log import Die
|
||||||
from claude_bottle.manifest import Bottle, Manifest
|
from claude_bottle.manifest import Bottle, Manifest
|
||||||
|
|
||||||
|
|
||||||
def _manifest_with_runtime(value: object) -> dict:
|
def _manifest_with_runtime(value: object) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"bottles": {"dev": {"runtime": value}},
|
"bottles": {"dev": {"runtime": value}},
|
||||||
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
from claude_bottle.backend.docker.pipelock import DockerPipelockProxy
|
from claude_bottle.backend.docker.pipelock import DockerPipelockProxy
|
||||||
from claude_bottle.manifest import Manifest
|
from claude_bottle.manifest import Manifest
|
||||||
@@ -27,19 +28,22 @@ class TestBuildConfig(unittest.TestCase):
|
|||||||
{"include_defaults": True, "scan_env": True}, cfg["dlp"]
|
{"include_defaults": True, "scan_env": True}, cfg["dlp"]
|
||||||
)
|
)
|
||||||
# Baked defaults always present.
|
# Baked defaults always present.
|
||||||
self.assertIn("api.anthropic.com", cfg["api_allowlist"])
|
self.assertIn("api.anthropic.com", cast(list[str], cfg["api_allowlist"]))
|
||||||
self.assertIn("raw.githubusercontent.com", cfg["api_allowlist"])
|
self.assertIn("raw.githubusercontent.com", cast(list[str], cfg["api_allowlist"]))
|
||||||
# No SSH entries → no trusted_domains, no ssrf.
|
# No SSH entries → no trusted_domains, no ssrf.
|
||||||
self.assertNotIn("trusted_domains", cfg)
|
self.assertNotIn("trusted_domains", cfg)
|
||||||
self.assertNotIn("ssrf", cfg)
|
self.assertNotIn("ssrf", cfg)
|
||||||
|
|
||||||
def test_ssh_shape(self):
|
def test_ssh_shape(self):
|
||||||
cfg = pipelock_build_config(fixture_with_ssh().bottles["dev"])
|
cfg = pipelock_build_config(fixture_with_ssh().bottles["dev"])
|
||||||
self.assertIn("github.com", cfg["trusted_domains"])
|
self.assertIn("github.com", cast(list[str], cfg["trusted_domains"]))
|
||||||
self.assertNotIn("100.78.141.42", cfg["trusted_domains"])
|
self.assertNotIn("100.78.141.42", cast(list[str], cfg["trusted_domains"]))
|
||||||
self.assertIn("100.78.141.42/32", cfg["ssrf"]["ip_allowlist"])
|
self.assertIn(
|
||||||
|
"100.78.141.42/32",
|
||||||
|
cast(dict[str, Any], cfg["ssrf"])["ip_allowlist"],
|
||||||
|
)
|
||||||
# Strict mode: IPv4 host is also in the api_allowlist union.
|
# Strict mode: IPv4 host is also in the api_allowlist union.
|
||||||
self.assertIn("100.78.141.42", cfg["api_allowlist"])
|
self.assertIn("100.78.141.42", cast(list[str], cfg["api_allowlist"]))
|
||||||
|
|
||||||
|
|
||||||
class TestRenderAndWrite(unittest.TestCase):
|
class TestRenderAndWrite(unittest.TestCase):
|
||||||
|
|||||||
Reference in New Issue
Block a user