Break tool config schema import cycle
This commit is contained in:
@@ -11,7 +11,7 @@ from nanobot.agent.tools.base import Tool, tool_parameters
|
||||
from nanobot.agent.tools.schema import ArraySchema, BooleanSchema, IntegerSchema, StringSchema, tool_parameters_schema
|
||||
from nanobot.security.workspace_access import current_tool_workspace
|
||||
from nanobot.apps.cli import CliAppError, CliAppManager, CliAppsRuntimeConfig
|
||||
from nanobot.config.schema import Base
|
||||
from nanobot.config_base import Base
|
||||
|
||||
|
||||
class CliAppsToolConfig(Base):
|
||||
|
||||
@@ -16,7 +16,7 @@ from nanobot.agent.tools.schema import (
|
||||
)
|
||||
from nanobot.security.workspace_access import current_tool_workspace
|
||||
from nanobot.config.paths import get_media_dir
|
||||
from nanobot.config.schema import Base
|
||||
from nanobot.config_base import Base
|
||||
from nanobot.providers.image_generation import (
|
||||
ImageGenerationError,
|
||||
ImageGenerationProvider,
|
||||
|
||||
@@ -10,7 +10,7 @@ from loguru import logger
|
||||
from nanobot.agent.tools.base import Tool
|
||||
from nanobot.agent.tools.context import ContextAware, RequestContext
|
||||
from nanobot.agent.tools.runtime_state import RuntimeState
|
||||
from nanobot.config.schema import Base
|
||||
from nanobot.config_base import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nanobot.agent.subagent import SubagentStatus
|
||||
|
||||
@@ -34,7 +34,7 @@ from nanobot.agent.tools.schema import (
|
||||
tool_parameters_schema,
|
||||
)
|
||||
from nanobot.config.paths import get_media_dir
|
||||
from nanobot.config.schema import Base
|
||||
from nanobot.config_base import Base
|
||||
from nanobot.security.workspace_access import current_scope_allows_loopback, current_tool_workspace
|
||||
from nanobot.security.workspace_policy import is_path_within
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from nanobot.agent.tools.schema import (
|
||||
StringSchema,
|
||||
tool_parameters_schema,
|
||||
)
|
||||
from nanobot.config.schema import Base
|
||||
from nanobot.config_base import Base
|
||||
from nanobot.utils.helpers import build_image_content_blocks
|
||||
|
||||
# Shared constants
|
||||
|
||||
@@ -4,10 +4,10 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic.alias_generators import to_camel
|
||||
from pydantic import AliasChoices, ConfigDict, Field, model_validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
from nanobot.config_base import Base
|
||||
from nanobot.cron.types import CronSchedule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -18,12 +18,6 @@ if TYPE_CHECKING:
|
||||
from nanobot.agent.tools.web import WebToolsConfig
|
||||
|
||||
|
||||
class Base(BaseModel):
|
||||
"""Base model that accepts both camelCase and snake_case keys."""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
|
||||
class ChannelsConfig(Base):
|
||||
"""Configuration for chat channels.
|
||||
|
||||
@@ -320,8 +314,8 @@ class ToolsConfig(Base):
|
||||
"""Tools configuration.
|
||||
|
||||
Field types for tool-specific sub-configs are resolved via model_rebuild()
|
||||
at the bottom of this file to avoid circular imports (tool modules import
|
||||
Base from schema.py).
|
||||
at the bottom of this file so tool config classes can stay next to their
|
||||
tool implementations.
|
||||
"""
|
||||
|
||||
web: WebToolsConfig = Field(default_factory=lambda: _lazy_default("nanobot.agent.tools.web", "WebToolsConfig"))
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Shared Pydantic base model for configuration DTOs.
|
||||
|
||||
This module intentionally lives outside the ``nanobot.config`` package so
|
||||
runtime modules can define local config DTOs without importing the full root
|
||||
configuration schema.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
|
||||
class Base(BaseModel):
|
||||
"""Base model that accepts both camelCase and snake_case keys."""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
||||
@@ -0,0 +1,38 @@
|
||||
import ast
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_config_base_import_does_not_load_config_schema():
|
||||
code = """
|
||||
import sys
|
||||
from nanobot.config_base import Base
|
||||
print("nanobot.config.schema" in sys.modules)
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", code],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.stdout.strip() == "False"
|
||||
|
||||
|
||||
def test_builtin_tool_configs_do_not_depend_on_config_schema_base():
|
||||
repo = Path(__file__).resolve().parents[2]
|
||||
tool_paths = sorted((repo / "nanobot/agent/tools").glob("*.py"))
|
||||
|
||||
violations = []
|
||||
for path in tool_paths:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.ImportFrom):
|
||||
continue
|
||||
if node.module != "nanobot.config.schema":
|
||||
continue
|
||||
if any(alias.name == "Base" for alias in node.names):
|
||||
violations.append(str(path.relative_to(repo)))
|
||||
|
||||
assert violations == []
|
||||
@@ -352,50 +352,6 @@ def test_mcp_wrappers_not_discoverable():
|
||||
assert MCPPromptWrapper._plugin_discoverable is False
|
||||
|
||||
|
||||
# --- Task 8: Config round-trip tests ---
|
||||
|
||||
|
||||
def test_config_round_trip():
|
||||
"""Verify config serialization is unchanged after moving config classes."""
|
||||
from nanobot.config.schema import Config
|
||||
|
||||
config_dict = {
|
||||
"tools": {
|
||||
"web": {"enable": True, "search": {"provider": "brave", "api_key": "test"}},
|
||||
"exec": {"enable": False, "timeout": 120, "pathPrepend": "/venv/bin"},
|
||||
"my": {"allowSet": True},
|
||||
"imageGeneration": {"enabled": True, "provider": "openrouter"},
|
||||
}
|
||||
}
|
||||
config = Config.model_validate(config_dict)
|
||||
dumped = config.model_dump(mode="json", by_alias=True)
|
||||
|
||||
assert dumped["tools"]["my"]["allowSet"] is True
|
||||
assert dumped["tools"]["imageGeneration"]["enabled"] is True
|
||||
assert dumped["tools"]["exec"]["pathPrepend"] == "/venv/bin"
|
||||
assert config.tools.exec.enable is False
|
||||
assert config.tools.exec.timeout == 120
|
||||
assert config.tools.exec.path_prepend == "/venv/bin"
|
||||
assert config.tools.web.search.provider == "brave"
|
||||
|
||||
|
||||
def test_config_defaults():
|
||||
"""Verify default values match the original hardcoded schema."""
|
||||
from nanobot.config.schema import Config
|
||||
|
||||
config = Config.model_validate({})
|
||||
assert config.tools.exec.enable is True
|
||||
assert config.tools.exec.timeout == 60
|
||||
assert config.tools.exec.path_prepend == ""
|
||||
assert config.tools.web.enable is True
|
||||
assert config.tools.web.search.provider == "duckduckgo"
|
||||
assert config.tools.my.enable is True
|
||||
assert config.tools.my.allow_set is False
|
||||
assert config.tools.image_generation.enabled is False
|
||||
assert config.tools.cli_apps.enable is True
|
||||
assert config.tools.restrict_to_workspace is False
|
||||
|
||||
|
||||
# --- Task 10: Integration test ---
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user