cli: Allow "project index" with project name (not just path), refactoring

This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-01-22 14:39:23 +01:00
1 parent 1d0e38be85
commit 1ec5a731a3
3 files changed
+54 -30

No files matched your search

+21 -24
View File
@@ -510,16 +510,16 @@ class ProjectCommands(AutoRegisteringGroup):
)
@staticmethod
def _create_project(project_path: str, name: str | None, language: tuple[str, ...]) -> ProjectConfig:
def _create_project(project_path: str, name: str | None, language: tuple[str, ...]) -> RegisteredProject:
"""
Helper method to create a project configuration file.
:param project_path: Path to the project directory
:param name: Optional project name (defaults to directory name if not specified)
:param language: Tuple of language names
:return: The generated ProjectConfig instance
:raises FileExistsError: If project.yml already exists
:raises ValueError: If an unsupported language is specified
:return: the RegisteredProject instance
"""
project_root = Path(project_path).resolve()
yml_path = ProjectConfig.path_to_project_yml(project_root)
@@ -544,13 +544,12 @@ class ProjectCommands(AutoRegisteringGroup):
# add to SerenaConfig's list of registered projects
serena_config = SerenaConfig.from_config_file()
if serena_config.get_registered_project(str(project_root)) is None:
registered_project = serena_config.get_registered_project(str(project_root))
if registered_project is None:
registered_project = RegisteredProject(str(project_root), generated_conf)
serena_config.projects.append(registered_project)
click.echo(f"Saving project registration to {serena_config}")
serena_config.save()
serena_config.add_registered_project(registered_project)
return generated_conf
return registered_project
@staticmethod
@click.command("create", help="Create a new Serena project configuration.", context_settings={"max_content_width": _MAX_CONTENT_WIDTH})
@@ -569,10 +568,10 @@ class ProjectCommands(AutoRegisteringGroup):
@click.option("--timeout", type=float, default=10, help="Timeout for indexing a single file (only used if --index is set).")
def create(project_path: str, name: str | None, language: tuple[str, ...], index: bool, log_level: str, timeout: float) -> None:
try:
ProjectCommands._create_project(project_path, name, language)
registered_project = ProjectCommands._create_project(project_path, name, language)
if index:
click.echo("Indexing project...")
ProjectCommands._index_project(project_path, log_level, timeout=timeout)
ProjectCommands._index_project(registered_project, log_level, timeout=timeout)
except FileExistsError as e:
raise click.ClickException(f"Project already exists: {e}\nUse 'serena project index' to index an existing project.")
except ValueError as e:
@@ -584,7 +583,7 @@ class ProjectCommands(AutoRegisteringGroup):
help="Index a project by saving symbols to the LSP cache. Auto-creates project.yml if it doesn't exist.",
context_settings={"max_content_width": _MAX_CONTENT_WIDTH},
)
@click.argument("project", type=click.Path(exists=True), default=os.getcwd(), required=False)
@click.argument("project", type=PROJECT_TYPE, default=os.getcwd(), required=False)
@click.option("--name", type=str, default=None, help="Project name (only used if auto-creating project.yml).")
@click.option(
"--language",
@@ -600,32 +599,30 @@ class ProjectCommands(AutoRegisteringGroup):
)
@click.option("--timeout", type=float, default=10, help="Timeout for indexing a single file.")
def index(project: str, name: str | None, language: tuple[str, ...], log_level: str, timeout: float) -> None:
# Check if project.yml exists, if not auto-create it
yml_path = os.path.join(project, ProjectConfig.rel_path_to_project_yml())
if not os.path.exists(yml_path):
click.echo(f"Project configuration not found at {yml_path}. Auto-creating...")
serena_config = SerenaConfig.from_config_file()
registered_project = serena_config.get_registered_project(project, autoregister=True)
if registered_project is None:
# Project not found; auto-create it
click.echo(f"No existing project found for '{project}'. Attempting auto-creation ...")
try:
ProjectCommands._create_project(project, name, language)
except FileExistsError:
# Race condition - file was created between check and creation
pass
except ValueError as e:
registered_project = ProjectCommands._create_project(project, name, language)
except Exception as e:
raise click.ClickException(str(e))
ProjectCommands._index_project(project, log_level, timeout=timeout)
ProjectCommands._index_project(registered_project, log_level, timeout=timeout)
@staticmethod
def _index_project(project: str, log_level: str, timeout: float) -> None:
def _index_project(registered_project: RegisteredProject, log_level: str, timeout: float) -> None:
lvl = logging.getLevelNamesMapping()[log_level.upper()]
logging.configure(level=lvl)
serena_config = SerenaConfig.from_config_file()
proj = Project.load(os.path.abspath(project))
click.echo(f"Indexing symbols in project {project} …")
proj = registered_project.get_project_instance()
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
)
try:
log_file = os.path.join(project, ".serena", "logs", "indexing.txt")
log_file = os.path.join(proj.project_root, ".serena", "logs", "indexing.txt")
files = proj.gather_source_files()
+30 -3
View File
@@ -374,6 +374,14 @@ class RegisteredProject(ToStringMixin):
project_instance=project_instance,
)
@classmethod
def from_project_root(cls, project_root: str | Path) -> "RegisteredProject":
project_config = ProjectConfig.load(project_root)
return RegisteredProject(
project_root=str(project_root),
project_config=project_config,
)
def matches_root_path(self, path: str | Path) -> bool:
"""
Check if the given path matches the project root path.
@@ -613,7 +621,12 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
def project_names(self) -> list[str]:
return sorted(project.project_config.project_name for project in self.projects)
def get_registered_project(self, project_root_or_name: str) -> 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 autoregister: whether to register the project if it exists but is not registered yet
:return: the registered project, or None if not found
"""
# look for project by name
project_candidates = []
for project in self.projects:
@@ -631,6 +644,14 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
for project in self.projects:
if project.matches_root_path(project_root_or_name):
return project
# no registered project found; auto-register if project configuration exists
if autoregister:
config_path = ProjectConfig.path_to_project_yml(project_root_or_name)
if os.path.isfile(config_path):
registered_project = RegisteredProject.from_project_root(project_root_or_name)
self.add_registered_project(registered_project)
return registered_project
# nothing found
return None
def get_project(self, project_root_or_name: str) -> Optional["Project"]:
@@ -640,6 +661,13 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
else:
return registered_project.get_project_instance()
def add_registered_project(self, registered_project: RegisteredProject) -> None:
"""
Adds a registered project, saving the configuration file.
"""
self.projects.append(registered_project)
self.save()
def add_project_from_path(self, project_root: Path | str) -> "Project":
"""
Add a new project to the Serena configuration from a given path, auto-generating the project
@@ -666,8 +694,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
project_config = ProjectConfig.load(project_root, autogenerate=True)
new_project = Project(project_root=str(project_root), project_config=project_config, is_newly_created=True)
self.projects.append(RegisteredProject.from_project_instance(new_project))
self.save()
self.add_registered_project(RegisteredProject.from_project_instance(new_project))
return new_project
+3 -3
View File
@@ -236,20 +236,20 @@ class TestProjectCreateHelper:
def test_create_project_helper_returns_config(self, temp_project_dir):
"""Test that _create_project returns a ProjectConfig with explicit language."""
config = ProjectCommands._create_project(temp_project_dir, "test-project", ("python",))
config = ProjectCommands._create_project(temp_project_dir, "test-project", ("python",)).project_config
assert isinstance(config, ProjectConfig)
assert config.project_name == "test-project"
def test_create_project_helper_with_auto_detect(self, temp_project_dir_with_python_file):
"""Test _create_project with auto-detected language."""
config = ProjectCommands._create_project(temp_project_dir_with_python_file, "my-project", ())
config = ProjectCommands._create_project(temp_project_dir_with_python_file, "my-project", ()).project_config
assert isinstance(config, ProjectConfig)
assert config.project_name == "my-project"
assert len(config.languages) >= 1
def test_create_project_helper_with_languages(self, temp_project_dir):
"""Test _create_project with language specification."""
config = ProjectCommands._create_project(temp_project_dir, None, ("python", "typescript"))
config = ProjectCommands._create_project(temp_project_dir, None, ("python", "typescript")).project_config
assert isinstance(config, ProjectConfig)
assert len(config.languages) >= 1