From a6dac0a845301e4c6a4951554594aa6b290cb5bc Mon Sep 17 00:00:00 2001 From: actae0n <19864268+xpcmdshell@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:41:07 -0800 Subject: [PATCH 1/2] fix: stabilize container tests and deps loading Skip docker cleanup in xdist workers to avoid killing active containers, and centralize deps/tool setup so executors load configs consistently. --- pyproject.toml | 1 + src/py_code_mode/bootstrap.py | 101 ++++++++---------- src/py_code_mode/deps/__init__.py | 2 + src/py_code_mode/deps/config.py | 28 +++++ .../execution/container/config.py | 5 - .../execution/container/executor.py | 22 ++-- .../execution/container/server.py | 15 +-- .../execution/in_process/executor.py | 22 +--- .../execution/in_process/skills_namespace.py | 10 +- .../execution/subprocess/executor.py | 28 ++--- tests/conftest.py | 29 +++-- tests/container/test_server.py | 49 +++++++++ tests/test_backend_user_journey.py | 6 +- tests/test_deps_config_gaps.py | 2 +- tests/test_skills_namespace_decoupling.py | 22 ++++ tests/test_storage_vector_store.py | 1 - uv.lock | 2 + 17 files changed, 201 insertions(+), 144 deletions(-) create mode 100644 src/py_code_mode/deps/config.py diff --git a/pyproject.toml b/pyproject.toml index 45c7c59..855c088 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,7 @@ dev-dependencies = [ "docker>=7.1.0", "pytest-xdist>=3.8.0", "testcontainers[redis]>=4.13.3", + "chromadb>=0.5", ] [tool.pytest.ini_options] diff --git a/src/py_code_mode/bootstrap.py b/src/py_code_mode/bootstrap.py index 5465587..a63516d 100644 --- a/src/py_code_mode/bootstrap.py +++ b/src/py_code_mode/bootstrap.py @@ -74,6 +74,44 @@ async def bootstrap_namespaces(config: dict[str, Any]) -> NamespaceBundle: raise ValueError(f"Unknown storage type: {storage_type!r}. Expected 'file' or 'redis'.") +async def _load_tools_namespace(tools_path_str: str | None) -> "ToolsNamespace": + """Load tools namespace from optional tools path.""" + from py_code_mode.tools import ToolRegistry, ToolsNamespace + + if tools_path_str: + tools_path = Path(tools_path_str) + registry = ToolRegistry() + await registry.load_from_directory(tools_path) + return ToolsNamespace(registry) + + return ToolsNamespace(ToolRegistry()) + + +def _build_namespace_bundle( + storage: Any, + tools_ns: "ToolsNamespace", + deps_ns: "DepsNamespace", + artifact_store: "ArtifactStoreProtocol", +) -> NamespaceBundle: + """Wire up namespaces into a NamespaceBundle.""" + from py_code_mode.execution.in_process.skills_namespace import SkillsNamespace + + namespace_dict: dict[str, Any] = {} + skills_ns = SkillsNamespace(storage.get_skill_library(), namespace_dict) + + namespace_dict["tools"] = tools_ns + namespace_dict["skills"] = skills_ns + namespace_dict["artifacts"] = artifact_store + namespace_dict["deps"] = deps_ns + + return NamespaceBundle( + tools=tools_ns, + skills=skills_ns, + artifacts=artifact_store, + deps=deps_ns, + ) + + async def _bootstrap_file_storage(config: dict[str, Any]) -> NamespaceBundle: """Bootstrap namespaces from FileStorage config. @@ -88,24 +126,12 @@ async def _bootstrap_file_storage(config: dict[str, Any]) -> NamespaceBundle: """ # Import lazily to avoid circular imports from py_code_mode.deps import DepsNamespace, FileDepsStore, PackageInstaller - from py_code_mode.execution.in_process.skills_namespace import SkillsNamespace from py_code_mode.storage import FileStorage - from py_code_mode.tools import ToolRegistry, ToolsNamespace base_path = Path(config["base_path"]) storage = FileStorage(base_path) - # Tools are owned by executor, loaded from config if provided - tools_path_str = config.get("tools_path") - if tools_path_str: - tools_path = Path(tools_path_str) - registry = ToolRegistry() - await registry.load_from_directory(tools_path) - tools_ns = ToolsNamespace(registry) - else: - # Empty registry when no tools_path provided - tools_ns = ToolsNamespace(ToolRegistry()) - + tools_ns = await _load_tools_namespace(config.get("tools_path")) artifact_store = storage.get_artifact_store() # Create deps namespace @@ -113,23 +139,7 @@ async def _bootstrap_file_storage(config: dict[str, Any]) -> NamespaceBundle: installer = PackageInstaller() deps_ns = DepsNamespace(deps_store, installer) - # SkillsNamespace needs a namespace dict for skill execution - # Create the dict first, wire up after creation - namespace_dict: dict[str, Any] = {} - skills_ns = SkillsNamespace(storage.get_skill_library(), namespace_dict) - - # Wire up circular references - namespace_dict["tools"] = tools_ns - namespace_dict["skills"] = skills_ns - namespace_dict["artifacts"] = artifact_store - namespace_dict["deps"] = deps_ns - - return NamespaceBundle( - tools=tools_ns, - skills=skills_ns, - artifacts=artifact_store, - deps=deps_ns, - ) + return _build_namespace_bundle(storage, tools_ns, deps_ns, artifact_store) async def _bootstrap_redis_storage(config: dict[str, Any]) -> NamespaceBundle: @@ -146,9 +156,7 @@ async def _bootstrap_redis_storage(config: dict[str, Any]) -> NamespaceBundle: """ # Import lazily to avoid circular imports from py_code_mode.deps import DepsNamespace, PackageInstaller, RedisDepsStore - from py_code_mode.execution.in_process.skills_namespace import SkillsNamespace from py_code_mode.storage import RedisStorage - from py_code_mode.tools import ToolRegistry, ToolsNamespace url = config["url"] prefix = config["prefix"] @@ -156,17 +164,7 @@ async def _bootstrap_redis_storage(config: dict[str, Any]) -> NamespaceBundle: # Connect to Redis storage = RedisStorage(url=url, prefix=prefix) - # Tools are owned by executor, loaded from config if provided - tools_path_str = config.get("tools_path") - if tools_path_str: - tools_path = Path(tools_path_str) - registry = ToolRegistry() - await registry.load_from_directory(tools_path) - tools_ns = ToolsNamespace(registry) - else: - # Empty registry when no tools_path provided - tools_ns = ToolsNamespace(ToolRegistry()) - + tools_ns = await _load_tools_namespace(config.get("tools_path")) artifact_store = storage.get_artifact_store() # Create deps namespace @@ -174,19 +172,4 @@ async def _bootstrap_redis_storage(config: dict[str, Any]) -> NamespaceBundle: installer = PackageInstaller() deps_ns = DepsNamespace(deps_store, installer) - # SkillsNamespace needs a namespace dict for skill execution - namespace_dict: dict[str, Any] = {} - skills_ns = SkillsNamespace(storage.get_skill_library(), namespace_dict) - - # Wire up circular references - namespace_dict["tools"] = tools_ns - namespace_dict["skills"] = skills_ns - namespace_dict["artifacts"] = artifact_store - namespace_dict["deps"] = deps_ns - - return NamespaceBundle( - tools=tools_ns, - skills=skills_ns, - artifacts=artifact_store, - deps=deps_ns, - ) + return _build_namespace_bundle(storage, tools_ns, deps_ns, artifact_store) diff --git a/src/py_code_mode/deps/__init__.py b/src/py_code_mode/deps/__init__.py index 8062a37..1628c70 100644 --- a/src/py_code_mode/deps/__init__.py +++ b/src/py_code_mode/deps/__init__.py @@ -3,6 +3,7 @@ Provides storage, installation, and namespace for Python package dependencies. """ +from py_code_mode.deps.config import collect_configured_deps from py_code_mode.deps.installer import PackageInstaller, SyncResult from py_code_mode.deps.namespace import ( ControlledDepsNamespace, @@ -12,6 +13,7 @@ from py_code_mode.deps.store import DepsStore, FileDepsStore, MemoryDepsStore, RedisDepsStore __all__ = [ + "collect_configured_deps", "DepsStore", "FileDepsStore", "MemoryDepsStore", diff --git a/src/py_code_mode/deps/config.py b/src/py_code_mode/deps/config.py new file mode 100644 index 0000000..68ec329 --- /dev/null +++ b/src/py_code_mode/deps/config.py @@ -0,0 +1,28 @@ +"""Helpers for parsing deps configuration.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Iterable + + +def collect_configured_deps( + deps: Iterable[str] | None, + deps_file: Path | None, +) -> list[str]: + """Collect configured deps from config list and optional file. + + Args: + deps: Iterable of deps from config (e.g. config.deps). + deps_file: Optional requirements.txt-style file path. + + Returns: + Combined list of dependency specifications. + """ + configured = list(deps) if deps else [] + if deps_file and deps_file.exists(): + for line in deps_file.read_text().splitlines(): + stripped = line.strip() + if stripped and not stripped.startswith("#"): + configured.append(stripped) + return configured diff --git a/src/py_code_mode/execution/container/config.py b/src/py_code_mode/execution/container/config.py index 699ff90..8055669 100644 --- a/src/py_code_mode/execution/container/config.py +++ b/src/py_code_mode/execution/container/config.py @@ -194,7 +194,6 @@ class ContainerConfig: # Container settings environment: dict[str, str] = field(default_factory=dict) remove_on_exit: bool = True - name: str | None = None # Container name (auto-generated if None) # Deps configuration allow_runtime_deps: bool = True @@ -323,8 +322,4 @@ def to_docker_config( if platform.system() == "Linux": config["extra_hosts"] = {"host.docker.internal": "host-gateway"} - # Add container name - if self.name: - config["name"] = self.name - return config diff --git a/src/py_code_mode/execution/container/executor.py b/src/py_code_mode/execution/container/executor.py index 32e7240..0193431 100644 --- a/src/py_code_mode/execution/container/executor.py +++ b/src/py_code_mode/execution/container/executor.py @@ -45,6 +45,7 @@ HTTPX_AVAILABLE = False httpx = None # type: ignore +from py_code_mode.deps import collect_configured_deps from py_code_mode.execution.container.client import SessionClient from py_code_mode.execution.container.config import DEFAULT_IMAGE, ContainerConfig from py_code_mode.execution.protocol import ( @@ -179,16 +180,7 @@ def get_configured_deps(self) -> list[str]: Returns: List of package specifications. """ - deps: list[str] = [] - if self.config.deps: - deps.extend(self.config.deps) - if self.config.deps_file and self.config.deps_file.exists(): - file_deps = self.config.deps_file.read_text().strip().splitlines() - for line in file_deps: - stripped = line.strip() - if stripped and not stripped.startswith("#"): - deps.append(stripped) - return deps + return collect_configured_deps(self.config.deps, self.config.deps_file) async def __aenter__(self) -> ContainerExecutor: """Start container and connect.""" @@ -390,7 +382,9 @@ async def start( tools_path = self.config.tools_path skills_path = access.skills_path artifacts_path = access.artifacts_path - deps_path = None # Deps owned by executor, not storage + deps_path = None + if artifacts_path is not None: + deps_path = artifacts_path.parent / "deps" # Create directories on host before mounting # Skills need to exist for volume mount if skills_path: @@ -398,6 +392,8 @@ async def start( # Artifacts need to exist for volume mount if artifacts_path: artifacts_path.mkdir(parents=True, exist_ok=True) + if deps_path: + deps_path.mkdir(parents=True, exist_ok=True) elif isinstance(access, RedisStorageAccess): redis_url = access.redis_url # Transform localhost URLs for Docker container access @@ -891,6 +887,10 @@ async def _wait_for_healthy(self) -> None: # HTTP-level errors also expected during startup logger.debug(f"Health check failed (HTTP error): {e}") last_error = e + except Exception as e: # pragma: no cover + # Unexpected errors - capture and continue retrying + logger.debug(f"Health check failed (unexpected error): {e}") + last_error = e await asyncio.sleep(self.config.health_check_interval) diff --git a/src/py_code_mode/execution/container/server.py b/src/py_code_mode/execution/container/server.py index 357cd1f..129e7c3 100644 --- a/src/py_code_mode/execution/container/server.py +++ b/src/py_code_mode/execution/container/server.py @@ -64,7 +64,6 @@ ) from py_code_mode.skills import FileSkillStore, SkillLibrary, create_skill_library # noqa: E402 from py_code_mode.tools import ToolRegistry # noqa: E402 -from py_code_mode.tools.adapters.cli import CLIAdapter # noqa: E402 # Session expiration (seconds) SESSION_EXPIRY = 3600 # 1 hour @@ -433,19 +432,7 @@ async def initialize_server(config: SessionConfig) -> None: tools_path = os.environ.get("TOOLS_PATH") if tools_path: logger.info(" Loading tools from directory: %s", tools_path) - tools_dir = Path(tools_path) - - # Load CLI tools from YAML files - cli_adapter = CLIAdapter(tools_path=tools_dir) - registry = ToolRegistry() - if cli_adapter.list_tools(): - registry.add_adapter(adapter=cli_adapter) - - # Also load MCP tools from the same directory - mcp_registry = await ToolRegistry.from_dir(tools_path) - for adapter in mcp_registry.get_adapters(): - registry.add_adapter(adapter=adapter) - + registry = await ToolRegistry.from_dir(tools_path) logger.info(" Tools in directory: %d", len(registry.list_tools())) else: # No TOOLS_PATH - no tools available diff --git a/src/py_code_mode/execution/in_process/executor.py b/src/py_code_mode/execution/in_process/executor.py index 6440009..cf3a182 100644 --- a/src/py_code_mode/execution/in_process/executor.py +++ b/src/py_code_mode/execution/in_process/executor.py @@ -22,6 +22,7 @@ DepsNamespace, FileDepsStore, PackageInstaller, + collect_configured_deps, ) from py_code_mode.execution.in_process.config import InProcessConfig from py_code_mode.execution.in_process.skills_namespace import SkillsNamespace @@ -128,16 +129,7 @@ def get_configured_deps(self) -> list[str]: Returns: List of package specifications. """ - deps: list[str] = [] - if self._config.deps: - deps.extend(self._config.deps) - if self._config.deps_file and self._config.deps_file.exists(): - file_deps = self._config.deps_file.read_text().strip().splitlines() - for line in file_deps: - stripped = line.strip() - if stripped and not stripped.startswith("#"): - deps.append(stripped) - return deps + return collect_configured_deps(self._config.deps, self._config.deps_file) async def run(self, code: str, timeout: float | None = None) -> ExecutionResult: """Run code and return result. @@ -288,15 +280,7 @@ async def start( # Deps from executor config (NOT storage) # Collect deps from config.deps list and config.deps_file - initial_deps: list[str] = [] - if self._config.deps: - initial_deps.extend(self._config.deps) - if self._config.deps_file and self._config.deps_file.exists(): - file_deps = self._config.deps_file.read_text().strip().splitlines() - for line in file_deps: - stripped = line.strip() - if stripped and not stripped.startswith("#"): - initial_deps.append(stripped) + initial_deps = collect_configured_deps(self._config.deps, self._config.deps_file) # Create deps namespace with initial deps from config if self._deps_namespace is None: diff --git a/src/py_code_mode/execution/in_process/skills_namespace.py b/src/py_code_mode/execution/in_process/skills_namespace.py index d72134e..91dcb71 100644 --- a/src/py_code_mode/execution/in_process/skills_namespace.py +++ b/src/py_code_mode/execution/in_process/skills_namespace.py @@ -158,15 +158,17 @@ def invoke(self, skill_name: str, **kwargs: Any) -> Any: "tools": self._namespace.get("tools"), "skills": self._namespace.get("skills"), "artifacts": self._namespace.get("artifacts"), + "deps": self._namespace.get("deps"), } code = compile(skill.source, f"", "exec") _run_code(code, skill_namespace) result = skill_namespace["run"](**kwargs) if inspect.iscoroutine(result): - if self._loop is not None: - future = asyncio.run_coroutine_threadsafe(result, self._loop) - return future.result() - return asyncio.run(result) + try: + asyncio.get_running_loop() + except RuntimeError: + return asyncio.run(result) + raise RuntimeError("Cannot invoke async skills from a running event loop") return result diff --git a/src/py_code_mode/execution/subprocess/executor.py b/src/py_code_mode/execution/subprocess/executor.py index ab48b2a..99f4a7a 100644 --- a/src/py_code_mode/execution/subprocess/executor.py +++ b/src/py_code_mode/execution/subprocess/executor.py @@ -19,7 +19,12 @@ if TYPE_CHECKING: from py_code_mode.storage.backends import StorageBackend -from py_code_mode.deps import DepsStore, FileDepsStore, MemoryDepsStore +from py_code_mode.deps import ( + DepsStore, + FileDepsStore, + MemoryDepsStore, + collect_configured_deps, +) from py_code_mode.execution.protocol import ( Capability, StorageAccess, @@ -441,16 +446,7 @@ def get_configured_deps(self) -> list[str]: Returns: List of package specifications. """ - deps: list[str] = [] - if self._config.deps: - deps.extend(self._config.deps) - if self._config.deps_file and self._config.deps_file.exists(): - file_deps = self._config.deps_file.read_text().strip().splitlines() - for line in file_deps: - stripped = line.strip() - if stripped and not stripped.startswith("#"): - deps.append(stripped) - return deps + return collect_configured_deps(self._config.deps, self._config.deps_file) async def start(self, storage: StorageBackend | None = None) -> None: """Start kernel: create venv, start kernel, initialize RPC. @@ -485,15 +481,7 @@ async def start(self, storage: StorageBackend | None = None) -> None: self._tool_registry = await load_tools_from_path(self._config.tools_path) # 2. Create deps store from executor config (NOT storage) - initial_deps: list[str] = [] - if self._config.deps: - initial_deps.extend(self._config.deps) - if self._config.deps_file and self._config.deps_file.exists(): - file_deps = self._config.deps_file.read_text().strip().splitlines() - for line in file_deps: - stripped = line.strip() - if stripped and not stripped.startswith("#"): - initial_deps.append(stripped) + initial_deps = collect_configured_deps(self._config.deps, self._config.deps_file) # Create deps store with initial deps from config if self._config.deps_file: diff --git a/tests/conftest.py b/tests/conftest.py index 86f694b..21c83ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,11 @@ import fnmatch import functools import os + +# Disable testcontainers Ryuk reaper by default in this test suite. +# We run many dockerized processes (our own ContainerExecutor + testcontainers). +# If Ryuk starts, it can race and reap non-testcontainers containers unexpectedly. +os.environ.setdefault("TESTCONTAINERS_RYUK_DISABLED", "true") import shutil import subprocess from datetime import UTC, datetime @@ -514,6 +519,12 @@ def pytest_sessionfinish(session, exitstatus): # noqa: ARG001 It ensures that any py-code-mode-tools containers that weren't properly cleaned up get stopped and removed. """ + # When running with xdist, each worker triggers pytest_sessionfinish. + # Avoid cleaning in workers to prevent killing containers still in use + # by other workers. + if getattr(session.config, "workerinput", None) is not None: + return + try: import logging # noqa: PLC0415 @@ -532,15 +543,15 @@ def pytest_sessionfinish(session, exitstatus): # noqa: ARG001 f"Found {len(containers)} orphaned py-code-mode-tools containers. Cleaning up..." ) - for container in containers: - container_id = container.id[:12] - try: - logger.info(f"Stopping container {container_id}") - container.stop(timeout=5) - logger.info(f"Removing container {container_id}") - container.remove() - except Exception as e: - logger.error(f"Failed to clean up container {container_id}: {e}") + for container in containers: + container_id = container.id[:12] + try: + logger.info(f"Stopping container {container_id}") + container.stop(timeout=5) + logger.info(f"Removing container {container_id}") + container.remove() + except Exception as e: + logger.error(f"Failed to clean up container {container_id}: {e}") except ImportError: # Docker not available, skip cleanup diff --git a/tests/container/test_server.py b/tests/container/test_server.py index 2700777..3501056 100644 --- a/tests/container/test_server.py +++ b/tests/container/test_server.py @@ -169,6 +169,55 @@ def config_with_artifacts(self, tmp_path): artifacts_path=tmp_path / "artifacts", ) + @pytest.fixture + def client_with_tools(self, tmp_path, monkeypatch): + """Create test client with tools loaded via TOOLS_PATH.""" + try: + from fastapi.testclient import TestClient + except ImportError: + pytest.skip("FastAPI not installed") + + from py_code_mode.execution.container.server import create_app + + tools_dir = tmp_path / "tools" + tools_dir.mkdir() + tool_yaml = tools_dir / "echo.yaml" + tool_yaml.write_text( + """ +name: echo +description: Echo text +command: echo +schema: + positional: + - name: text + type: string + required: true + description: Text to echo +recipes: + say: + description: Echo text + params: + text: {} +""".strip() + ) + + monkeypatch.setenv("TOOLS_PATH", str(tools_dir)) + + config = SessionConfig(artifacts_path=tmp_path / "artifacts", auth_disabled=True) + app = create_app(config) + with TestClient(app) as client: + yield client + + def test_info_endpoint_includes_tools(self, client_with_tools) -> None: + """Info endpoint lists tools loaded from TOOLS_PATH.""" + response = client_with_tools.get("/info") + + assert response.status_code == 200 + data = response.json() + assert "tools" in data + tool_names = {tool["name"] for tool in data["tools"]} + assert "echo" in tool_names + def test_session_creates_artifact_store(self, config_with_artifacts) -> None: """Sessions have artifact store initialized.""" import asyncio diff --git a/tests/test_backend_user_journey.py b/tests/test_backend_user_journey.py index 4de3380..525ec53 100644 --- a/tests/test_backend_user_journey.py +++ b/tests/test_backend_user_journey.py @@ -166,7 +166,11 @@ async def test_agent_full_workflow_container( """ storage = FileStorage(tools_storage) executor = ContainerExecutor( - ContainerConfig(timeout=60.0, auth_disabled=True, tools_path=tools_dir) + ContainerConfig( + timeout=60.0, + auth_disabled=True, + tools_path=tools_dir, + ) ) async with Session(storage=storage, executor=executor) as session: diff --git a/tests/test_deps_config_gaps.py b/tests/test_deps_config_gaps.py index 36e3228..19c12d8 100644 --- a/tests/test_deps_config_gaps.py +++ b/tests/test_deps_config_gaps.py @@ -1095,7 +1095,7 @@ async def test_subprocess_bypass_via_wrapped_blocked(self, tmp_path: Path) -> No @pytest.mark.slow -@pytest.mark.xdist_group("container") +@pytest.mark.xdist_group("docker") class TestContainerExecutorDepsConfigGaps: """Tests for ContainerExecutor with deps configuration features. diff --git a/tests/test_skills_namespace_decoupling.py b/tests/test_skills_namespace_decoupling.py index 3f11967..2ed8829 100644 --- a/tests/test_skills_namespace_decoupling.py +++ b/tests/test_skills_namespace_decoupling.py @@ -131,6 +131,28 @@ def test_invoke_uses_artifacts_from_namespace(self, skill_library: SkillLibrary) assert result is True + def test_invoke_uses_deps_from_namespace(self, skill_library: SkillLibrary) -> None: + """Skill invocation can access deps from namespace dict.""" + skill = PythonSkill.from_source( + name="use_deps", + source="async def run() -> str:\n return str(deps)", + description="Checks deps access", + ) + skill_library.add(skill) + + mock_deps = MagicMock(name="my_deps") + namespace = { + "tools": None, + "skills": None, + "artifacts": None, + "deps": mock_deps, + } + + skills_ns = SkillsNamespace(skill_library, namespace) + result = skills_ns.invoke("use_deps") + + assert "MagicMock" in result + class TestNamespaceIsolation: """Skills cannot modify the parent namespace.""" diff --git a/tests/test_storage_vector_store.py b/tests/test_storage_vector_store.py index 6f46621..9a6d557 100644 --- a/tests/test_storage_vector_store.py +++ b/tests/test_storage_vector_store.py @@ -49,7 +49,6 @@ def test_get_vector_store_returns_chroma_when_available(self, tmp_path: Path) -> Breaks when: Returns None despite chromadb being available. """ - # Assume chromadb is installed in test environment storage = FileStorage(tmp_path) vector_store = storage.get_vector_store() diff --git a/uv.lock b/uv.lock index 6702762..93ff406 100644 --- a/uv.lock +++ b/uv.lock @@ -2766,6 +2766,7 @@ redis = [ [package.dev-dependencies] dev = [ { name = "aiohttp" }, + { name = "chromadb" }, { name = "docker" }, { name = "fastapi" }, { name = "httpx" }, @@ -2812,6 +2813,7 @@ provides-extras = ["dev", "jupyter", "chromadb", "redis", "mcp", "http", "contai [package.metadata.requires-dev] dev = [ { name = "aiohttp", specifier = ">=3.13.2" }, + { name = "chromadb", specifier = ">=0.5" }, { name = "docker", specifier = ">=7.1.0" }, { name = "fastapi", specifier = ">=0.124.4" }, { name = "httpx", specifier = ">=0.28.1" }, From 181ff2d6ff57198371d5886cd1daa19e2ec6f2ce Mon Sep 17 00:00:00 2001 From: actae0n <19864268+xpcmdshell@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:06:50 -0800 Subject: [PATCH 2/2] fix: resolve ruff lint violations Tidy example imports, resolve undefined tools references in example skills, and adjust type annotations to satisfy Ruff rules. --- examples/autogen-direct/test_skills/get_hn_headlines.py | 2 ++ examples/azure-container-apps/agent.py | 2 +- examples/azure-container-apps/agent_server.py | 2 +- examples/shared/skills/analyze_repo.py | 1 + src/py_code_mode/bootstrap.py | 8 ++++---- src/py_code_mode/deps/config.py | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/autogen-direct/test_skills/get_hn_headlines.py b/examples/autogen-direct/test_skills/get_hn_headlines.py index 825175a..209d94a 100644 --- a/examples/autogen-direct/test_skills/get_hn_headlines.py +++ b/examples/autogen-direct/test_skills/get_hn_headlines.py @@ -11,6 +11,8 @@ def run(count: int = 10) -> list[str]: import html import re + tools = globals()["tools"] + # Fetch the HackerNews front page with enough content to get all headlines html_content = tools.web.fetch(url="https://news.ycombinator.com/", raw=True, max_length=20000) diff --git a/examples/azure-container-apps/agent.py b/examples/azure-container-apps/agent.py index af76589..13c63f6 100644 --- a/examples/azure-container-apps/agent.py +++ b/examples/azure-container-apps/agent.py @@ -46,8 +46,8 @@ def get_model_client(): """Get Azure OpenAI model client.""" - from autogen_ext.models.openai import AzureOpenAIChatCompletionClient from autogen_core.models import ModelFamily, ModelInfo + from autogen_ext.models.openai import AzureOpenAIChatCompletionClient from azure.identity import DefaultAzureCredential, get_bearer_token_provider # Use managed identity for Azure OpenAI auth diff --git a/examples/azure-container-apps/agent_server.py b/examples/azure-container-apps/agent_server.py index 952260d..28cfa60 100644 --- a/examples/azure-container-apps/agent_server.py +++ b/examples/azure-container-apps/agent_server.py @@ -26,8 +26,8 @@ class TaskResponse(BaseModel): def get_model_client(): """Get Azure OpenAI model client.""" - from autogen_ext.models.openai import AzureOpenAIChatCompletionClient from autogen_core.models import ModelFamily, ModelInfo + from autogen_ext.models.openai import AzureOpenAIChatCompletionClient from azure.identity import DefaultAzureCredential, get_bearer_token_provider # Use managed identity for Azure OpenAI auth diff --git a/examples/shared/skills/analyze_repo.py b/examples/shared/skills/analyze_repo.py index 64c522e..459b2ad 100644 --- a/examples/shared/skills/analyze_repo.py +++ b/examples/shared/skills/analyze_repo.py @@ -23,6 +23,7 @@ async def run(repo: str) -> dict: base_url = f"https://api.github.com/repos/{repo}" results = {} + tools = globals()["tools"] # Step 1: Fetch repo metadata repo_raw = tools.curl(url=base_url) diff --git a/src/py_code_mode/bootstrap.py b/src/py_code_mode/bootstrap.py index a63516d..a4d7fbc 100644 --- a/src/py_code_mode/bootstrap.py +++ b/src/py_code_mode/bootstrap.py @@ -74,7 +74,7 @@ async def bootstrap_namespaces(config: dict[str, Any]) -> NamespaceBundle: raise ValueError(f"Unknown storage type: {storage_type!r}. Expected 'file' or 'redis'.") -async def _load_tools_namespace(tools_path_str: str | None) -> "ToolsNamespace": +async def _load_tools_namespace(tools_path_str: str | None) -> ToolsNamespace: """Load tools namespace from optional tools path.""" from py_code_mode.tools import ToolRegistry, ToolsNamespace @@ -89,9 +89,9 @@ async def _load_tools_namespace(tools_path_str: str | None) -> "ToolsNamespace": def _build_namespace_bundle( storage: Any, - tools_ns: "ToolsNamespace", - deps_ns: "DepsNamespace", - artifact_store: "ArtifactStoreProtocol", + tools_ns: ToolsNamespace, + deps_ns: DepsNamespace, + artifact_store: ArtifactStoreProtocol, ) -> NamespaceBundle: """Wire up namespaces into a NamespaceBundle.""" from py_code_mode.execution.in_process.skills_namespace import SkillsNamespace diff --git a/src/py_code_mode/deps/config.py b/src/py_code_mode/deps/config.py index 68ec329..ca7f40d 100644 --- a/src/py_code_mode/deps/config.py +++ b/src/py_code_mode/deps/config.py @@ -2,8 +2,8 @@ from __future__ import annotations +from collections.abc import Iterable from pathlib import Path -from typing import Iterable def collect_configured_deps(