From 1af824c025c47c398105eb6f31aa6a4c43c5496f Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 28 Feb 2026 13:36:17 +0100 Subject: [PATCH] Project: Make SerenaConfig compulsory, simplifying LanguageServerManager creation --- src/serena/agent.py | 16 +------- src/serena/cli.py | 4 +- src/serena/config/serena_config.py | 2 +- src/serena/project.py | 37 +++++++++---------- test/conftest.py | 5 ++- .../config/test_global_ignored_paths.py | 22 ++--------- test/serena/config/test_serena_config.py | 16 +++++--- test/serena/test_serena_agent.py | 7 ++-- 8 files changed, 41 insertions(+), 68 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 7017518f..36ac3fc9 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -799,21 +799,7 @@ class SerenaAgent: """ Starts/resets the language server manager for the current project """ - tool_timeout = self.serena_config.tool_timeout - if tool_timeout is None or tool_timeout < 0: - ls_timeout = None - else: - if tool_timeout < 10: - raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds") - ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout - - # instantiate and start the necessary language servers - self.get_active_project_or_raise().create_language_server_manager( - log_level=self.serena_config.log_level, - ls_timeout=ls_timeout, - trace_lsp_communication=self.serena_config.trace_lsp_communication, - ls_specific_settings=self.serena_config.ls_specific_settings, - ) + self.get_active_project_or_raise().create_language_server_manager() def add_language(self, language: Language) -> None: """ diff --git a/src/serena/cli.py b/src/serena/cli.py index 711e8e9a..0c77310b 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -640,9 +640,7 @@ class ProjectCommands(AutoRegisteringGroup): serena_config = SerenaConfig.from_config_file() proj = registered_project.get_project_instance(serena_config=serena_config) click.echo(f"Indexing symbols in {proj} …") - ls_mgr = proj.create_language_server_manager( - log_level=lvl, ls_timeout=timeout, ls_specific_settings=serena_config.ls_specific_settings - ) + ls_mgr = proj.create_language_server_manager() try: log_file = os.path.join(proj.project_root, ".serena", "logs", "indexing.txt") diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index a8679525..30a47c30 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -488,7 +488,7 @@ class RegisteredProject(ToStringMixin): """ return self.project_root.samefile(Path(path).resolve()) - def get_project_instance(self, serena_config: "SerenaConfig | None") -> "Project": + def get_project_instance(self, serena_config: "SerenaConfig") -> "Project": """ Returns the project instance for this registered project, loading it if necessary. """ diff --git a/src/serena/project.py b/src/serena/project.py index 59b89ec4..59cb8098 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -10,7 +10,6 @@ from sensai.util.logging import LogTime from sensai.util.string import ToStringMixin from serena.config.serena_config import ( - DEFAULT_TOOL_TIMEOUT, ProjectConfig, get_serena_managed_in_project_dir, ) @@ -117,13 +116,16 @@ class MemoriesManager: class Project(ToStringMixin): def __init__( self, + *, project_root: str, project_config: ProjectConfig, + serena_config: "SerenaConfig", is_newly_created: bool = False, - serena_config: "SerenaConfig | None" = None, ): + assert serena_config is not None self.project_root = project_root self.project_config = project_config + self._serena_config = serena_config self.memories_manager = MemoriesManager(project_root) self.language_server_manager: LanguageServerManager | None = None self._is_newly_created = is_newly_created @@ -137,7 +139,6 @@ class Project(ToStringMixin): f.write(f"/{SolidLanguageServer.CACHE_FOLDER_NAME}\n") # prepare ignore spec asynchronously, ensuring immediate project activation. - self._serena_config = serena_config self.__ignored_patterns: list[str] self.__ignore_spec: pathspec.PathSpec self._ignore_spec_available = threading.Event() @@ -147,7 +148,7 @@ class Project(ToStringMixin): with LogTime(f"Gathering ignore spec for project {self.project_config.project_name}", logger=log): # gather ignored paths from the global configuration, project configuration, and gitignore files - global_ignored_paths = self._serena_config.ignored_paths if self._serena_config else [] + global_ignored_paths = self._serena_config.ignored_paths ignored_patterns = list(global_ignored_paths) + list(self.project_config.ignored_paths) if len(global_ignored_paths) > 0: log.info(f"Using {len(global_ignored_paths)} ignored paths from the global configuration.") @@ -189,7 +190,7 @@ class Project(ToStringMixin): def load( cls, project_root: str | Path, - serena_config: "SerenaConfig | None", + serena_config: "SerenaConfig", autogenerate: bool = True, ) -> "Project": project_root = Path(project_root).resolve() @@ -464,23 +465,21 @@ class Project(ToStringMixin): source_file_path=relative_file_path, ) - def create_language_server_manager( - self, - log_level: int = logging.INFO, - ls_timeout: float | None = DEFAULT_TOOL_TIMEOUT - 5, - trace_lsp_communication: bool = False, - ls_specific_settings: dict[Language, Any] | None = None, - ) -> LanguageServerManager: + def create_language_server_manager(self) -> LanguageServerManager: """ Creates the language server manager for the project, starting one language server per configured programming language. - :param log_level: the log level for the language server - :param ls_timeout: the timeout for the language server - :param trace_lsp_communication: whether to trace LSP communication - :param ls_specific_settings: optional LS specific configuration of the language server, - see docstrings in the inits of subclasses of SolidLanguageServer to see what values may be passed. :return: the language server manager, which is also stored in the project instance """ + # determine timeout to use for LS calls + tool_timeout = self._serena_config.tool_timeout + if tool_timeout is None or tool_timeout < 0: + ls_timeout = None + else: + if tool_timeout < 10: + raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds") + ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout + # if there is an existing instance, stop its language servers first if self.language_server_manager is not None: log.info("Stopping existing language server manager ...") @@ -493,8 +492,8 @@ class Project(ToStringMixin): encoding=self.project_config.encoding, ignored_patterns=self._ignored_patterns, ls_timeout=ls_timeout, - ls_specific_settings=ls_specific_settings, - trace_lsp_communication=trace_lsp_communication, + ls_specific_settings=self._serena_config.ls_specific_settings, + trace_lsp_communication=self._serena_config.trace_lsp_communication, ) self.language_server_manager = LanguageServerManager.from_languages(self.project_config.languages, factory) return self.language_server_manager diff --git a/test/conftest.py b/test/conftest.py index 565d40b7..6a93ff30 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -10,7 +10,7 @@ from typing import Any import pytest from sensai.util.logging import configure -from serena.config.serena_config import SerenaPaths +from serena.config.serena_config import SerenaConfig, SerenaPaths from serena.constants import SERENA_MANAGED_DIR_NAME from serena.project import Project from serena.util.file_system import GitignoreParser @@ -117,7 +117,8 @@ def start_default_ls_context(language: Language) -> Iterator[SolidLanguageServer def _create_default_project(language: Language) -> Project: repo_path = str(get_repo_path(language)) - return Project.load(repo_path, serena_config=None) + serena_config = SerenaConfig() + return Project.load(repo_path, serena_config=serena_config) @pytest.fixture(scope="session") diff --git a/test/serena/config/test_global_ignored_paths.py b/test/serena/config/test_global_ignored_paths.py index 23481373..b804fe8b 100644 --- a/test/serena/config/test_global_ignored_paths.py +++ b/test/serena/config/test_global_ignored_paths.py @@ -20,9 +20,7 @@ def _create_test_project( ignored_paths=project_ignored_paths or [], ignore_all_files_in_gitignore=False, ) - serena_config: SerenaConfig | None = None - if global_ignored_paths: - serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=global_ignored_paths) + serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=global_ignored_paths) return Project( project_root=str(project_root), project_config=config, @@ -84,21 +82,6 @@ class TestGlobalIgnoredPaths: # Non-ignored files still accessible assert not project.is_ignored_path(str(self.project_path / "node_modules" / "pkg" / "index.js")) - def test_default_global_ignored_paths_backward_compatible(self) -> None: - """Project created without global_ignored_paths parameter works as before.""" - config = ProjectConfig( - project_name="test_project", - languages=[Language.PYTHON], - ignored_paths=["build"], - ignore_all_files_in_gitignore=False, - ) - project = Project( - project_root=str(self.project_path), - project_config=config, - ) - assert project.is_ignored_path(str(self.project_path / "build" / "output.js")) - assert not project.is_ignored_path(str(self.project_path / "node_modules" / "pkg" / "index.js")) - def test_duplicate_patterns_across_global_and_project(self) -> None: """Duplicate patterns across global and project do not cause errors.""" project = _create_test_project( @@ -161,7 +144,8 @@ class TestRegisteredProjectGlobalIgnoredPaths: project_root=str(self.project_path), project_config=config, ) - project = registered.get_project_instance(serena_config=None) + serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=[]) + project = registered.get_project_instance(serena_config=serena_config) assert not project.is_ignored_path(str(self.project_path / "node_modules" / "pkg.js")) def test_from_project_root_passes_global_ignored_paths(self) -> None: diff --git a/test/serena/config/test_serena_config.py b/test/serena/config/test_serena_config.py index 87e295b7..63f5b30f 100644 --- a/test/serena/config/test_serena_config.py +++ b/test/serena/config/test_serena_config.py @@ -191,6 +191,12 @@ def _make_config_with_project( global_backend: LanguageBackend = LanguageBackend.LSP, ) -> tuple[SerenaConfig, str]: """Create a SerenaConfig with a single registered project and return (config, project_name).""" + config = SerenaConfig( + gui_log_window=False, + web_dashboard=False, + log_level=logging.ERROR, + language_backend=global_backend, + ) project = Project( project_root=str(Path(__file__).parent.parent / "resources" / "repos" / "python" / "test_repo"), project_config=ProjectConfig( @@ -198,12 +204,7 @@ def _make_config_with_project( languages=[Language.PYTHON], language_backend=language_backend, ), - ) - config = SerenaConfig( - gui_log_window=False, - web_dashboard=False, - log_level=logging.ERROR, - language_backend=global_backend, + serena_config=config, ) config.projects = [RegisteredProject.from_project_instance(project)] return config, project_name @@ -259,6 +260,7 @@ class TestEffectiveLanguageBackend: languages=[Language.PYTHON], language_backend=LanguageBackend.JETBRAINS, ), + serena_config=config, ) config.projects.append(RegisteredProject.from_project_instance(jb_project)) @@ -281,6 +283,7 @@ class TestEffectiveLanguageBackend: languages=[Language.PYTHON], language_backend=LanguageBackend.LSP, ), + serena_config=config, ) config.projects.append(RegisteredProject.from_project_instance(lsp_project2)) @@ -303,6 +306,7 @@ class TestEffectiveLanguageBackend: languages=[Language.PYTHON], language_backend=None, ), + serena_config=config, ) config.projects.append(RegisteredProject.from_project_instance(proj2)) diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 8d8a5193..a0f978a6 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -21,7 +21,8 @@ from test.solidlsp import clojure as clj @pytest.fixture def serena_config(): - """Create an in-memory configuration for tests with test repositories pre-registered.""" + config = SerenaConfig(gui_log_window=False, web_dashboard=False, log_level=logging.ERROR) + # Create test projects for all supported languages test_projects = [] for language in [ @@ -47,16 +48,16 @@ def serena_config(): project_name=project_name, languages=[language], ignored_paths=[], - excluded_tools=set(), + excluded_tools=[], read_only=False, ignore_all_files_in_gitignore=True, initial_prompt="", encoding="utf-8", ), + serena_config=config, ) test_projects.append(RegisteredProject.from_project_instance(project)) - config = SerenaConfig(gui_log_window=False, web_dashboard=False, log_level=logging.ERROR) config.projects = test_projects return config