Merge branch 'main' of github.com:oraios/serena

This commit is contained in:
Dominik Jain committed 2025-04-01 20:03:45 +02:00
commit f488116bb5
14 files changed
+302 -1952

No files matched your search

+5 -1
View File
@@ -144,4 +144,8 @@ pylint.html
/src/multilspy/language_servers/*/static
# Cache is in .multilspy
**/.multilspy/**
**/.multilspy/**
tmp/
.serena/
+4 -1
View File
@@ -1,7 +1,10 @@
{
"cSpell.words": [
"agno",
"genai",
"langsrv",
"multilspy",
"pixi"
"pixi",
"sensai"
]
}
+3 -2
View File
@@ -74,7 +74,7 @@ We thus built Serena with the prospect of being able to cancel most other subscr
"mcpServers": {
"serena": {
"command": "/abs/path/to/uv",
"args": ["run", "--directory", "/abs/path/to/serena", "serena", "/abs/path/to/myproject.yml"]
"args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server", "/abs/path/to/myproject.yml"]
}
}
}
@@ -169,7 +169,8 @@ without modifying the codebase, you can consider disabling the editing tools in
In general, be sure to back up your work and use a version control system in order to avoid
losing any work.
Here a complete list of Serena's tools:
Here a complete list of Serena's tools
(you can get an up-to-date list by running `uv run serena-list-tools`)
* `check_onboarding_performed`: Checks whether the onboarding was already performed.
* `create_text_file`: Creates/overwrites a file in the project directory.
+44 -1
View File
@@ -4,7 +4,50 @@ project_root: /path/to/project
language: python
# list of directories to ignore: either names (e.g. "temp") or relative paths (e.g. "build/foo")
ignored_dirs: [".git", "temp"]
# whether to open a graphical window with Serena's logs (not supported on MacOS)
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run serena-list-tools`.
#
# check_onboarding_performed
# create_text_file
# delete_lines
# delete_memory
# execute_shell_command
# find_referencing_symbols
# find_symbol
# get_dir_overview
# get_document_overview
# insert_after_symbol
# insert_at_line
# insert_before_symbol
# list_dir
# list_memories
# onboarding
# prepare_for_new_conversation
# read_file
# read_memory
# replace_symbol_body
# search_in_all_code
# summarize_changes
# think_about_collected_information
# think_about_task_adherence
# think_about_whether_you_are_done
# write_memory
excluded_tools: []
# Whether to open a graphical window with Serena's logs (not supported on MacOS).
# This is useful both for troubleshooting and for monitoring the tool calls,
# especially when using the agno playground, since the tool calls are not always shown,
# and the input params are never shown in the agno UI.
# When used as MCP server for Claude Desktop, the logs are primarily for troubleshooting.
# Note: unfortunately, the various entities starting the Serena server or agent do so in
# mysterious ways, often starting multiple instances of the process without shutting down
# previous instances. This leads to multiple log windows being opened, and only the last
# window being updated. Since we can't control how agno or Claude Desktop starts Serena, we have to live with this limitation for now.
gui_log_window: True
# minimum log level for the GUI log window (10 = debug, 20 = info, 30 = warning, 40 = error)
gui_log_level: 20
+2 -1
View File
@@ -33,7 +33,8 @@ dependencies = [
]
[project.scripts]
serena = "serena.mcp:start_mcp_server"
serena-mcp-server = "serena.mcp:start_mcp_server"
serena-list-tools = "serena.agent:print_tool_overview"
[project.license]
text = "MIT"
+55 -28
View File
@@ -1,12 +1,16 @@
import os
from logging import Logger
from agno.agent import Agent
from agno.memory import AgentMemory
from agno.models.anthropic import Claude
from agno.models.google import Gemini
from agno.playground import Playground, serve_playground_app
import click
from agno.agent.agent import Agent
from agno.memory.agent import AgentMemory
from agno.models.anthropic.claude import Claude
from agno.models.google.gemini import Gemini
from agno.playground.playground import Playground
from agno.playground.serve import serve_playground_app
from agno.storage.sqlite import SqliteStorage
from dotenv import load_dotenv
from fastapi import FastAPI
from sensai.util import logging
from sensai.util.helper import mark_used
@@ -20,29 +24,52 @@ Logger.root.setLevel(logging.INFO)
load_dotenv()
project_file_path = "../myproject.yml"
serena_agent = SerenaAgent(project_file_path, start_language_server=True)
model = Claude(id="claude-3-7-sonnet-20250219")
# model = Gemini(id="gemini-2.5-pro-exp-03-25")
agno_agent = Agent(
name="Serena",
model=model,
description="A fully-featured coding assistant",
tools=[SerenaAgnoToolkit(serena_agent)], # type: ignore
show_tool_calls=False,
markdown=True,
system_message=serena_agent.prompt_factory.create_system_prompt(),
read_tool_call_history=False,
telemetry=False,
memory=AgentMemory(),
add_history_to_messages=True,
num_history_responses=100, # you might want to adjust this (expense vs. history awareness)
)
# The app object must be in the module scope so that the server can access it for hot reloading
app = Playground(agents=[agno_agent]).get_app()
app: FastAPI | None = None
@click.command()
@click.option("--project-file", default="../myproject.yml", help="Path to the project configuration YAML file")
def main(project_file):
global app # noqa: PLW0603
serena_agent = SerenaAgent(project_file, start_language_server=True)
# Even though we don't want to keep history between sessions,
# for the agno ui to work as a conversation, we a persistent storage on disk
# This storage should be deleted between sessions.
# Note that this might collide with custom options for the agent, like adding vector-search based tools.
# See here for an explanation: https://www.reddit.com/r/agno/comments/1jk6qea/regarding_the_built_in_memory/
sql_db_path = os.path.join("tmp", "agent_storage.db")
os.makedirs(os.path.dirname(sql_db_path), exist_ok=True)
# delete the db file if it exists
if os.path.exists(sql_db_path):
os.remove(sql_db_path)
model = Claude(id="claude-3-7-sonnet-20250219")
# Or use any other model supported by agno, e.g. the newest Gemini:
# model = Gemini(id="gemini-2.5-pro-exp-03-25")
agno_agent = Agent(
name="Serena",
model=model,
# See explanation above on why storage is needed
storage=SqliteStorage(table_name="serena_agent_sessions", db_file=sql_db_path),
description="A fully-featured coding assistant",
tools=[SerenaAgnoToolkit(serena_agent)], # type: ignore
# The tool calls will be shown in the UI anyway since whether to show them is configurable per tool
# To see detailed logs, you should use the serena logger (configure it in the project file path)
show_tool_calls=False,
markdown=True,
system_message=serena_agent.prompt_factory.create_system_prompt(),
telemetry=False,
memory=AgentMemory(),
add_history_to_messages=True,
num_history_responses=100, # you might want to adjust this (expense vs. history awareness)
)
app = Playground(agents=[agno_agent]).get_app()
serve_playground_app("agno_agent:app", reload=False)
if __name__ == "__main__":
serve_playground_app("agno_agent:app", reload=False)
main()
-10
View File
@@ -1,10 +0,0 @@
from serena.agent import SerenaAgent
if __name__ == "__main__":
agent = SerenaAgent("myproject.yml")
tools = {}
for tool in agent.tools.values():
tools[tool.get_name()] = tool
for tool_name in sorted(tools.keys()):
tool = tools[tool_name]
print(f" * `{tool_name}`: {tool.get_tool_description().strip()}")
+82 -22
View File
@@ -6,38 +6,37 @@ The details of Language Specific configuration are not exposed to the user.
"""
import asyncio
from collections import defaultdict
from copy import copy
import dataclasses
from fnmatch import fnmatch
import hashlib
import json
import pickle
import logging
import os
import pathlib
import pickle
import re
import threading
from collections import defaultdict
from contextlib import asynccontextmanager, contextmanager
from copy import copy
from fnmatch import fnmatch
from pathlib import Path, PurePath
from typing import AsyncIterator, Dict, Iterator, List, Optional, Tuple, Union, cast
from serena.text_utils import MatchedConsecutiveLines, LineType, TextLine, search_text
from .lsp_protocol_handler.lsp_constants import LSPConstants
from .lsp_protocol_handler import lsp_types as LSPTypes
from serena.text_utils import LineType, MatchedConsecutiveLines, TextLine, search_text
from . import multilspy_types
from .multilspy_logger import MultilspyLogger
from .lsp_protocol_handler import lsp_types as LSPTypes
from .lsp_protocol_handler.lsp_constants import LSPConstants
from .lsp_protocol_handler.lsp_types import SymbolKind
from .lsp_protocol_handler.server import (
LanguageServerHandler,
ProcessLaunchInfo,
)
from .multilspy_config import MultilspyConfig, Language
from .multilspy_config import Language, MultilspyConfig
from .multilspy_exceptions import MultilspyException
from .multilspy_utils import PathUtils, FileUtils, TextUtils
from pathlib import Path, PurePath
from typing import AsyncIterator, Iterator, List, Dict, Optional, TypedDict, Union, Tuple, cast
from .multilspy_logger import MultilspyLogger
from .multilspy_utils import FileUtils, PathUtils, TextUtils
from .type_helpers import ensure_all_methods_implemented
# Serena dependencies
# We will need to watch out for circular imports, but it's probably better to not
# move all generic util code from serena into multilspy.
@@ -664,7 +663,7 @@ class LanguageServer:
:param relative_file_path: The relative path of the file that has the symbols
:param include_body: whether to include the body of the symbols in the result.
:return: A list of symbols in the file, and a list of root symbols that represent the tree structure of the symbols.
:return: A list of symbols in the file, and a list of root symbols that represent the tree structure of the symbols.
Each symbol in hierarchy starting from the roots has a children attribute.
All symbols will have a location and a children attribute.
"""
@@ -712,6 +711,10 @@ class LanguageServer:
assert isinstance(response, list)
root_nodes: List[multilspy_types.UnifiedSymbolInformation] = []
for item in response:
if "range" not in item and "location" not in item:
if item["kind"] in [SymbolKind.File, SymbolKind.Module]:
...
turn_item_into_symbol_with_children(item)
item = cast(multilspy_types.UnifiedSymbolInformation, item)
root_nodes.append(item)
@@ -753,7 +756,7 @@ class LanguageServer:
Will ignore all directories that start with a dot (.) and __pycache__ directories.
Args:
start_package_relative_path: if passed, only the symbols within this directory will be considered.
start_dir_relative_path: if passed, only the symbols within this directory will be considered.
include_body: whether to include the body of the symbols in the result.
Returns:
@@ -829,12 +832,17 @@ class LanguageServer:
fix_relative_path(root_nodes)
# Create file symbol
file_rel_path = str(Path(abs_item_path).resolve().relative_to(self.repository_root_path))
with self.open_file(file_rel_path) as file_data:
fileRange = self._get_range_from_file_content(file_data.contents)
file_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore
name=os.path.splitext(item)[0],
kind=multilspy_types.SymbolKind.File,
range=fileRange,
selectionRange=fileRange,
location=multilspy_types.Location(
uri=str(pathlib.Path(abs_item_path).as_uri()),
range={"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}},
range=fileRange,
absolutePath=str(abs_item_path),
relativePath=str(Path(abs_item_path).resolve().relative_to(self.repository_root_path)),
),
@@ -848,7 +856,20 @@ class LanguageServer:
# Start from the root or the specified directory
start_path = start_dir_relative_path or "."
return await process_directory(start_path)
@staticmethod
def _get_range_from_file_content(file_content: str) -> multilspy_types.Range:
"""
Get the range for the given file.
"""
lines = file_content.split("\n")
end_line = len(lines)
end_column = len(lines[-1])
return multilspy_types.Range(
start=multilspy_types.Position(line=0, character=0),
end=multilspy_types.Position(line=end_line, character=end_column)
)
async def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]:
"""
An overview of the given directory.
@@ -1049,6 +1070,7 @@ class LanguageServer:
include_imports: bool = True,
include_self: bool = False,
include_body: bool = False,
include_file_symbols: bool = False,
) -> List[multilspy_types.UnifiedSymbolInformation]:
"""
Finds all symbols that reference the symbol at the given location.
@@ -1064,6 +1086,8 @@ class LanguageServer:
:param include_self: whether to include the references that is the "input symbol" itself.
Only has an effect if the relative_file_path, line and column point to a symbol, for example a definition.
:param include_body: whether to include the body of the symbols in the result.
:param include_file_symbols: whether to include references that are file symbols. This
is often a fallback mechanism for when the reference cannot be resolved to a symbol.
:return: List of symbols that reference the target symbol.
"""
if not self.server_started:
@@ -1115,9 +1139,41 @@ class LanguageServer:
containing_symbol["location"] = ref
containing_symbol["range"] = ref["range"]
break
if containing_symbol is None:
self.logger.log(f"Could not find containing symbol for {ref_path}:{ref_line}:{ref_col}", logging.WARNING)
# We failed retrieving the symbol, falling back to creating a file symbol
if containing_symbol is None and include_file_symbols:
self.logger.log(
f"Could not find containing symbol for {ref_path}:{ref_line}:{ref_col}. Returning file symbol instead",
logging.WARNING
)
fileRange = self._get_range_from_file_content(file_data.contents)
location = multilspy_types.Location(
uri=str(pathlib.Path(os.path.join(self.repository_root_path, ref_path)).as_uri()),
range=fileRange,
absolutePath=str(os.path.join(self.repository_root_path, ref_path)),
relativePath=ref_path,
)
name = os.path.splitext(os.path.basename(ref_path))[0]
if include_body:
body = self.retrieve_full_file_content(ref_path)
else:
body = ""
containing_symbol = multilspy_types.UnifiedSymbolInformation(
kind=multilspy_types.SymbolKind.File,
range=fileRange,
selectionRange=fileRange,
location=location,
name=name,
children=[],
body=body,
)
if containing_symbol is None or not include_file_symbols and containing_symbol["kind"] == multilspy_types.SymbolKind.File:
continue
assert "location" in containing_symbol
assert "selectionRange" in containing_symbol
# Checking for self-reference
if (
@@ -1198,7 +1254,7 @@ class LanguageServer:
f"Passing empty lines to request_container_symbol is currently not supported, {relative_file_path=}, {line=}",
logging.ERROR,
)
return
return None
symbols, _ = await self.request_document_symbols(relative_file_path)
@@ -1220,7 +1276,7 @@ class LanguageServer:
assert "range" in location
location["absolutePath"] = absolute_file_path
location["relativePath"] = relative_file_path
location["uri"] = f"file:/{absolute_file_path}"
location["uri"] = Path(absolute_file_path).as_uri()
# Allowed container kinds, currently only for Python
container_symbol_kinds = {
@@ -1610,6 +1666,7 @@ class SyncLanguageServer:
self, relative_file_path: str, line: int, column: int,
include_imports: bool = True, include_self: bool = False,
include_body: bool = False,
include_file_symbols: bool = False,
) -> List[multilspy_types.UnifiedSymbolInformation]:
"""
Finds all symbols that reference the symbol at the given location.
@@ -1625,6 +1682,8 @@ class SyncLanguageServer:
:param include_self: whether to include the references that is the "input symbol" itself.
Only has an effect if the relative_file_path, line and column point to a symbol, for example a definition.
:param include_body: whether to include the body of the symbols in the result.
:param include_file_symbols: whether to include references that are file symbols. This
is often a fallback mechanism for when the reference cannot be resolved to a symbol.
:return: List of symbols that reference the target symbol.
"""
assert self.loop
@@ -1636,6 +1695,7 @@ class SyncLanguageServer:
include_imports=include_imports,
include_self=include_self,
include_body=include_body,
include_file_symbols=include_file_symbols,
),
self.loop
).result()
+30 -9
View File
@@ -11,7 +11,7 @@ import traceback
import warnings
from abc import ABC
from collections import defaultdict
from collections.abc import Callable, Iterator
from collections.abc import Callable, Generator, Iterable, Iterator
from contextlib import contextmanager
from logging import Logger
from pathlib import Path
@@ -29,6 +29,7 @@ from serena.gui_log_viewer import GuiLogViewer, GuiLogViewerHandler
from serena.llm.prompt_factory import PromptFactory
from serena.symbol import SymbolLocation, SymbolManager
from serena.util.file_system import scan_directory
from serena.util.inspection import iter_subclasses
from serena.util.shell import execute_shell_command
log = logging.getLogger(__name__)
@@ -101,11 +102,12 @@ class SerenaAgent:
self.lines_read = LinesRead()
# find all tool classes and instantiate them
excluded_tools = project_config.get("excluded_tools", [])
self.tools: dict[type[Tool], Tool] = {}
tool_classes = [
cls for name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass) if issubclass(cls, Tool) and cls is not Tool
]
for tool_class in tool_classes:
for tool_class in iter_tool_classes():
if (tool_name := tool_class.get_name()) in excluded_tools:
log.info(f"Skipping tool {tool_name} because it is in the exclude list")
continue
tool_instance = tool_class(self)
self.tools[tool_class] = tool_instance
log.info(f"Loaded tools: {', '.join([tool.get_name() for tool in self.tools.values()])}")
@@ -115,11 +117,12 @@ class SerenaAgent:
log.info("Starting the language server ...")
self.language_server.start()
self._is_initialized = True
def get_tool(self, tool_class: type[TTool]) -> TTool:
return self.tools[tool_class] # type: ignore
def print_tool_overview(self) -> None:
_print_tool_overview(self.tools.values())
def get_serena_managed_dir(self) -> str:
return os.path.join(self.project_root, ".serena")
@@ -212,8 +215,9 @@ class Tool(Component):
raise Exception(f"{self} does not define method apply")
return apply_fn
def get_tool_description(self) -> str:
docstring = self.__class__.__doc__
@classmethod
def get_tool_description(cls) -> str:
docstring = cls.__doc__
if docstring is None:
return ""
return docstring.strip()
@@ -928,3 +932,20 @@ class ExecuteShellCommandTool(Tool):
result = execute_shell_command(command, cwd=_cwd, capture_stderr=capture_stderr)
result = result.json()
return self._limit_length(result, max_answer_chars)
def iter_tool_classes() -> Generator[type[Tool], None, None]:
return iter_subclasses(Tool)
def print_tool_overview() -> None:
_print_tool_overview(iter_tool_classes())
def _print_tool_overview(tools: Iterable[type[Tool] | Tool]) -> None:
tool_dict: dict[str, type[Tool] | Tool] = {}
for tool in tools:
tool_dict[tool.get_name()] = tool
for tool_name in sorted(tool_dict.keys()):
tool = tool_dict[tool_name]
print(f" * `{tool_name}`: {tool.get_tool_description().strip()}")
+2 -1
View File
@@ -1,7 +1,8 @@
import logging
from typing import Any
from agno.tools import Function, Toolkit
from agno.tools.function import Function
from agno.tools.toolkit import Toolkit
from serena.agent import SerenaAgent, Tool
+12
View File
@@ -0,0 +1,12 @@
from collections.abc import Generator
from typing import TypeVar
T = TypeVar("T")
def iter_subclasses(cls: type[T], recursive: bool = True) -> Generator[type[T], None, None]:
"""Iterate over all subclasses of a class. If recursive is True, also iterate over all subclasses of all subclasses."""
for subclass in cls.__subclasses__():
yield subclass
if recursive:
yield from iter_subclasses(subclass, recursive)
+6 -5
View File
@@ -5,6 +5,7 @@ These tests validate the functionality of the language server APIs
like request_references using the test repository.
"""
import os
from pathlib import Path
from multilspy.language_server import SyncLanguageServer
@@ -17,7 +18,7 @@ class TestLanguageServerBasics:
def test_request_references_user_class(self, language_server: SyncLanguageServer, repo_path: Path):
"""Test request_references on the User class."""
# Get references to the User class in models.py
file_path = str(repo_path / "test_repo" / "models.py")
file_path = os.path.join("test_repo", "models.py")
# Line 31 contains the User class definition
references = language_server.request_references(file_path, 31, 6)
@@ -30,7 +31,7 @@ class TestLanguageServerBasics:
def test_request_references_item_class(self, language_server: SyncLanguageServer, repo_path: Path):
"""Test request_references on the Item class."""
# Get references to the Item class in models.py
file_path = str(repo_path / "test_repo" / "models.py")
file_path = os.path.join("test_repo", "models.py")
# Line 56 contains the Item class definition
references = language_server.request_references(file_path, 56, 6)
@@ -44,7 +45,7 @@ class TestLanguageServerBasics:
def test_request_references_function_parameter(self, language_server: SyncLanguageServer, repo_path: Path):
"""Test request_references on a function parameter."""
# Get references to the id parameter in get_user method
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 24 contains the get_user method with id parameter
references = language_server.request_references(file_path, 24, 16)
@@ -53,7 +54,7 @@ class TestLanguageServerBasics:
def test_request_references_create_user_method(self, language_server: SyncLanguageServer, repo_path: Path):
# Get references to the create_user method in UserService
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 15 contains the create_user method definition
references = language_server.request_references(file_path, 15, 9)
@@ -62,7 +63,7 @@ class TestLanguageServerBasics:
def test_retrieve_content_around_line(self, language_server: SyncLanguageServer, repo_path: Path):
"""Test retrieve_content_around_line functionality with various scenarios."""
file_path = str(repo_path / "test_repo" / "models.py")
file_path = os.path.join("test_repo", "models.py")
# Scenario 1: Just a single line (User class definition)
line_31 = language_server.retrieve_content_around_line(file_path, 31)
+57 -40
View File
@@ -7,7 +7,6 @@ These tests focus on the following methods:
"""
import os
from pathlib import Path
from multilspy.language_server import SyncLanguageServer
from multilspy.multilspy_types import SymbolKind
@@ -16,10 +15,10 @@ from multilspy.multilspy_types import SymbolKind
class TestLanguageServerSymbols:
"""Test the language server's symbol-related functionality."""
def test_request_containing_symbol_function(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_containing_symbol_function(self, language_server: SyncLanguageServer):
"""Test request_containing_symbol for a function."""
# Test for a position inside the create_user method
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 17 is inside the create_user method body
containing_symbol = language_server.request_containing_symbol(file_path, 17, 20, include_body=True)
@@ -30,9 +29,9 @@ class TestLanguageServerSymbols:
if "body" in containing_symbol:
assert containing_symbol["body"].strip().startswith("def create_user(self")
def test_references_to_variables(self, language_server: SyncLanguageServer, repo_path: Path):
def test_references_to_variables(self, language_server: SyncLanguageServer):
"""Test request_referencing_symbols for a variable."""
file_path = str(repo_path / "test_repo" / "variables.py")
file_path = os.path.join("test_repo", "variables.py")
# Line 75 contains the field status that is later modified
ref_symbols = language_server.request_referencing_symbols(file_path, 74, 4)
@@ -44,10 +43,10 @@ class TestLanguageServerSymbols:
assert "dataclass_instance" in ref_names
assert "second_dataclass" in ref_names
def test_request_containing_symbol_class(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_containing_symbol_class(self, language_server: SyncLanguageServer):
"""Test request_containing_symbol for a class."""
# Test for a position inside the UserService class but outside any method
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 9 is the class definition line for UserService
containing_symbol = language_server.request_containing_symbol(file_path, 9, 7)
@@ -56,10 +55,10 @@ class TestLanguageServerSymbols:
assert containing_symbol["name"] == "UserService"
assert containing_symbol["kind"] == SymbolKind.Class
def test_request_containing_symbol_nested(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_containing_symbol_nested(self, language_server: SyncLanguageServer):
"""Test request_containing_symbol with nested scopes."""
# Test for a position inside a method which is inside a class
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 18 is inside the create_user method inside UserService class
containing_symbol = language_server.request_containing_symbol(file_path, 18, 25)
@@ -81,20 +80,20 @@ class TestLanguageServerSymbols:
assert parent_symbol["name"] == "UserService"
assert parent_symbol["kind"] == SymbolKind.Class
def test_request_containing_symbol_none(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_containing_symbol_none(self, language_server: SyncLanguageServer):
"""Test request_containing_symbol for a position with no containing symbol."""
# Test for a position outside any function/class (e.g., in imports)
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 1 is in imports, not inside any function or class
containing_symbol = language_server.request_containing_symbol(file_path, 1, 10)
# Should return None or an empty dictionary
assert containing_symbol is None or containing_symbol == {}
def test_request_referencing_symbols_function(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_referencing_symbols_function(self, language_server: SyncLanguageServer):
"""Test request_referencing_symbols for a function."""
# Test referencing symbols for create_user function
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 15 contains the create_user function definition
ref_symbols = language_server.request_referencing_symbols(file_path, 15, 9)
@@ -109,10 +108,10 @@ class TestLanguageServerSymbols:
assert "start" in symbol["location"]["range"]
assert "end" in symbol["location"]["range"]
def test_request_referencing_symbols_class(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_referencing_symbols_class(self, language_server: SyncLanguageServer):
"""Test request_referencing_symbols for a class."""
# Test referencing symbols for User class
file_path = str(repo_path / "test_repo" / "models.py")
file_path = os.path.join("test_repo", "models.py")
# Line 31 contains the User class definition
ref_symbols = language_server.request_referencing_symbols(file_path, 31, 6)
@@ -128,10 +127,10 @@ class TestLanguageServerSymbols:
assert len(services_references) > 0
def test_request_referencing_symbols_parameter(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_referencing_symbols_parameter(self, language_server: SyncLanguageServer):
"""Test request_referencing_symbols for a function parameter."""
# Test referencing symbols for id parameter in get_user
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 24 contains the get_user method with id parameter
ref_symbols = language_server.request_referencing_symbols(file_path, 24, 16)
@@ -147,12 +146,12 @@ class TestLanguageServerSymbols:
assert len(method_refs) > 0
def test_request_referencing_symbols_none(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_referencing_symbols_none(self, language_server: SyncLanguageServer):
"""Test request_referencing_symbols for a position with no symbol."""
# For positions with no symbol, the method might throw an error or return None/empty list
# We'll modify our test to handle this by using a try-except block
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 3 is a blank line or comment
try:
ref_symbols = language_server.request_referencing_symbols(file_path, 3, 0)
@@ -164,10 +163,10 @@ class TestLanguageServerSymbols:
pass
# Tests for request_defining_symbol
def test_request_defining_symbol_variable(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_defining_symbol_variable(self, language_server: SyncLanguageServer):
"""Test request_defining_symbol for a variable usage."""
# Test finding the definition of a symbol in the create_user method
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 21 contains self.users[id] = user
defining_symbol = language_server.request_defining_symbol(file_path, 21, 10)
@@ -182,10 +181,10 @@ class TestLanguageServerSymbols:
if "location" in defining_symbol and "uri" in defining_symbol["location"]:
assert "services.py" in defining_symbol["location"]["uri"]
def test_request_defining_symbol_imported_class(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_defining_symbol_imported_class(self, language_server: SyncLanguageServer):
"""Test request_defining_symbol for an imported class."""
# Test finding the definition of the 'User' class used in the UserService.create_user method
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 20 references 'User' which was imported from models
defining_symbol = language_server.request_defining_symbol(file_path, 20, 15)
@@ -193,10 +192,10 @@ class TestLanguageServerSymbols:
assert defining_symbol is not None
assert defining_symbol.get("name") == "User"
def test_request_defining_symbol_method_call(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_defining_symbol_method_call(self, language_server: SyncLanguageServer):
"""Test request_defining_symbol for a method call."""
# Create an example file path for a file that calls UserService.create_user
examples_file_path = str(repo_path / "examples" / "user_management.py")
examples_file_path = os.path.join("examples", "user_management.py")
# Find the line number where create_user is called
# This could vary, so we'll use a relative position that makes sense
@@ -217,20 +216,20 @@ class TestLanguageServerSymbols:
warnings.warn("Could not verify method call definition - file structure may differ from expected")
def test_request_defining_symbol_none(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_defining_symbol_none(self, language_server: SyncLanguageServer):
"""Test request_defining_symbol for a position with no symbol."""
# Test for a position with no symbol (e.g., whitespace or comment)
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 3 is a blank line
defining_symbol = language_server.request_defining_symbol(file_path, 3, 0)
# Should return None for positions with no symbol
assert defining_symbol is None
def test_request_containing_symbol_variable(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_containing_symbol_variable(self, language_server: SyncLanguageServer):
"""Test request_containing_symbol where the symbol is a variable."""
# Test for a position inside a variable definition
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# Line 74 defines the 'user' variable
containing_symbol = language_server.request_containing_symbol(file_path, 73, 1)
@@ -239,10 +238,10 @@ class TestLanguageServerSymbols:
assert containing_symbol["name"] == "user_var_str"
assert containing_symbol["kind"] == SymbolKind.Variable
def test_request_defining_symbol_nested_function(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_defining_symbol_nested_function(self, language_server: SyncLanguageServer):
"""Test request_defining_symbol for a nested function or closure."""
# Use the existing nested.py file which contains nested classes and methods
file_path = str(repo_path / "test_repo" / "nested.py")
file_path = os.path.join("test_repo", "nested.py")
# Test 1: Find definition of nested method - line with 'b = OuterClass().NestedClass().find_me()'
defining_symbol = language_server.request_defining_symbol(file_path, 15, 35) # Position of find_me() call
@@ -289,12 +288,12 @@ class TestLanguageServerSymbols:
assert defining_symbol.get("name") == "func_within_func"
assert defining_symbol.get("kind") == SymbolKind.Function.value
def test_symbol_methods_integration(self, language_server: SyncLanguageServer, repo_path: Path):
def test_symbol_methods_integration(self, language_server: SyncLanguageServer):
"""Test the integration between different symbol-related methods."""
# This test demonstrates using the various symbol methods together
# by finding a symbol and then checking its definition
file_path = str(repo_path / "test_repo" / "services.py")
file_path = os.path.join("test_repo", "services.py")
# First approach: Use a method from the UserService class
# Step 1: Find a method we know exists
@@ -337,7 +336,7 @@ class TestLanguageServerSymbols:
warnings.warn("Could not verify container hierarchy - implementation detail")
def test_symbol_tree_structure(self, language_server: SyncLanguageServer, repo_path: Path):
def test_symbol_tree_structure(self, language_server: SyncLanguageServer):
"""Test that the symbol tree structure is correctly built."""
# Get all symbols in the test file
repo_structure = language_server.request_full_symbol_tree()
@@ -361,13 +360,13 @@ class TestLanguageServerSymbols:
if "location" in user_management_node and "relativePath" in user_management_node["location"]:
user_management_rel_path = user_management_node["location"]["relativePath"]
assert user_management_rel_path == os.path.join("examples", "user_management.py")
_, user_management_roots = language_server.request_document_symbols(str(repo_path / "examples" / "user_management.py"))
_, user_management_roots = language_server.request_document_symbols(os.path.join("examples", "user_management.py"))
assert user_management_roots == user_management_node["children"]
def test_symbol_tree_structure_subdir(self, language_server: SyncLanguageServer, repo_path: Path):
def test_symbol_tree_structure_subdir(self, language_server: SyncLanguageServer):
"""Test that the symbol tree structure is correctly built."""
# Get all symbols in the test file
examples_package_roots = language_server.request_full_symbol_tree(start_package_relative_path=str(repo_path / "examples"))
examples_package_roots = language_server.request_full_symbol_tree(start_package_relative_path="examples")
assert len(examples_package_roots) == 1
examples_package = examples_package_roots[0]
assert examples_package["name"] == "examples"
@@ -381,10 +380,10 @@ class TestLanguageServerSymbols:
if "location" in user_management_node and "relativePath" in user_management_node["location"]:
user_management_rel_path = user_management_node["location"]["relativePath"]
assert user_management_rel_path == os.path.join("examples", "user_management.py")
_, user_management_roots = language_server.request_document_symbols(str(repo_path / "examples" / "user_management.py"))
_, user_management_roots = language_server.request_document_symbols(os.path.join("examples", "user_management.py"))
assert user_management_roots == user_management_node["children"]
def test_request_dir_overview(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_dir_overview(self, language_server: SyncLanguageServer):
"""Test that request_dir_overview returns correct symbol information for files in a directory."""
# Get overview of the examples directory
overview = language_server.request_dir_overview("test_repo")
@@ -408,7 +407,7 @@ class TestLanguageServerSymbols:
for symbol in expected_symbols:
assert symbol in services_symbols
def test_request_document_overview(self, language_server: SyncLanguageServer, repo_path: Path):
def test_request_document_overview(self, language_server: SyncLanguageServer):
"""Test that request_document_overview returns correct symbol information for a file."""
# Get overview of the user_management.py file
overview = language_server.request_document_overview(os.path.join("examples", "user_management.py"))
@@ -416,3 +415,21 @@ class TestLanguageServerSymbols:
# Verify that we have entries for both files
symbol_names = {s_info[0] for s_info in overview}
assert {"UserStats", "UserManager", "process_user_data", "main"}.issubset(symbol_names)
def test_containing_symbol_of_var_is_file(self, language_server: SyncLanguageServer):
"""Test that the containing symbol of a variable is the file itself."""
# Get the containing symbol of a variable in a file
file_path = os.path.join("test_repo", "services.py")
# import of typing
references_to_typing = language_server.request_referencing_symbols(
file_path, 4, 6, include_imports=False, include_file_symbols=True
)
assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File}
assert {ref["body"] for ref in references_to_typing} == {""}
# now include bodies
references_to_typing = language_server.request_referencing_symbols(
file_path, 4, 6, include_imports=False, include_file_symbols=True, include_body=True
)
assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File}
assert references_to_typing[0]["body"]
Generated
-1831
View File
File diff suppressed because it is too large. Load diff