Merge pull request #274 from oraios/simplify-config

Simplify & improve Serena configuration
This commit is contained in:
Dominik Jain authored and GitHub committed 2025-07-02 23:30:05 +02:00
commit cb755868fb
5 files changed
+176 -304

No files matched your search

+7 -1
View File
@@ -202,6 +202,8 @@ This and other settings can be adjusted in the [configuration](#configuration) a
cd serena
```
2. Optionally create a config file from the template and adjust it according to your preferences.
You can either place it in your user directory (`~/.serena/serena_config.yml`) or into
the Serena installation directory:
```shell
cp src/serena/resources/serena_config.template.yml serena_config.yml
```
@@ -279,7 +281,11 @@ Run with parameter `--help` to get a list of available options.
Serena's behavior (active tools and prompts as well as logging configuration, etc.) is configured in four places:
1. The `serena_config.yml` for general settings that apply to all clients and projects
1. The `serena_config.yml` for general settings that apply to all clients and projects.
Serena will look for this file in your user directory under `.serena/serena_config.yml`
or within the Serena installation directory.
If you do not explicitly create the file, it will be auto-generated in the former location
when you first run Serena.
2. In the arguments passed to the `serena-mcp-server` in your client's config (see below),
which will apply to all sessions started by the respective client. In particular, the [context](#contexts) parameter
should be set appropriately for Serena to be best adjusted to existing tools and capabilities of your client.
+1 -12
View File
@@ -8,19 +8,8 @@ from pprint import pprint
from serena.agent import *
from serena.constants import REPO_ROOT
@dataclass
class InMemorySerenaConfig(SerenaConfigBase):
"""
In-memory implementation of Serena configuration with the GUI disabled.
"""
gui_log_window_enabled: bool = False
web_dashboard: bool = False
if __name__ == "__main__":
agent = SerenaAgent(project=REPO_ROOT, serena_config=InMemorySerenaConfig())
agent = SerenaAgent(project=REPO_ROOT, serena_config=SerenaConfig(gui_log_window_enabled=False, web_dashboard=False))
# apply a tool
find_refs_tool = agent.get_tool(FindReferencingSymbolsTool)
+149 -263
View File
@@ -18,18 +18,17 @@ from collections import defaultdict
from collections.abc import Callable, Generator, Iterable, Sequence
from concurrent.futures import Future, ThreadPoolExecutor
from copy import copy, deepcopy
from dataclasses import asdict, dataclass, field
from dataclasses import dataclass, field
from fnmatch import fnmatch
from functools import cached_property
from logging import Logger
from pathlib import Path
from types import TracebackType
from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast
from typing import TYPE_CHECKING, Any, Self, TypeVar, Union
import click
import yaml
from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
from overrides import override
from pathspec import PathSpec
from ruamel.yaml.comments import CommentedMap
from sensai.util import logging
@@ -169,14 +168,14 @@ class ProjectConfig(ToStringMixin):
config_with_comments["language"] = dominant_language
if save_to_disk:
save_yaml(str(project_root / cls.rel_path_to_project_yml()), config_with_comments, preserve_comments=True)
return cls.from_json_dict(config_with_comments)
return cls._from_dict(config_with_comments)
@classmethod
def rel_path_to_project_yml(cls) -> str:
return os.path.join(SERENA_MANAGED_DIR_NAME, cls.SERENA_DEFAULT_PROJECT_FILE)
@classmethod
def from_json_dict(cls, data: dict[str, Any]) -> Self:
def _from_dict(cls, data: dict[str, Any]) -> Self:
"""
Create a ProjectConfig instance from a configuration dictionary
"""
@@ -201,12 +200,6 @@ class ProjectConfig(ToStringMixin):
encoding=data.get("encoding", DEFAULT_ENCODING),
)
def to_json_dict(self) -> dict[str, Any]:
result = asdict(self)
result["language"] = result["language"].value
result["excluded_tools"] = list(result["excluded_tools"])
return result
@classmethod
def load(cls, project_root: Path | str, autogenerate: bool = True) -> Self:
"""
@@ -223,7 +216,7 @@ class ProjectConfig(ToStringMixin):
yaml_data = yaml.safe_load(f)
if "project_name" not in yaml_data:
yaml_data["project_name"] = project_root.name
return cls.from_json_dict(yaml_data)
return cls._from_dict(yaml_data)
def get_excluded_tool_classes(self) -> set[type["Tool"]]:
return set(ToolRegistry.get_tool_class_by_name(tool_name) for tool_name in self.excluded_tools)
@@ -254,21 +247,16 @@ class Project:
project_config = ProjectConfig.load(project_root, autogenerate=autogenerate)
return cls(project_root=str(project_root), project_config=project_config)
@classmethod
def from_json_dict(cls, data: dict) -> Self:
return cls(project_root=data["project_root"], project_config=ProjectConfig.from_json_dict(data["project_config"]))
def to_json_dict(self) -> dict:
return {"project_root": self.project_root, "project_config": self.project_config.to_json_dict()}
def path_to_project_yml(self) -> str:
return os.path.join(self.project_root, self.project_config.rel_path_to_project_yml())
@dataclass(kw_only=True)
class SerenaConfigBase(ABC):
class SerenaConfig:
"""
Abstract base class for Serena configuration handling
Holds the Serena agent configuration, which is typically loaded from a YAML configuration file
(when instantiated via :method:`from_config_file`), which is updated when projects are added or removed.
For testing purposes, it can also be instantiated directly with the desired parameters.
"""
projects: list[Project] = field(default_factory=list)
@@ -278,6 +266,131 @@ class SerenaConfigBase(ABC):
web_dashboard: bool = True
web_dashboard_open_on_launch: bool = True
tool_timeout: float = DEFAULT_TOOL_TIMEOUT
loaded_commented_yaml: CommentedMap | None = None
config_file_path: str | None = None
"""
the path to the configuration file to which updates of the configuration shall be saved;
if None, the configuration is not saved to disk
"""
CONFIG_FILE = "serena_config.yml"
CONFIG_FILE_DOCKER = "serena_config.docker.yml" # Docker-specific config file; auto-generated if missing, mounted via docker-compose for user customization
@classmethod
def _generate_config_file(cls, config_file_path: str) -> None:
"""
Generates a Serena configuration file at the specified path from the template file.
:param config_file_path: the path where the configuration file should be generated
"""
log.info(f"Auto-generating Serena configuration file in {config_file_path}")
loaded_commented_yaml = load_yaml(SELENA_CONFIG_TEMPLATE_FILE, preserve_comments=True)
save_yaml(config_file_path, loaded_commented_yaml, preserve_comments=True)
@classmethod
def _determine_config_file_path(cls) -> str:
"""
:return: the location where the Serena configuration file is stored/should be stored
"""
if is_running_in_docker():
return os.path.join(REPO_ROOT, cls.CONFIG_FILE_DOCKER)
else:
candidates = [
str(Path.home() / SERENA_MANAGED_DIR_NAME / cls.CONFIG_FILE),
os.path.join(REPO_ROOT, cls.CONFIG_FILE),
]
for candidate in candidates:
if os.path.exists(candidate):
return candidate
return candidates[0]
@classmethod
def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig":
"""
Static constructor to create SerenaConfig from the configuration file
"""
config_file_path = cls._determine_config_file_path()
# create the configuration file from the template if necessary
if not os.path.exists(config_file_path):
if not generate_if_missing:
raise FileNotFoundError(f"Serena configuration file not found: {config_file_path}")
log.info(f"Serena configuration file not found at {config_file_path}, autogenerating...")
cls._generate_config_file(config_file_path)
# load the configuration
log.info(f"Loading Serena configuration from {config_file_path}")
try:
loaded_commented_yaml = load_yaml(config_file_path, preserve_comments=True)
except Exception as e:
raise ValueError(f"Error loading Serena configuration from {config_file_path}: {e}") from e
# create the configuration instance
instance = cls(loaded_commented_yaml=loaded_commented_yaml, config_file_path=config_file_path)
# read projects
if "projects" not in loaded_commented_yaml:
raise SerenaConfigError("`projects` key not found in Serena configuration. Please update your `serena_config.yml` file.")
# load list of known projects
instance.projects = []
num_project_migrations = 0
for path in loaded_commented_yaml["projects"]:
path = Path(path).resolve()
if not path.exists() or (path.is_dir() and not (path / ProjectConfig.rel_path_to_project_yml()).exists()):
log.warning(f"Project path {path} does not exist or does not contain a project configuration file, skipping.")
continue
if path.is_file():
path = cls._migrate_out_of_project_config_file(path)
if path is None:
continue
num_project_migrations += 1
project = Project.load(path)
instance.projects.append(project)
# set other configuration parameters
if is_running_in_docker():
instance.gui_log_window_enabled = False # not supported in Docker
else:
instance.gui_log_window_enabled = loaded_commented_yaml.get("gui_log_window", False)
instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO))
instance.web_dashboard = loaded_commented_yaml.get("web_dashboard", True)
instance.web_dashboard_open_on_launch = loaded_commented_yaml.get("web_dashboard_open_on_launch", True)
instance.tool_timeout = loaded_commented_yaml.get("tool_timeout", DEFAULT_TOOL_TIMEOUT)
instance.trace_lsp_communication = loaded_commented_yaml.get("trace_lsp_communication", False)
# re-save the configuration file if any migrations were performed
if num_project_migrations > 0:
log.info(
f"Migrated {num_project_migrations} project configurations from legacy format to in-project configuration; re-saving configuration"
)
instance.save()
return instance
@classmethod
def _migrate_out_of_project_config_file(cls, path: Path) -> Path | None:
"""
Migrates a legacy project configuration file (which is a YAML file containing the project root) to the
in-project configuration file (project.yml) inside the project root directory.
:param path: the path to the legacy project configuration file
:return: the project root path if the migration was successful, None otherwise.
"""
log.info(f"Found legacy project configuration file {path}, migrating to in-project configuration.")
try:
with open(path, encoding="utf-8") as f:
project_config_data = yaml.safe_load(f)
if "project_name" not in project_config_data:
project_name = path.stem
with open(path, "a", encoding="utf-8") as f:
f.write(f"\nproject_name: {project_name}")
project_root = project_config_data["project_root"]
shutil.move(str(path), str(Path(project_root) / ProjectConfig.rel_path_to_project_yml()))
return Path(project_root).resolve()
except Exception as e:
log.error(f"Error migrating configuration file: {e}")
return None
@cached_property
def project_paths(self) -> list[str]:
@@ -336,17 +449,11 @@ class SerenaConfigBase(ABC):
new_project_config_generated = True
new_project = Project(project_root=str(project_root), project_config=project_config)
self._add_new_project(new_project)
self.projects.append(new_project)
self.save()
return new_project, new_project_config_generated
def _add_new_project(self, project: Project) -> None:
"""
Adds a new project to the Serena configuration. No checks are performed here,
this method is intended to be overridden by subclasses.
"""
self.projects.append(project)
def remove_project(self, project_name: str) -> None:
# find the index of the project with the desired name and remove it
for i, project in enumerate(self.projects):
@@ -355,162 +462,20 @@ class SerenaConfigBase(ABC):
break
else:
raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}")
def to_json_dict(self) -> dict:
"""Convert configuration to dictionary for serialization."""
result = asdict(self)
result["projects"] = [project.to_json_dict() for project in self.projects]
return result
@classmethod
def from_json_dict(cls, data: dict) -> Self:
"""Create configuration from dictionary."""
data = copy(data)
data["projects"] = [Project.from_json_dict(project_data) for project_data in data["projects"]]
return cls(**data)
@dataclass(kw_only=True)
class SerenaConfig(SerenaConfigBase):
"""
Handles user-defined Serena configuration based on the (fixed) Serena configuration file.
Updates to the instance will be automatically saved to the configuration file.
Usually, there should be only one instance of this class in the application.
"""
loaded_commented_yaml: CommentedMap
CONFIG_FILE = "serena_config.yml"
CONFIG_FILE_DOCKER = "serena_config.docker.yml" # Docker-specific config file; auto-generated if missing, mounted via docker-compose for user customization
@classmethod
def autogenerate(cls) -> None:
log.info("Autogenerating Serena configuration file")
if os.path.exists(cls.get_config_file_path()):
raise FileExistsError(
f"Serena configuration file already exists at {cls.get_config_file_path()}. Please remove it if you want to autogenerate a new one."
)
loaded_commented_yaml = load_yaml(SELENA_CONFIG_TEMPLATE_FILE, preserve_comments=True)
save_yaml(cls.get_config_file_path(), loaded_commented_yaml, preserve_comments=True)
@classmethod
def get_config_file_path(cls) -> str:
config_file = cls.CONFIG_FILE_DOCKER if is_running_in_docker() else cls.CONFIG_FILE
return os.path.join(REPO_ROOT, config_file)
@classmethod
def _load_commented_yaml(cls, config_file: str, generate_if_missing: bool = True) -> CommentedMap:
if not os.path.exists(config_file):
if not generate_if_missing:
raise FileNotFoundError(f"Serena configuration file not found: {config_file}")
log.info(f"Serena configuration file not found at {config_file}, autogenerating...")
cls.autogenerate()
try:
return load_yaml(config_file, preserve_comments=True)
except Exception as e:
raise ValueError(f"Error loading Serena configuration from {config_file}: {e}") from e
@classmethod
def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig":
"""
Static constructor to create SerenaConfig from the configuration file
"""
config_file = cls.get_config_file_path()
log.info(f"Loading Serena configuration from {config_file}")
loaded_commented_yaml = cls._load_commented_yaml(config_file, generate_if_missing)
# Create instance
instance = cls(loaded_commented_yaml=loaded_commented_yaml)
# read projects
if "projects" not in loaded_commented_yaml:
raise SerenaConfigError("`projects` key not found in Serena configuration. Please update your `serena_config.yml` file.")
# load list of known projects
instance.projects = []
num_project_migrations = 0
for path in loaded_commented_yaml["projects"]:
path = Path(path).resolve()
if not path.exists() or (path.is_dir() and not (path / ProjectConfig.rel_path_to_project_yml()).exists()):
log.warning(f"Project path {path} does not exist or does not contain a project configuration file, skipping.")
continue
if path.is_file():
path = cls._migrate_out_of_project_config_file(path)
if path is None:
continue
num_project_migrations += 1
project = Project.load(path)
instance.projects.append(project)
# Force disable GUI in Docker environment
if is_running_in_docker():
instance.gui_log_window_enabled = False
else:
instance.gui_log_window_enabled = loaded_commented_yaml.get("gui_log_window", False)
instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO))
instance.web_dashboard = loaded_commented_yaml.get("web_dashboard", True)
instance.web_dashboard_open_on_launch = loaded_commented_yaml.get("web_dashboard_open_on_launch", True)
instance.tool_timeout = loaded_commented_yaml.get("tool_timeout", DEFAULT_TOOL_TIMEOUT)
instance.trace_lsp_communication = loaded_commented_yaml.get("trace_lsp_communication", False)
# re-save the configuration file if any migrations were performed
if num_project_migrations > 0:
log.info(
f"Migrated {num_project_migrations} project configurations from legacy format to in-project configuration; re-saving configuration"
)
instance.save()
return instance
@classmethod
def _migrate_out_of_project_config_file(cls, path: Path) -> Path | None:
"""
Migrates a legacy project configuration file (which is a YAML file containing the project root) to the
in-project configuration file (project.yml) inside the project root directory.
:param path: the path to the legacy project configuration file
:return: the project root path if the migration was successful, None otherwise.
"""
log.info(f"Found legacy project configuration file {path}, migrating to in-project configuration.")
try:
with open(path, encoding="utf-8") as f:
project_config_data = yaml.safe_load(f)
if "project_name" not in project_config_data:
project_name = path.stem
with open(path, "a", encoding="utf-8") as f:
f.write(f"\nproject_name: {project_name}")
project_root = project_config_data["project_root"]
shutil.move(str(path), str(Path(project_root) / ProjectConfig.rel_path_to_project_yml()))
return Path(project_root).resolve()
except Exception as e:
log.error(f"Error migrating configuration file: {e}")
return None
self.save()
def save(self) -> None:
"""
Saves the configuration to the file from which it was loaded (if any)
"""
if self.config_file_path is None:
return
assert self.loaded_commented_yaml is not None, "Cannot save configuration without loaded YAML"
loaded_original_yaml = deepcopy(self.loaded_commented_yaml)
# projects are unique absolute paths
# we also canonicalize them before saving
loaded_original_yaml["projects"] = sorted({str(Path(project.project_root).resolve()) for project in self.projects})
save_yaml(self.get_config_file_path(), loaded_original_yaml, preserve_comments=True)
@override
def _add_new_project(self, project: Project) -> None:
super()._add_new_project(project)
self.save()
@override
def remove_project(self, project_name: str) -> None:
super().remove_project(project_name)
self.save()
def to_json_dict(self) -> dict:
result = super().to_json_dict()
result.pop("loaded_commented_yaml", None)
return result
@classmethod
def from_json_dict(cls, data: dict) -> Self:
data["loaded_commented_yaml"] = cls._load_commented_yaml(cls.get_config_file_path())
return super().from_json_dict(data)
save_yaml(self.config_file_path, loaded_original_yaml, preserve_comments=True)
class LinesRead:
@@ -580,71 +545,6 @@ class MemoriesManagerMDFilesInProject(MemoriesManager):
return f"Memory {name} deleted."
def create_serena_config(
serena_config: SerenaConfigBase | None = None,
enable_web_dashboard: bool | None = None,
enable_gui_log_window: bool | None = None,
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
trace_lsp_communication: bool | None = None,
tool_timeout: float | None = None,
) -> SerenaConfig:
"""
Create a SerenaConfig instance without instantiating a full SerenaAgent.
This function extracts the configuration creation logic from SerenaAgent.__init__
to allow creating configurations independently for process isolation and other use cases.
:param serena_config: the base Serena configuration or None to read from default location
:param enable_web_dashboard: Whether to enable the web dashboard
:param enable_gui_log_window: Whether to enable the GUI log window
:param log_level: Log level
:param trace_lsp_communication: Whether to trace LSP communication
:param tool_timeout: Timeout in seconds for tool execution
:return: A fully configured SerenaConfig instance
"""
# obtain serena configuration
if serena_config is not None:
# If a complete SerenaConfig is provided, use it directly
if isinstance(serena_config, SerenaConfig):
config = serena_config
else:
# For SerenaConfigBase instances (like test configs), create an in-memory SerenaConfig
# that preserves the base config attributes without loading from file
from ruamel.yaml.comments import CommentedMap
config = SerenaConfig.__new__(SerenaConfig) # Create without calling __init__
# Initialize basic attributes from base config
config.projects = getattr(serena_config, "projects", [])
config.gui_log_window_enabled = serena_config.gui_log_window_enabled
config.log_level = serena_config.log_level
config.trace_lsp_communication = serena_config.trace_lsp_communication
config.web_dashboard = serena_config.web_dashboard
config.tool_timeout = serena_config.tool_timeout
# Set empty yaml for in-memory config
config.loaded_commented_yaml = CommentedMap()
else:
config = SerenaConfig.from_config_file()
# Apply parameter overrides
if enable_web_dashboard is not None:
config.web_dashboard = enable_web_dashboard
if enable_gui_log_window is not None:
config.gui_log_window_enabled = enable_gui_log_window
if log_level is not None:
log_level = cast(Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], log_level.upper())
# transform to int
config.log_level = logging.getLevelNamesMapping()[log_level]
if trace_lsp_communication is not None:
config.trace_lsp_communication = trace_lsp_communication
if tool_timeout is not None:
config.tool_timeout = tool_timeout
# Note: Project registration/activation is handled separately by the caller
# since it involves complex logic that may require the full agent context
return config
def create_ls_for_project(
project: str | Project,
log_level: int = logging.INFO,
@@ -718,38 +618,22 @@ class SerenaAgent:
self,
project: str | None = None,
project_activation_callback: Callable[[], None] | None = None,
serena_config: SerenaConfigBase | None = None,
serena_config: SerenaConfig | None = None,
context: SerenaAgentContext | None = None,
modes: list[SerenaAgentMode] | None = None,
enable_web_dashboard: bool | None = None,
enable_gui_log_window: bool | None = None,
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
trace_lsp_communication: bool | None = None,
tool_timeout: float | None = None,
):
"""
:param project: the project to load immediately or None to not load any project; may be a path to the project or a name of
an already registered project;
:param project_activation_callback: a callback function to be called when a project is activated.
:param serena_config: the Serena configuration or None to read the configuration from the default location.
:param context: the context in which the agent is operating, None for default context.
The context may adjust prompts, tool availability, and tool descriptions.
:param modes: list of modes in which the agent is operating (they will be combined), None for default modes.
The modes may adjust prompts, tool availability, and tool descriptions.
:param serena_config: the Serena configuration or None to read the configuration from the default location.
:param enable_web_dashboard: whether to enable the web dashboard; If None, will take the value from the Serena configuration.
:param enable_gui_log_window: whether to enable the GUI log window; If None, will take the value from the Serena configuration.
:param log_level: the log level for the GUI log window; If None, will take the value from the serena configuration.
:param tool_timeout: the timeout in seconds for tool execution. If None, will take the value from the serena configuration.
"""
# obtain serena configuration using the decoupled factory function
self.serena_config = create_serena_config(
serena_config=serena_config,
enable_web_dashboard=enable_web_dashboard,
enable_gui_log_window=enable_gui_log_window,
log_level=log_level,
trace_lsp_communication=trace_lsp_communication,
tool_timeout=tool_timeout,
)
self.serena_config = serena_config or SerenaConfig.from_config_file()
# adjust log level
serena_log_level = self.serena_config.log_level
@@ -802,7 +686,9 @@ class SerenaAgent:
with contextlib.redirect_stdout(fnull), contextlib.redirect_stderr(fnull):
webbrowser.open(f"http://localhost:{port}/dashboard/index.html")
# log fundamental information
log.info(f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()})")
log.info("Configuration file: %s", self.serena_config.config_file_path)
log.info("Available projects: {}".format(", ".join(self.serena_config.project_names)))
# create executor for starting the language server and running tools in another thread
+17 -10
View File
@@ -9,7 +9,7 @@ from contextlib import asynccontextmanager
from dataclasses import dataclass
from logging import Formatter, Logger, StreamHandler
from pathlib import Path
from typing import Any, Literal
from typing import Any, Literal, cast
import click
import docstring_parser
@@ -23,7 +23,6 @@ from serena.agent import (
SerenaAgent,
SerenaConfig,
ToolInterface,
create_serena_config,
show_fatal_exception_safe,
)
from serena.config import SerenaAgentContext, SerenaAgentMode
@@ -149,15 +148,23 @@ class SerenaMCPFactory:
:param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration.
"""
try:
serena_config = create_serena_config(
enable_web_dashboard=enable_web_dashboard,
enable_gui_log_window=enable_gui_log_window,
log_level=log_level,
trace_lsp_communication=trace_lsp_communication,
tool_timeout=tool_timeout,
)
config = SerenaConfig.from_config_file()
# update configuration with the provided parameters
if enable_web_dashboard is not None:
config.web_dashboard = enable_web_dashboard
if enable_gui_log_window is not None:
config.gui_log_window_enabled = enable_gui_log_window
if log_level is not None:
log_level = cast(Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], log_level.upper())
config.log_level = logging.getLevelNamesMapping()[log_level]
if trace_lsp_communication is not None:
config.trace_lsp_communication = trace_lsp_communication
if tool_timeout is not None:
config.tool_timeout = tool_timeout
modes_instances = [SerenaAgentMode.load(mode) for mode in modes]
self._instantiate_agent(serena_config, modes_instances)
self._instantiate_agent(config, modes_instances)
except Exception as e:
show_fatal_exception_safe(e)
+2 -18
View File
@@ -1,31 +1,15 @@
import json
import os
import time
from dataclasses import dataclass
import pytest
import test.solidlsp.clojure as clj
from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, Project, ProjectConfig, SerenaAgent, SerenaConfigBase
from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, Project, ProjectConfig, SerenaAgent, SerenaConfig
from solidlsp.ls_config import Language
from test.conftest import get_repo_path
@dataclass
class SerenaConfigForTests(SerenaConfigBase):
"""
In-memory implementation of Serena configuration with the GUI disabled.
"""
gui_log_window_enabled: bool = False
web_dashboard: bool = False
def __post_init__(self):
# Initialize with empty projects list if not already set
if not hasattr(self, "projects") or self.projects is None:
self.projects = []
@pytest.fixture
def serena_config():
"""Create an in-memory configuration for tests with test repositories pre-registered."""
@@ -59,7 +43,7 @@ def serena_config():
)
test_projects.append(project)
config = SerenaConfigForTests()
config = SerenaConfig(gui_log_window_enabled=False, web_dashboard=False)
config.projects = test_projects
return config