Merge pull request #1685 from oraios/async-project-config-autogen

Use asynchronous project language determination to speed up MCP startup
This commit is contained in:
Dr. Dominik Jain authored and GitHub committed 2026-07-15 18:47:19 +02:00
commit 8e90d923fb
5 files changed
+188 -90

No files matched your search

+2
View File
@@ -3,6 +3,8 @@
Status of the `main` branch. Changes prior to the next official version change will appear here. Status of the `main` branch. Changes prior to the next official version change will appear here.
* General: * General:
- Speed up MCP startup when auto-creating projects at startup (e.g. using `--project-from-cwd`) by
determining the project's languages in a background thread #1683
- Fix: in `glob_to_regex` / `search_text(is_glob=True)`, a `?` wildcard matched two characters instead - Fix: in `glob_to_regex` / `search_text(is_glob=True)`, a `?` wildcard matched two characters instead
of one (it emitted `..` rather than `.`); it now matches a single character, consistent with the of one (it emitted `..` rather than `.`); it now matches a single character, consistent with the
`?` semantics documented for `glob_match` and the `test_??.py` example in `search_text`'s docstring. `?` semantics documented for `glob_match` and the `test_??.py` example in `search_text`'s docstring.
+1 -1
View File
@@ -1300,7 +1300,7 @@ class SerenaAgent:
if project_instance is not None: if project_instance is not None:
log.info(f"Found registered project '{project_instance.project_name}' at path {project_instance.project_root}") log.info(f"Found registered project '{project_instance.project_name}' at path {project_instance.project_root}")
elif os.path.isdir(project_root_or_name): elif os.path.isdir(project_root_or_name):
project_instance = self.serena_config.add_project_from_path(project_root_or_name) project_instance = self.serena_config.add_project_from_path(project_root_or_name, asynchronous_autogen=True)
log.info(f"Added new project {project_instance.project_name} for path {project_instance.project_root}") log.info(f"Added new project {project_instance.project_name} for path {project_instance.project_root}")
if project_instance is None: if project_instance is None:
+4 -10
View File
@@ -1124,17 +1124,11 @@ class MemoryCommands(AutoRegisteringGroup):
exist at the configured location). Never auto-creates the ``.serena`` directory — exist at the configured location). Never auto-creates the ``.serena`` directory —
if no project configuration is found, the user is directed at ``serena project create``. if no project configuration is found, the user is directed at ``serena project create``.
""" """
from serena.project import Project
serena_config = SerenaConfig.from_config_file() serena_config = SerenaConfig.from_config_file()
project_path = os.path.abspath(project) registered_project = serena_config.get_registered_project(project)
try: if registered_project is None:
proj = Project.load(project_path, serena_config=serena_config, autogenerate=False) raise click.UsageError(f"No Serena project found for '{project}'. Create one first.")
except FileNotFoundError as e: return registered_project.get_project_instance(serena_config).memory_manager
raise click.UsageError(
f"{e}\nNo Serena project found at {project_path}. Create one first with:\n serena project create {project_path}"
) from e
return proj.memory_manager
@staticmethod @staticmethod
@click.command( @click.command(
+174 -77
View File
@@ -6,6 +6,7 @@ import dataclasses
import os import os
import re import re
import shutil import shutil
import threading
from collections.abc import Iterator, Sequence from collections.abc import Iterator, Sequence
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -298,6 +299,25 @@ Uses $projectDir and $projectFolderName as placeholders.
""" """
class ProjectConfigAutoGenerationMode(Enum):
NONE = "none"
"""
no auto-generation
"""
SYNCHRONOUS = "sync"
"""
synchronous auto-generation, i.e. the configuration is fully generated before returning from the function call
"""
ASYNCHRONOUS = "async"
"""
asynchronous auto-generation, where time-consuming configuration parts (currently only the
list of programming languages) are determined in a background thread and initialised as empty
"""
def is_autogen_enabled(self):
return self != ProjectConfigAutoGenerationMode.NONE
@dataclass(kw_only=True) @dataclass(kw_only=True)
class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes): class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
project_name: str project_name: str
@@ -315,7 +335,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
# internal fields which are not mapped to/from the configuration file (must start with "_") # internal fields which are not mapped to/from the configuration file (must start with "_")
_local_override_keys: list[str] = field(default_factory=list) _local_override_keys: list[str] = field(default_factory=list)
# class-level constants # class-level members
SERENA_PROJECT_FILE = "project.yml" SERENA_PROJECT_FILE = "project.yml"
SERENA_LOCAL_PROJECT_FILE = "project.local.yml" SERENA_LOCAL_PROJECT_FILE = "project.local.yml"
FIELDS_WITHOUT_DEFAULTS = {"project_name", "languages"} FIELDS_WITHOUT_DEFAULTS = {"project_name", "languages"}
@@ -325,10 +345,59 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
the comment normalisation strategy to use when loading/saving project configuration files. the comment normalisation strategy to use when loading/saving project configuration files.
The template file must match this configuration (i.e. it must use leading comments if this is set to LEADING). The template file must match this configuration (i.e. it must use leading comments if this is set to LEADING).
""" """
_async_completion_events = {}
"""
maps the object id of a ProjectConfig instance to an event which is set when the asynchronous auto-generation of
the configuration is complete (if applicable).
"""
_save_lock = threading.Lock()
def _tostring_includes(self) -> list[str]: def _tostring_includes(self) -> list[str]:
return ["project_name"] return ["project_name"]
@classmethod
def _determine_project_languages(cls, project_root: str, interactive: bool) -> list[Language]:
# determine languages automatically
log.info("Determining programming languages used in the project")
language_composition = determine_programming_language_composition(project_root)
log.info("Language composition: %s", language_composition)
if len(language_composition) == 0:
log.warning(
"No source files for supported language servers were found in %s. "
"Creating project with no configured languages. "
"Symbol-related tools (e.g. find_symbol, get_symbols_overview) will not work "
"when using the LSP backend. You can add languages later via the Serena dashboard "
"or by manually editing the project configuration.",
project_root,
)
languages_to_use = []
else:
# sort languages by number of files found
languages_and_percentages = sorted(
language_composition.items(), key=lambda item: (item[1], item[0].get_priority()), reverse=True
)
# find the language with the highest percentage and enable it
top_language_pair = languages_and_percentages[0]
other_language_pairs = languages_and_percentages[1:]
languages_to_use = [top_language_pair[0]]
# if in interactive mode, ask the user which other languages to enable
if len(other_language_pairs) > 0 and interactive:
print(
"Detected and enabled main language '%s' (%.2f%% of source files)." % (top_language_pair[0].value, top_language_pair[1])
)
print(f"Additionally detected {len(other_language_pairs)} other language(s).\n")
print("Note: Enable only languages you need symbolic retrieval/editing capabilities for.")
print(" Additional language servers use resources and some languages may require additional")
print(" system-level installations/configuration (see Serena documentation).")
print("\nWhich additional languages do you want to enable?")
for lang, perc in other_language_pairs:
enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (lang.value, perc), default=False)
if enable:
languages_to_use.append(lang)
print()
log.info("Using languages: %s", languages_to_use)
return languages_to_use
@classmethod @classmethod
def autogenerate( def autogenerate(
cls, cls,
@@ -338,6 +407,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
languages: list[Language] | None = None, languages: list[Language] | None = None,
save_to_disk: bool = True, save_to_disk: bool = True,
interactive: bool = False, interactive: bool = False,
asynchronous: bool = False,
) -> Self: ) -> Self:
""" """
Autogenerate a project configuration for a given project root. Autogenerate a project configuration for a given project root.
@@ -349,8 +419,12 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
:param languages: the languages of the project; if None, they will be determined automatically :param languages: the languages of the project; if None, they will be determined automatically
:param save_to_disk: whether to save the project configuration to disk :param save_to_disk: whether to save the project configuration to disk
:param interactive: whether to run in interactive CLI mode, asking the user for input where appropriate :param interactive: whether to run in interactive CLI mode, asking the user for input where appropriate
:param asynchronous: whether to run in asynchronous mode, where time-consuming configuration parts (currently only the
determination of the list of programming languages) are determined in a background thread and initialised as empty
:return: the project configuration :return: the project configuration
""" """
if interactive and asynchronous:
raise ValueError("Cannot use interactive mode with asynchronous auto-generation")
project_root = Path(project_root).resolve() project_root = Path(project_root).resolve()
if not project_root.exists(): if not project_root.exists():
raise FileNotFoundError(f"Project root not found: {project_root}") raise FileNotFoundError(f"Project root not found: {project_root}")
@@ -358,61 +432,62 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
log.info("Project root: %s", project_root) log.info("Project root: %s", project_root)
project_folder_name = project_root.name project_folder_name = project_root.name
project_name = project_name or project_folder_name project_name = project_name or project_folder_name
use_asynchronous_language_determination = False
if languages is None: if languages is None:
# determine languages automatically if asynchronous:
log.info("Determining programming languages used in the project") use_asynchronous_language_determination = True
language_composition = determine_programming_language_composition(str(project_root)) languages_to_use = [] # temporarily empty, will be determined in background thread
log.info("Language composition: %s", language_composition)
if len(language_composition) == 0:
log.warning(
"No source files for supported language servers were found in %s. "
"Creating project with no configured languages. "
"Symbol-related tools (e.g. find_symbol, get_symbols_overview) will not work "
"when using the LSP backend. You can add languages later via the Serena dashboard "
"or by manually editing the project configuration.",
project_root,
)
languages_to_use: list[str] = []
else: else:
# sort languages by number of files found determined_languages = cls._determine_project_languages(str(project_root), interactive=interactive)
languages_and_percentages = sorted( languages_to_use = [l.value for l in determined_languages]
language_composition.items(), key=lambda item: (item[1], item[0].get_priority()), reverse=True
)
# find the language with the highest percentage and enable it
top_language_pair = languages_and_percentages[0]
other_language_pairs = languages_and_percentages[1:]
languages_to_use = [top_language_pair[0].value]
# if in interactive mode, ask the user which other languages to enable
if len(other_language_pairs) > 0 and interactive:
print(
"Detected and enabled main language '%s' (%.2f%% of source files)."
% (top_language_pair[0].value, top_language_pair[1])
)
print(f"Additionally detected {len(other_language_pairs)} other language(s).\n")
print("Note: Enable only languages you need symbolic retrieval/editing capabilities for.")
print(" Additional language servers use resources and some languages may require additional")
print(" system-level installations/configuration (see Serena documentation).")
print("\nWhich additional languages do you want to enable?")
for lang, perc in other_language_pairs:
enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (lang.value, perc), default=False)
if enable:
languages_to_use.append(lang.value)
print()
log.info("Using languages: %s", languages_to_use)
else: else:
languages_to_use = [lang.value for lang in languages] languages_to_use = [lang.value for lang in languages]
config_with_comments, _ = cls._load_yaml_dict(PROJECT_TEMPLATE_FILE) config_with_comments, _ = cls._load_yaml_dict(PROJECT_TEMPLATE_FILE)
config_with_comments["project_name"] = project_name config_with_comments["project_name"] = project_name
config_with_comments["languages"] = languages_to_use config_with_comments["languages"] = languages_to_use
project_yml_path = serena_config.get_project_yml_location(str(project_root))
if save_to_disk: if save_to_disk:
project_yml_path = serena_config.get_project_yml_location(str(project_root))
log.info("Saving project configuration to %s", project_yml_path) log.info("Saving project configuration to %s", project_yml_path)
save_yaml(project_yml_path, config_with_comments) with cls._save_lock:
save_yaml(project_yml_path, config_with_comments)
project_local_yml_path = os.path.join(os.path.dirname(project_yml_path), cls.SERENA_LOCAL_PROJECT_FILE) project_local_yml_path = os.path.join(os.path.dirname(project_yml_path), cls.SERENA_LOCAL_PROJECT_FILE)
shutil.copy(PROJECT_LOCAL_TEMPLATE_FILE, project_local_yml_path) shutil.copy(PROJECT_LOCAL_TEMPLATE_FILE, project_local_yml_path)
return cls._from_dict(config_with_comments, local_override_keys=[]) project_config = cls._from_dict(config_with_comments, local_override_keys=[])
# if asynchronous language determination is used, start a background thread which updates and saves the configuration
# and set an event which can be awaited to ensure that the configuration is complete
if use_asynchronous_language_determination:
event = threading.Event()
project_config._async_completion_events[id(project_config)] = event
def async_language_determination():
try:
with LogTime("Asynchronous language determination", logger=log):
project_config.languages = cls._determine_project_languages(str(project_root), interactive=False)
if save_to_disk:
project_config.save(project_yml_path)
finally:
event.set()
threading.Thread(target=async_language_determination, name="project-language-determination", daemon=True).start()
return project_config
def await_asynchronous_completion(self):
"""
Wait for the asynchronous auto-generation of the configuration to complete (if applicable), ensuring
that, in particular, the list of programming languages is complete, which may be determined asynchronously
when first creating the project configuration.
"""
event = ProjectConfig._async_completion_events.get(id(self))
if event is None:
return
if not event.is_set():
log.info("Waiting for asynchronous auto-generation of project configuration to complete ...")
event.wait()
log.info("Asynchronous auto-generation of project configuration completed.")
@classmethod @classmethod
def default_project_yml_path(cls, project_root: str | Path) -> str: def default_project_yml_path(cls, project_root: str | Path) -> str:
@@ -590,13 +665,18 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
return os.path.join(os.path.dirname(project_yml_path), cls.SERENA_LOCAL_PROJECT_FILE) return os.path.join(os.path.dirname(project_yml_path), cls.SERENA_LOCAL_PROJECT_FILE)
@classmethod @classmethod
def load(cls, project_root: Path | str, serena_config: "SerenaConfig", autogenerate: bool = False) -> Self: def load(
cls,
project_root: Path | str,
serena_config: "SerenaConfig",
autogen: ProjectConfigAutoGenerationMode = ProjectConfigAutoGenerationMode.NONE,
) -> Self:
""" """
Load a ProjectConfig instance from the path to the project root. Load a ProjectConfig instance from the path to the project root.
:param project_root: the path to the project root :param project_root: the path to the project root
:param serena_config: the global Serena configuration :param serena_config: the global Serena configuration
:param autogenerate: whether to auto-generate the configuration if it does not exist :param autogen: the auto-generation mode to apply if the project configuration does not yet exist
""" """
project_root = Path(project_root) project_root = Path(project_root)
project_folder_name = project_root.name project_folder_name = project_root.name
@@ -605,8 +685,8 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
# auto-generate if necessary # auto-generate if necessary
if not os.path.exists(yaml_path): if not os.path.exists(yaml_path):
if autogenerate: if autogen.is_autogen_enabled():
return cls.autogenerate(project_root, serena_config) return cls.autogenerate(project_root, serena_config, asynchronous=autogen == ProjectConfigAutoGenerationMode.ASYNCHRONOUS)
else: else:
raise FileNotFoundError(f"Project configuration file not found: {yaml_path}") raise FileNotFoundError(f"Project configuration file not found: {yaml_path}")
@@ -653,34 +733,35 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
config_path = project_yml_path config_path = project_yml_path
log.info("Saving updated project configuration to %s", config_path) log.info("Saving updated project configuration to %s", config_path)
# get the current configuration as a dictionary with self._save_lock:
cur_dict = self._to_yaml_dict() # get the current configuration as a dictionary
cur_dict = self._to_yaml_dict()
# load commented map from the original file and update all non-overridden keys # load commented map from the original file and update all non-overridden keys
config_with_comments, _ = self._load_yaml_dict(config_path, self.YAML_COMMENT_NORMALISATION) config_with_comments, _ = self._load_yaml_dict(config_path, self.YAML_COMMENT_NORMALISATION)
for key in cur_dict: for key in cur_dict:
if key not in self._local_override_keys: if key not in self._local_override_keys:
config_with_comments[key] = cur_dict[key] config_with_comments[key] = cur_dict[key]
# transfer missing comments from the template file # transfer missing comments from the template file
template_config, _ = self._load_yaml_dict(PROJECT_TEMPLATE_FILE, self.YAML_COMMENT_NORMALISATION) template_config, _ = self._load_yaml_dict(PROJECT_TEMPLATE_FILE, self.YAML_COMMENT_NORMALISATION)
transfer_yaml_comments(template_config, config_with_comments, self.YAML_COMMENT_NORMALISATION, force_update_all=True) transfer_yaml_comments(template_config, config_with_comments, self.YAML_COMMENT_NORMALISATION, force_update_all=True)
# save project.yml # save project.yml
save_yaml(config_path, config_with_comments) save_yaml(config_path, config_with_comments)
# update project.local.yml to reflect overridden keys if necessary # update project.local.yml to reflect overridden keys if necessary
if save_project_local_yml: if save_project_local_yml:
project_local_yml_path = self._project_local_yml_path(project_yml_path) project_local_yml_path = self._project_local_yml_path(project_yml_path)
if self._local_override_keys and os.path.exists(project_local_yml_path): if self._local_override_keys and os.path.exists(project_local_yml_path):
log.info("Saving updated local project configuration to %s", project_local_yml_path) log.info("Saving updated local project configuration to %s", project_local_yml_path)
local_config_with_comments, _ = self._load_yaml_dict( local_config_with_comments, _ = self._load_yaml_dict(
project_local_yml_path, comment_normalisation=YamlCommentNormalisation.NONE, apply_defaults=False project_local_yml_path, comment_normalisation=YamlCommentNormalisation.NONE, apply_defaults=False
) )
for key in self._local_override_keys: for key in self._local_override_keys:
if key in cur_dict: if key in cur_dict:
local_config_with_comments[key] = cur_dict[key] local_config_with_comments[key] = cur_dict[key]
save_yaml(project_local_yml_path, local_config_with_comments) save_yaml(project_local_yml_path, local_config_with_comments)
class RegisteredProject(ToStringMixin): class RegisteredProject(ToStringMixin):
@@ -717,8 +798,21 @@ class RegisteredProject(ToStringMixin):
) )
@classmethod @classmethod
def from_project_root(cls, project_root: str | Path, serena_config: "SerenaConfig") -> "RegisteredProject": def from_project_root(
project_config = ProjectConfig.load(project_root, serena_config=serena_config) cls,
project_root: str | Path,
serena_config: "SerenaConfig",
autogen: ProjectConfigAutoGenerationMode = ProjectConfigAutoGenerationMode.NONE,
) -> "RegisteredProject":
"""
Creates a RegisteredProject instance from a project root path, which must exist on disk.
:param project_root: path to an existing directory
:param serena_config: the Serena configuration
:param autogen: the auto-generation mode to use for the project configuration if it does not yet exist
:return: the RegisteredProject instance
"""
project_config = ProjectConfig.load(project_root, serena_config=serena_config, autogen=autogen)
return RegisteredProject( return RegisteredProject(
project_root=str(project_root), project_root=str(project_root),
project_config=project_config, project_config=project_config,
@@ -1085,7 +1179,8 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
def get_registered_project(self, project_root_or_name: str, autoregister: bool = False) -> Optional[RegisteredProject]: def get_registered_project(self, project_root_or_name: str, autoregister: bool = False) -> Optional[RegisteredProject]:
""" """
:param project_root_or_name: path to the project root or the name of the project :param project_root_or_name: path to the project root or the name of the project
:param autoregister: whether to register the project if it exists but is not registered yet :param autoregister: whether to auto-register projects that are not yet registered in Serena's global configuration
but have an existing project configuration file. Project configuration files are never auto-generated.
:return: the registered project, or None if not found :return: the registered project, or None if not found
""" """
# look for project by name # look for project by name
@@ -1105,7 +1200,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
for project in self.projects: for project in self.projects:
if project.matches_root_path(project_root_or_name): if project.matches_root_path(project_root_or_name):
return project return project
# no registered project found; auto-register if project configuration exists # no registered project found; optionally auto-register if a project configuration already exists
if autoregister: if autoregister:
config_path = self.get_project_yml_location(project_root_or_name) config_path = self.get_project_yml_location(project_root_or_name)
if os.path.isfile(config_path): if os.path.isfile(config_path):
@@ -1129,13 +1224,14 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
self.projects.append(registered_project) self.projects.append(registered_project)
self._persist_projects() self._persist_projects()
def add_project_from_path(self, project_root: Path | str) -> "Project": def add_project_from_path(self, project_root: Path | str, asynchronous_autogen: bool = False) -> "Project":
""" """
Adds a new project to the Serena configuration from a given path, auto-generating the project Adds a new project to the Serena configuration from a given path, auto-generating the project
with defaults if it does not exist. with defaults if it does not exist.
Will raise a FileExistsError if a project already exists at the path. Will raise a FileExistsError if a project already exists at the path.
:param project_root: the path to the project to add :param project_root: the path to the project to add
:param asynchronous_autogen: whether to use asynchronous auto-generation for the project configuration
:return: the project that was added :return: the project that was added
""" """
from ..project import Project from ..project import Project
@@ -1152,7 +1248,8 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
f"Project with path {project_root} was already added with name '{already_registered_project.project_name}'." f"Project with path {project_root} was already added with name '{already_registered_project.project_name}'."
) )
project_config = ProjectConfig.load(project_root, serena_config=self, autogenerate=True) autogen = ProjectConfigAutoGenerationMode.ASYNCHRONOUS if asynchronous_autogen else ProjectConfigAutoGenerationMode.SYNCHRONOUS
project_config = ProjectConfig.load(project_root, serena_config=self, autogen=autogen)
new_project = Project( new_project = Project(
project_root=str(project_root), project_root=str(project_root),
+7 -2
View File
@@ -11,6 +11,7 @@ from sensai.util.string import TextBuilder, ToStringMixin
from serena.config.serena_config import ( from serena.config.serena_config import (
LanguageBackend, LanguageBackend,
ProjectConfig, ProjectConfig,
ProjectConfigAutoGenerationMode,
SerenaConfig, SerenaConfig,
) )
from serena.ls_manager import LanguageServerFactory, LanguageServerManager from serena.ls_manager import LanguageServerFactory, LanguageServerManager
@@ -137,13 +138,13 @@ class Project(ToStringMixin):
cls, cls,
project_root: str | Path, project_root: str | Path,
serena_config: "SerenaConfig", serena_config: "SerenaConfig",
autogenerate: bool = True, autogen: ProjectConfigAutoGenerationMode = ProjectConfigAutoGenerationMode.SYNCHRONOUS,
) -> "Project": ) -> "Project":
assert serena_config is not None assert serena_config is not None
project_root = Path(project_root).resolve() project_root = Path(project_root).resolve()
if not project_root.exists(): if not project_root.exists():
raise FileNotFoundError(f"Project root not found: {project_root}") raise FileNotFoundError(f"Project root not found: {project_root}")
project_config = ProjectConfig.load(project_root, serena_config=serena_config, autogenerate=autogenerate) project_config = ProjectConfig.load(project_root, serena_config=serena_config, autogen=autogen)
return Project(project_root=str(project_root), project_config=project_config, serena_config=serena_config) return Project(project_root=str(project_root), project_config=project_config, serena_config=serena_config)
def save_config(self) -> None: def save_config(self) -> None:
@@ -438,6 +439,10 @@ class Project(ToStringMixin):
:return: the language server manager, which is also stored in the project instance :return: the language server manager, which is also stored in the project instance
""" """
try: try:
# ensure that the project configuration, particularly the list of languages is complete,
# despite asynchronous first-time project configuration generation (which may not have completed yet)
self.project_config.await_asynchronous_completion()
# determine timeout to use for LS calls # determine timeout to use for LS calls
tool_timeout = self.serena_config.tool_timeout tool_timeout = self.serena_config.tool_timeout
if tool_timeout is None or tool_timeout < 0: if tool_timeout is None or tool_timeout < 0: