From 75cf8421804208267b788915457ee6d5baccba5f Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 5 Mar 2026 16:54:39 +0100 Subject: [PATCH] Add support for project.local.yml --- docs/02-usage/040_workflow.md | 5 +++++ src/serena/config/serena_config.py | 17 +++++++++-------- src/serena/project.py | 1 + test/serena/config/test_serena_config.py | 6 +++--- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/02-usage/040_workflow.md b/docs/02-usage/040_workflow.md index 1b73e622..4e7821e5 100644 --- a/docs/02-usage/040_workflow.md +++ b/docs/02-usage/040_workflow.md @@ -59,6 +59,11 @@ The file allows you to configure ... For detailed information on the parameters and possible settings, see the [template file](https://github.com/oraios/serena/blob/main/src/serena/resources/project.template.yml). +**Local Overrides**. The project.yml file is intended to be versioned together with the project. +You can specify local overrides for the settings in a `project.local.yml` file in the same directory +(which, by default, is ignored by git). +Any keys defined therein will override the respective key in `project.yml`. + (indexing)= ### Indexing diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index c47f52d3..3b2dd9be 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -213,7 +213,8 @@ class ProjectConfig(SharedConfig): initial_prompt: str = "" encoding: str = DEFAULT_SOURCE_FILE_ENCODING - SERENA_DEFAULT_PROJECT_FILE = "project.yml" + SERENA_PROJECT_FILE = "project.yml" + SERENA_LOCAL_PROJECT_FILE = "project.local.yml" FIELDS_WITHOUT_DEFAULTS = {"project_name", "languages"} YAML_COMMENT_NORMALISATION = YamlCommentNormalisation.LEADING """ @@ -296,7 +297,7 @@ class ProjectConfig(SharedConfig): log.info("Using languages: %s", languages_to_use) else: languages_to_use = [lang.value for lang in languages] - config_with_comments, _ = cls._load_yaml(PROJECT_TEMPLATE_FILE) + config_with_comments, _ = cls._load_yaml_dict(PROJECT_TEMPLATE_FILE) config_with_comments["project_name"] = project_name config_with_comments["languages"] = languages_to_use if save_to_disk: @@ -312,10 +313,10 @@ class ProjectConfig(SharedConfig): This is suitable as a fallback when no ``SerenaConfig`` is available to resolve a potentially customised location. """ - return os.path.join(str(project_root), SERENA_MANAGED_DIR_NAME, cls.SERENA_DEFAULT_PROJECT_FILE) + return os.path.join(str(project_root), SERENA_MANAGED_DIR_NAME, cls.SERENA_PROJECT_FILE) @classmethod - def _load_yaml( + def _load_yaml_dict( cls, yml_path: str, comment_normalisation: YamlCommentNormalisation = YamlCommentNormalisation.NONE ) -> tuple[CommentedMap, bool]: """ @@ -429,7 +430,7 @@ class ProjectConfig(SharedConfig): raise FileNotFoundError(f"Project configuration file not found: {yaml_path}") # load the configuration dictionary - yaml_data, was_complete = cls._load_yaml(str(yaml_path)) + yaml_data, was_complete = cls._load_yaml_dict(str(yaml_path)) if "project_name" not in yaml_data: yaml_data["project_name"] = project_folder_name @@ -453,11 +454,11 @@ class ProjectConfig(SharedConfig): log.info("Saving updated project configuration to %s", config_path) # load original commented map and update it with current values - config_with_comments, _ = self._load_yaml(config_path, self.YAML_COMMENT_NORMALISATION) + config_with_comments, _ = self._load_yaml_dict(config_path, self.YAML_COMMENT_NORMALISATION) config_with_comments.update(self._to_yaml_dict()) # transfer missing comments from the template file - template_config, _ = self._load_yaml(PROJECT_TEMPLATE_FILE, self.YAML_COMMENT_NORMALISATION) + template_config, _ = self._load_yaml_dict(PROJECT_TEMPLATE_FILE, self.YAML_COMMENT_NORMALISATION) transfer_missing_yaml_comments(template_config, config_with_comments, self.YAML_COMMENT_NORMALISATION) save_yaml(config_path, config_with_comments) @@ -981,7 +982,7 @@ class SerenaConfig(SharedConfig): :return: the resolved absolute path to the project's project.yml file """ serena_folder = self.get_project_serena_folder(project_root) - return os.path.join(serena_folder, ProjectConfig.SERENA_DEFAULT_PROJECT_FILE) + return os.path.join(serena_folder, ProjectConfig.SERENA_PROJECT_FILE) def propagate_settings(self) -> None: """ diff --git a/src/serena/project.py b/src/serena/project.py index bc3f0613..8bdf9f61 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -219,6 +219,7 @@ class Project(ToStringMixin): log.info(f"Creating .gitignore file in {serena_data_gitignore_path}") with open(serena_data_gitignore_path, "w", encoding="utf-8") as f: f.write(f"/{SolidLanguageServer.CACHE_FOLDER_NAME}\n") + f.write(f"/{ProjectConfig.SERENA_LOCAL_PROJECT_FILE}\n") # prepare ignore spec asynchronously, ensuring immediate project activation. self.__ignored_patterns: list[str] diff --git a/test/serena/config/test_serena_config.py b/test/serena/config/test_serena_config.py index 0e018c50..274823a3 100644 --- a/test/serena/config/test_serena_config.py +++ b/test/serena/config/test_serena_config.py @@ -135,7 +135,7 @@ class TestProjectConfigAutogenerate: class TestProjectConfig: def test_template_is_complete(self): - _, is_complete = ProjectConfig._load_yaml(PROJECT_TEMPLATE_FILE) + _, is_complete = ProjectConfig._load_yaml_dict(PROJECT_TEMPLATE_FILE) assert is_complete, "Project template YAML is incomplete; all fields must be present (with descriptions)." @@ -177,7 +177,7 @@ class TestProjectConfigLanguageBackend: def test_language_backend_parsed_from_dict(self): """Test that _from_dict parses language_backend correctly.""" template_path = PROJECT_TEMPLATE_FILE - data, _ = ProjectConfig._load_yaml(template_path) + data, _ = ProjectConfig._load_yaml_dict(template_path) data["project_name"] = "test" data["languages"] = ["python"] data["language_backend"] = "JetBrains" @@ -187,7 +187,7 @@ class TestProjectConfigLanguageBackend: def test_language_backend_none_when_missing_from_dict(self): """Test that _from_dict handles missing language_backend gracefully.""" template_path = PROJECT_TEMPLATE_FILE - data, _ = ProjectConfig._load_yaml(template_path) + data, _ = ProjectConfig._load_yaml_dict(template_path) data["project_name"] = "test" data["languages"] = ["python"] data.pop("language_backend", None)