Minor cleanup after serena

This commit is contained in:
Michael Panchenko committed 2025-04-18 20:53:29 +02:00
1 parent 468beb8fb1
commit efb0690cf9
4 files changed
+31 -131

No files matched your search

-46
View File
@@ -1,46 +0,0 @@
# Read-Only Mode
Serena supports a read-only mode that prevents any modifications to the codebase while still allowing analysis and exploration. This feature is useful for:
- Reviewing and analyzing code without the risk of accidental modifications
- Allowing junior developers to explore a codebase with AI assistance but without modification privileges
- Generating documentation or explanations without changing the underlying code
- Creating proof-of-concept designs without implementing them
## Enabling Read-Only Mode
To enable read-only mode, set the `read_only` flag to `true` in your project configuration file:
```yaml
# whether the project is in read-only mode
read_only: true
```
## How It Works
When read-only mode is enabled:
1. All tools that perform editing operations are automatically disabled
2. Any attempt to use an editing tool will result in an error message
3. All non-editing tools remain fully functional
The editing tools that get disabled include:
- `create_text_file`: Creating or overwriting files
- `replace_symbol_body`: Replacing code symbol definitions
- `insert_after_symbol`: Inserting code after symbols
- `insert_before_symbol`: Inserting code before symbols
- `delete_lines`: Deleting lines from files
- `replace_lines`: Replacing lines in files
- `insert_at_line`: Inserting content at specific lines
- `execute_shell_command`: Executing shell commands that could potentially modify files
- `insert_at_line`: Inserting content at specific lines
## Implementation Details
Serena uses the `EditingTool` marker class to identify tools that can modify code. The base `Tool` class includes a `can_edit()` method that checks if a tool is a subclass of `EditingTool`.
When a project is configured in read-only mode:
1. During project activation, all editing tools are automatically excluded from the active tools list
2. An additional runtime check prevents any editing tools from being executed, even if they somehow remain in the active tools list
This ensures that no modifications to the codebase can occur while in read-only mode.
+28 -31
View File
@@ -31,7 +31,6 @@ from serena.text_utils import search_files
from serena.util.class_decorators import singleton
from serena.util.file_system import scan_directory
from serena.util.inspection import iter_subclasses
from serena.editingtool import EditingTool
from serena.util.shell import execute_shell_command
if TYPE_CHECKING:
@@ -117,7 +116,6 @@ class ProjectConfig(ToStringMixin):
return os.path.join(self.project_root, self.SERENA_MANAGED_DIR)
@singleton
class SerenaConfig:
"""
@@ -269,7 +267,6 @@ class SerenaAgent:
# When project activation is not enabled, we only expose the active tools
return list(self._active_tools.values())
def activate_project(self, project_config: ProjectConfig) -> None:
log.info(f"Activating {project_config}")
self.project_config = project_config
@@ -282,13 +279,13 @@ class SerenaAgent:
log.info(f"Active tools after exclusions ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}")
else:
self._active_tools = dict(self._all_tools)
# if read_only mode is enabled, exclude all editing tools
if self.project_config.read_only:
self._active_tools = {
key: tool for key, tool in self._active_tools.items() if not key.can_edit()
}
log.info(f"Project is in read-only mode. Editing tools excluded. Active tools ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}")
self._active_tools = {key: tool for key, tool in self._active_tools.items() if not key.can_edit()}
log.info(
f"Project is in read-only mode. Editing tools excluded. Active tools ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}"
)
# start the language server
self.reset_language_server()
@@ -302,7 +299,6 @@ class SerenaAgent:
if self._project_activation_callback is not None:
self._project_activation_callback()
def get_active_tool_names(self) -> list[str]:
"""
:return: the list of names of the active tools for the current project
@@ -436,6 +432,16 @@ class Component(ABC):
_DEFAULT_MAX_ANSWER_LENGTH = int(2e5)
class ToolMarkerCanEdit:
"""
Marker class for all tools that can perform editing operations on files.
"""
class ToolMarkerDoesNotRequireActiveProject:
pass
class Tool(Component):
# NOTE: each tool should implement the apply method, which is then used in
# the central method of the Tool class `apply_ex`.
@@ -461,15 +467,15 @@ class Tool(Component):
if apply_fn is None:
raise RuntimeError(f"apply not defined in {self}. Did you forget to implement it?")
return apply_fn
@classmethod
def can_edit(cls) -> bool:
"""
Returns whether this tool can perform editing operations on code.
:return: True if the tool can edit code, False otherwise
"""
return issubclass(cls, EditingTool)
return issubclass(cls, ToolMarkerCanEdit)
@classmethod
def get_tool_description(cls) -> str:
@@ -533,13 +539,9 @@ class Tool(Component):
f"Error: Tool '{self.get_name()}' is disabled for the active project ('{self.project_config.project_name}'); "
f"active tools: {self.agent.get_active_tool_names()}"
)
# check if the project is in read-only mode and this is an editing tool
if (
self.agent.project_config is not None
and self.agent.project_config.read_only
and self.__class__.can_edit()
):
if self.agent.project_config is not None and self.agent.project_config.read_only and self.__class__.can_edit():
return (
f"Error: Tool '{self.get_name()}' cannot be used because the project '{self.project_config.project_name}' "
f"is in read-only mode. Editing operations are not allowed."
@@ -561,11 +563,6 @@ class Tool(Component):
return result
class ToolMarkerDoesNotRequireActiveProject:
pass
class RestartLanguageServerTool(Tool):
"""Restarts the language server, may be necessary when edits not through Serena happen."""
@@ -613,7 +610,7 @@ class ReadFileTool(Tool):
return self._limit_length(result, max_answer_chars)
class CreateTextFileTool(Tool, EditingTool):
class CreateTextFileTool(Tool, ToolMarkerCanEdit):
"""
Creates/overwrites a file in the project directory.
"""
@@ -865,7 +862,7 @@ class FindReferencingCodeSnippetsTool(Tool):
return self._limit_length(result_json_str, max_answer_chars)
class ReplaceSymbolBodyTool(Tool, EditingTool):
class ReplaceSymbolBodyTool(Tool, ToolMarkerCanEdit):
"""
Replaces the full definition of a symbol.
"""
@@ -894,7 +891,7 @@ class ReplaceSymbolBodyTool(Tool, EditingTool):
return SUCCESS_RESULT
class InsertAfterSymbolTool(Tool, EditingTool):
class InsertAfterSymbolTool(Tool, ToolMarkerCanEdit):
"""
Inserts content after the end of the definition of a given symbol.
"""
@@ -923,7 +920,7 @@ class InsertAfterSymbolTool(Tool, EditingTool):
return SUCCESS_RESULT
class InsertBeforeSymbolTool(Tool, EditingTool):
class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit):
"""
Inserts content before the beginning of the definition of a given symbol.
"""
@@ -952,7 +949,7 @@ class InsertBeforeSymbolTool(Tool, EditingTool):
return SUCCESS_RESULT
class DeleteLinesTool(Tool, EditingTool):
class DeleteLinesTool(Tool, ToolMarkerCanEdit):
"""
Deletes a range of lines within a file.
"""
@@ -979,7 +976,7 @@ class DeleteLinesTool(Tool, EditingTool):
return SUCCESS_RESULT
class ReplaceLinesTool(Tool, EditingTool):
class ReplaceLinesTool(Tool, ToolMarkerCanEdit):
"""
Replaces a range of lines within a file with new content.
"""
@@ -1010,7 +1007,7 @@ class ReplaceLinesTool(Tool, EditingTool):
return SUCCESS_RESULT
class InsertAtLineTool(Tool, EditingTool):
class InsertAtLineTool(Tool, ToolMarkerCanEdit):
"""
Inserts content at a given line in a file.
"""
@@ -1282,7 +1279,7 @@ class SearchForPatternTool(Tool):
return self._limit_length(result, max_answer_chars)
class ExecuteShellCommandTool(Tool, EditingTool):
class ExecuteShellCommandTool(Tool, ToolMarkerCanEdit):
"""
Executes a shell command.
"""
-20
View File
@@ -1,20 +0,0 @@
from typing import Type
class EditingTool:
"""
A marker class for all tools that perform editing operations on code.
This class is used to identify tools that modify code in the project.
Tools that inherit from this class will be excluded when the project
is in read-only mode.
"""
@classmethod
def can_edit(cls) -> bool:
"""
Returns True as editing tools are designed to modify code.
This method can be overridden by subclasses if needed.
:return: True if the tool can edit code, False otherwise
"""
return True
+3 -34
View File
@@ -1,44 +1,13 @@
import pytest
from serena.agent import Tool, CreateTextFileTool, ReadFileTool, ProjectConfig
from serena.editingtool import EditingTool
from serena.agent import CreateTextFileTool, ReadFileTool, Tool
class TestEditMarker:
def test_edit_marker_class(self):
"""Test that EditingTool marker class exists"""
assert EditingTool.__doc__ is not None
assert hasattr(EditingTool, 'can_edit')
assert EditingTool.can_edit() is True
def test_tool_can_edit_method(self):
"""Test that Tool.can_edit() method works correctly"""
# Non-editing tool should return False
assert issubclass(ReadFileTool, Tool)
assert not issubclass(ReadFileTool, EditingTool)
assert not ReadFileTool.can_edit()
# Editing tool should return True
assert issubclass(CreateTextFileTool, Tool)
assert issubclass(CreateTextFileTool, EditingTool)
assert CreateTextFileTool.can_edit()
def test_project_config_read_only(self):
"""Test that ProjectConfig has read_only property"""
config = ProjectConfig({
"language": "python",
"project_root": "/tmp",
"ignore_all_files_in_gitignore": True,
"read_only": True
}, "test_project")
assert hasattr(config, 'read_only')
assert config.read_only is True
config = ProjectConfig({
"language": "python",
"project_root": "/tmp",
"ignore_all_files_in_gitignore": True,
# read_only not specified
}, "test_project")
assert hasattr(config, 'read_only')
assert config.read_only is False # Default should be False