Support default project file location via specification of project root directory

when specifying list of projects in serena_config.yml:
  /path/to/project/.serena/project.yml
This commit is contained in:
Dominik Jain committed 2025-04-06 23:22:06 +02:00
1 parent 9c1c2a4d2a
commit 8f9415ec3c
3 files changed
+34 -21

No files matched your search

+2 -1
View File
@@ -1,4 +1,5 @@
# absolute path to the project
# absolute path to the project you want Serena to work on (where all the source code, etc. is located)
# This is optional if this file is placed in the project directory under `.serena/project.yml`.
project_root: /path/to/project
# language of the project (csharp, python, rust, java, typescript, javascript, go, or ruby)
+1
View File
@@ -234,6 +234,7 @@ ignore = [
"B009",
"SIM103", # forbids multiple returns
"SIM110", # requires use of any(...) instead of for-loop
"G001", # forbids str.format in log statements
]
unfixable = [
"F841",
+31 -20
View File
@@ -39,11 +39,14 @@ SUCCESS_RESULT = "OK"
class ProjectConfig(ToStringMixin):
SERENA_MANAGED_DIR = ".serena"
SERENA_DEFAULT_PROJECT_FILE = "project.yml"
def __init__(self, config_dict: dict[str, Any], project_name: str):
def __init__(self, config_dict: dict[str, Any], project_name: str, project_root: Path | None = None):
self.project_name: str = project_name
self.language: Language = Language(config_dict["language"])
self.project_root: str = str(Path(config_dict["project_root"]).resolve())
if project_root is None:
project_root = Path(config_dict["project_root"])
self.project_root: str = str(project_root.resolve())
self.ignored_dirs: list[str] = config_dict.get("ignored_dirs", [])
self.excluded_tools: set[str] = set(config_dict.get("excluded_tools", []))
@@ -51,14 +54,16 @@ class ProjectConfig(ToStringMixin):
return ["ignored_dirs", "excluded_tools"]
@classmethod
def project_name_from_yml_path(cls, yml_file_path: str | Path) -> str:
return Path(yml_file_path).stem
@classmethod
def from_yml(cls, yml_path: str | Path) -> Self:
def from_yml(cls, yml_path: Path) -> Self:
with open(yml_path, encoding="utf-8") as f:
config_dict = yaml.safe_load(f)
return cls(config_dict, cls.project_name_from_yml_path(yml_path))
if yml_path.parent.name == cls.SERENA_MANAGED_DIR:
project_root = yml_path.parent.parent
project_name = project_root.name
else:
project_root = None
project_name = yml_path.stem
return cls(config_dict, project_name=project_name, project_root=project_root)
def get_serena_managed_dir(self) -> str:
return os.path.join(self.project_root, self.SERENA_MANAGED_DIR)
@@ -79,23 +84,28 @@ class SerenaConfig:
with open(config_file, encoding="utf-8") as f:
config_yaml = yaml.safe_load(f)
self.projects: dict[str, Path] = {}
for project_yaml_path in config_yaml["projects"]:
project_yaml_path = Path(project_yaml_path)
if not project_yaml_path.is_absolute():
project_yaml_path = Path(serena_root_path()) / project_yaml_path
project_name = project_yaml_path.stem
self.projects[project_name] = project_yaml_path
# read projects
self.projects: dict[str, ProjectConfig] = {}
for project_config_path in config_yaml["projects"]:
project_config_path = Path(project_config_path)
if not project_config_path.is_absolute():
project_config_path = Path(serena_root_path()) / project_config_path
if project_config_path.is_dir(): # assume project file in default location
project_config_path = project_config_path / ProjectConfig.SERENA_MANAGED_DIR / ProjectConfig.SERENA_DEFAULT_PROJECT_FILE
if not project_config_path.is_file():
raise FileNotFoundError(f"Project file not found: {project_config_path}")
project_config = ProjectConfig.from_yml(project_config_path)
self.projects[project_config.project_name] = project_config
self.project_names = list(self.projects.keys())
self.gui_log_window_enabled = config_yaml.get("gui_log_window", True)
self.gui_log_window_level = config_yaml.get("gui_log_level", logging.INFO)
self.enable_project_activation = config_yaml.get("enable_project_activation", True)
def read_project_configuration(self, project_name: str) -> ProjectConfig:
def get_project_configuration(self, project_name: str) -> ProjectConfig:
if project_name not in self.projects:
raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}")
return ProjectConfig.from_yml(self.projects[project_name])
return self.projects[project_name]
class LinesRead:
@@ -139,6 +149,7 @@ class SerenaAgent:
Logger.root.addHandler(self._gui_log_handler)
log.info(f"Starting Serena server (process id={os.getpid()}, parent process id={os.getppid()})")
log.info("Available projects: {}".format(", ".join(self.serena_config.project_names)))
self.prompt_factory = PromptFactory()
self._project_activation_callback = project_activation_callback
@@ -172,13 +183,13 @@ class SerenaAgent:
if project_file_path is not None:
if not os.path.exists(project_file_path):
raise FileNotFoundError(f"Project file not found: {project_file_path}")
project_config = ProjectConfig.from_yml(project_file_path)
project_config = ProjectConfig.from_yml(Path(project_file_path))
else:
match len(self.serena_config.projects):
case 0:
raise RuntimeError(f"No projects found in {SerenaConfig.CONFIG_FILE} and no project file specified.")
case 1:
project_config = self.serena_config.read_project_configuration(self.serena_config.project_names[0])
project_config = self.serena_config.get_project_configuration(self.serena_config.project_names[0])
if project_config is not None:
self.activate_project(project_config)
else:
@@ -1168,7 +1179,7 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
:param project_name: the name of the project to activate
"""
try:
project_config = self.agent.serena_config.read_project_configuration(project_name)
project_config = self.agent.serena_config.get_project_configuration(project_name)
except ValueError as e:
return str(e)
self.agent.activate_project(project_config)