diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 130f23b4..7bfbb3e8 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -5,7 +5,7 @@ The Serena Model Context Protocol (MCP) Server import dataclasses import os import shutil -from collections.abc import Sequence +from collections.abc import Iterator, Sequence from copy import deepcopy from dataclasses import dataclass, field from datetime import datetime @@ -448,6 +448,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): # *** static members *** CONFIG_FILE = "serena_config.yml" + CONFIG_FIELDS_WITH_TYPE_CONVERSION = {"projects", "language_backend"} # *** methods *** @@ -455,6 +456,15 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): def config_file_path(self) -> str | None: return self._config_file_path + def _iter_config_file_mapped_fields_without_type_conversion(self) -> Iterator[str]: + for field_info in dataclasses.fields(self): + field_name = field_info.name + if field_name.startswith("_"): + continue + if field_name in self.CONFIG_FIELDS_WITH_TYPE_CONVERSION: + continue + yield field_name + def _tostring_includes(self) -> list[str]: return ["config_file_path"] @@ -509,14 +519,23 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): # create the configuration instance instance = cls(_loaded_commented_yaml=loaded_commented_yaml, _config_file_path=config_file_path) + num_migrations = 0 + + def get_value_or_default(field_name: str) -> Any: + nonlocal num_migrations + if field_name not in loaded_commented_yaml: + num_migrations += 1 + return loaded_commented_yaml.get(field_name, get_dataclass_default(SerenaConfig, field_name)) + + # transfer regular fields that do not require type conversion + for field_name in instance._iter_config_file_mapped_fields_without_type_conversion(): + assert hasattr(instance, field_name) + setattr(instance, field_name, get_value_or_default(field_name)) # 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_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()): @@ -534,12 +553,6 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): ) instance.projects.append(project) - def get_value_or_default(field_name: str) -> Any: - nonlocal num_migrations - if field_name not in loaded_commented_yaml: - num_migrations += 1 - return loaded_commented_yaml.get(field_name, get_dataclass_default(SerenaConfig, field_name)) - # determine language backend language_backend = get_dataclass_default(SerenaConfig, "language_backend") if "language_backend" in loaded_commented_yaml: @@ -554,20 +567,12 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): del loaded_commented_yaml["jetbrains"] instance.language_backend = language_backend - # set other configuration parameters (primitive types) - instance.gui_log_window = get_value_or_default("gui_log_window") - instance.web_dashboard_listen_address = get_value_or_default("web_dashboard_listen_address") - instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO)) - instance.web_dashboard = get_value_or_default("web_dashboard") - instance.web_dashboard_open_on_launch = get_value_or_default("web_dashboard_open_on_launch") - instance.jetbrains_plugin_server_address = get_value_or_default("jetbrains_plugin_server_address") - instance.tool_timeout = get_value_or_default("tool_timeout") - instance.trace_lsp_communication = get_value_or_default("trace_lsp_communication") - instance.excluded_tools = get_value_or_default("excluded_tools") - instance.included_optional_tools = get_value_or_default("included_optional_tools") - instance.token_count_estimator = get_value_or_default("token_count_estimator") - instance.default_max_tool_answer_chars = get_value_or_default("default_max_tool_answer_chars") - instance.ls_specific_settings = get_value_or_default("ls_specific_settings") + # migrate deprecated "gui_log_level" field if necessary + if "gui_log_level" in loaded_commented_yaml: + num_migrations += 1 + if "log_level" not in loaded_commented_yaml: + instance.log_level = loaded_commented_yaml["gui_log_level"] + del loaded_commented_yaml["gui_log_level"] # re-save the configuration file if any migrations were performed if num_migrations > 0: @@ -677,23 +682,19 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin): assert self._loaded_commented_yaml is not None, "Cannot save configuration without loaded YAML" - loaded_original_yaml = deepcopy(self._loaded_commented_yaml) + commented_yaml = deepcopy(self._loaded_commented_yaml) # update fields with current values - # iterate over all dataclass fields - for field_info in dataclasses.fields(self): - field_name = field_info.name - if field_name in ["projects", "language_backend"] or field_name.startswith("_"): - continue - loaded_original_yaml[field_name] = getattr(self, field_name) + for field_name in self._iter_config_file_mapped_fields_without_type_conversion(): + commented_yaml[field_name] = getattr(self, field_name) # convert project objects into list of paths - loaded_original_yaml["projects"] = sorted({str(project.project_root) for project in self.projects}) + commented_yaml["projects"] = sorted({str(project.project_root) for project in self.projects}) # convert language backend to string - loaded_original_yaml["language_backend"] = self.language_backend.value + commented_yaml["language_backend"] = self.language_backend.value - save_yaml(self.config_file_path, loaded_original_yaml, preserve_comments=True) + save_yaml(self.config_file_path, commented_yaml, preserve_comments=True) def propagate_settings(self) -> None: """