Allow Serena home directory to be overridden with SERENA_HOME env var,

defining the relevant path variables in SerenaPaths
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2025-12-07 13:39:08 +01:00
1 parent 904a25053e
commit 7b287fa602
7 files changed
+64 -49

No files matched your search

+15 -18
View File
@@ -21,14 +21,10 @@ from serena.config.serena_config import ProjectConfig, SerenaConfig, SerenaPaths
from serena.constants import (
DEFAULT_CONTEXT,
DEFAULT_MODES,
PROMPT_TEMPLATES_DIR_IN_USER_HOME,
PROMPT_TEMPLATES_DIR_INTERNAL,
SERENA_LOG_FORMAT,
SERENA_MANAGED_DIR_IN_HOME,
SERENAS_OWN_CONTEXT_YAMLS_DIR,
SERENAS_OWN_MODE_YAMLS_DIR,
USER_CONTEXT_YAMLS_DIR,
USER_MODE_YAMLS_DIR,
)
from serena.mcp import SerenaMCPFactory, SerenaMCPFactorySingleProcess
from serena.project import Project
@@ -270,7 +266,7 @@ class ModeCommands(AutoRegisteringGroup):
if not (name or from_internal):
raise click.UsageError("Provide at least one of --name or --from-internal.")
mode_name = name or from_internal
dest = os.path.join(USER_MODE_YAMLS_DIR, f"{mode_name}.yml")
dest = os.path.join(SerenaPaths().user_modes_dir, f"{mode_name}.yml")
src = (
os.path.join(SERENAS_OWN_MODE_YAMLS_DIR, f"{from_internal}.yml")
if from_internal
@@ -289,7 +285,7 @@ class ModeCommands(AutoRegisteringGroup):
@click.command("edit", help="Edit a custom mode YAML file.")
@click.argument("mode_name")
def edit(mode_name: str) -> None:
path = os.path.join(USER_MODE_YAMLS_DIR, f"{mode_name}.yml")
path = os.path.join(SerenaPaths().user_modes_dir, f"{mode_name}.yml")
if not os.path.exists(path):
if mode_name in SerenaAgentMode.list_registered_mode_names(include_user_modes=False):
click.echo(
@@ -305,7 +301,7 @@ class ModeCommands(AutoRegisteringGroup):
@click.command("delete", help="Delete a custom mode file.")
@click.argument("mode_name")
def delete(mode_name: str) -> None:
path = os.path.join(USER_MODE_YAMLS_DIR, f"{mode_name}.yml")
path = os.path.join(SerenaPaths().user_modes_dir, f"{mode_name}.yml")
if not os.path.exists(path):
click.echo(f"Custom mode '{mode_name}' not found.")
return
@@ -347,7 +343,7 @@ class ContextCommands(AutoRegisteringGroup):
if not (name or from_internal):
raise click.UsageError("Provide at least one of --name or --from-internal.")
ctx_name = name or from_internal
dest = os.path.join(USER_CONTEXT_YAMLS_DIR, f"{ctx_name}.yml")
dest = os.path.join(SerenaPaths().user_contexts_dir, f"{ctx_name}.yml")
src = (
os.path.join(SERENAS_OWN_CONTEXT_YAMLS_DIR, f"{from_internal}.yml")
if from_internal
@@ -366,7 +362,7 @@ class ContextCommands(AutoRegisteringGroup):
@click.command("edit", help="Edit a custom context YAML file.")
@click.argument("context_name")
def edit(context_name: str) -> None:
path = os.path.join(USER_CONTEXT_YAMLS_DIR, f"{context_name}.yml")
path = os.path.join(SerenaPaths().user_contexts_dir, f"{context_name}.yml")
if not os.path.exists(path):
if context_name in SerenaAgentContext.list_registered_context_names(include_user_contexts=False):
click.echo(
@@ -382,7 +378,7 @@ class ContextCommands(AutoRegisteringGroup):
@click.command("delete", help="Delete a custom context file.")
@click.argument("context_name")
def delete(context_name: str) -> None:
path = os.path.join(USER_CONTEXT_YAMLS_DIR, f"{context_name}.yml")
path = os.path.join(SerenaPaths().user_contexts_dir, f"{context_name}.yml")
if not os.path.exists(path):
click.echo(f"Custom context '{context_name}' not found.")
return
@@ -401,10 +397,9 @@ class SerenaConfigCommands(AutoRegisteringGroup):
"edit", help="Edit serena_config.yml in your default editor. Will create a config file from the template if no config is found."
)
def edit() -> None:
config_path = os.path.join(SERENA_MANAGED_DIR_IN_HOME, "serena_config.yml")
if not os.path.exists(config_path):
SerenaConfig.generate_config_file(config_path)
_open_in_editor(config_path)
serena_config = SerenaConfig.from_config_file()
assert serena_config.config_file_path is not None
_open_in_editor(serena_config.config_file_path)
class ProjectCommands(AutoRegisteringGroup):
@@ -811,8 +806,9 @@ class PromptCommands(AutoRegisteringGroup):
@staticmethod
def _get_user_prompt_yaml_path(prompt_yaml_name: str) -> str:
os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True)
return os.path.join(PROMPT_TEMPLATES_DIR_IN_USER_HOME, prompt_yaml_name)
templates_dir = SerenaPaths().user_prompt_templates_dir
os.makedirs(templates_dir, exist_ok=True)
return os.path.join(templates_dir, prompt_yaml_name)
@staticmethod
@click.command("list", help="Lists yamls that are used for defining prompts.")
@@ -863,9 +859,10 @@ class PromptCommands(AutoRegisteringGroup):
@staticmethod
@click.command("list-overrides", help="List existing prompt override files")
def list_overrides() -> None:
os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True)
user_templates_dir = SerenaPaths().user_prompt_templates_dir
os.makedirs(user_templates_dir, exist_ok=True)
serena_prompt_yaml_names = [os.path.basename(f) for f in glob.glob(PROMPT_TEMPLATES_DIR_INTERNAL + "/*.yml")]
override_files = glob.glob(os.path.join(PROMPT_TEMPLATES_DIR_IN_USER_HOME, "*.yml"))
override_files = glob.glob(os.path.join(user_templates_dir, "*.yml"))
for file_path in override_files:
if os.path.basename(file_path) in serena_prompt_yaml_names:
click.echo(file_path)
+7 -9
View File
@@ -11,7 +11,7 @@ import yaml
from sensai.util import logging
from sensai.util.string import ToStringMixin
from serena.config.serena_config import ToolInclusionDefinition
from serena.config.serena_config import SerenaPaths, ToolInclusionDefinition
from serena.constants import (
DEFAULT_CONTEXT,
DEFAULT_MODES,
@@ -19,8 +19,6 @@ from serena.constants import (
SERENA_FILE_ENCODING,
SERENAS_OWN_CONTEXT_YAMLS_DIR,
SERENAS_OWN_MODE_YAMLS_DIR,
USER_CONTEXT_YAMLS_DIR,
USER_MODE_YAMLS_DIR,
)
if TYPE_CHECKING:
@@ -65,14 +63,14 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin):
def get_path(cls, name: str) -> str:
"""Get the path to the YAML file for a mode."""
fname = f"{name}.yml"
custom_mode_path = os.path.join(USER_MODE_YAMLS_DIR, fname)
custom_mode_path = os.path.join(SerenaPaths().user_modes_dir, fname)
if os.path.exists(custom_mode_path):
return custom_mode_path
own_yaml_path = os.path.join(SERENAS_OWN_MODE_YAMLS_DIR, fname)
if not os.path.exists(own_yaml_path):
raise FileNotFoundError(
f"Mode {name} not found in {USER_MODE_YAMLS_DIR} or in {SERENAS_OWN_MODE_YAMLS_DIR}."
f"Mode {name} not found in {SerenaPaths().user_modes_dir} or in {SERENAS_OWN_MODE_YAMLS_DIR}."
f"Available modes:\n{cls.list_registered_mode_names()}"
)
return own_yaml_path
@@ -102,7 +100,7 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin):
@classmethod
def list_custom_mode_names(cls) -> list[str]:
"""Names of all custom modes defined by the user."""
return [f.stem for f in Path(USER_MODE_YAMLS_DIR).glob("*.yml")]
return [f.stem for f in Path(SerenaPaths().user_modes_dir).glob("*.yml")]
@classmethod
def load_default_modes(cls) -> list[Self]:
@@ -166,14 +164,14 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin):
def get_path(cls, name: str) -> str:
"""Get the path to the YAML file for a context."""
fname = f"{name}.yml"
custom_context_path = os.path.join(USER_CONTEXT_YAMLS_DIR, fname)
custom_context_path = os.path.join(SerenaPaths().user_contexts_dir, fname)
if os.path.exists(custom_context_path):
return custom_context_path
own_yaml_path = os.path.join(SERENAS_OWN_CONTEXT_YAMLS_DIR, fname)
if not os.path.exists(own_yaml_path):
raise FileNotFoundError(
f"Context {name} not found in {USER_CONTEXT_YAMLS_DIR} or in {SERENAS_OWN_CONTEXT_YAMLS_DIR}."
f"Context {name} not found in {SerenaPaths().user_contexts_dir} or in {SERENAS_OWN_CONTEXT_YAMLS_DIR}."
f"Available contexts:\n{cls.list_registered_context_names()}"
)
return own_yaml_path
@@ -211,7 +209,7 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin):
@classmethod
def list_custom_context_names(cls) -> list[str]:
"""Names of all custom contexts defined by the user."""
return [f.stem for f in Path(USER_CONTEXT_YAMLS_DIR).glob("*.yml")]
return [f.stem for f in Path(SerenaPaths().user_contexts_dir).glob("*.yml")]
@classmethod
def load_default(cls) -> Self:
+29 -7
View File
@@ -25,7 +25,6 @@ from serena.constants import (
REPO_ROOT,
SERENA_CONFIG_TEMPLATE_FILE,
SERENA_FILE_ENCODING,
SERENA_MANAGED_DIR_IN_HOME,
SERENA_MANAGED_DIR_NAME,
)
from serena.util.general import load_yaml, save_yaml
@@ -53,9 +52,32 @@ class SerenaPaths:
"""
def __init__(self) -> None:
self.user_config_dir: str = SERENA_MANAGED_DIR_IN_HOME
home_dir = os.getenv("SERENA_HOME")
if home_dir is None or home_dir.strip() == "":
home_dir = str(Path.home() / SERENA_MANAGED_DIR_NAME)
else:
home_dir = home_dir.strip()
self.serena_user_home_dir: str = home_dir
"""
the path to the user's Serena configuration directory, which is typically ~/.serena
the path to the Serena home directory, where the user's configuration/data is stored.
This is ~/.serena by default, but it can be overridden via the SERENA_HOME environment variable.
"""
self.user_prompt_templates_dir: str = os.path.join(self.serena_user_home_dir, "prompt_templates")
"""
directory containing prompt templates defined by the user.
Prompts defined by the user take precedence over Serena's built-in prompt templates.
"""
self.user_contexts_dir: str = os.path.join(self.serena_user_home_dir, "contexts")
"""
directory containing contexts defined by the user.
If a name of a context matches a name of a context in SERENAS_OWN_CONTEXT_YAMLS_DIR,
the user context will override the default context definition.
"""
self.user_modes_dir: str = os.path.join(self.serena_user_home_dir, "modes")
"""
directory containing modes defined by the user.
If a name of a mode matches a name of a mode in SERENAS_OWN_MODES_YAML_DIR,
the user mode will override the default mode definition.
"""
def get_next_log_file_path(self, prefix: str) -> str:
@@ -63,7 +85,7 @@ class SerenaPaths:
:param prefix: the filename prefix indicating the type of the log file
:return: the full path to the log file to use
"""
log_dir = os.path.join(self.user_config_dir, "logs", datetime.now().strftime("%Y-%m-%d"))
log_dir = os.path.join(self.serena_user_home_dir, "logs", datetime.now().strftime("%Y-%m-%d"))
os.makedirs(log_dir, exist_ok=True)
return os.path.join(log_dir, prefix + "_" + datetime_tag() + ".txt")
@@ -377,7 +399,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
return ["config_file_path"]
@classmethod
def generate_config_file(cls, config_file_path: str) -> None:
def _generate_config_file(cls, config_file_path: str) -> None:
"""
Generates a Serena configuration file at the specified path from the template file.
@@ -395,7 +417,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
if is_running_in_docker():
return os.path.join(REPO_ROOT, cls.CONFIG_FILE_DOCKER)
else:
config_path = os.path.join(SERENA_MANAGED_DIR_IN_HOME, cls.CONFIG_FILE)
config_path = os.path.join(SerenaPaths().serena_user_home_dir, cls.CONFIG_FILE)
# if the config file does not exist, check if we can migrate it from the old location
if not os.path.exists(config_path):
@@ -419,7 +441,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
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)
cls._generate_config_file(config_file_path)
# load the configuration
log.info(f"Loading Serena configuration from {config_file_path}")
-8
View File
@@ -4,22 +4,14 @@ _repo_root_path = Path(__file__).parent.parent.parent.resolve()
_serena_pkg_path = Path(__file__).parent.resolve()
SERENA_MANAGED_DIR_NAME = ".serena"
_serena_in_home_managed_dir = Path.home() / ".serena"
SERENA_MANAGED_DIR_IN_HOME = str(_serena_in_home_managed_dir)
# TODO: Path-related constants should be moved to SerenaPaths; don't add further constants here.
REPO_ROOT = str(_repo_root_path)
PROMPT_TEMPLATES_DIR_INTERNAL = str(_serena_pkg_path / "resources" / "config" / "prompt_templates")
PROMPT_TEMPLATES_DIR_IN_USER_HOME = str(_serena_in_home_managed_dir / "prompt_templates")
SERENAS_OWN_CONTEXT_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "contexts")
"""The contexts that are shipped with the Serena package, i.e. the default contexts."""
USER_CONTEXT_YAMLS_DIR = str(_serena_in_home_managed_dir / "contexts")
"""Contexts defined by the user. If a name of a context matches a name of a context in SERENAS_OWN_CONTEXT_YAMLS_DIR, the user context will override the default one."""
SERENAS_OWN_MODE_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "modes")
"""The modes that are shipped with the Serena package, i.e. the default modes."""
USER_MODE_YAMLS_DIR = str(_serena_in_home_managed_dir / "modes")
"""Modes defined by the user. If a name of a mode matches a name of a mode in SERENAS_OWN_MODE_YAMLS_DIR, the user mode will override the default one."""
INTERNAL_MODE_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "internal_modes")
"""Internal modes, never overridden by user modes."""
SERENA_DASHBOARD_DIR = str(_serena_pkg_path / "resources" / "dashboard")
+3 -2
View File
@@ -4,7 +4,8 @@ from collections.abc import Iterator
from sensai.util.logging import LogTime
from serena.constants import SERENA_MANAGED_DIR_IN_HOME, SERENA_MANAGED_DIR_NAME
from serena.config.serena_config import SerenaPaths
from serena.constants import SERENA_MANAGED_DIR_NAME
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.settings import SolidLSPSettings
@@ -43,7 +44,7 @@ class LanguageServerFactory:
self.project_root,
timeout=self.ls_timeout,
solidlsp_settings=SolidLSPSettings(
solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME,
solidlsp_dir=SerenaPaths().serena_user_home_dir,
project_data_relative_path=SERENA_MANAGED_DIR_NAME,
ls_specific_settings=self.ls_specific_settings or {},
),
+5 -3
View File
@@ -1,6 +1,7 @@
import os
from serena.constants import PROMPT_TEMPLATES_DIR_IN_USER_HOME, PROMPT_TEMPLATES_DIR_INTERNAL
from serena.config.serena_config import SerenaPaths
from serena.constants import PROMPT_TEMPLATES_DIR_INTERNAL
from serena.generated.generated_prompt_factory import PromptFactory
@@ -10,5 +11,6 @@ class SerenaPromptFactory(PromptFactory):
"""
def __init__(self) -> None:
os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True)
super().__init__(prompts_dir=[PROMPT_TEMPLATES_DIR_IN_USER_HOME, PROMPT_TEMPLATES_DIR_INTERNAL])
user_templates_dir = SerenaPaths().user_prompt_templates_dir
os.makedirs(user_templates_dir, exist_ok=True)
super().__init__(prompts_dir=[user_templates_dir, PROMPT_TEMPLATES_DIR_INTERNAL])
+5 -2
View File
@@ -7,7 +7,8 @@ import pytest
from blib2to3.pgen2.parse import contextmanager
from sensai.util.logging import configure
from serena.constants import SERENA_MANAGED_DIR_IN_HOME, SERENA_MANAGED_DIR_NAME
from serena.config.serena_config import SerenaPaths
from serena.constants import SERENA_MANAGED_DIR_NAME
from serena.project import Project
from serena.util.file_system import GitignoreParser
from solidlsp.ls import SolidLanguageServer
@@ -49,7 +50,9 @@ def _create_ls(
return SolidLanguageServer.create(
config,
repo_path,
solidlsp_settings=SolidLSPSettings(solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME, project_data_relative_path=SERENA_MANAGED_DIR_NAME),
solidlsp_settings=SolidLSPSettings(
solidlsp_dir=SerenaPaths().serena_user_home_dir, project_data_relative_path=SERENA_MANAGED_DIR_NAME
),
)