diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 0000000..b231667 --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,25 @@ +# Codespell configuration is within pyproject.toml +--- +name: Codespell + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + codespell: + name: Check for spelling errors + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Annotate locations with typos + uses: codespell-project/codespell-problem-matcher@v1 + - name: Codespell + uses: codespell-project/actions-codespell@v2 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..7e15e4c --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,68 @@ +name: Build and Push Docker Images + +on: + push: + branches: [ main ] + tags: [ 'v*' ] + pull_request: + branches: [ main ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push production image + uses: docker/build-push-action@v5 + with: + context: . + target: production + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build and push development image + uses: docker/build-push-action@v5 + with: + context: . + target: development + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 6d28afc..64dd29f 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -55,11 +55,4 @@ jobs: run: uv pip install -e ".[dev]" - name: Test with pytest shell: bash - # Currently, java and rust seem to cause problems in CI, and snapshot tests seem to cause problems on Windows - # probably due to the way the language servers are started and stopped - run: | - if [[ "$RUNNER_OS" == "Windows" ]]; then - uv run pytest -m "not java and not rust and not snapshot" - else - uv run pytest -m "not java and not rust" - fi + run: uv run poe test \ No newline at end of file diff --git a/.gitignore b/.gitignore index fb78967..03279f6 100644 --- a/.gitignore +++ b/.gitignore @@ -203,6 +203,7 @@ pylint.html # dynamic LS installations /src/multilspy/language_servers/*/static +/src/solidlsp/language_servers/*/static # clojure-lsp temporary files .calva/ diff --git a/.serena/memories/current_solid_lsp_architecture.md b/.serena/memories/current_solid_lsp_architecture.md new file mode 100644 index 0000000..90a60ad --- /dev/null +++ b/.serena/memories/current_solid_lsp_architecture.md @@ -0,0 +1,123 @@ +# Current Architecture: Solid-LSP Implementation + +## Overview + +Solid-LSP (`src/solidlsp/`) is Serena's current language server integration layer, designed as a **simplified, deadlock-resistant** replacement for the problematic multilspy architecture. It enables **single-process operation** with MCP servers while maintaining all semantic code analysis capabilities. + +## Current System Status + +- **Default Implementation**: Solid-LSP is the only language server implementation (multilspy completely removed) +- **Process Isolation**: **Disabled by default** (`USE_PROCESS_ISOLATION = False`) +- **MCP Integration**: Runs safely in the same process as MCP server +- **Performance**: Lower latency and resource usage than process-isolated multilspy + +## Core Architecture + +### Primary Components + +#### 1. **`SolidLanguageServer`** (`src/solidlsp/ls.py`) +- **1,634 lines**: Main language server interface +- **Enhanced Methods**: Same semantic capabilities as multilspy but with improved async handling +- **Lifecycle Control**: `start()`, `stop()`, `is_running()`, `language_server()` property +- **Clean Async Patterns**: No coroutine leakage between async contexts + +#### 2. **`SolidLanguageServerHandler`** (`src/solidlsp/ls_handler.py`) +- **512 lines**: Simplified LSP protocol handler +- **Direct Process Management**: More straightforward than multilspy's complex orchestration +- **Resource Management**: Better cleanup and process termination + +#### 3. **Protocol Layer** (`src/solidlsp/lsp_protocol_handler/`) +- **Simplified Protocol**: Direct LSP communication without excessive abstraction +- **Types and Constants**: LSP type definitions and protocol constants +- **Request Handling**: Streamlined request/response pattern + +### Language Server Support + +Solid-LSP maintains **identical language support** to multilspy: + +#### Primary Languages (Directly Supported) +- **Python**: Pyright Language Server (`src/solidlsp/language_servers/pyright_language_server/`) +- **Java**: Eclipse JDTLS (`src/solidlsp/language_servers/eclipse_jdtls/`) +- **TypeScript/JavaScript**: TypeScript Language Server (`src/solidlsp/language_servers/typescript_language_server/`) + +#### Additional Languages (Full Support) +- **C#**: OmniSharp (`src/solidlsp/language_servers/omnisharp/`) +- **Rust**: Rust-Analyzer (`src/solidlsp/language_servers/rust_analyzer/`) +- **Go**: Gopls (`src/solidlsp/language_servers/gopls/`) +- **Ruby**: Solargraph (`src/solidlsp/language_servers/solargraph/`) +- **C++**: Clangd (`src/solidlsp/language_servers/clangd_language_server/`) +- **Dart**: Dart Language Server (`src/solidlsp/language_servers/dart_language_server/`) +- **PHP**: Intelephense (`src/solidlsp/language_servers/intelephense/`) +- **Kotlin**: Kotlin Language Server (`src/solidlsp/language_servers/kotlin_language_server/`) + +## Key Architectural Improvements + +### 1. **Simplified Async Patterns** +- **Clean Boundaries**: Proper async context management prevents MCP server contamination +- **No Coroutine Leakage**: Eliminates the unawaited coroutine warnings from multilspy +- **Direct Operations**: Fewer abstraction layers where async context could be corrupted + +### 2. **Single Process Safety** +- **Safe MCP Integration**: Designed to work within MCP server process without deadlocks +- **Eliminated IPC Overhead**: Direct method calls instead of inter-process communication +- **Resource Efficiency**: Lower memory usage and faster startup times + +### 3. **Enhanced Process Control** +- **Direct Process Management**: `_start_server_process()`, `_start_server()` methods +- **Better State Management**: `_server_context` attribute for enhanced control +- **Improved Cleanup**: More reliable resource management and process termination + +## Integration Points + +### MCP Server Integration +Current implementation in `src/serena/mcp.py:590`: +```python +if not USE_PROCESS_ISOLATION: + mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) +else: + mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) +``` + +### Agent Integration +Language server creation in `src/serena/agent.py:642-689`: +```python +def create_ls_for_project(...) -> SolidLanguageServer: + # Creates LanguageServerConfig with project settings + # Returns SolidLanguageServer.create() instance +``` + +## Configuration and Settings + +### Core Configuration +- **`USE_PROCESS_ISOLATION = False`**: Process isolation disabled by default +- **Language Detection**: Automatic based on project composition +- **Timeout Settings**: Configurable language server timeouts +- **LSP Communication Tracing**: Optional debugging support + +### Project-Level Settings +- **Ignored Paths**: Respects project configuration and gitignore +- **Language Selection**: Automatic or manual language server selection +- **Timeout Configuration**: Per-project timeout settings + +## Performance Benefits + +### vs. Multilspy + Process Isolation +1. **Lower Latency**: Direct method calls instead of IPC +2. **Reduced Memory**: Single process instead of multiple processes +3. **Faster Startup**: No process creation overhead +4. **Simpler Debugging**: All components in same process with unified stack traces + +### Stability Improvements +1. **No Asyncio Deadlocks**: Clean async boundaries prevent contamination +2. **Reliable Operations**: `find_symbol` and other tools work consistently +3. **Resource Management**: Better cleanup prevents resource leaks +4. **Error Handling**: Simplified error propagation and handling + +## Current Operational State + +- **Production Ready**: Default implementation for all Serena deployments +- **Fully Tested**: All semantic tools (`find_symbol`, `replace_symbol_body`, etc.) working reliably +- **MCP Compatible**: Stable operation with Claude Desktop and other MCP clients +- **Cross-Platform**: Works on all supported operating systems + +This architecture represents the successful resolution of the multilspy asyncio contamination issues while maintaining full semantic code analysis capabilities and improving overall performance. \ No newline at end of file diff --git a/.serena/memories/language_servers.md b/.serena/memories/language_servers.md deleted file mode 100644 index f6ea4d0..0000000 --- a/.serena/memories/language_servers.md +++ /dev/null @@ -1,16 +0,0 @@ -# Available Language Servers - -Serena supports multiple programming languages through language servers. The language servers currently available in the project are: - -## Directly Supported (Out of the Box) -- **Python**: Using Pyright Language Server (previously used Jedi) -- **Java**: Using Eclipse JDTLS (Note: startup is slow, especially initial startup) -- **TypeScript/JavaScript**: Using TypeScript Language Server - -## Indirectly Supported (May Require Manual Setup) -- **Ruby**: Using Solargraph -- **Go**: Using Gopls -- **C#**: Using OmniSharp -- **Rust**: Using Rust-Analyzer - -These language servers utilize the Language Server Protocol (LSP) to provide semantic code analysis capabilities. The system can be extended to support additional languages by providing adapters for new language server implementations. \ No newline at end of file diff --git a/.serena/memories/multilspy_historical_context.md b/.serena/memories/multilspy_historical_context.md new file mode 100644 index 0000000..7f73a58 --- /dev/null +++ b/.serena/memories/multilspy_historical_context.md @@ -0,0 +1,73 @@ +# Historical Context: Multilspy Era and Migration to Solid-LSP + +## What multilspy Was + +Multilspy was Serena's original language server integration layer that provided semantic code analysis through Language Server Protocol (LSP). It was located in `src/multilspy/` and included: + +### Core Components (No Longer Present) +- **`multilspy.LanguageServer`**: Main language server interface with async methods like `request_full_symbol_tree()` +- **`LanguageServerHandler`**: Complex async task management and protocol handling +- **`lsp_protocol_handler`**: Separate protocol abstraction layer +- **Process Management**: Complex threading and async orchestration + +### Supported Languages +Multilspy supported the same languages now supported by solid-lsp: +- Python (Pyright), Java (Eclipse JDTLS), TypeScript/JavaScript, C#, Rust, Go, Ruby, C++, Dart, PHP, Kotlin + +## Why We Moved Away from Multilspy + +### 1. **Asyncio Contamination Issues** +- **Root Problem**: MCP server runs its own asyncio event loop, while multilspy created additional asyncio loops +- **Coroutine Leakage**: `multilspy.LanguageServer.request_full_symbol_tree()` coroutines leaked into MCP server context but were never awaited +- **Evidence**: MCP server logs showed: `RuntimeWarning: coroutine 'LanguageServer.request_full_symbol_tree' was never awaited` +- **Result**: Accumulated unawaited coroutines caused resource exhaustion and apparent "hangs" + +### 2. **Dual Event Loop Interference** +- **MCP Server**: Asyncio loop A for handling requests +- **SerenaAgent**: Asyncio loop B for language servers + loop C for dashboard +- **Conflict**: Multiple asyncio contexts caused contamination and deadlocks + +### 3. **Complex Architecture** +- **Over-abstraction**: Multiple layers of protocol handling increased complexity +- **Threading Issues**: Complex async task management made debugging difficult +- **Resource Overhead**: Separate protocol handler processes added overhead + +### 4. **MCP-Specific Problems** +- **Only in MCP environments**: Issues didn't occur in direct script execution +- **Mandatory Process Isolation**: Required separate processes to prevent asyncio contamination +- **Performance Penalty**: IPC overhead was necessary for stability + +## The Process Isolation Workaround + +Before solid-lsp, the **only way** to use multilspy with MCP was through process isolation: + +```python +# Old architecture (when multilspy was still present) +if USE_SOLID_LSP: + mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) +else: + # multilspy required process isolation to prevent asyncio contamination + mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) +``` + +### Why Process Isolation Was Mandatory for Multilspy +- **Complete Separation**: MCP server, SerenaAgent, and language servers in different processes +- **IPC Overhead**: All communication through inter-process communication +- **Resource Cost**: Higher memory usage and startup time +- **Debugging Complexity**: Distributed architecture made troubleshooting harder + +## Timeline of Changes + +1. **Pre-solid-lsp**: Multilspy was the only option, required process isolation for MCP +2. **Solid-lsp Introduction**: New implementation designed to eliminate asyncio issues +3. **Current State**: Multilspy completely removed, solid-lsp is the only implementation +4. **Process Isolation**: Now disabled by default (`USE_PROCESS_ISOLATION = False`) since it's no longer needed + +## Key Lessons Learned + +- **Asyncio Complexity**: Multiple event loops in the same application are extremely difficult to manage correctly +- **MCP Integration Challenges**: MCP server's async nature requires careful consideration of async boundaries +- **Simplicity Benefits**: Removing abstraction layers often improves stability and performance +- **Process Isolation Trade-offs**: While effective for isolation, the performance cost is significant when not necessary + +This migration represents a successful architectural evolution from a complex, problematic system to a simpler, more reliable one. \ No newline at end of file diff --git a/.serena/memories/process_isolation_current_status.md b/.serena/memories/process_isolation_current_status.md new file mode 100644 index 0000000..67d9490 --- /dev/null +++ b/.serena/memories/process_isolation_current_status.md @@ -0,0 +1,126 @@ +# Process Isolation: Current Status and Architecture + +## Current Status: Disabled by Default + +Process isolation is **disabled by default** in the current Serena implementation: +- **Configuration**: `USE_PROCESS_ISOLATION = False` in `src/serena/constants.py:22` +- **Reason**: No longer needed with solid-lsp architecture +- **Default Operation**: Single process mode with MCP server, agent, and language servers in same process + +## Why Process Isolation Is No Longer Needed + +### Historical Context +Process isolation was **mandatory** when using multilspy because: +1. **Asyncio Contamination**: Multilspy leaked coroutines into MCP server's event loop +2. **Event Loop Conflicts**: Multiple asyncio contexts caused deadlocks +3. **Only Solution**: Complete process separation was the only way to prevent issues + +### Current Architecture Benefits +With solid-lsp, process isolation became **unnecessary** because: +1. **Clean Async Boundaries**: No coroutine leakage between MCP server and language server contexts +2. **Single Process Safety**: Solid-lsp designed to work safely within MCP server process +3. **Performance Gains**: Direct method calls instead of expensive IPC + +## Architecture Components (Still Present) + +The process isolation infrastructure remains **available but unused** by default: + +### Core Components (`src/serena/process_isolated_agent.py`) + +#### 1. **ProcessIsolatedSerenaAgent** (lines 392-550) +- **Purpose**: Wrapper that manages isolated agent process +- **Communication**: Uses `multiprocessing.Pipe()` for bidirectional communication +- **Process Management**: Creates and manages `SerenaAgentWorker` subprocess +- **Status**: **Available but not used by default** + +#### 2. **SerenaAgentWorker** (lines 173-389) +- **Purpose**: Worker process hosting actual SerenaAgent +- **Event Loop**: Polling loop checking for requests every 500ms +- **Request Handling**: INITIALIZE, TOOL_CALL, SHUTDOWN, etc. +- **Status**: **Available but not used by default** + +#### 3. **ProcessIsolatedDashboard** (lines 133-170) +- **Purpose**: Runs web dashboard in separate process +- **Integration**: Async web server with port management +- **Status**: **Available but not used by default** + +### Global Synchronization (lines 25-28) +```python +_global_log_queue: multiprocessing.Queue = multiprocessing.Queue() +_dashboard_ready_event = multiprocessing.Event() +_dashboard_port_value = multiprocessing.Value("i", 0) +global_shutdown_event = multiprocessing.Event() +``` +**Status**: **Available but not actively used** + +## Current MCP Factory Selection + +In `src/serena/mcp.py:590`: +```python +if not USE_PROCESS_ISOLATION: + mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) +else: + mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) +``` + +### Default Flow +1. **`USE_PROCESS_ISOLATION = False`** → **`SerenaMCPFactorySingleProcess`** +2. **Single Process**: MCP server, SerenaAgent, and solid-lsp in same process +3. **Direct Communication**: No IPC overhead, direct method calls + +### Fallback Option +If `USE_PROCESS_ISOLATION = True`: +1. **`SerenaMCPFactoryWithProcessIsolation`** would be used +2. **Separate Processes**: MCP server and SerenaAgent in different processes +3. **IPC Communication**: Higher latency but complete isolation + +## Benefits of Current Single Process Architecture + +### 1. **Performance Improvements** +- **Lower Latency**: Direct method calls vs IPC communication +- **Reduced Memory**: No duplicate process memory footprints +- **Faster Startup**: No process creation and initialization overhead + +### 2. **Operational Simplicity** +- **Unified Logging**: All components log to same destination +- **Simpler Debugging**: Single process, unified stack traces +- **Resource Management**: Simpler cleanup and shutdown procedures + +### 3. **Stability Benefits** +- **No IPC Failures**: Eliminated inter-process communication failure modes +- **Consistent State**: No synchronization issues between processes +- **Reliable Shutdown**: No orphaned processes or cleanup complexity + +## When Process Isolation Might Still Be Useful + +While not needed by default, process isolation could still be beneficial for: + +### 1. **Fault Isolation** +- **Agent Crashes**: Prevent agent failures from affecting MCP server +- **Memory Protection**: Isolate memory leaks to specific processes +- **Recovery**: Restart failed components without full system restart + +### 2. **Resource Management** +- **Memory Limits**: Constrain memory usage of specific components +- **CPU Isolation**: Prevent CPU-intensive operations from blocking MCP server +- **Security**: Additional process boundaries for security-sensitive environments + +### 3. **Debugging and Development** +- **Component Isolation**: Debug specific components in isolation +- **Performance Analysis**: Measure resource usage per component +- **Development Safety**: Prevent development errors from affecting stable components + +## Configuration Management + +### Enabling Process Isolation +To re-enable process isolation: +1. **Set**: `USE_PROCESS_ISOLATION = True` in `src/serena/constants.py` +2. **Result**: Automatic fallback to `SerenaMCPFactoryWithProcessIsolation` +3. **Trade-off**: Higher resource usage but complete component isolation + +### Current Recommendation +- **Default**: Keep `USE_PROCESS_ISOLATION = False` for optimal performance +- **Special Cases**: Enable only when specific isolation requirements exist +- **Testing**: Both modes should be tested to ensure compatibility + +The current architecture successfully eliminated the need for process isolation while maintaining the capability as a fallback option for specialized use cases. \ No newline at end of file diff --git a/.serena/memories/project_structure.md b/.serena/memories/project_structure.md deleted file mode 100644 index c0e7712..0000000 --- a/.serena/memories/project_structure.md +++ /dev/null @@ -1,16 +0,0 @@ -# Project Structure - -The Serena codebase is organized as follows: - -- **src/serena/**: Main package with core functionality - - **llm/**: LLM integration - - **util/**: Utilities - - Key files: mcp.py, agno.py, agent.py -- **src/multilspy/**: Language server integration - - **language_servers/**: Implementations for different languages - - Python (pyright/jedi) - - Java (Eclipse JDTLS) - - TypeScript/JavaScript - - C#, Rust, Go, Ruby, C++ -- **test/**: Tests -- **scripts/**: Utility scripts \ No newline at end of file diff --git a/.serena/memories/serena_project_structure.md b/.serena/memories/serena_project_structure.md new file mode 100644 index 0000000..4475b0b --- /dev/null +++ b/.serena/memories/serena_project_structure.md @@ -0,0 +1,173 @@ +# Serena Project Structure Overview + +## Top-Level Organization + +Serena is organized into several key directories, each serving distinct architectural purposes: + +``` +serena/ +├── src/ # Main source code +│ ├── interprompt/ # Template and prompt management system +│ ├── serena/ # Core Serena functionality +│ └── solidlsp/ # Language server integration layer +├── tests/ # Test suites +├── docs/ # Documentation +├── prompts/ # Prompt templates and configurations +├── contexts/ # Context definitions (desktop-app, agent, ide-assistant) +├── modes/ # Mode definitions (planning, editing, interactive, one-shot) +└── pyproject.toml # Project configuration and dependencies +``` + +## Core Source Structure (`src/`) + +### 1. **Serena Core (`src/serena/`)** +The heart of the Serena system containing: + +#### **Agent and Core Logic** +- **`agent.py`**: Main `SerenaAgent` class, tool implementations, project management +- **`mcp.py`**: Model Context Protocol server implementation and MCP factories +- **`config.py`**: Configuration system (contexts, modes, registered configurations) +- **`constants.py`**: System constants including `USE_PROCESS_ISOLATION = False` + +#### **Specialized Components** +- **`dashboard.py`**: Web dashboard for monitoring and logging (`MemoryLogHandler`, `SerenaDashboardAPI`) +- **`symbol.py`**: Symbol management (`SymbolLocation`, `Symbol`, `SymbolManager`) +- **`text_utils.py`**: Text processing utilities for search and file operations +- **`gui_log_viewer.py`**: GUI log window implementation + +#### **Integration Layers** +- **`agno.py`**: Agno framework integration (`SerenaAgnoToolkit`, `SerenaAgnoAgentProvider`) +- **`process_isolated_agent.py`**: Process isolation infrastructure (available but unused by default) +- **`prompt_factory.py`**: Prompt generation and management + +#### **Utility Modules (`src/serena/util/`)** +- **`file_system.py`**: File scanning, gitignore parsing (`GitignoreParser`, `scan_directory`) +- **`git.py`**: Git operations and status checking +- **`shell.py`**: Shell command execution (`execute_shell_command`) +- **`thread.py`**: Threading utilities with timeout support +- **`inspection.py`**: Code inspection and language detection utilities + +### 2. **Solid-LSP (`src/solidlsp/`)** +Current language server integration layer: + +#### **Core Language Server Components** +- **`ls.py`**: Main `SolidLanguageServer` class (1,634 lines) +- **`ls_handler.py`**: `SolidLanguageServerHandler` for protocol management (512 lines) +- **`ls_request.py`**: Request handling (`LanguageServerRequest`) +- **`ls_types.py`**: LSP type definitions +- **`ls_utils.py`**: Utility classes (`TextUtils`, `PathUtils`, `FileUtils`, `SymbolUtils`) + +#### **Protocol Handling (`src/solidlsp/lsp_protocol_handler/`)** +- **`server.py`**: LSP protocol server implementation +- **`lsp_requests.py`**: LSP request and notification classes +- **`lsp_types.py`**: Complete LSP type system definitions +- **`lsp_constants.py`**: LSP protocol constants + +#### **Language Server Implementations (`src/solidlsp/language_servers/`)** +Each language has its own subdirectory with specific implementations: +- **`pyright_language_server/`**: Python support via Pyright +- **`eclipse_jdtls/`**: Java support via Eclipse JDTLS +- **`typescript_language_server/`**: TypeScript/JavaScript support +- **`omnisharp/`**: C# support via OmniSharp +- **`rust_analyzer/`**: Rust support via Rust-Analyzer +- **`gopls/`**: Go support via Gopls +- **`solargraph/`**: Ruby support via Solargraph +- **`clangd_language_server/`**: C++ support via Clangd +- **`dart_language_server/`**: Dart support +- **`intelephense/`**: PHP support via Intelephense +- **`kotlin_language_server/`**: Kotlin support + +### 3. **Interprompt (`src/interprompt/`)** +Template and prompt management system: + +- **`multilang_prompt.py`**: Multi-language prompt templates (`MultiLangPromptTemplate`) +- **`jinja_template.py`**: Jinja2 template integration (`JinjaTemplate`) +- **`prompt_factory.py`**: Prompt factory base classes +- **`util/class_decorators.py`**: Utility decorators like `@singleton` + +## Configuration and Templates + +### **Prompt Templates (`prompts/`)** +Organized by context and functionality: +- **Context-specific prompts**: Different prompt sets for different execution contexts +- **Tool-specific prompts**: Specialized prompts for specific tools and operations +- **Multi-language support**: Prompts available in multiple languages when applicable + +### **Contexts (`contexts/`)** +Execution environment definitions: +- **`desktop-app.yml`**: Desktop application context +- **`agent.yml`**: Agent-specific context +- **`ide-assistant.yml`**: IDE assistant context +- **Custom contexts**: Support for user-defined contexts + +### **Modes (`modes/`)** +Behavior mode definitions: +- **`planning.yml`**: Planning mode behavior +- **`editing.yml`**: Editing mode behavior +- **`interactive.yml`**: Interactive mode behavior +- **`one-shot.yml`**: One-shot execution mode + +## Key Architectural Patterns + +### 1. **Four-Layer Configuration Hierarchy** +1. **Global**: `serena_config.yml` +2. **CLI Arguments**: Runtime overrides +3. **Project**: `.serena/project.yml` +4. **Active Modes**: Runtime behavior modification + +### 2. **Tool System Architecture** +- **Base Classes**: `ToolInterface`, `Tool` in `agent.py` +- **Marker Interfaces**: `ToolMarkerCanEdit`, `ToolMarkerDoesNotRequireActiveProject` +- **Tool Registry**: `ToolRegistry` with automatic tool discovery via `_iter_tool_classes()` +- **Tool Categories**: Semantic tools, file operations, project management, meta-tools + +### 3. **Integration Patterns** +- **MCP Server**: Primary integration via `SerenaMCPFactory` classes +- **Agno Agent**: Model-agnostic integration via `SerenaAgnoToolkit` +- **Process Isolation**: Optional isolation via `ProcessIsolatedSerenaAgent` + +### 4. **Memory Management** +- **Project Memories**: `.serena/memories/` directory for project-specific information +- **Memory Tools**: `WriteMemoryTool`, `ReadMemoryTool`, `ListMemoriesTool` +- **Memory Managers**: `MemoriesManager`, `MemoriesManagerMDFilesInProject` + +## Dependencies and Build System + +### **Development Tools** +- **`uv`**: Dependency management and virtual environment +- **`poe`**: Task orchestration (defined in `pyproject.toml`) +- **Essential Commands**: `uv run poe lint`, `uv run poe format`, `uv run poe type-check`, `uv run poe test` + +### **Key Dependencies** +- **Language Server Protocol**: LSP implementations for each supported language +- **MCP (Model Context Protocol)**: For integration with Claude Desktop and other MCP clients +- **Agno Framework**: For model-agnostic agent implementations +- **AsyncIO**: For concurrent operations (carefully managed to avoid contamination) + +## Project State Management + +### **Project Configuration** +- **Global Config**: `serena_config.yml` in user home directory +- **Project Config**: `.serena/project.yml` in project root +- **Auto-generation**: Automatic creation of default configurations when missing + +### **Language Detection** +- **Automatic**: Based on file composition analysis +- **Manual Override**: Via project configuration +- **Multi-language**: Support for projects with multiple languages + +## Runtime Architecture + +### **Default Operation Mode** +- **Single Process**: MCP server, agent, and language servers in same process +- **Direct Communication**: No IPC overhead +- **Solid-LSP**: Only language server implementation +- **Process Isolation**: Disabled by default (`USE_PROCESS_ISOLATION = False`) + +### **Semantic Tool Integration** +- **Symbol-based Operations**: `find_symbol`, `replace_symbol_body`, `insert_after_symbol` +- **Regex-based Operations**: `replace_regex` for fine-grained edits +- **File Operations**: `read_file`, `create_text_file`, `list_dir` +- **Project Operations**: `search_for_pattern`, `get_symbols_overview` + +This structure reflects Serena's evolution from a complex, multi-process system to a streamlined, single-process architecture that maintains all semantic capabilities while improving performance and reliability. \ No newline at end of file diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md index 920b4d0..296c37b 100644 --- a/.serena/memories/suggested_commands.md +++ b/.serena/memories/suggested_commands.md @@ -4,10 +4,24 @@ The following tasks should generally be executed using `uv run poe `. -- `lint`: This is the **only** allowed command for linting. Run as `uv run poe lint`. - `format`: This is the **only** allowed command for formatting. Run as `uv run poe format`. - `type-check`: This is the **only** allowed command for type checking. Run as `uv run poe type-check`. -- `test`: This is the preferred command for running tests (`uv run poe test [args]`). However, running tests directly with `uv run pytest [args]` is also permitted. +- `test`: This is the preferred command for running tests (`uv run poe test [args]`). You can select subsets of tests with markers, + the current markers are + ```toml + markers = [ + "python: language server running for Python", + "go: language server running for Go", + "java: language server running for Java", + "rust: language server running for Rust", + "typescript: language server running for TypeScript", + "php: language server running for PHP", + "snapshot: snapshot tests for symbolic editing operations", + "isolated_process: test runs with process isolated agent", + ] + ``` + By default, `uv run poe test` uses the markers set in the env var `PYTEST_MARKERS`, or, if it unset, uses `-m "not java and not rust and not isolated process"`. + You can override this behavior by simply passing the `-m` option to `uv run poe test`, e.g. `uv run poe test -m "python or go"`. For finishing a task, make sure format, type-check and test pass! Run them at the end of the task and if needed fix any issues that come up and run them again until they pass. \ No newline at end of file diff --git a/.serena/memories/supported_language_servers.md b/.serena/memories/supported_language_servers.md new file mode 100644 index 0000000..e145528 --- /dev/null +++ b/.serena/memories/supported_language_servers.md @@ -0,0 +1,97 @@ +# Supported Language Servers in Solid-LSP + +Serena currently supports multiple programming languages through language servers via the solid-lsp architecture located in `src/solidlsp/language_servers/`. + +## Primary Languages (Directly Supported) + +### Python +- **Language Server**: Pyright Language Server +- **Location**: `src/solidlsp/language_servers/pyright_language_server/` +- **Features**: Full semantic analysis, type checking, symbol navigation +- **Previously**: Also supported Jedi (now deprecated in favor of Pyright) + +### Java +- **Language Server**: Eclipse JDTLS (Java Development Tools Language Server) +- **Location**: `src/solidlsp/language_servers/eclipse_jdtls/` +- **Features**: Full Java language support, Maven/Gradle integration +- **Note**: Startup can be slow, especially on initial launch + +### TypeScript/JavaScript +- **Language Server**: TypeScript Language Server +- **Location**: `src/solidlsp/language_servers/typescript_language_server/` +- **Features**: TypeScript and JavaScript support, type checking, IntelliSense + +## Additional Supported Languages + +### C# +- **Language Server**: OmniSharp +- **Location**: `src/solidlsp/language_servers/omnisharp/` +- **Features**: Full C# language support, .NET integration + +### Rust +- **Language Server**: Rust-Analyzer +- **Location**: `src/solidlsp/language_servers/rust_analyzer/` +- **Features**: Rust language support, cargo integration + +### Go +- **Language Server**: Gopls +- **Location**: `src/solidlsp/language_servers/gopls/` +- **Features**: Go language support, module system integration + +### Ruby +- **Language Server**: Solargraph +- **Location**: `src/solidlsp/language_servers/solargraph/` +- **Features**: Ruby language support, gem integration + +### C++ +- **Language Server**: Clangd +- **Location**: `src/solidlsp/language_servers/clangd_language_server/` +- **Features**: C++ language support, CMake integration + +### Dart +- **Language Server**: Dart Language Server +- **Location**: `src/solidlsp/language_servers/dart_language_server/` +- **Features**: Dart and Flutter support + +### PHP +- **Language Server**: Intelephense +- **Location**: `src/solidlsp/language_servers/intelephense/` +- **Features**: PHP language support, Composer integration + +### Kotlin +- **Language Server**: Kotlin Language Server +- **Location**: `src/solidlsp/language_servers/kotlin_language_server/` +- **Features**: Kotlin language support, Gradle integration + +## Language Server Protocol (LSP) Integration + +All language servers utilize the Language Server Protocol (LSP) to provide: +- **Semantic Analysis**: Symbol definitions, references, and relationships +- **Code Navigation**: Go-to-definition, find-references, symbol search +- **Error Detection**: Syntax and semantic error reporting +- **Code Completion**: IntelliSense-style code completion +- **Documentation**: Hover information and signature help + +## Architecture Integration + +### Solid-LSP Framework +- **Unified Interface**: All language servers use the same `SolidLanguageServer` interface +- **Configuration**: `LanguageServerConfig` handles language-specific settings +- **Lifecycle Management**: Start, stop, and restart capabilities for all servers +- **Process Management**: Direct process control for each language server + +### Project Integration +- **Automatic Detection**: Language selection based on project file composition +- **Configuration**: Project-specific language server settings via `.serena/project.yml` +- **Ignored Paths**: Respects gitignore and custom ignore patterns +- **Timeout Settings**: Configurable per-language server timeouts + +## Adding New Language Servers + +The architecture supports adding new language servers by: +1. **Creating Language Server Class**: Implement language-specific server in `src/solidlsp/language_servers/` +2. **Configuration**: Add language detection and configuration rules +3. **Integration**: Register with the solid-lsp framework +4. **Testing**: Ensure semantic tools work correctly with the new language server + +The system is designed to be extensible while maintaining consistent behavior across all supported languages. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aa710c..1f5d4b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,34 @@ # Latest -Status of the main branch. Changes prior to the next official version change will appear here. -## Highlights +Status of the `main` branch. Changes prior to the next official version change will appear here. -### This version is a major change and improvement of Serena +* **Reduce the use of asyncio to a minimum**, improving stability and reducing the need for workarounds + * Switch to newly developed fully synchronous LSP library `solidlsp` (derived from `multilspy`), + removing our fork of `multilspy` (src/multilspy) + * Switch from fastapi (which uses asyncio) to Flask in the Serena dashboard + * The MCP server is the only asyncio-based component now, which resolves cross-component loop contamination, + such that process isolation is no longer required. + Neither are non-graceful shutdowns on Windows. +* Better default and description for restricting the search in `search_for_pattern` +* **Improved editing tools**: The editing logic was simplified and improved, making it more robust. + * The "minimal indentation" logic was removed, because LLMs did not understand it. + * The logic for the insertion of empty lines was improved (mostly controlled by the LLM now) +* Add a task queue for the agent, which is executed in a separate and thread and + * allows the language server to be initialized in the background, making the MCP server respond to requests + immediately upon startup, + * ensures that all tool executions are fully synchronized (executed linearly). + +Fixes: +* Fix `ExecuteShellCommandTool` and `GetCurrentConfigTool` hanging on Windows +* Fix project activation by name via `--project` not working (was broken in previous release) +* Improve handling of indentation and newlines in symbolic editing tools +* Fix `InsertAfterSymbolTool` failing for insertions at the end of a file that did not end with a newline +* Fix `InsertBeforeSymbolTool` inserting in the wrong place in the absence of empty lines above the reference symbol +* Fix `ReplaceSymbolBodyTool` changing whitespace before/after the symbol +* Fix repository indexing not following links and catch exceptions during indexing, allowing indexing + to continue even if unexpected errors occur for individual files. + +# 2025-06-20 * **Overhaul and major improvement of editing tools!** This represents a very important change in Serena. Symbols can now be addressed by their `name_path` (including nested ones) @@ -14,24 +39,23 @@ Status of the main branch. Changes prior to the next official version change wil create `project.yaml` for each project. Project activation is now always available. Any project can now be activated by just asking the LLM to do so and passing the path to a repo. * Dashboard as web app and possibility to shut down Serena from it (or the old log GUI). +* Possibility to index your project beforehand, accelerating Serena's tools. * Initial prompt for project supported (has to be added manually for the moment) * Massive performance improvement of pattern search tool +* Use **process isolation** to fix stability issues and deadlocks (see #170). + This uses separate process for the MCP server, the Serena agent and the dashboard in order to fix asyncio-related issues. # 2025-05-24 -## Highlights - -Important new feature: configurability of mode and context, allowing better integration in a variety of clients. -See corresponding section in readme - Serena can now be integrated in IDE assistants in a more productive way. - -You can now also do things like switching to one-shot planning mode, ask to plan something (which will create a memory), -then switch to interactive editing mode in the next conversation and work through the plan read from the memory. - -Also some improvements to prompts. +* Important new feature: **configurability of mode and context**, allowing better integration in a variety of clients. + See corresponding section in readme - Serena can now be integrated in IDE assistants in a more productive way. + You can now also do things like switching to one-shot planning mode, ask to plan something (which will create a memory), + then switch to interactive editing mode in the next conversation and work through the plan read from the memory. +* Some improvements to prompts. # 2025-05-21 -**Signficant improvement in symbol finding!** +**Significant improvement in symbol finding!** * Serena core: * `FindSymbolTool` now can look for symbols by specifying paths to them, not just the symbol name diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ae5217f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,66 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Essential Commands + +This project uses `uv` for dependency management and `poe` for task orchestration: + +- **Linting**: `uv run poe lint` (only allowed command for linting) +- **Formatting**: `uv run poe format` (only allowed command for formatting) +- **Type checking**: `uv run poe type-check` (only allowed command for type checking) +- **Testing**: `uv run poe test [args]` (preferred) or `uv run pytest [args]` + +**Important**: Always run `format`, `type-check` and `test` at the end of tasks to ensure code quality. Fix any issues and re-run until they pass. + +## Architecture Overview + +Serena is a powerful coding agent toolkit that turns LLMs into fully-featured agents working directly on codebases through semantic code retrieval and editing tools. + +### Core Components + +- **src/serena/**: Main package containing core functionality + - `mcp.py`: Model Context Protocol server implementation + - `agno.py`: Agno framework integration for model-agnostic agents + - `agent.py`: Core agent implementation with semantic tools + - **llm/**: LLM integration modules + - **util/**: Utility functions and helpers + +- **src/multilspy/**: Language server integration layer + - **language_servers/**: Language-specific implementations + - Python (pyright/jedi), Java (Eclipse JDTLS), TypeScript/JavaScript + - C#, Rust, Go, Ruby, C++, PHP support + - Provides semantic code analysis through Language Server Protocol (LSP) + +- **src/interprompt/**: Template and prompt management system + +### Key Design Principles + +- **Semantic Code Understanding**: Uses language servers (LSP) for symbol-level code analysis rather than text-based approaches +- **Multiple Integration Methods**: Can be used as MCP server, Agno agent, or integrated into custom frameworks +- **Language Agnostic**: Supports multiple programming languages through language server adapters +- **Context and Mode System**: Configurable behavior for different environments (desktop-app, ide-assistant, agent) + +### Integration Patterns + +- **MCP Server**: Primary integration method for Claude Desktop and other MCP clients +- **Agno Agent**: Model-agnostic agent framework for any LLM with GUI support +- **Framework Adapter**: Tools can be adapted to any agent framework (example: SerenaAgnoToolkit) + +### Configuration System + +Four-layer configuration hierarchy: +1. `serena_config.yml` - Global settings +2. CLI arguments - Client-specific overrides +3. `.serena/project.yml` - Project-specific settings +4. Active modes - Runtime behavior modification + +The codebase implements sophisticated semantic code operations through language servers, enabling precise symbol-level editing and code understanding that goes beyond simple text manipulation. + +## Working on Serena's Code + +**Important**: When working on this codebase, remember that you are using Serena's own tools to improve Serena itself. This means: + +- You can use Serena's semantic tools (`find_symbol`, `replace_symbol_body`, `search_for_pattern`, etc.) to analyze and edit Serena's own source code +- When asked to "perform a task on Serena", you're being asked to modify/improve the current Serena codebase +- You have access to the full power of Serena's semantic code understanding to work on Serena's code \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54e67a4..3e75556 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,21 +55,18 @@ An example script for running tools is provided in [scripts/demo_run_tools.py](s ## Adding a New Supported Language Serena interacts with code through language servers which are included in -the `multilspy` package. It is rather easy to include a new supported language +the `solidlsp` package. It is rather easy to include a new supported language if an LSP implementation for it exists. You just need to: -1. create a new subclass of `LanguageServer` +1. create a new subclass of `SolidLanguageServer` 2. add a new value to the `Language` enum -3. make a new `elif` case in the `LanguageServer.create` method -4. write minor tests +3. make a new `elif` case in the `SolidLanguageServer.create` method +4. add a test repo of the new language to `test/resources/repos//test_repo` + and new tests in `test/solidlsp/`. Similar to existing tests for other languages +5. also add a new case to the parameterized tests in `test/serena/test_serena_agent` The subclasses are typically easy to write, have a look at the [PyrightLanguageServer](src/multilspy/language_servers/pyright_language_server/pyright_server.py) for an example, or at any other implementation to see how non-python dependencies for language servers are handled there. There are also some tips from the multilspy admin [here](https://github.com/microsoft/multilspy/issues/5). - -⚠️ Important: The LSP allows for lot of optional fields and symbols, so the language servers may differ -in some details, even if they follow the LSP. Therefore you should include some code of the new -language in `test/resources` and add tests for symbolic read operations on that code. Have a look -at `test/multilspy/python/test_symbol_retrieval.py` for an example of such tests for the python LS. diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..5ee8829 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,159 @@ +# Docker Setup for Serena (Experimental) + +⚠️ **EXPERIMENTAL FEATURE**: The Docker setup for Serena is currently experimental and has several limitations. Please read this entire document before using Docker with Serena. + +## Overview + +Docker support allows you to run Serena in an isolated container environment, which provides better security isolation for the shell tool and consistent dependencies across different systems. + +## Benefits + +- **Safer shell tool execution**: Commands run in an isolated container environment +- **Consistent dependencies**: No need to manage language servers and dependencies on your host system +- **Cross-platform support**: Works consistently across Windows, macOS, and Linux + +## Important Limitations and Caveats + +### 1. Configuration File Conflicts + +⚠️ **Critical**: Docker uses a separate configuration file (`serena_config.docker.yml`) to avoid path conflicts. When running in Docker: +- Container paths will be stored in the configuration (e.g., `/workspaces/serena/...`) +- These paths are incompatible with non-Docker usage +- After using Docker, you cannot directly switch back to non-Docker usage without manual configuration adjustment + +### 2. Project Activation Limitations + +- **Only mounted directories work**: Projects must be mounted as volumes to be accessible +- Projects outside the mounted directories cannot be activated or accessed +- Default setup only mounts the current directory + +### 3. GUI Window Disabled + +- The GUI log window option is automatically disabled in Docker environments +- Use the web dashboard instead (see below) + +### 4. Dashboard Port Configuration + +The web dashboard runs on port 24282 (0x5EDA) by default. You can configure this using environment variables: + +```bash +# Use default ports +docker-compose up serena + +# Use custom ports +SERENA_DASHBOARD_PORT=8080 docker-compose up serena +``` + +⚠️ **Note**: If the local port is occupied, you'll need to specify a different port using the environment variable. + +### 5. Line Ending Issues on Windows + +⚠️ **Windows Users**: Be aware of potential line ending inconsistencies: +- Files edited within the Docker container may use Unix line endings (LF) +- Your Windows system may expect Windows line endings (CRLF) +- This can cause issues with version control and text editors +- Configure your Git settings appropriately: `git config core.autocrlf true` + +## Quick Start + +### Using Docker Compose (Recommended) + +1. **Production mode** (for using Serena as MCP server): + ```bash + docker-compose up serena + ``` + +2. **Development mode** (with source code mounted): + ```bash + docker-compose up serena-dev + ``` + +### Using Docker directly + +```bash +# Build the image +docker build -t serena . + +# Run with current directory mounted +docker run -it --rm \ + -v "$(pwd)":/workspace \ + -p 9121:9121 \ + -p 24282:24282 \ + -e SERENA_DOCKER=1 \ + serena +``` + +## Accessing the Dashboard + +Once running, access the web dashboard at: +- Default: http://localhost:24282/dashboard +- Custom port: http://localhost:${SERENA_DASHBOARD_PORT}/dashboard + +## Volume Mounting + +To work with projects, you must mount them as volumes: + +```yaml +# In compose.yaml +volumes: + - ./my-project:/workspace/my-project + - /path/to/another/project:/workspace/another-project +``` + +## Environment Variables + +- `SERENA_DOCKER=1`: Set automatically to indicate Docker environment +- `SERENA_PORT`: MCP server port (default: 9121) +- `SERENA_DASHBOARD_PORT`: Web dashboard port (default: 24282) + +## Troubleshooting + +### Port Already in Use + +If you see "port already in use" errors: +```bash +# Check what's using the port +lsof -i :24282 # macOS/Linux +netstat -ano | findstr :24282 # Windows + +# Use a different port +SERENA_DASHBOARD_PORT=8080 docker-compose up serena +``` + +### Configuration Issues + +If you need to reset Docker configuration: +```bash +# Remove Docker-specific config +rm serena_config.docker.yml + +# Serena will auto-generate a new one on next run +``` + +### Project Access Issues + +Ensure projects are properly mounted: +- Check volume mounts in `docker-compose.yaml` +- Use absolute paths for external projects +- Verify permissions on mounted directories + +## Migration Path + +To switch between Docker and non-Docker usage: + +1. **Docker to Non-Docker**: + - Manually edit project paths in `serena_config.yml` + - Change container paths to host paths + - Or use separate config files for each environment + +2. **Non-Docker to Docker**: + - Projects will be re-registered with container paths + - Original config remains unchanged + +## Future Improvements + +We're working on: +- Automatic config migration between environments +- Better project path handling +- Dynamic port allocation +- Windows line-ending handling. diff --git a/Dockerfile b/Dockerfile index 4521867..d88d4e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# Use the official Python image for the base image. -FROM python:3.11-slim +# Base stage with common dependencies +FROM python:3.11-slim AS base SHELL ["/bin/bash", "-c"] # Set environment variables to make Python print directly to the terminal and avoid .pyc files. @@ -21,18 +21,18 @@ RUN python3 -m pip install --no-cache-dir pipx \ # Add local bin to the path ENV PATH="${PATH}:/root/.local/bin" - # Install the latest version of uv RUN curl -LsSf https://astral.sh/uv/install.sh | sh # Set the working directory WORKDIR /workspaces/serena -# Copy required files into the image -COPY pyproject.toml /workspaces/serena/ -COPY README.md /workspaces/serena/ +# Development target +FROM base AS development +# Copy all files for development +COPY . /workspaces/serena/ -# Create virtual environment and install dependencies +# Create virtual environment and install dependencies with dev extras RUN uv venv RUN . .venv/bin/activate RUN uv pip install --all-extras -r pyproject.toml -e . @@ -41,3 +41,19 @@ ENV PATH="/workspaces/serena/.venv/bin:${PATH}" # Entrypoint to ensure environment is activated ENTRYPOINT ["/bin/bash", "-c", "source .venv/bin/activate && $0 $@"] +# Production target +FROM base AS production +# Copy only necessary files for production +COPY pyproject.toml /workspaces/serena/ +COPY README.md /workspaces/serena/ +COPY src/ /workspaces/serena/src/ + +# Create virtual environment and install dependencies (production only) +RUN uv venv +RUN . .venv/bin/activate +RUN uv pip install -r pyproject.toml -e . +ENV PATH="/workspaces/serena/.venv/bin:${PATH}" + +# Entrypoint to ensure environment is activated +ENTRYPOINT ["/bin/bash", "-c", "source .venv/bin/activate && $0 $@"] + diff --git a/README.md b/README.md index 6d48de7..8e315b2 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,14 @@ https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 Serena provides the necessary [tools](#full-list-of-tools) for coding workflows, but an LLM is required to do the actual work, orchestrating tool use. +For example, **supercharge the performance of Claude Code** with a [one-line shell command](#claude-code). + Serena can be integrated with an LLM in several ways: * by using the **model context protocol (MCP)**. Serena provides an MCP server which integrates with - * Claude Desktop, + * Claude Code and Claude Desktop, * IDEs like VSCode, Cursor or IntelliJ, * Extensions like Cline or Roo Code - * Goose (for a nice CLI experience) * and many others, including [the ChatGPT app soon](https://x.com/OpenAIDevs/status/1904957755829481737) * by using **Agno – the model-agnostic agent framework**. Serena's Agno-based agent allows you to turn virtually any LLM into a coding agent, whether it's provided by Google, OpenAI or Anthropic (with a paid API key) @@ -67,7 +68,7 @@ With Serena, we provide * direct, out-of-the-box support for: * Python * TypeScript/Javascript - * PhP + * PHP * Go (need to install go and gopls first) * Rust * C/C++ @@ -78,8 +79,8 @@ With Serena, we provide * Kotlin (untested) * Dart (untested) - These languages are supported by the language server library [multilspy](https://github.com/microsoft/multilspy), which Serena uses under the hood. - But we did not explicitly test whether the support for these languages actually works. + These languages are supported by the language server library, but + we did not explicitly test whether the support for these languages actually works flawlessly. Further languages can, in principle, easily be supported by providing a shallow adapter for a new language server implementation. @@ -87,7 +88,7 @@ implementation. ## Table of Contents - + @@ -95,14 +96,18 @@ implementation. - [What Can I Use Serena For?](#what-can-i-use-serena-for) - [Free Coding Agents with Serena](#free-coding-agents-with-serena) - [Quick Start](#quick-start) - * [Setup](#setup) + * [Running the Serena MCP Server](#running-the-serena-mcp-server) + + [Usage](#usage) + * [Local Installation](#local-installation) + - [Using uvx](#using-uvx) + - [Using Docker (Experimental)](#using-docker-experimental) + + [SSE Mode](#sse-mode) + + [Command-Line Arguments](#command-line-arguments) * [Configuration](#configuration) - * [Project Activation](#project-activation) - * [MCP Server (Claude Desktop)](#mcp-server-claude-desktop) - + [Troubleshooting](#troubleshooting) + * [Project Activation & Indexing](#project-activation--indexing) * [Claude Code](#claude-code) + * [Claude Desktop](#claude-desktop) * [Other MCP Clients (Cline, Roo-Code, Cursor, Windsurf, etc.)](#other-mcp-clients-cline-roo-code-cursor-windsurf-etc) - * [Goose](#goose) * [Agno Agent](#agno-agent) * [Other Agent Frameworks](#other-agent-frameworks) - [Detailed Usage and Recommendations](#detailed-usage-and-recommendations) @@ -122,7 +127,7 @@ implementation. * [Running Out of Context](#running-out-of-context) * [Combining Serena with Other MCP Servers](#combining-serena-with-other-mcp-servers) * [Serena's Logs: The Dashboard and GUI Tool](#serenas-logs-the-dashboard-and-gui-tool) - * [Troubleshooting](#troubleshooting-1) + * [Troubleshooting](#troubleshooting) - [Comparison with Other Coding Agents](#comparison-with-other-coding-agents) * [Subscription-Based Coding Agents](#subscription-based-coding-agents) * [API-Based Coding Agents](#api-based-coding-agents) @@ -166,42 +171,112 @@ We thus built Serena with the prospect of being able to cancel most other subscr Serena can be used in various ways, below you will find instructions for selected integrations. -- If you just want to turn Claude into a free-to-use coding agent, we recommend using Serena through Claude Desktop. -- If you want to use Gemini or any other model and you want a GUI experience, you should use [Agno](#agno-agent). On macOS you can also use the GUI of [goose](#goose). -- If you prefer using Serena through a CLI, you can use [goose](#goose). There again almost any model is possible. +- If you just want to turn Claude into a free-to-use coding agent, we recommend using Serena through [Claude Code](#claude-code) or [Claude Desktop](#claude-desktop). +- If you want to use Gemini or any other model, and you want a GUI experience, you can use [Agno](#agno-agent) or one of the many other GUIs that support MCP servers. - If you want to use Serena integrated in your IDE, see the section on [other MCP clients](#other-mcp-clients---cline-roo-code-cursor-windsurf-etc). -### Setup - Serena is managed by `uv`, so you will need to [install it](https://docs.astral.sh/uv/getting-started/installation/)). -Then you can either +### Running the Serena MCP Server -1. Clone the repository and cd into it. +You have several options for running the MCP server, which are explained in the subsections below. + +#### Usage + +The typical usage involves the client (Claude Code, Claude Desktop, etc.) running +the MCP server as a subprocess (using stdio communication), +so the client needs to be provided with the command to run the MCP server. +(Alternatively, you can run the MCP server in SSE mode and tell your client +how to connect to it.) + +Note that no matter how you run the MCP server, Serena will, by default, start a small web-based dashboard on localhost that will display logs and allow shutting down the +MCP server (since many clients fail to clean up processes correctly). +This and other settings can be adjusted in the [configuration](#configuration) and/or by providing [command-line arguments](#command-line-arguments). + +###### Local Installation + +1. Clone the repository and change into it. + ```shell + git clone https://github.com/oraios/serena + cd serena + ``` 2. Optionally create a config file from the template and adjust it according to your preferences. ```shell cp src/serena/resources/serena_config.template.yml serena_config.yml ``` If you just want the default config, you can skip this part, and a config file will be created when you first run Serena. +3. Run the server with `uv`: + ```shell + uv run serena-mcp-server + ``` + When running from outside the serena installation directory, be sure to pass it, i.e. use + ```shell + uv run --directory /abs/path/to/serena serena-mcp-server + ``` -or use `uvx` to run Serena directly by relying on +##### Using uvx + +`uvx` can be used to run the latest version of Serena directly from the repository, without an explicit local installation. + +* Windows: + ```shell + uvx --from git+https://github.com/oraios/serena serena-mcp-server.exe + ``` +* Other operating systems: + ```shell + uvx --from git+https://github.com/oraios/serena serena-mcp-server + ``` + +##### Using Docker (Experimental) + +⚠️ Docker support is currently experimental with several limitations. Please read the [Docker documentation](DOCKER.md) for important caveats before using it. + +You can run the Serena MCP server directly via docker as follows, +assuming that the projects you want to work on are all located in `/path/to/your/projects`: ```shell -uvx --from git+https://github.com/oraios/serena serena-mcp-server.exe +docker run --rm -i --network host -v /path/to/your/projects:/workspaces/projects ghcr.io/oraios/serena:latest serena-mcp-server --transport stdio ``` -in your MCP config (delete the `.exe` on macOS or Linux). +Replace `/path/to/your/projects` with the absolute path to your projects directory. The Docker approach provides: +- Better security isolation for shell command execution +- No need to install language servers and dependencies locally +- Consistent environment across different systems -You can now add Serena to your MCP client as described below for various clients and -[activate your first project](#project-activation). +See the [Docker documentation](DOCKER.md) for detailed setup instructions, configuration options, and known limitations. + +#### SSE Mode + +ℹ️ Note that MCP servers which use stdio as a protocol are somewhat unusual as far as client/server architectures go, as the server +necessarily has to be started by the client in order for communication to take place via the server's standard input/output stream. +In other words, you do not need to start the server yourself. The client application (e.g. Claude Desktop) takes care of this and +therefore needs to be configured with a launch command. + +When using instead the SSE mode, which uses HTTP-based communication, you control the server lifecycle yourself, +i.e. you start the server and provide the client with the URL to connect to it. + +Simply provide `serena-mcp-server` with the `--transport sse` option and optionally provide the port. +For example, to run the Serena MCP server in SSE mode on port 9121 using a local installation, +you would run this command from the Serena directory, + +```shell +uv run serena-mcp-server --transport sse --port 9121 +``` + +and then configure your client to connect to `http://localhost/sse:9121`. + + +#### Command-Line Arguments + +The Serena MCP server supports a wide range of additional command-line options, including the option to run in SSE mode +and to adapt Serena to various [contexts and modes of operation](#modes-and-contexts). + +Run with parameter `--help` to get a list of available options. -> In the default configuration, Serena will start a small dashboard on localhost that will display logs and allow shutting down the -> MCP server (since many clients fail to cleanup processes, leaving zombies behind). If you don't want that, simply set `web_dashboard` to `False` -> in your `serena_config.yml` or pass ### Configuration -Serena's behavior (like available projects, active tools and prompts) is configured in four places: +Serena's behavior (active tools and prompts as well as logging configuration, etc.) is configured in four places: 1. The `serena_config.yml` for general settings that apply to all clients and projects 2. In the arguments passed to the `serena-mcp-server` in your client's config (see below), @@ -224,10 +299,13 @@ want to use Serena. You can just ask the LLM to show you the config of your session, Serena has a tool for it. -### Project Activation +### Project Activation & Indexing The recommended way is to just ask the LLM to activate a project by providing it an absolute path to, or, -in case the project was activated in the past, by it's name. The default project name is the directory name. +in case the project was activated in the past, by its name. The default project name is the directory name. + + * "Activate the project /path/to/my_project" + * "Activate the project my_project" All projects that have been activated will be automatically added to your `serena_config.yml`, and for each project, the file `.serena/project.yml` will be generated. You can adjust the latter, e.g., by changing the name @@ -237,55 +315,85 @@ same name. If you are mostly working with the same project, you can also configure to always activate a project at startup by passing `--project ` to the `serena-mcp-server` command in your client's MCP config. -### MCP Server (Claude Desktop) +ℹ️ For larger projects, we recommend that you index your project to accelerate Serena's tools; otherwise the first +tool application may be very slow. +To do so, run one of these commands the project directory or pass the path to the project as an argument: -Configure the MCP server in your client. -For [Claude Desktop](https://claude.ai/download) (available for Windows and macOS), go to File / Settings / Developer / MCP Servers / Edit Config, -which will let you open the JSON file `claude_desktop_config.json`. Add the following (with adjusted paths) to enable Serena: +* When using a local installation: + ```shell + uv run --directory /abs/path/to/serena index-project + ``` +* When using uvx: + ```shell + uvx --from git+https://github.com/oraios/serena index-project + ``` -```json -{ - "mcpServers": { - "serena": { - "command": "/abs/path/to/uv", - "args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server"] - } - } -} +### Claude Code + +Serena is a great way to make Claude Code both cheaper and more powerful! + +From your project directory, add serena with a command like this, + +```shell +claude mcp add serena -- --context ide-assistant --project $(pwd) ``` +where `` is your way of [running the Serena MCP server](#running-the-serena-mcp-server). +For example, when using `uvx`, you would run +```shell +claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena-mcp-server --context ide-assistant --project $(pwd) +``` + +ℹ️ Once in Claude Code, you should ask Claude to "Read the initial instructions" as your first prompt, such that it will receive information +on how to use Serena's tools. + + +### Claude Desktop + +For [Claude Desktop](https://claude.ai/download) (available for Windows and macOS), go to File / Settings / Developer / MCP Servers / Edit Config, +which will let you open the JSON file `claude_desktop_config.json`. +Add the `serena` MCP server configuration, using a [run command](#running-the-serena-mcp-server) depending on your setup. + +* local installation: + ```json + { + "mcpServers": { + "serena": { + "command": "/abs/path/to/uv", + "args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server"] + } + } + } + ``` +* uvx: + ```json + { + "mcpServers": { + "serena": { + "command": "/abs/path/to/uvx", + "args": ["--from", "git+https://github.com/oraios/serena", "serena-mcp-server"] + } + } + } + ``` +* docker: + ```json + { + "mcpServers": { + "serena": { + "command": "docker", + "args": ["run", "--rm", "-i", "--network", "host", "-v", "/path/to/your/projects:/workspaces/projects", "ghcr.io/oraios/serena:latest", "serena-mcp-server", "--transport", "stdio"] + } + } + } + ``` + If you are using paths containing backslashes for paths on Windows (note that you can also just use forward slashes), be sure to escape them correctly (`\\`). -That's it! Save the config and then restart Claude Desktop. You are ready for activating your first project +That's it! Save the config and then restart Claude Desktop. You are ready for activating your first project. -ℹ️ You can further customize the run command, see - -```shell -uv run serena-mcp-server --help -``` - -ℹ️ You can use Serena without cloning or configuring it explicitly by - -{ - "mcpServers": { - "serena": { - "command": "/abs/path/to/uv", - "args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server"] - } - } -} - -#### Troubleshooting - -Some client/OS/setup configurations were reported to cause issues when using Serena with the standard `stdio` protocol, where the MCP server is started by the client application. -If you experience such problems, you can start Serena in `sse` mode by running, e.g., - -```shell -uv run serena-mcp-server --transport sse --port 9121 -``` - -Then configure your client to connect to `http://localhost:9121`. +ℹ️ You can further customize the run command using additional arguments (see [above](#command-line-arguments)). Note: on Windows and macOS there are official Claude Desktop applications by Anthropic, for Linux there is an [open-source community version](https://github.com/aaddrick/claude-desktop-debian). @@ -298,35 +406,14 @@ community version](https://github.com/aaddrick/claude-desktop-debian). After restarting, you should see Serena's tools in your chat interface (notice the small hammer icon). -⚠️ Tool Names: Claude Desktop (and most MCP Clients) don't resolve the name of the server. So you shouldn't -say something like "use Serena's tools". Instead, you can instruct the LLM to use symbolic tools or to -use a particular tool by referring to its name. Moreover, in some clients, if you use multiple MCP Servers, you might get -**tool name collisions** which lead to undefined behavior. - -ℹ️ Note that MCP servers which use stdio as a protocol are somewhat unusual as far as client/server architectures go, as the server -necessarily has to be started by the client in order for communication to take place via the server's standard input/output stream. -In other words, you do not need to start the server yourself. The client application (e.g. Claude Desktop) takes care of this and -therefore needs to be configured with a launch command. In SSE transport you control the lifetime of the server yourself. - For more information on MCP servers with Claude Desktop, see [the official quick start guide](https://modelcontextprotocol.io/quickstart/user). -### Claude Code - -Serena is a great way to make Claude Code both cheaper and more powerful! We are collecting -several examples for that and have heard very positive feedback so far. Claude Code users can -add serena with - -```shell -claude mcp add serena -- /path/to/uv "run" --directory /path/to/serena serena-mcp-server --context ide-assistant -``` - - ### Other MCP Clients (Cline, Roo-Code, Cursor, Windsurf, etc.) Being an MCP Server, Serena can be included in any MCP Client. The same configuration as above, perhaps with small client-specific modifications, should work. Most of the popular existing coding assistants (IDE extensions or VSCode-like IDEs) support connections -to MCP Servers. It is ** recommended to use the `ide-assistant` context** for these integrations by adding `"--context", "ide-assistant"` to the `args` in your MCP client's configuration. Including Serena generally boosts their performance +to MCP Servers. It is **recommended to use the `ide-assistant` context** for these integrations by adding `"--context", "ide-assistant"` to the `args` in your MCP client's configuration. Including Serena generally boosts their performance by providing them tools for symbolic operations. In this case, the billing for the usage continues to be controlled by the client of your choice @@ -334,42 +421,14 @@ In this case, the billing for the usage continues to be controlled by the client e.g., for one of the following reasons: 1. You are already using a coding assistant (say Cline or Cursor) and just want to make it more powerful. -2. You are on Linux and don't want to use the [community-created Claude Desktop](https://github.com/aaddrick/claude-desktop-debian) -3. You want tighter integration of Serena into your IDE and don't mind paying for that - -### Goose - -[goose](https://github.com/block/goose) is a standalone coding agent which has an integration for MCP servers and offers a CLI (as well as a GUI on macOS). -Using goose is currently the simplest way of running Serena through a CLI-based UI with an LLM of your choice. - -Follow the instructions [here](https://block.github.io/goose/docs/getting-started/installation/) to install it. - -After that, use `goose configure` to add an extension. For adding Serena, choose the option `Command-line Extension`, name it `Serena` and add the following as command: - -``` -/abs/path/to/uv run --directory /abs/path/to/serena serena-mcp-server --project /optional/abs/path/to/project -``` - -Since Serena can do all necessary editing and command operations, you should disable the `developer` extension that goose enables by default. -For that execute - -```shell -goose configure -``` -again, choose the option `Toggle Extensions`, and make sure Serena is enabled selected while `developer` is not. - -That's it. Read through the configuration options of goose to see what you can do with it (which is a lot, like setting different levels of permissions for tool execution). - -> Goose does not seem to always properly terminate python processes for MCP servers when a session ends. -> You may want to disable the Serena GUI and/or to manually cleanup any running python processes after finishing your work -> with goose. +2. You are on Linux and don't want to use the [community-created Claude Desktop](https://github.com/aaddrick/claude-desktop-debian). +3. You want tighter integration of Serena into your IDE and don't mind paying for that. ### Agno Agent Agno is a model-agnostic agent framework that allows you to turn Serena into an agent (independent of the MCP technology) with a large number of underlying LLMs. Agno is currently -the simplest way of running Serena in a chat GUI with an LLM of your choice -(unless you are using a Mac, then you might prefer goose, which requires almost no setup). +the simplest way of running Serena in a chat GUI with an LLM of your choice. While Agno is not yet entirely stable, we chose it, because it comes with its own open-source UI, making it easy to directly use the agent using a chat interface. With Agno, Serena is turned into an agent @@ -399,7 +458,7 @@ Here's how it works (see also [Agno's documentation](https://docs.agno.com/intro 3. Copy `.env.example` to `.env` and fill in the API keys for the provider(s) you intend to use. -5. Start the agno agent app with +4. Start the agno agent app with ```shell uv run python scripts/agno_agent.py ``` @@ -653,9 +712,7 @@ platform and from client to client. We recommend always using absolute paths, as errors. The language server is running in a separate sub-process and is called with asyncio – sometimes a client may make it crash. If you have Serena's log window enabled, and it disappears, you'll know what happened. -Some clients (like goose) may not properly terminate MCP servers, -look out for hanging python processes and terminate them -manually, if needed. +Some clients may not properly terminate MCP servers, look out for hanging python processes and terminate them manually, if needed. ## Comparison with Other Coding Agents @@ -725,17 +782,16 @@ larger codebases. We built Serena on top of multiple existing open-source technologies, the most important ones being: 1. [multilspy](https://github.com/microsoft/multilspy). - A beautifully designed wrapper around language servers following the LSP. It - was not easily extendable with the symbolic - logic that Serena required, so instead of incorporating it as dependency, we - copied the source code - and adapted it to our needs. + A library which wraps language server implementations and adapts them for interaction via Python + and which provided the basis for our library Solid-LSP (src/solidlsp). + Solid-LSP provides pure synchronous LSP calls and extends the original library with the symbolic logic + that Serena required. 2. [Python MCP SDK](https://github.com/modelcontextprotocol/python-sdk) 3. [Agno](https://github.com/agno-agi/agno) and the associated [agent-ui](https://github.com/agno-agi/agent-ui), which we use to allow Serena to work with any model, beyond the ones supporting the MCP. -4. All the language servers that we use through multilspy. +4. All the language servers that we use through Solid-LSP. Without these projects, Serena would not have been possible (or would have been significantly more difficult to build). diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..bb8f683 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,32 @@ +services: + serena: + image: serena:latest + build: + context: ./ + dockerfile: Dockerfile + target: production + ports: + - "${SERENA_PORT:-9121}:9121" # MCP server port + - "${SERENA_DASHBOARD_PORT:-24282}:24282" # Dashboard port (default 0x5EDA = 24282) + environment: + - SERENA_DOCKER=1 + command: + - "uv run --directory . serena-mcp-server --transport sse --port 9121 --host 0.0.0.0" + + serena-dev: + image: serena:dev + build: + context: ./ + dockerfile: Dockerfile + target: development + tty: true + stdin_open: true + environment: + - SERENA_DOCKER=1 + volumes: + - .:/workspaces/serena + ports: + - "${SERENA_PORT:-9121}:9121" # MCP server port + - "${SERENA_DASHBOARD_PORT:-24282}:24282" # Dashboard port + command: + - "uv run --directory . serena-mcp-server" diff --git a/lessons_learned.md b/lessons_learned.md index 795c76d..b6d15a7 100644 --- a/lessons_learned.md +++ b/lessons_learned.md @@ -56,7 +56,7 @@ When developing the `ReplaceRegexTool` we were initially not able to make Claude examples nor explicit instructions helped. It was only after adding ``` -IMPORTANT: REMEMBER TO USE WILDCARDS WEHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! +IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! ``` to the initial instructions and to the tool description that Claude finally started following the instructions. @@ -70,6 +70,14 @@ many clients, including Claude Desktop, fail to properly clean up, leaving zombi We mitigate this through the GUI window and the dashboard, so the user sees whether Serena is running and can terminate it there. +### Trusting Asyncio + +Running multiple asyncio apps led to non-deterministic +event loop contamination and deadlocks, which were very hard to debug +and understand. We solved this with a large hammer, by putting all asyncio apps into a separate +process. It made the code much more complex and slightly enhanced RAM requirements, but it seems +like that was the only way to reliably overcome asyncio deadlock issues. + ### Cross-OS Tkinter GUI Different OS have different limitations when it comes to starting a window or dealing with Tkinter diff --git a/pyproject.toml b/pyproject.toml index 0c81ab9..600e018 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,17 @@ [build-system] build-backend = "hatchling.build" -requires = [ - "hatchling" -] +requires = ["hatchling"] [project] name = "serena" version = "0.1.0" description = "" -authors = [ - {name = "Oraios AI", email = "info@oraios-ai.de"} -] +authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }] readme = "README.md" requires-python = ">=3.11, <3.12" classifiers = [ "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.11" + "Programming Language :: Python :: 3.11", ] dependencies = [ "requests>=2.32.3,<3", @@ -23,8 +19,7 @@ dependencies = [ "overrides>=7.7.0,<8", "python-dotenv>=1.0.0, <2", "mcp>=1.5.0", - "fastapi>=0.115.12", - "fastmcp>=0.4.1", + "flask>=3.0.0", "sensai-utils>=1.4.0", "pydantic>=2.10.6", "types-pyyaml>=6.0.12.20241230", @@ -36,10 +31,12 @@ dependencies = [ "psutil>=7.0.0", "docstring_parser>=0.16", "joblib>=1.5.1", + "tqdm>=4.67.1", ] [project.scripts] serena-mcp-server = "serena.mcp:start_mcp_server" +index-project = "serena.agent:index_project" [project.license] text = "MIT" @@ -57,28 +54,24 @@ dev = [ "types-pyyaml>=6.0.12.20241230", "syrupy>=4.9.1", ] -agno = [ - "agno>=1.2.6", - "sqlalchemy>=2.0.40", -] -anthropic = [ - "anthropic>=0.49.0", -] -google = [ - "google-genai>=1.8.0", -] +agno = ["agno>=1.2.6", "sqlalchemy>=2.0.40"] +anthropic = ["anthropic>=0.49.0"] +google = ["google-genai>=1.8.0"] [project.urls] Homepage = "https://github.com/oraios/serena" [tool.hatch.build.targets.wheel] -packages = ["src/serena", "src/multilspy", "src/interprompt"] +packages = ["src/serena", "src/interprompt", "src/solidlsp"] [tool.black] line-length = 140 -target-version = [ - "py311" -] +target-version = ["py311"] +exclude = ''' +/( + src/solidlsp/language_servers/.*/static|src/multilspy +)/ +''' [tool.doc8] max-line-length = 1000 @@ -100,34 +93,30 @@ warn_no_return = true warn_redundant_casts = true warn_unreachable = true warn_unused_configs = true -warn_unused_ignores = true +warn_unused_ignores = false exclude = "^build/|^docs/" [tool.poe.env] PYDEVD_DISABLE_FILE_VALIDATION = "1" [tool.poe.tasks] -test = "pytest test --color=yes" -_black_check = "black --check --exclude src/multilspy/ src scripts test" -_ruff_check = "ruff check --exclude .venv/ --exclude src/multilspy/ src scripts test" -_black_format = "black --exclude .venv/|src/multilspy/ src scripts test" -_ruff_format = "ruff check --exclude src/multilspy/ --fix src scripts test" -lint = [ - "_black_check", - "_ruff_check", -] -format = [ - "_ruff_format", - "_black_format" -] +# Uses PYTEST_MARKERS env var for default markers +# For custom markers, one can either adjust the env var or just use -m option in the command line, +# as the second -m option will override the first one. +test = "pytest test -vv -m \"${PYTEST_MARKERS:-not java and not rust and not isolated_process}\"" +_black_check = "black --check src scripts test" +_ruff_check = "ruff check src scripts test" +_black_format = "black src scripts test" +_ruff_format = "ruff check --fix src scripts test" +lint = ["_black_check", "_ruff_check"] +format = ["_ruff_format", "_black_format"] _mypy = "mypy src/serena" -type-check = [ - "_mypy", -] +type-check = ["_mypy"] [tool.ruff] target-version = "py311" line-length = 140 +exclude = ["src/solidlsp/language_servers/**/static", "src/multilspy"] [tool.ruff.format] quote-style = "double" @@ -162,7 +151,7 @@ select = [ "TID", "UP", "W", - "YTT" + "YTT", ] ignore = [ "RUF002", @@ -222,33 +211,27 @@ ignore = [ "W291", "W293", "B009", - "SIM103", # forbids multiple returns - "SIM110", # requires use of any(...) instead of for-loop - "G001", # forbids str.format in log statements - "E722", # forbids unspecific except clause -] -unfixable = [ - "F841", - "F601", - "F602", - "B018" -] -extend-fixable = [ - "F401", - "B905", - "W291" + "SIM103", # forbids multiple returns + "SIM110", # requires use of any(...) instead of for-loop + "G001", # forbids str.format in log statements + "E722", # forbids unspecific except clause + "SIM105", # forbids empty/general except clause + "SIM113", # wants to enforce use of enumerate + "E712", # forbids equality comparison with True/False + "UP007", # forbids some uses of Union + "TID252", # forbids relative imports + "B904", # forces use of raise from other_exception + "RUF012", # forbids mutable attributes as ClassVar ] +unfixable = ["F841", "F601", "F602", "B018"] +extend-fixable = ["F401", "B905", "W291"] [tool.ruff.lint.mccabe] max-complexity = 20 [tool.ruff.lint.per-file-ignores] -"tests/**" = [ - "D103" -] -"scripts/**" = [ - "D103" -] +"tests/**" = ["D103"] +"scripts/**" = ["D103"] [tool.pytest.ini_options] markers = [ @@ -259,5 +242,13 @@ markers = [ "rust: language server running for Rust", "typescript: language server running for TypeScript", "php: language server running for PHP", - "snapshot: snapshot tests", + "snapshot: snapshot tests for symbolic editing operations", + "isolated_process: test runs with process isolated agent", ] + +[tool.codespell] +# Ref: https://github.com/codespell-project/codespell#using-a-config-file +skip = '.git*,*.svg,*.lock,*.min.*' +check-hidden = true +# ignore-regex = '' +# ignore-words-list = '' diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..22e867b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --snapshot-patch-pycharm-diff diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index 23726f1..8822db7 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -20,10 +20,10 @@ class InMemorySerenaConfig(SerenaConfigBase): if __name__ == "__main__": - # project_path = str(Path("test") / "resources" / "repos" / "python" / "test_repo") agent = SerenaAgent(project=REPO_ROOT) # apply a tool find_refs_tool = agent.get_tool(FindReferencingSymbolsTool) print("Finding the symbol 'SyncLanguageServer'\n") - pprint(json.loads(find_refs_tool.apply(name_path="SyncLanguageServer", relative_path="src/multilspy/language_server.py"))) + result = agent.execute_task(lambda: find_refs_tool.apply(name_path="SolidLanguageServer", relative_path="src/solidlsp/ls.py")) + pprint(json.loads(result)) diff --git a/src/README.md b/src/README.md index fc5a9c9..4198627 100644 --- a/src/README.md +++ b/src/README.md @@ -1,4 +1,4 @@ Serena uses (modified) versions of other libraries/packages: - * [multilspy](https://github.com/oraios/multilspy) (for language server protocol support); original repo: microsoft/multilspy + * solidlsp (our fork of [microsoft/multilspy](https://github.com/microsoft/multilspy) for fully synchronous language server communication) * [interprompt](https://github.com/oraios/interprompt) (our prompt templating library) diff --git a/src/multilspy/.syncCommitId.remote b/src/multilspy/.syncCommitId.remote deleted file mode 100644 index 20a711e..0000000 --- a/src/multilspy/.syncCommitId.remote +++ /dev/null @@ -1 +0,0 @@ -606542ed8766bfbcc6490b3115eb0aea78c693e5 \ No newline at end of file diff --git a/src/multilspy/.syncCommitId.this b/src/multilspy/.syncCommitId.this deleted file mode 100644 index 8467819..0000000 --- a/src/multilspy/.syncCommitId.this +++ /dev/null @@ -1 +0,0 @@ -2f5651bd7846ebb6abde121223233bf975ce98ee \ No newline at end of file diff --git a/src/multilspy/__init__.py b/src/multilspy/__init__.py deleted file mode 100644 index a8229e9..0000000 --- a/src/multilspy/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -This module contains the multilspy API -""" - -from . import multilspy_types as Types -from .language_server import LanguageServer, SyncLanguageServer - -__all__ = ["LanguageServer", "Types", "SyncLanguageServer"] diff --git a/src/multilspy/type_helpers.py b/src/multilspy/type_helpers.py deleted file mode 100644 index 0a805e4..0000000 --- a/src/multilspy/type_helpers.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -This module provides type-helpers used across multilspy implementation -""" - -import inspect - -from typing import Callable, TypeVar, Type - -R = TypeVar("R", bound=object) - -def ensure_all_methods_implemented( - source_cls: Type[object], -) -> Callable[[Type[R]], Type[R]]: - """ - A decorator to ensure that all methods of source_cls class are implemented in the decorated class. - """ - - def check_all_methods_implemented(target_cls: R) -> R: - for name, _ in inspect.getmembers(source_cls, inspect.isfunction): - if name.startswith("_"): - continue - if name not in target_cls.__dict__ or not callable(target_cls.__dict__[name]): - raise NotImplementedError(f"{name} is not implemented in {target_cls}") - - return target_cls - - return check_all_methods_implemented \ No newline at end of file diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 1a6b238..1643d83 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,20 +1,23 @@ -__version__ = "2025-05-21" +__version__ = "2025-06-21" + +import logging + +log = logging.getLogger(__name__) def serena_version() -> str: """ :return: the version of the package, including git status if available. """ + from serena.util.git import get_git_status + version = __version__ try: - from sensai.util.git import git_status - from sensai.util.logging import LoggingDisabledContext - - with LoggingDisabledContext(): - git_status = git_status() - version += f"-{git_status.commit[:8]}" - if not git_status.is_clean: - version += "-dirty" + git_status = get_git_status() + if git_status is not None: + version += f"-{git_status.commit[:8]}" + if not git_status.is_clean: + version += "-dirty" except: pass return version diff --git a/src/serena/agent.py b/src/serena/agent.py index cde3bc1..7da88e1 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -9,13 +9,15 @@ import platform import re import shutil import sys +import threading import traceback import webbrowser from abc import ABC, abstractmethod from collections import defaultdict from collections.abc import Callable, Generator, Iterable, Sequence +from concurrent.futures import Future, ThreadPoolExecutor from copy import copy, deepcopy -from dataclasses import dataclass, field +from dataclasses import asdict, dataclass, field from fnmatch import fnmatch from functools import cached_property from logging import Logger @@ -23,36 +25,45 @@ from pathlib import Path from types import TracebackType from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast +import click import yaml +from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata from overrides import override +from pathspec import PathSpec from ruamel.yaml.comments import CommentedMap from sensai.util import logging -from sensai.util.logging import FallbackHandler +from sensai.util.logging import FallbackHandler, LogTime from sensai.util.string import ToStringMixin, dict_string -from multilspy import SyncLanguageServer -from multilspy.multilspy_config import Language, MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_types import SymbolKind from serena import serena_version from serena.config import SerenaAgentContext, SerenaAgentMode -from serena.constants import PROJECT_TEMPLATE_FILE, REPO_ROOT, SELENA_CONFIG_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME +from serena.constants import ( + DEFAULT_ENCODING, + PROJECT_TEMPLATE_FILE, + REPO_ROOT, + SELENA_CONFIG_TEMPLATE_FILE, + SERENA_LOG_FORMAT, + SERENA_MANAGED_DIR_NAME, +) from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI from serena.prompt_factory import PromptFactory, SerenaPromptFactory from serena.symbol import SymbolManager from serena.text_utils import search_files -from serena.util.file_system import scan_directory +from serena.util.file_system import GitignoreParser, match_path, scan_directory from serena.util.general import load_yaml, save_yaml from serena.util.inspection import determine_programming_language_composition, iter_subclasses from serena.util.shell import execute_shell_command -from serena.util.thread import ExecutionResult, execute_with_timeout +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_types import SymbolKind if TYPE_CHECKING: from serena.gui_log_viewer import GuiLogViewerHandler log = logging.getLogger(__name__) -LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" TTool = TypeVar("TTool", bound="Tool") +T = TypeVar("T") SUCCESS_RESULT = "OK" DEFAULT_TOOL_TIMEOUT: float = 240 @@ -102,6 +113,19 @@ def get_serena_managed_dir(project_root: str | Path) -> str: return os.path.join(project_root, SERENA_MANAGED_DIR_NAME) +def is_running_in_docker() -> bool: + """Check if we're running inside a Docker container.""" + # Check for Docker-specific files + if os.path.exists("/.dockerenv"): + return True + # Check cgroup for docker references + try: + with open("/proc/self/cgroup") as f: + return "docker" in f.read() + except FileNotFoundError: + return False + + @dataclass class ProjectConfig(ToStringMixin): project_name: str @@ -111,7 +135,7 @@ class ProjectConfig(ToStringMixin): read_only: bool = False ignore_all_files_in_gitignore: bool = True initial_prompt: str = "" - encoding: str = "utf-8" + encoding: str = DEFAULT_ENCODING SERENA_DEFAULT_PROJECT_FILE = "project.yml" @@ -144,50 +168,70 @@ class ProjectConfig(ToStringMixin): config_with_comments["language"] = dominant_language if save_to_disk: save_yaml(str(project_root / cls.rel_path_to_project_yml()), config_with_comments, preserve_comments=True) - return cls._from_yml_data(config_with_comments) + return cls.from_json_dict(config_with_comments) @classmethod def rel_path_to_project_yml(cls) -> str: return os.path.join(SERENA_MANAGED_DIR_NAME, cls.SERENA_DEFAULT_PROJECT_FILE) @classmethod - def _from_yml_data(cls, yaml_data: dict[str, Any]) -> Self: + def from_json_dict(cls, data: dict[str, Any]) -> Self: """ Create a ProjectConfig instance from a configuration dictionary """ + language_str = data["language"].lower() + project_name = data["project_name"] + # backwards compatibility + if language_str == "javascript": + log.warning(f"Found deprecated project language `javascript` in project {project_name}, please change to `typescript`") + language_str = "typescript" try: - yaml_data["language"] = Language(yaml_data["language"].lower()) + language = Language(language_str) except ValueError as e: - raise ValueError(f"Invalid language: {yaml_data['language']}.\nValid languages are: {[l.value for l in Language]}") from e + raise ValueError(f"Invalid language: {data['language']}.\nValid languages are: {[l.value for l in Language]}") from e return cls( - project_name=yaml_data["project_name"], - language=yaml_data["language"], - ignored_paths=yaml_data.get("ignored_paths", []), - excluded_tools=set(yaml_data.get("excluded_tools", [])), - read_only=yaml_data.get("read_only", False), - ignore_all_files_in_gitignore=yaml_data.get("ignore_all_files_in_gitignore", True), - initial_prompt=yaml_data.get("initial_prompt", ""), + project_name=project_name, + language=language, + ignored_paths=data.get("ignored_paths", []), + excluded_tools=set(data.get("excluded_tools", [])), + read_only=data.get("read_only", False), + ignore_all_files_in_gitignore=data.get("ignore_all_files_in_gitignore", True), + initial_prompt=data.get("initial_prompt", ""), + encoding=data.get("encoding", DEFAULT_ENCODING), ) + def to_json_dict(self) -> dict[str, Any]: + result = asdict(self) + result["language"] = result["language"].value + result["excluded_tools"] = list(result["excluded_tools"]) + return result + @classmethod - def load(cls, project_root: Path | str) -> Self: + def load(cls, project_root: Path | str, autogenerate: bool = True) -> Self: """ Load a ProjectConfig instance from the path to the project root. """ project_root = Path(project_root) yaml_path = project_root / cls.rel_path_to_project_yml() if not yaml_path.exists(): - raise FileNotFoundError(f"Project configuration file not found: {yaml_path}") + if autogenerate: + return cls.autogenerate(project_root) + else: + raise FileNotFoundError(f"Project configuration file not found: {yaml_path}") with open(yaml_path, encoding="utf-8") as f: yaml_data = yaml.safe_load(f) if "project_name" not in yaml_data: yaml_data["project_name"] = project_root.name - return cls._from_yml_data(yaml_data) + return cls.from_json_dict(yaml_data) def get_excluded_tool_classes(self) -> set[type["Tool"]]: return set(ToolRegistry.get_tool_class_by_name(tool_name) for tool_name in self.excluded_tools) +class ProjectNotFoundError(Exception): + pass + + @dataclass class Project: project_root: str @@ -202,13 +246,20 @@ class Project: return self.project_config.language @classmethod - def load(cls, project_root: str | Path) -> Self: + def load(cls, project_root: str | Path, autogenerate: bool = True) -> Self: project_root = Path(project_root).resolve() if not project_root.exists(): raise FileNotFoundError(f"Project root not found: {project_root}") - project_config = ProjectConfig.load(project_root) + project_config = ProjectConfig.load(project_root, autogenerate=autogenerate) return cls(project_root=str(project_root), project_config=project_config) + @classmethod + def from_json_dict(cls, data: dict) -> Self: + return cls(project_root=data["project_root"], project_config=ProjectConfig.from_json_dict(data["project_config"])) + + def to_json_dict(self) -> dict: + return {"project_root": self.project_root, "project_config": self.project_config.to_json_dict()} + def path_to_project_yml(self) -> str: return os.path.join(self.project_root, self.project_config.rel_path_to_project_yml()) @@ -303,6 +354,19 @@ class SerenaConfigBase(ABC): else: raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}") + def to_json_dict(self) -> dict: + """Convert configuration to dictionary for serialization.""" + result = asdict(self) + result["projects"] = [project.to_json_dict() for project in self.projects] + return result + + @classmethod + def from_json_dict(cls, data: dict) -> Self: + """Create configuration from dictionary.""" + data = copy(data) + data["projects"] = [Project.from_json_dict(project_data) for project_data in data["projects"]] + return cls(**data) + @dataclass(kw_only=True) class SerenaConfig(SerenaConfigBase): @@ -315,6 +379,7 @@ class SerenaConfig(SerenaConfigBase): loaded_commented_yaml: CommentedMap CONFIG_FILE = "serena_config.yml" + CONFIG_FILE_DOCKER = "serena_config.docker.yml" # Docker-specific config file; auto-generated if missing, mounted via docker-compose for user customization @classmethod def autogenerate(cls) -> None: @@ -328,7 +393,20 @@ class SerenaConfig(SerenaConfigBase): @classmethod def get_config_file_path(cls) -> str: - return os.path.join(REPO_ROOT, cls.CONFIG_FILE) + config_file = cls.CONFIG_FILE_DOCKER if is_running_in_docker() else cls.CONFIG_FILE + return os.path.join(REPO_ROOT, config_file) + + @classmethod + def _load_commented_yaml(cls, config_file: str, generate_if_missing: bool = True) -> CommentedMap: + if not os.path.exists(config_file): + if not generate_if_missing: + raise FileNotFoundError(f"Serena configuration file not found: {config_file}") + log.info(f"Serena configuration file not found at {config_file}, autogenerating...") + cls.autogenerate() + try: + return load_yaml(config_file, preserve_comments=True) + except Exception as e: + raise ValueError(f"Error loading Serena configuration from {config_file}: {e}") from e @classmethod def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig": @@ -336,17 +414,8 @@ class SerenaConfig(SerenaConfigBase): Static constructor to create SerenaConfig from the configuration file """ config_file = cls.get_config_file_path() - if not os.path.exists(config_file): - if not generate_if_missing: - raise FileNotFoundError(f"Serena configuration file not found: {config_file}") - cls.autogenerate() - log.info(f"Loading Serena configuration from {config_file}") - try: - loaded_commented_yaml = load_yaml(config_file, preserve_comments=True) - except Exception as e: - raise ValueError(f"Error loading Serena configuration from {config_file}: {e}") from e - + loaded_commented_yaml = cls._load_commented_yaml(config_file, generate_if_missing) # Create instance instance = cls(loaded_commented_yaml=loaded_commented_yaml) @@ -370,10 +439,15 @@ class SerenaConfig(SerenaConfigBase): project = Project.load(path) instance.projects.append(project) - instance.gui_log_window_enabled = loaded_commented_yaml.get("gui_log_window", False) + # Force disable GUI in Docker environment + if is_running_in_docker(): + instance.gui_log_window_enabled = False + else: + instance.gui_log_window_enabled = loaded_commented_yaml.get("gui_log_window", False) instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO)) instance.web_dashboard = loaded_commented_yaml.get("web_dashboard", True) instance.tool_timeout = loaded_commented_yaml.get("tool_timeout", DEFAULT_TOOL_TIMEOUT) + instance.trace_lsp_communication = loaded_commented_yaml.get("trace_lsp_communication", False) # re-save the configuration file if any migrations were performed if num_project_migrations > 0: @@ -425,6 +499,16 @@ class SerenaConfig(SerenaConfigBase): super().remove_project(project_name) self.save() + def to_json_dict(self) -> dict: + result = super().to_json_dict() + result.pop("loaded_commented_yaml", None) + return result + + @classmethod + def from_json_dict(cls, data: dict) -> Self: + data["loaded_commented_yaml"] = cls._load_commented_yaml(cls.get_config_file_path()) + return super().from_json_dict(data) + class LinesRead: def __init__(self) -> None: @@ -493,6 +577,139 @@ class MemoriesManagerMDFilesInProject(MemoriesManager): return f"Memory {name} deleted." +def create_serena_config( + serena_config: SerenaConfigBase | None = None, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, +) -> SerenaConfig: + """ + Create a SerenaConfig instance without instantiating a full SerenaAgent. + + This function extracts the configuration creation logic from SerenaAgent.__init__ + to allow creating configurations independently for process isolation and other use cases. + + :param serena_config: the base Serena configuration or None to read from default location + :param enable_web_dashboard: Whether to enable the web dashboard + :param enable_gui_log_window: Whether to enable the GUI log window + :param log_level: Log level + :param trace_lsp_communication: Whether to trace LSP communication + :param tool_timeout: Timeout in seconds for tool execution + :return: A fully configured SerenaConfig instance + """ + # obtain serena configuration + if serena_config is not None: + # If a complete SerenaConfig is provided, use it directly + if isinstance(serena_config, SerenaConfig): + config = serena_config + else: + # For SerenaConfigBase instances (like test configs), create an in-memory SerenaConfig + # that preserves the base config attributes without loading from file + from ruamel.yaml.comments import CommentedMap + + config = SerenaConfig.__new__(SerenaConfig) # Create without calling __init__ + # Initialize basic attributes from base config + config.projects = getattr(serena_config, "projects", []) + config.gui_log_window_enabled = serena_config.gui_log_window_enabled + config.log_level = serena_config.log_level + config.trace_lsp_communication = serena_config.trace_lsp_communication + config.web_dashboard = serena_config.web_dashboard + config.tool_timeout = serena_config.tool_timeout + # Set empty yaml for in-memory config + config.loaded_commented_yaml = CommentedMap() + else: + config = SerenaConfig.from_config_file() + + # Apply parameter overrides + if enable_web_dashboard is not None: + config.web_dashboard = enable_web_dashboard + if enable_gui_log_window is not None: + config.gui_log_window_enabled = enable_gui_log_window + if log_level is not None: + log_level = cast(Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], log_level.upper()) + # transform to int + config.log_level = logging.getLevelNamesMapping()[log_level] + if trace_lsp_communication is not None: + config.trace_lsp_communication = trace_lsp_communication + if tool_timeout is not None: + config.tool_timeout = tool_timeout + + # Note: Project registration/activation is handled separately by the caller + # since it involves complex logic that may require the full agent context + + return config + + +def create_ls_for_project( + project: str | Project, + log_level: int = logging.INFO, + ls_timeout: float | None = DEFAULT_TOOL_TIMEOUT - 5, + trace_lsp_communication: bool = False, +) -> SolidLanguageServer: + """ + Create a language server for a project. Note that you will have to start it + before performing any LS operations. + + :param project: either a path to the project root or a ProjectConfig instance. + If no project.yml is found, the default project configuration will be used. + :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 + :return: the language server + """ + if isinstance(project, str): + project_instance = Project.load(project, autogenerate=True) + else: + project_instance = project + + project_config = project_instance.project_config + ignored_paths = project_config.ignored_paths + if len(ignored_paths) > 0: + log.info(f"Using {len(ignored_paths)} ignored paths from the explicit project configuration.") + log.debug(f"Ignored paths: {ignored_paths}") + if project_config.ignore_all_files_in_gitignore: + log.info(f"Parsing all gitignore files in {project_instance.project_root}") + gitignore_parser = GitignoreParser(project_instance.project_root) + log.info(f"Found {len(gitignore_parser.get_ignore_specs())} gitignore files.") + for spec in gitignore_parser.get_ignore_specs(): + log.debug(f"Adding {len(spec.patterns)} patterns from {spec.file_path} to the ignored paths.") + ignored_paths.extend(spec.patterns) + log.debug(f"Using {len(ignored_paths)} ignored paths in total.") + multilspy_config = LanguageServerConfig( + code_language=project_instance.language, + ignored_paths=ignored_paths, + trace_lsp_communication=trace_lsp_communication, + ) + ls_logger = LanguageServerLogger(log_level=log_level) + log.info(f"Creating language server instance for {project_instance.project_root}.") + return SolidLanguageServer.create( + multilspy_config, + ls_logger, + project_instance.project_root, + timeout=ls_timeout, + ) + + +@click.command() +@click.argument("project", type=click.Path(exists=True), required=False, default=os.getcwd()) +@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="WARNING") +def index_project(project: str, log_level: str = "INFO") -> None: + """ + Index a project by saving the symbols of files to Serena's language server cache. + + :param project: the project to index. By default, the current working directory is used. + """ + log_level_int = logging.getLevelNamesMapping()[log_level.upper()] + project = os.path.abspath(project) + print(f"Indexing symbols in project {project}") + ls = create_ls_for_project(project, log_level=log_level_int) + with ls.start_server(): + ls.index_repository() + print(f"Symbols saved to {ls.cache_path}") + + class SerenaAgent: def __init__( self, @@ -505,6 +722,7 @@ class SerenaAgent: enable_gui_log_window: bool | None = None, log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, ): """ :param project: the project to load immediately or None to not load any project; may be a path to the project or a name of @@ -515,23 +733,20 @@ class SerenaAgent: :param modes: list of modes in which the agent is operating (they will be combined), None for default modes. The modes may adjust prompts, tool availability, and tool descriptions. :param serena_config: the Serena configuration or None to read the configuration from the default location. - :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. - :param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. - If not specified, will take the value from the serena configuration. - :param gui_log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration. + :param enable_web_dashboard: whether to enable the web dashboard; If None, will take the value from the Serena configuration. + :param enable_gui_log_window: whether to enable the GUI log window; If None, will take the value from the Serena configuration. + :param log_level: the log level for the GUI log window; If None, will take the value from the serena configuration. + :param tool_timeout: the timeout in seconds for tool execution. If None, will take the value from the serena configuration. """ - # obtain serena configuration - self.serena_config = serena_config or SerenaConfig.from_config_file() - if enable_web_dashboard is not None: - self.serena_config.web_dashboard = enable_web_dashboard - if enable_gui_log_window is not None: - self.serena_config.gui_log_window_enabled = enable_gui_log_window - if log_level is not None: - log_level = cast(Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], log_level.upper()) - # transform to int - self.serena_config.log_level = logging.getLevelNamesMapping()[log_level] - if trace_lsp_communication is not None: - self.serena_config.trace_lsp_communication = trace_lsp_communication + # obtain serena configuration using the decoupled factory function + self.serena_config = create_serena_config( + serena_config=serena_config, + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) # adjust log level serena_log_level = self.serena_config.log_level @@ -550,13 +765,13 @@ class SerenaAgent: from serena.gui_log_viewer import GuiLogViewer, GuiLogViewerHandler self._gui_log_handler = GuiLogViewerHandler( - GuiLogViewer("dashboard", title="Serena Logs"), level=serena_log_level, format_string=LOG_FORMAT + GuiLogViewer("dashboard", title="Serena Logs"), level=serena_log_level, format_string=SERENA_LOG_FORMAT ) Logger.root.addHandler(self._gui_log_handler) # instantiate all tool classes self._all_tools: dict[type[Tool], Tool] = {tool_class: tool_class(self) for tool_class in ToolRegistry.get_all_tool_classes()} - tool_names = [tool.get_name() for tool in self._all_tools.values()] + tool_names = [tool.get_name_from_cls() for tool in self._all_tools.values()] # If GUI log window is enabled, set the tool names for highlighting if self._gui_log_handler is not None: @@ -572,6 +787,12 @@ class SerenaAgent: log.info(f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()})") log.info("Available projects: {}".format(", ".join(self.serena_config.project_names))) + # create executor for starting the language server and running tools in another thread + # This executor is used to achieve linear task execution, so it is important to use a single-threaded executor. + self._task_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="SerenaAgentExecutor") + self._task_executor_lock = threading.Lock() + self._task_executor_task_index = 1 + # Initialize the prompt factory self.prompt_factory = SerenaPromptFactory() self._project_activation_callback = project_activation_callback @@ -579,10 +800,12 @@ class SerenaAgent: # project-specific instances, which will be initialized upon project activation self._active_project: Project | None = None self._active_project_root: str | None = None - self.language_server: SyncLanguageServer | None = None + self.language_server: SolidLanguageServer | None = None self.symbol_manager: SymbolManager | None = None self.memories_manager: MemoriesManager | None = None self.lines_read: LinesRead | None = None + self.ignore_spec: PathSpec # not set to None to avoid assert statements + """Ignore spec, extracted from the project's gitignore files and the explicitly configured ignored paths.""" # Apply context and mode tool configurations if context is None: @@ -591,7 +814,7 @@ class SerenaAgent: modes = SerenaAgentMode.load_default_modes() self._context = context self._modes = modes - log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name() for tool in self._all_tools.values()])}") + log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name_from_cls() for tool in self._all_tools.values()])}") self._active_tools: dict[type[Tool], Tool] = {} self._update_active_tools() @@ -600,7 +823,7 @@ class SerenaAgent: if project is not None: try: self.activate_project_from_path_or_name(project) - except Exception as e: + except ProjectNotFoundError as e: log.error( f"Error activating project '{project}': {e}; Note that out-of-project configurations were migrated. " "You should now pass either --project or --project ." @@ -615,6 +838,46 @@ class SerenaAgent: raise ValueError("Cannot get project root if no project is active.") return project.project_root + def path_is_inside_project(self, path: str | Path) -> bool: + """ + Checks if the given (absolute or relative) path is inside the project directory. + Note that even relative paths may be outside if the contain ".." or point to symlinks. + """ + path = Path(path) + _proj_root = Path(self.get_project_root()) + if not path.is_absolute(): + path = _proj_root / path + + path = path.resolve() + return path.is_relative_to(_proj_root) + + def path_is_gitignored(self, path: str | Path) -> bool: + """ + Checks if the given path is ignored by git. Non absolute paths are assumed to be relative to the project root. + """ + path = Path(path) + if path.is_absolute(): + relative_path = path.relative_to(self.get_project_root()) + else: + relative_path = path + + # always ignore paths inside .git + if len(relative_path.parts) > 0 and relative_path.parts[0] == ".git": + return True + + return match_path(str(relative_path), self.ignore_spec) + + def validate_relative_path(self, relative_path: str) -> None: + """ + Validates that the given relative path is safe to read or edit, + meaning it's inside the project directory and is not ignored by git. + """ + if not self.path_is_inside_project(relative_path): + raise ValueError(f"{relative_path=} points to path outside of the repository root, can't use it for safety reasons") + + if self.path_is_gitignored(relative_path): + raise ValueError(f"File {relative_path} is gitignored, can't read or edit it for safety reasons") + def get_exposed_tool_instances(self) -> list["Tool"]: """ :return: all tool instances, including the non-active ones. For MCP clients, we need to expose them all since typical @@ -660,12 +923,27 @@ class SerenaAgent: excluded_tool_classes: set[type[Tool]] = set() # modes for mode in self._modes: - excluded_tool_classes.update(mode.get_excluded_tool_classes()) + mode_excluded_tool_classes = mode.get_excluded_tool_classes() + if len(mode_excluded_tool_classes) > 0: + log.info( + f"Mode {mode.name} excluded {len(mode_excluded_tool_classes)} tools: {', '.join([tool.get_name_from_cls() for tool in mode_excluded_tool_classes])}" + ) + excluded_tool_classes.update(mode_excluded_tool_classes) # context - excluded_tool_classes.update(self._context.get_excluded_tool_classes()) + context_excluded_tool_classes = self._context.get_excluded_tool_classes() + if len(context_excluded_tool_classes) > 0: + log.info( + f"Context {self._context.name} excluded {len(context_excluded_tool_classes)} tools: {', '.join([tool.get_name_from_cls() for tool in context_excluded_tool_classes])}" + ) + excluded_tool_classes.update(context_excluded_tool_classes) # project config if self._active_project is not None: - excluded_tool_classes.update(self._active_project.project_config.get_excluded_tool_classes()) + project_excluded_tool_classes = self._active_project.project_config.get_excluded_tool_classes() + if len(project_excluded_tool_classes) > 0: + log.info( + f"Project {self._active_project.project_name} excluded {len(project_excluded_tool_classes)} tools: {', '.join([tool.get_name_from_cls() for tool in project_excluded_tool_classes])}" + ) + excluded_tool_classes.update(project_excluded_tool_classes) if self._active_project.project_config.read_only: for tool_class in self._all_tools: if tool_class.can_edit(): @@ -677,20 +955,63 @@ class SerenaAgent: log.info(f"Active tools after all exclusions ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}") + def issue_task(self, task: Callable[[], Any], name: str | None = None) -> Future: + """ + Issue a task to the executor for asynchronous execution. + It is ensured that tasks are executed in the order they are issued, one after another. + + :param task: the task to execute + :param name: the name of the task for logging purposes; if None, use the task function's name + :return: a Future object representing the execution of the task + """ + with self._task_executor_lock: + task_name = f"Task-{self._task_executor_task_index}[{name or task.__name__}]" + self._task_executor_task_index += 1 + + def task_execution_wrapper() -> Any: + with LogTime(task_name, logger=log): + return task() + + log.info(f"Scheduling {task_name}") + return self._task_executor.submit(task_execution_wrapper) + + def execute_task(self, task: Callable[[], T]) -> T: + """ + Executes the given task synchronously via the agent's task executor. + This is useful for tasks that need to be executed immediately and whose results are needed right away. + + :param task: the task to execute + :return: the result of the task execution + """ + future = self.issue_task(task) + return future.result() + def _activate_project(self, project: Project) -> None: log.info(f"Activating {project.project_name} at {project.project_root}") self._active_project = project self._update_active_tools() - # start the language server - self.reset_language_server() - assert self.language_server is not None - - # initialize project-specific instances - self.symbol_manager = SymbolManager(self.language_server, self) + # initialize project-specific instances which do not depend on the language server self.memories_manager = MemoriesManagerMDFilesInProject(project.project_root) self.lines_read = LinesRead() + # reset project-specific instances that depend on the language server + self.symbol_manager = None + + def init_language_server() -> None: + # start the language server + with LogTime("Language server initialization", logger=log): + self.reset_language_server() + assert self.language_server is not None + self.ignore_spec = self.language_server.get_ignore_spec() + + # initialize project-specific instances which depend on the language server + log.debug(f"Initializing symbol and memories manager for {project.project_name} at {project.project_root}") + self.symbol_manager = SymbolManager(self.language_server, self) + + # initialize the language server in the background + self.issue_task(init_language_server) + if self._project_activation_callback is not None: self._project_activation_callback() @@ -710,7 +1031,7 @@ class SerenaAgent: log.info(f"Found registered project {project_instance.project_name} at path {project_instance.project_root}.") else: if not os.path.isdir(project_root_or_name): - raise ValueError( + raise ProjectNotFoundError( f"Project '{project_root_or_name}' not found: Not a valid project name or directory. " f"Existing project names: {self.serena_config.project_names}" ) @@ -735,7 +1056,7 @@ class SerenaAgent: """ :return: the list of names of the active tools for the current project """ - return sorted([tool.get_name() for tool in self.get_active_tool_classes()]) + return sorted([tool.get_name_from_cls() for tool in self.get_active_tool_classes()]) def tool_is_active(self, tool_class: type["Tool"] | str) -> bool: """ @@ -752,6 +1073,8 @@ class SerenaAgent: :return: a string overview of the current configuration, including the active and available configuration options """ result_str = "Current configuration:\n" + result_str += f"Serena version: {serena_version()}\n" + result_str += f"Loglevel: {self.serena_config.log_level}, trace_lsp_communication={self.serena_config.trace_lsp_communication}\n" if self._active_project is not None: result_str += f"Active project: {self._active_project.project_name}\n" else: @@ -779,7 +1102,7 @@ class SerenaAgent: result_str += " " + ", ".join(chunk) + "\n" # Available but not active tools - all_tool_names = sorted([tool.get_name() for tool in self._all_tools.values()]) + all_tool_names = sorted([tool.get_name_from_cls() for tool in self._all_tools.values()]) inactive_tool_names = [tool for tool in all_tool_names if tool not in active_tool_names] if inactive_tool_names: result_str += "Available but not active tools:\n" @@ -796,33 +1119,40 @@ class SerenaAgent: """ Starts/resets the language server 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 + # stop the language server if it is running if self.is_language_server_running(): - log.info(f"Stopping the current language server at {self.language_server.repository_root_path} ...") assert self.language_server is not None + log.info(f"Stopping the current language server at {self.language_server.repository_root_path} ...") self.language_server.stop() self.language_server = None # instantiate and start the language server assert self._active_project is not None - multilspy_config = MultilspyConfig( - code_language=self._active_project.project_config.language, - ignored_paths=self._active_project.project_config.ignored_paths, + self.language_server = create_ls_for_project( + self._active_project, + log_level=self.serena_config.log_level, + ls_timeout=ls_timeout, trace_lsp_communication=self.serena_config.trace_lsp_communication, ) - ls_logger = MultilspyLogger(log_level=self.serena_config.log_level) - log.info(f"Starting language server for {self._active_project.project_root}.") - self.language_server = SyncLanguageServer.create( - multilspy_config, - ls_logger, - self._active_project.project_root, - add_gitignore_content_to_config=self._active_project.project_config.ignore_all_files_in_gitignore, - ) + log.info(f"Starting the language server for {self._active_project.project_name}") self.language_server.start() if not self.language_server.is_running(): raise RuntimeError( f"Failed to start the language server for {self._active_project.project_name} at {self._active_project.project_root}" ) + if self.symbol_manager is not None: + log.debug("Setting the language server in the agent's symbol manager") + self.symbol_manager.set_language_server(self.language_server) + else: + log.debug("No symbol manager available yet, skipping setting the language server") def get_tool(self, tool_class: type[TTool]) -> TTool: return self._all_tools[tool_class] # type: ignore @@ -857,7 +1187,7 @@ class Component(ABC): self.agent = agent @property - def language_server(self) -> SyncLanguageServer: + def language_server(self) -> SolidLanguageServer: assert self.agent.language_server is not None return self.agent.language_server @@ -900,7 +1230,31 @@ class ToolMarkerDoesNotRequireActiveProject: pass -class Tool(Component): +class ToolInterface(ABC): + """Protocol defining the complete interface that make_tool() expects from a tool.""" + + @abstractmethod + def get_name(self) -> str: + """Get the tool name.""" + ... + + @abstractmethod + def get_apply_docstring(self) -> str: + """Get the docstring for the tool application, used by the MCP server.""" + ... + + @abstractmethod + def get_apply_fn_metadata(self) -> FuncMetadata: + """Get the metadata for the tool application function, used by the MCP server.""" + ... + + @abstractmethod + def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str: + """Apply the tool with logging and exception handling.""" + ... + + +class Tool(Component, ToolInterface): # NOTE: each tool should implement the apply method, which is then used in # the central method of the Tool class `apply_ex`. # Failure to do so will result in a RuntimeError at tool execution time. @@ -912,7 +1266,7 @@ class Tool(Component): # and to validate the tool call arguments. @classmethod - def get_name(cls) -> str: + def get_name_from_cls(cls) -> str: name = cls.__name__ if name.endswith("Tool"): name = name[:-4] @@ -920,6 +1274,9 @@ class Tool(Component): name = "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_") return name + def get_name(self) -> str: + return self.get_name_from_cls() + def get_apply_fn(self) -> Callable: apply_fn = getattr(self, "apply") if apply_fn is None: @@ -942,12 +1299,48 @@ class Tool(Component): return "" return docstring.strip() - def get_function_description(self) -> str: - apply_fn = self.get_apply_fn() + @classmethod + def get_apply_docstring_from_cls(cls) -> str: + """Get the docstring for the apply method from the class (static metadata). + Needed for creating MCP tools in a separate process without running into serialization issues. + """ + # First try to get from __dict__ to handle dynamic docstring changes + if "apply" in cls.__dict__: + apply_fn = cls.__dict__["apply"] + else: + # Fall back to getattr for inherited methods + apply_fn = getattr(cls, "apply", None) + if apply_fn is None: + raise AttributeError(f"apply method not defined in {cls}. Did you forget to implement it?") + docstring = apply_fn.__doc__ - if docstring is None: - raise Exception(f"Missing docstring for {self}") - return docstring + if not docstring: + raise AttributeError(f"apply method has no (or empty) docstring in {cls}. Did you forget to implement it?") + return docstring.strip() + + def get_apply_docstring(self) -> str: + """Get the docstring for the apply method (instance method implementing ToolProtocol).""" + return self.get_apply_docstring_from_cls() + + def get_apply_fn_metadata(self) -> FuncMetadata: + """Get the metadata for the apply method (instance method implementing ToolProtocol).""" + return self.get_apply_fn_metadata_from_cls() + + @classmethod + def get_apply_fn_metadata_from_cls(cls) -> FuncMetadata: + """Get the metadata for the apply method from the class (static metadata). + Needed for creating MCP tools in a separate process without running into serialization issues. + """ + # First try to get from __dict__ to handle dynamic docstring changes + if "apply" in cls.__dict__: + apply_fn = cls.__dict__["apply"] + else: + # Fall back to getattr for inherited methods + apply_fn = getattr(cls, "apply", None) + if apply_fn is None: + raise AttributeError(f"apply method not defined in {cls}. Did you forget to implement it?") + + return func_metadata(apply_fn, skip_names=["self", "cls"]) def _log_tool_application(self, frame: Any) -> None: params = {} @@ -959,7 +1352,7 @@ class Tool(Component): params.update(value) else: params[param] = value - log.info(f"{self.get_name()}: {dict_string(params)}") + log.info(f"{self.get_name_from_cls()}: {dict_string(params)}") @staticmethod def _limit_length(result: str, max_answer_chars: int) -> str: @@ -977,53 +1370,56 @@ class Tool(Component): """ Applies the tool with the given arguments """ - apply_fn = self.get_apply_fn() - try: - if not self.is_active(): - return f"Error: Tool '{self.get_name()}' is not active. Active tools: {self.agent.get_active_tool_names()}" - except Exception as e: - return f"RuntimeError while checking if tool {self.get_name()} is active: {e}" + def task() -> str: + apply_fn = self.get_apply_fn() - if log_call: - self._log_tool_application(inspect.currentframe()) - try: - # check whether the tool requires an active project and language server - if not isinstance(self, ToolMarkerDoesNotRequireActiveProject): - if self.agent._active_project is None: - return ( - "Error: No active project. Ask to user to select a project from this list: " - + f"{self.agent.serena_config.project_names}" - ) - if not self.agent.is_language_server_running(): - log.info("Language server is not running. Starting it ...") - self.agent.reset_language_server() + try: + if not self.is_active(): + return f"Error: Tool '{self.get_name_from_cls()}' is not active. Active tools: {self.agent.get_active_tool_names()}" + except Exception as e: + return f"RuntimeError while checking if tool {self.get_name_from_cls()} is active: {e}" - # apply the actual tool with a timeout - execution_fn = lambda: apply_fn(**kwargs) - execution_result = execute_with_timeout(execution_fn, self.agent.serena_config.tool_timeout, self.get_name()) - if execution_result.status == ExecutionResult.Status.SUCCESS: - result = cast(str, execution_result.result_value) - else: - assert execution_result.exception is not None - raise execution_result.exception + if log_call: + self._log_tool_application(inspect.currentframe()) + try: + # check whether the tool requires an active project and language server + if not isinstance(self, ToolMarkerDoesNotRequireActiveProject): + if self.agent._active_project is None: + return ( + "Error: No active project. Ask to user to select a project from this list: " + + f"{self.agent.serena_config.project_names}" + ) + if not self.agent.is_language_server_running(): + log.info("Language server is not running. Starting it ...") + self.agent.reset_language_server() - except Exception as e: - if not catch_exceptions: - raise - msg = f"Error executing tool: {e}\n{traceback.format_exc()}" - log.error(f"Error executing tool: {e}", exc_info=e) - result = msg + # apply the actual tool + result = apply_fn(**kwargs) - if log_call: - log.info(f"Result: {result}") + except Exception as e: + if not catch_exceptions: + raise + msg = f"Error executing tool: {e}\n{traceback.format_exc()}" + log.error( + f"Error executing tool: {e}. " + f"Consider restarting the language server to solve this (especially, if it's a timeout of a symbolic operation)", + exc_info=e, + ) + result = msg - try: - self.language_server.save_cache() - except Exception as e: - log.error(f"Error saving language server cache: {e}") + if log_call: + log.info(f"Result: {result}") - return result + try: + self.language_server.save_cache() + except Exception as e: + log.error(f"Error saving language server cache: {e}") + + return result + + future = self.agent.issue_task(task, name=self.__class__.__name__) + return future.result(timeout=self.agent.serena_config.tool_timeout) class RestartLanguageServerTool(Tool): @@ -1061,6 +1457,8 @@ class ReadFileTool(Tool): required for the task. :return: the full text of the file at the given relative path """ + self.agent.validate_relative_path(relative_path) + result = self.language_server.retrieve_full_file_content(relative_path) result_lines = result.splitlines() if end_line is None: @@ -1093,11 +1491,17 @@ class CreateTextFileTool(Tool, ToolMarkerCanEdit): :param content: the (utf-8-encoded) content to write to the file :return: a message indicating success or failure """ - absolute_path = os.path.join(self.get_project_root(), relative_path) - os.makedirs(os.path.dirname(absolute_path), exist_ok=True) - with open(absolute_path, "w", encoding="utf-8") as f: - f.write(content) - return f"File created: {relative_path}" + self.agent.validate_relative_path(relative_path) + + abs_path = (Path(self.get_project_root()) / relative_path).resolve() + will_overwrite_existing = abs_path.exists() + + abs_path.parent.mkdir(parents=True, exist_ok=True) + abs_path.write_text(content, encoding="utf-8") + answer = f"File created: {relative_path}." + if will_overwrite_existing: + answer += " Overwrote existing file." + return answer class ListDirTool(Tool): @@ -1107,7 +1511,7 @@ class ListDirTool(Tool): def apply(self, relative_path: str, recursive: bool, max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH) -> str: """ - Lists files and directories in the given directory (optionally with recursion). + Lists all non-gitignored files and directories in the given directory (optionally with recursion). :param relative_path: the relative path to the directory to list; pass "." to scan the project root :param recursive: whether to scan subdirectories recursively @@ -1116,17 +1520,14 @@ class ListDirTool(Tool): required for the task. :return: a JSON object with the names of directories and files within the given directory """ - - def is_ignored_path(abs_path: str) -> bool: - rel_path = os.path.relpath(abs_path, self.get_project_root()) - return self.language_server.is_ignored_path(rel_path, ignore_unsupported_files=False) + self.agent.validate_relative_path(relative_path) dirs, files = scan_directory( os.path.join(self.get_project_root(), relative_path), relative_to=self.get_project_root(), recursive=recursive, - is_ignored_dir=is_ignored_path, - is_ignored_file=is_ignored_path, + is_ignored_dir=self.agent.path_is_gitignored, + is_ignored_file=self.agent.path_is_gitignored, ) result = json.dumps({"dirs": dirs, "files": files}) @@ -1140,31 +1541,27 @@ class FindFileTool(Tool): def apply(self, file_mask: str, relative_path: str) -> str: """ - Finds files matching the given file mask within the given relative path + Finds non-gitignored files matching the given file mask within the given relative path :param file_mask: the filename or file mask (using the wildcards * or ?) to search for :param relative_path: the relative path to the directory to search in; pass "." to scan the project root :return: a JSON object with the list of matching files """ + self.agent.validate_relative_path(relative_path) - def is_ignored_path(abs_path: str) -> bool: - rel_path = os.path.relpath(abs_path, self.get_project_root()) - return self.language_server.is_ignored_path(rel_path, ignore_unsupported_files=False) + dir_to_scan = os.path.join(self.get_project_root(), relative_path) + # find the files by ignoring everything that doesn't match def is_ignored_file(abs_path: str) -> bool: - if is_ignored_path(abs_path): + if self.agent.path_is_gitignored(abs_path): return True filename = os.path.basename(abs_path) - is_ignored = not fnmatch(filename, file_mask) - if not is_ignored: - is_ignored = not fnmatch(filename, file_mask) - return is_ignored + return not fnmatch(filename, file_mask) dirs, files = scan_directory( - os.path.join(self.get_project_root(), relative_path), - relative_to=self.get_project_root(), + path=dir_to_scan, recursive=True, - is_ignored_dir=is_ignored_path, + is_ignored_dir=self.agent.path_is_gitignored, is_ignored_file=is_ignored_file, ) @@ -1348,27 +1745,16 @@ class ReplaceSymbolBodyTool(Tool, ToolMarkerCanEdit): r""" Replaces the body of the symbol with the given `name_path`. - Important: - You don't need to provide an adjusted indentation, - as the tool will automatically add the indentation of the original symbol to each line. For example, - for replacing a method in python, you can just write (using the standard python indentation): - body="def my_method_replacement(self, ...):\n first_line\n second_line...". So each line after the first line only has - an indentation of 4 (the indentation relative to the first characted), - since the additional indentation will be added by the tool. Same for more deeply nested - cases. You always only need to write the relative indentation to the first character of the first line, and that - in turn should not have any indentation. - ALWAYS REMEMBER TO USE THE CORRECT INDENTATION IN THE BODY! - :param name_path: for finding the symbol to replace, same logic as in the `find_symbol` tool. :param relative_path: the relative path to the file containing the symbol - :param body: the new symbol body. - + :param body: the new symbol body. Important: Begin directly with the symbol definition and provide no + leading indentation for the first line (but do indent the rest of the body according to the context). """ self.symbol_manager.replace_body( name_path, relative_file_path=relative_path, body=body, - use_same_indentation=True, + use_same_indentation=False, ) return SUCCESS_RESULT @@ -1388,17 +1774,12 @@ class InsertAfterSymbolTool(Tool, ToolMarkerCanEdit): Inserts the given body/content after the end of the definition of the given symbol (via the symbol's location). A typical use case is to insert a new class, function, method, field or variable assignment. - :param name_path: for finding the symbol to insert after, same logic as in the `find_symbol` tool. + :param name_path: name path of the symbol after which to insert content (definitions in the `find_symbol` tool apply) :param relative_path: the relative path to the file containing the symbol - :param body: the body/content to be inserted. Important: the insterted code will automatically have the - same indentation as the symbol's body, so you do not need to provide any additional indentation. + :param body: the body/content to be inserted. The inserted code shall begin with the next line after + the symbol. """ - self.symbol_manager.insert_after_symbol( - name_path, - relative_file_path=relative_path, - body=body, - use_same_indentation=True, - ) + self.symbol_manager.insert_after_symbol(name_path, relative_file_path=relative_path, body=body, use_same_indentation=False) return SUCCESS_RESULT @@ -1418,17 +1799,11 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit): A typical use case is to insert a new class, function, method, field or variable assignment. It also can be used to insert a new import statement before the first symbol in the file. - :param name_path: for finding the symbol to insert before, same logic as in the `find_symbol` tool. + :param name_path: name path of the symbol before which to insert content (definitions in the `find_symbol` tool apply) :param relative_path: the relative path to the file containing the symbol - :param body: the body/content to be inserted. Important: the insterted code will automatically have the - same indentation as the symbol's body, so you do not need to provide any additional indentation. + :param body: the body/content to be inserted before the line in which the referenced symbol is defined """ - self.symbol_manager.insert_before_symbol( - name_path, - relative_file_path=relative_path, - body=body, - use_same_indentation=True, - ) + self.symbol_manager.insert_before_symbol(name_path, relative_file_path=relative_path, body=body, use_same_indentation=False) return SUCCESS_RESULT @@ -1500,7 +1875,7 @@ class ReplaceRegexTool(Tool, ToolMarkerCanEdit): Always try to use wildcards to avoid specifying the exact content of the code to be replaced, especially if it spans several lines. - IMPORTANT: REMEMBER TO USE WILDCARDS WEHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! + IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! :param relative_path: the relative path to the file :param regex: a Python-style regular expression, matches of which will be replaced. @@ -1512,6 +1887,7 @@ class ReplaceRegexTool(Tool, ToolMarkerCanEdit): If this is set to False and the regex matches multiple occurrences, an error will be returned (and you may retry with a revised, more specific regex). """ + self.agent.validate_relative_path(relative_path) with EditedFileContext(relative_path, self.agent) as context: original_content = context.get_original_content() updated_content, n = re.subn(regex, repl, original_content, flags=re.DOTALL | re.MULTILINE) @@ -1548,7 +1924,7 @@ class DeleteLinesTool(Tool, ToolMarkerCanEdit): """ if not self.lines_read.were_lines_read(relative_path, (start_line, end_line)): read_lines_tool = self.agent.get_tool(ReadFileTool) - return f"Error: Must call `{read_lines_tool.get_name()}` first to read exactly the affected lines." + return f"Error: Must call `{read_lines_tool.get_name_from_cls()}` first to read exactly the affected lines." self.symbol_manager.delete_lines(relative_path, start_line, end_line) return SUCCESS_RESULT @@ -1801,7 +2177,7 @@ class SearchForPatternTool(Tool): context_lines_after: int = 0, paths_include_glob: str | None = None, paths_exclude_glob: str | None = None, - only_in_code_files: bool = True, + restrict_search_to_code_files: bool = False, max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH, ) -> str: """ @@ -1814,15 +2190,22 @@ class SearchForPatternTool(Tool): :param context_lines_after: Number of lines of context to include after each match :param paths_include_glob: optional glob pattern specifying files to include in the search; if not provided, search globally. :param paths_exclude_glob: optional glob pattern specifying files to exclude from the search (takes precedence over paths_include_glob). - :param only_in_code_files: whether to search only in code files or in the entire code base. - The explicitly ignored files (from serena config and gitignore) are never searched. :param max_answer_chars: if the output is longer than this number of characters, no content will be returned. Don't adjust unless there is really no other way to get the content required for the task. Instead, if the output is too long, you should make a stricter query. + :param restrict_search_to_code_files: whether to restrict the search to only those files where + analyzed code symbols can be found. Otherwise, will search all non-ignored files. + Set this to True if your search is only meant to discover code that can be manipulated with symbolic tools. + For example, for finding classes or methods from a name pattern. + Setting to False is a better choice if you also want to search in non-code files, like in html or yaml files, + which is why it is the default. :return: A JSON object mapping file paths to lists of matched consecutive lines (with context, if requested). """ - if only_in_code_files: + # this was previously a kwarg and was true by default + # However, the LLM doesn't really know which files are taken into account by the language server + # and which onees + if restrict_search_to_code_files: matches = self.language_server.search_files_for_pattern( pattern=pattern, context_lines_before=context_lines_before, @@ -1832,21 +2215,20 @@ class SearchForPatternTool(Tool): ) else: # we walk through all files in the project starting from the root - files_to_search = [] - ignore_spec = self.language_server.get_ignore_spec() - for root, dirs, files in os.walk(self.get_project_root()): - # Don't go into directories that are ignored by modifying dirs inplace - # Explanation for the + "/" part: - # pathspec can't handle the matching of directories if they don't end with a slash! - # see https://github.com/cpburnz/python-pathspec/issues/89 - dirs[:] = [d for d in dirs if not ignore_spec.match_file(d + "/")] + project_root = self.get_project_root() + rel_paths_to_search = [] + for root, dirs, files in os.walk(project_root): + # don't explore ignored dirs + dirs[:] = [d for d in dirs if not self.agent.path_is_gitignored(os.path.join(root, d))] for file in files: - if not ignore_spec.match_file(os.path.join(root, file)): - files_to_search.append(os.path.join(root, file)) + file_path = os.path.join(root, file) + if not self.agent.path_is_gitignored(file_path): + relative_path = os.path.relpath(file_path, project_root) + rel_paths_to_search.append(relative_path) # TODO (maybe): not super efficient to walk through the files again and filter if glob patterns are provided # but it probably never matters and this version required no further refactoring matches = search_files( - files_to_search, + rel_paths_to_search, pattern, paths_include_glob=paths_include_glob, paths_exclude_glob=paths_exclude_glob, @@ -2010,7 +2392,7 @@ def _iter_tool_classes(same_module_only: bool = True) -> Generator[type[Tool], N yield tool_class -_TOOL_REGISTRY_DICT: dict[str, type[Tool]] = {tool_class.get_name(): tool_class for tool_class in _iter_tool_classes()} +_TOOL_REGISTRY_DICT: dict[str, type[Tool]] = {tool_class.get_name_from_cls(): tool_class for tool_class in _iter_tool_classes()} """maps tool name to the corresponding tool class""" @@ -2046,7 +2428,7 @@ class ToolRegistry: tool_dict: dict[str, type[Tool] | Tool] = {} for tool_class in tools: - tool_dict[tool_class.get_name()] = tool_class + tool_dict[tool_class.get_name_from_cls()] = tool_class for tool_name in sorted(tool_dict.keys()): tool_class = tool_dict[tool_name] print(f" * `{tool_name}`: {tool_class.get_tool_description().strip()}") diff --git a/src/serena/agno.py b/src/serena/agno.py index c915069..0bb0e70 100644 --- a/src/serena/agno.py +++ b/src/serena/agno.py @@ -25,7 +25,7 @@ class SerenaAgnoToolkit(Toolkit): def __init__(self, serena_agent: SerenaAgent): super().__init__("Serena") for tool in serena_agent.get_exposed_tool_instances(): - self.functions[tool.get_name()] = self._create_agno_function(tool) + self.functions[tool.get_name_from_cls()] = self._create_agno_function(tool) log.info("Agno agent functions: %s", list(self.functions.keys())) @staticmethod @@ -39,7 +39,7 @@ class SerenaAgnoToolkit(Toolkit): return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) function = Function.from_callable(tool.get_apply_fn()) - function.name = tool.get_name() + function.name = tool.get_name_from_cls() function.entrypoint = entrypoint function.skip_entrypoint_processing = True return function diff --git a/src/serena/config.py b/src/serena/config.py index 0b9a85d..2f2659d 100644 --- a/src/serena/config.py +++ b/src/serena/config.py @@ -3,7 +3,9 @@ Context and Mode configuration loader """ import os -from dataclasses import dataclass, field +from copy import copy +from dataclasses import asdict, dataclass, field +from enum import Enum from pathlib import Path from typing import TYPE_CHECKING, Self @@ -30,6 +32,17 @@ class SerenaAgentMode: description: str = "" excluded_tools: set[str] = field(default_factory=set) + def to_json_dict(self) -> dict[str, str | list[str]]: + result = asdict(self) + result["excluded_tools"] = list(result["excluded_tools"]) + return result + + @classmethod + def from_json_dict(cls, data: dict) -> Self: + data = copy(data) + data["excluded_tools"] = set(data["excluded_tools"]) + return cls(**data) + def print_overview(self) -> None: """Print an overview of the mode.""" print(f"{self.name}:\n {self.description}") @@ -91,6 +104,17 @@ class SerenaAgentContext: description: str = "" excluded_tools: set[str] = field(default_factory=set) + def to_json_dict(self) -> dict[str, str | list[str]]: + result = asdict(self) + result["excluded_tools"] = list(result["excluded_tools"]) + return result + + @classmethod + def from_json_dict(cls, data: dict) -> Self: + data = copy(data) + data["excluded_tools"] = set(data["excluded_tools"]) + return cls(**data) + def get_excluded_tool_classes(self) -> list[type["Tool"]]: """Get the list of tool classes that are excluded from the context.""" from serena.agent import ToolRegistry @@ -144,3 +168,35 @@ class SerenaAgentContext: print(f"{self.name}:\n {self.description}") if self.excluded_tools: print(" excluded tools:\n " + ", ".join(sorted(self.excluded_tools))) + + +class RegisteredContext(Enum): + """A registered context.""" + + IDE_ASSISTANT = "ide-assistant" + """For Serena running within an assistant that already has basic tools, like Claude Code, Cline, Cursor, etc.""" + DESKTOP_APP = "desktop-app" + """For Serena running within Claude Desktop or a similar app which does not have built-in tools for code editing.""" + AGENT = "agent" + """For Serena running as a standalone agent, e.g. through agno.""" + + def load(self) -> SerenaAgentContext: + """Load the context.""" + return SerenaAgentContext.from_name(self.value) + + +class RegisteredMode(Enum): + """A registered mode.""" + + INTERACTIVE = "interactive" + """Interactive mode, for multi-turn interactions.""" + EDITING = "editing" + """Editing tools are activated.""" + PLANNING = "planning" + """Editing tools are deactivated.""" + ONE_SHOT = "one-shot" + """Non-interactive mode, where the goal is to finish a task autonomously.""" + + def load(self) -> SerenaAgentMode: + """Load the mode.""" + return SerenaAgentMode.from_name(self.value) diff --git a/src/serena/constants.py b/src/serena/constants.py index 9e0980f..368a743 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -12,8 +12,13 @@ SERENA_ICON_DIR = str(_serena_pkg_path / "resources" / "icons") SERENA_MANAGED_DIR_NAME = ".serena" +DEFAULT_ENCODING = "utf-8" DEFAULT_CONTEXT = "desktop-app" DEFAULT_MODES = ("interactive", "editing") PROJECT_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "project.template.yml") SELENA_CONFIG_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "serena_config.template.yml") + +USE_PROCESS_ISOLATION = False + +SERENA_LOG_FORMAT = "%(levelname)-5s %(asctime)-15s [%(threadName)s] %(name)s:%(funcName)s:%(lineno)d - %(message)s" diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 4b0419a..4cd98b6 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -1,24 +1,26 @@ import os import queue import socket -import sys import threading +from collections.abc import Callable +from typing import Any -import uvicorn -from fastapi import FastAPI -from fastapi.staticfiles import StaticFiles +from flask import Flask, Response, request, send_from_directory from pydantic import BaseModel from sensai.util import logging -from serena.constants import SERENA_DASHBOARD_DIR +from serena.constants import SERENA_DASHBOARD_DIR, SERENA_LOG_FORMAT log = logging.getLogger(__name__) +# disable Werkzeug's logging to avoid cluttering the output +logging.getLogger("werkzeug").setLevel(logging.WARNING) + class MemoryLogHandler(logging.Handler): def __init__(self, level: int = logging.NOTSET) -> None: super().__init__(level=level) - self.setFormatter(logging.Formatter(logging.LOG_DEFAULT_FORMAT)) + self.setFormatter(logging.Formatter(SERENA_LOG_FORMAT)) self._log_buffer = LogBuffer() self._log_queue: queue.Queue[str] = queue.Queue() self._stop_event = threading.Event() @@ -68,33 +70,70 @@ class ResponseToolNames(BaseModel): class SerenaDashboardAPI: log = logging.getLogger(__qualname__) - def __init__(self, memory_log_handler: MemoryLogHandler, tool_names: list[str]) -> None: + def __init__( + self, memory_log_handler: MemoryLogHandler, tool_names: list[str], shutdown_callback: Callable[[], None] | None = None + ) -> None: self._memory_log_handler = memory_log_handler self._tool_names = tool_names - self._app = FastAPI(title="Serena Dashboard") + self._shutdown_callback = shutdown_callback + self._app = Flask(__name__) self._setup_routes() + @property + def memory_log_handler(self) -> MemoryLogHandler: + return self._memory_log_handler + def _setup_routes(self) -> None: - self._app.mount("/dashboard", StaticFiles(directory=SERENA_DASHBOARD_DIR), name="dashboard") + # Static files + @self._app.route("/dashboard/") + def serve_dashboard(filename: str) -> Response: + return send_from_directory(SERENA_DASHBOARD_DIR, filename) - self._app.add_api_route("/get_log_messages", self._get_log_messages, methods=["POST"], response_model=ResponseLog) - self._app.add_api_route("/get_tool_names", self._get_tool_names, methods=["GET"], response_model=ResponseToolNames) - self._app.add_api_route("/shutdown", self._shutdown, methods=["PUT"]) + @self._app.route("/dashboard/") + def serve_dashboard_index() -> Response: + return send_from_directory(SERENA_DASHBOARD_DIR, "index.html") - async def _get_log_messages(self, request: RequestLog) -> ResponseLog: + # API routes + @self._app.route("/get_log_messages", methods=["POST"]) + def get_log_messages() -> dict[str, Any]: + request_data = request.get_json() + if not request_data: + request_log = RequestLog() + else: + request_log = RequestLog.model_validate(request_data) + + result = self._get_log_messages(request_log) + return result.model_dump() + + @self._app.route("/get_tool_names", methods=["GET"]) + def get_tool_names() -> dict[str, Any]: + result = self._get_tool_names() + return result.model_dump() + + @self._app.route("/shutdown", methods=["PUT"]) + def shutdown() -> dict[str, str]: + self._shutdown() + return {"status": "shutting down"} + + def _get_log_messages(self, request_log: RequestLog) -> ResponseLog: all_messages = self._memory_log_handler.get_log_messages() - requested_messages = all_messages[request.start_idx :] if request.start_idx <= len(all_messages) else [] + requested_messages = all_messages[request_log.start_idx :] if request_log.start_idx <= len(all_messages) else [] return ResponseLog(messages=requested_messages, max_idx=len(all_messages) - 1) - async def _get_tool_names(self) -> ResponseToolNames: + def _get_tool_names(self) -> ResponseToolNames: return ResponseToolNames(tool_names=self._tool_names) - async def _shutdown(self) -> None: - print("Shutdown initiated by dashbaord ...", file=sys.stderr) + def _shutdown(self) -> None: log.info("Shutting down Serena") - # noinspection PyUnresolvedReferences - # noinspection PyProtectedMember - os._exit(0) + if self._shutdown_callback: + self._shutdown_callback() + else: + # Try to use the global shutdown function from process_isolated_agent + from serena.process_isolated_agent import request_global_shutdown + + request_global_shutdown() + # noinspection PyProtectedMember + os._exit(0) @staticmethod def _find_first_free_port(start_port: int) -> int: @@ -109,11 +148,16 @@ class SerenaDashboardAPI: raise RuntimeError(f"No free ports found starting from {start_port}") - def run(self, host: str = "127.0.0.1", port: int = 0x5EDA) -> int: + def run(self, host: str = "0.0.0.0", port: int = 0x5EDA) -> int: """ Runs the dashboard on the given host and port and returns the port number. """ - uvicorn.run(self._app, host=host, port=port, workers=1, log_config=None, log_level="critical") + # patch flask.cli.show_server to avoid printing the server info + from flask import cli + + cli.show_server_banner = lambda *args, **kwargs: None + + self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True) return port def run_in_thread(self) -> tuple[threading.Thread, int]: diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 3f9d4ef..db74203 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -2,26 +2,48 @@ The Serena Model Context Protocol (MCP) Server """ +import asyncio +import contextlib +import os +import signal import sys -from collections.abc import AsyncIterator, Sequence +import threading +import time +from abc import abstractmethod +from collections.abc import AsyncIterator, Iterator, Sequence from contextlib import asynccontextmanager from dataclasses import dataclass from logging import Formatter, Logger, StreamHandler from pathlib import Path from typing import Any, Literal -import click # Add click import +import click import docstring_parser from mcp.server.fastmcp import server from mcp.server.fastmcp.server import FastMCP, Settings from mcp.server.fastmcp.tools.base import Tool as MCPTool -from mcp.server.fastmcp.utilities.func_metadata import func_metadata +from pydantic_settings import SettingsConfigDict from sensai.util import logging -from sensai.util.helper import mark_used -from serena.agent import SerenaAgent, Tool, show_fatal_exception_safe -from serena.config import SerenaAgentContext, SerenaAgentMode -from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES +from serena.agent import ( + ActivateProjectTool, + Project, + SerenaAgent, + SerenaConfig, + ToolInterface, + ToolRegistry, + create_serena_config, + show_fatal_exception_safe, +) +from serena.config import RegisteredContext, SerenaAgentContext, SerenaAgentMode +from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES, USE_PROCESS_ISOLATION +from serena.process_isolated_agent import ( + ProcessIsolatedDashboard, + ProcessIsolatedSerenaAgent, + ProcessIsolatedTool, + global_shutdown_event, + request_global_shutdown, +) log = logging.getLogger(__name__) LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" @@ -45,134 +67,390 @@ class SerenaMCPRequestContext: agent: SerenaAgent -def make_tool( - tool: Tool, -) -> MCPTool: - func_name = tool.get_name() +class SerenaMCPFactory: + def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None): + """ + :param context: The context name or path to context file + :param project: Either an absolute path to the project directory or a name of an already registered project. + If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name + afterward. + """ + self.context = SerenaAgentContext.load(context) + self.project = project - apply_fn = getattr(tool, "apply") - if apply_fn is None: - raise ValueError(f"Tool does not have an apply method: {tool}") + @staticmethod + def make_mcp_tool(tool: ToolInterface) -> MCPTool: + func_name = tool.get_name() + func_doc = tool.get_apply_docstring() or "" + func_arg_metadata = tool.get_apply_fn_metadata() + is_async = False + parameters = func_arg_metadata.arg_model.model_json_schema() - func_doc = apply_fn.__doc__ or "" - is_async = False + docstring = docstring_parser.parse(func_doc) - func_arg_metadata = func_metadata(apply_fn) - parameters = func_arg_metadata.arg_model.model_json_schema() + # Mount the tool description as a combination of the docstring description and + # the return value description, if it exists. + if docstring.description: + func_doc = f"{docstring.description.strip().strip('.')}." + else: + func_doc = "" + if docstring.returns and (docstring_returns_descr := docstring.returns.description): + # Only add a space before "Returns" if func_doc is not empty + prefix = " " if func_doc else "" + func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}." - docstring = docstring_parser.parse(func_doc) + # Parse the parameter descriptions from the docstring and add pass its description + # to the parameter schema. + docstring_params = {param.arg_name: param for param in docstring.params} + parameters_properties: dict[str, dict[str, Any]] = parameters["properties"] + for parameter, properties in parameters_properties.items(): + if (param_doc := docstring_params.get(parameter)) and param_doc.description: + param_desc = f"{param_doc.description.strip().strip('.') + '.'}" + properties["description"] = param_desc[0].upper() + param_desc[1:] - # Mount the tool description as a combination of the docstring description and - # the return value description, if it exists. - if docstring.description: - func_doc = f"{docstring.description.strip().strip('.')}." - else: - func_doc = "" - if (docstring.returns) and (docstring_returns := docstring.returns.description): - # Only add a space before "Returns" if func_doc is not empty - prefix = " " if func_doc else "" - func_doc = f"{func_doc}{prefix}Returns {docstring_returns.strip().strip('.')}." + def execute_fn(**kwargs) -> str: # type: ignore + return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) - # Parse the parameter descriptions from the docstring and add pass its description - # to the parameters schema. - docstring_params = {param.arg_name: param for param in docstring.params} - parameters_properties: dict[str, dict[str, Any]] = parameters["properties"] - for parameter, properties in parameters_properties.items(): - if (param_doc := docstring_params.get(parameter)) and (param_doc.description): - param_desc = f"{param_doc.description.strip().strip('.') + '.'}" - properties["description"] = param_desc[0].upper() + param_desc[1:] - - def execute_fn(**kwargs) -> str: # type: ignore - return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) - - return MCPTool( - fn=execute_fn, - name=func_name, - description=func_doc, - parameters=parameters, - fn_metadata=func_arg_metadata, - is_async=is_async, - context_kwarg=None, - ) - - -def create_mcp_server_and_agent( - project: str | None, - host: str = "0.0.0.0", - port: int = 8000, - context: str = DEFAULT_CONTEXT, - modes: Sequence[str] = DEFAULT_MODES, - enable_web_dashboard: bool | None = None, - enable_gui_log_window: bool | None = None, - log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, - trace_lsp_communication: bool | None = None, -) -> tuple[FastMCP, SerenaAgent]: - """ - Create an MCP server. - - :param project: "Either an absolute path to the project directory or a name of an already registered project. " - "If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name " - "afterwards. - :param host: The host to bind to - :param port: The port to bind to - :param context: The context name or path to context file - :param modes: List of mode names or paths to mode files - :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. - :param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. - If not specified, will take the value from the serena configuration. - :param log_level: Log level. If not specified, will take the value from the serena configuration. - :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. - This is useful for debugging language server issues. - """ - mcp: FastMCP | None = None - context_instance = SerenaAgentContext.load(context) - modes_instances = [SerenaAgentMode.load(mode) for mode in modes] - - try: - agent = SerenaAgent( - project=project, - # Callback disabled for the time being (see above) - # project_activation_callback=update_tools - context=context_instance, - modes=modes_instances, - enable_web_dashboard=enable_web_dashboard, - enable_gui_log_window=enable_gui_log_window, - log_level=log_level, - trace_lsp_communication=trace_lsp_communication, + return MCPTool( + fn=execute_fn, + name=func_name, + description=func_doc, + parameters=parameters, + fn_metadata=func_arg_metadata, + is_async=is_async, + context_kwarg=None, + annotations=None, ) - except Exception as e: - show_fatal_exception_safe(e) - raise - def update_tools() -> None: - """Update the tools in the MCP server.""" - # Tools may change as a result of project activation. - # NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not, - # unfortunately, query for changed tools. It only queries for changed resources and prompts regularly, - # so we need to register all tools at startup, unfortunately. - nonlocal mcp, agent - tools = agent.get_exposed_tool_instances() + @abstractmethod + def _iter_tools(self) -> Iterator[ToolInterface]: + pass + + # noinspection PyProtectedMember + def _set_mcp_tools(self, mcp: FastMCP) -> None: + """Update the tools in the MCP server""" if mcp is not None: mcp._tool_manager._tools = {} - for tool in tools: - # noinspection PyProtectedMember - mcp._tool_manager._tools[tool.get_name()] = make_tool(tool) + for tool in self._iter_tools(): + mcp_tool = self.make_mcp_tool(tool) + mcp._tool_manager._tools[tool.get_name()] = mcp_tool + + @abstractmethod + def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: + pass + + def create_mcp_server( + self, + host: str = "0.0.0.0", + port: int = 8000, + modes: Sequence[str] = DEFAULT_MODES, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, + ) -> FastMCP: + """ + Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. + + :param host: The host to bind to + :param port: The port to bind to + :param modes: List of mode names or paths to mode files + :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. + :param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. + If not specified, will take the value from the serena configuration. + :param log_level: Log level. If not specified, will take the value from the serena configuration. + :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. + This is useful for debugging language server issues. + :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. + """ + try: + serena_config = create_serena_config( + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) + modes_instances = [SerenaAgentMode.load(mode) for mode in modes] + self._instantiate_agent(serena_config, modes_instances) + + except Exception as e: + show_fatal_exception_safe(e) + raise + + # Override model_config to disable the use of `.env` files for reading settings, because user projects are likely to contain + # `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings; + # retain only FASTMCP_ prefix for already set environment variables. + Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") + + mcp_settings: Settings = Settings(lifespan=self.server_lifespan, host=host, port=port) + mcp = FastMCP(**mcp_settings.model_dump()) + return mcp @asynccontextmanager - async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: + @abstractmethod + async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" - nonlocal agent - mark_used(mcp_server) + yield None # ensures MyPy understands we yield None + + +class SerenaMCPFactorySingleProcess(SerenaMCPFactory): + """ + MCP server factory where the SerenaAgent and its language server run in the same process as the MCP server + """ + + def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None): + """ + :param context: The context name or path to context file + :param project: Either an absolute path to the project directory or a name of an already registered project. + If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name + afterward. + """ + super().__init__(context=context, project=project) + self.agent: SerenaAgent | None = None + + def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: + self.agent = SerenaAgent(project=self.project, serena_config=serena_config, context=self.context, modes=modes) + + def _iter_tools(self) -> Iterator[ToolInterface]: + assert self.agent is not None + yield from self.agent.get_exposed_tool_instances() + + @asynccontextmanager + async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: + self._set_mcp_tools(mcp_server) + log.info("MCP server lifetime setup complete") yield - if agent.language_server is not None: - agent.language_server.stop() - mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port) - mcp = FastMCP(**mcp_settings.model_dump()) - update_tools() +class SerenaMCPFactoryWithProcessIsolation(SerenaMCPFactory): + """ + MCP server factory with process isolation for the SerenaAgent and its language server; they run in a separate process + from the MCP server. + """ - return mcp, agent + def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None): + """ + :param context: The context name or path to context file + :param project: Either an absolute path to the project directory or a name of an already registered project. + If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name + afterward. + """ + super().__init__(context=context, project=project) + + self.active_tool_names: set[str] | None = None + self.serena_agent_process: ProcessIsolatedSerenaAgent | None = None + self.serena_dashboard_process: ProcessIsolatedDashboard | None = None + + @staticmethod + def _determine_active_tool_names(context: SerenaAgentContext, project: Project | None) -> set[str]: + """ + Determine the names of tools that should be included in this session based on the context. + """ + tools_excluded_in_this_session = context.get_excluded_tool_classes() + + # if a project has been loaded, it will be activated at startup and in ide-assistant context, + # we assume that no other project will be activated in this session. + # Therefore, we exclude the activate project tool + is_ide_assistant = context.name == RegisteredContext.IDE_ASSISTANT.value + if is_ide_assistant and project is not None: + tools_excluded_in_this_session.extend(project.project_config.get_excluded_tool_classes()) + tools_excluded_in_this_session.append(ActivateProjectTool) + + tool_names_excluded_in_this_session = {tool.get_name_from_cls() for tool in tools_excluded_in_this_session} + + all_tool_names = set(ToolRegistry.get_tool_names()) + tool_names_included_in_this_session = all_tool_names - tool_names_excluded_in_this_session + return tool_names_included_in_this_session + + @staticmethod + def make_mcp_tool(tool: ToolInterface) -> MCPTool: + func_name = tool.get_name() + func_doc = tool.get_apply_docstring() or "" + func_arg_metadata = tool.get_apply_fn_metadata() + is_async = False + parameters = func_arg_metadata.arg_model.model_json_schema() + + docstring = docstring_parser.parse(func_doc) + + # Mount the tool description as a combination of the docstring description and + # the return value description, if it exists. + if docstring.description: + func_doc = f"{docstring.description.strip().strip('.')}." + else: + func_doc = "" + if docstring.returns and (docstring_returns_descr := docstring.returns.description): + # Only add a space before "Returns" if func_doc is not empty + prefix = " " if func_doc else "" + func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}." + + # Parse the parameter descriptions from the docstring and add pass its description + # to the parameter schema. + docstring_params = {param.arg_name: param for param in docstring.params} + parameters_properties: dict[str, dict[str, Any]] = parameters["properties"] + for parameter, properties in parameters_properties.items(): + if (param_doc := docstring_params.get(parameter)) and param_doc.description: + param_desc = f"{param_doc.description.strip().strip('.') + '.'}" + properties["description"] = param_desc[0].upper() + param_desc[1:] + + def execute_fn(**kwargs) -> str: # type: ignore + return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) + + return MCPTool( + fn=execute_fn, + name=func_name, + description=func_doc, + parameters=parameters, + fn_metadata=func_arg_metadata, + is_async=is_async, + context_kwarg=None, + annotations=None, + ) + + def _iter_tools(self) -> Iterator[ToolInterface]: + assert self.active_tool_names is not None + assert self.serena_agent_process is not None + for tool_name in self.active_tool_names: + yield ProcessIsolatedTool(process_agent=self.serena_agent_process, tool_name=tool_name) + + # noinspection PyProtectedMember + def _set_mcp_tools(self, mcp: FastMCP) -> None: + """Update the tools in the MCP server""" + if mcp is not None: + mcp._tool_manager._tools = {} + for tool in self._iter_tools(): + mcp_tool = self.make_mcp_tool(tool) + mcp._tool_manager._tools[tool.get_name()] = mcp_tool + + def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: + if self.project is not None: + self.project_instance = serena_config.get_project(self.project) + self.serena_agent_process = ProcessIsolatedSerenaAgent( + project=self.project, serena_config=serena_config, modes=modes, context=self.context + ) + self.active_tool_names = self._determine_active_tool_names(self.context, self.project_instance) + if serena_config.web_dashboard: + assert self.active_tool_names is not None + self.serena_dashboard_process = ProcessIsolatedDashboard(tool_names=sorted(self.active_tool_names)) + + def create_mcp_server( + self, + host: str = "0.0.0.0", + port: int = 8000, + modes: Sequence[str] = DEFAULT_MODES, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, + ) -> FastMCP: + """ + Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. + + :param host: The host to bind to + :param port: The port to bind to + :param modes: List of mode names or paths to mode files + :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. + :param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. + If not specified, will take the value from the serena configuration. + :param log_level: Log level. If not specified, will take the value from the serena configuration. + :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. + This is useful for debugging language server issues. + :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. + """ + try: + serena_config = create_serena_config( + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) + modes_instances = [SerenaAgentMode.load(mode) for mode in modes] + self._instantiate_agent(serena_config, modes_instances) + + except Exception as e: + show_fatal_exception_safe(e) + raise + + # Override model_config to disable the use of `.env` files for reading settings, because user projects are likely to contain + # `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings; + # retain only FASTMCP_ prefix for already set environment variables. + Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") + + mcp_settings = Settings(lifespan=self.server_lifespan, host=host, port=port) + mcp = FastMCP(**mcp_settings.model_dump()) + return mcp + + @asynccontextmanager + async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: + """Manage server startup and shutdown lifecycle.""" + + def signal_handler(signum: int, frame: Any) -> None: + log.info(f"Received signal {signum} in main process") + request_global_shutdown() + + def force_exit() -> None: + time.sleep(2.0) # Wait 2 seconds for graceful shutdown + log.warning("Forcing exit after timeout") + # noinspection PyProtectedMember + # noinspection PyUnresolvedReferences + os._exit(1) + + threading.Thread(target=force_exit, daemon=True).start() + + # Install signal handlers + sigint_singal = signal.signal(signal.SIGINT, signal_handler) + sigterm_signal = signal.signal(signal.SIGTERM, signal_handler) + + if self.serena_dashboard_process is not None: + log.info("Starting dashboard process") + assert self.serena_dashboard_process is not None + self.serena_dashboard_process.start() + log.info("Starting serena agent process") + assert self.serena_agent_process is not None + self.serena_agent_process.start() + + self._set_mcp_tools(mcp_server) + + async def monitor_global_shutdown() -> None: + """Monitor the global shutdown event and trigger local shutdown.""" + while not global_shutdown_event.is_set(): + # Poll the multiprocessing Event in async context + await asyncio.sleep(0.1) + continue + log.info("Global shutdown event detected, initiating server shutdown") + request_global_shutdown() + # Send SIGTERM to self to trigger graceful shutdown + os.kill(os.getpid(), signal.SIGTERM) + + # Start monitoring task + monitor_task = asyncio.create_task(monitor_global_shutdown()) + + log.info("MCP server lifetime setup complete") + try: + yield + except (KeyboardInterrupt, SystemExit): + log.info("Received shutdown signal") + request_global_shutdown() + except Exception as e: + log.error(f"Error in server lifespan: {e}") + request_global_shutdown() + finally: + # Cancel monitor task + monitor_task.cancel() + + with contextlib.suppress(asyncio.CancelledError): + await monitor_task + + self.serena_agent_process.stop() + if self.serena_dashboard_process is not None: + self.serena_dashboard_process.stop() + request_global_shutdown() + log.info("Shutting down all processes") + signal.signal(signal.SIGINT, sigint_singal) + signal.signal(signal.SIGTERM, sigterm_signal) class ProjectType(click.ParamType): @@ -191,7 +469,7 @@ PROJECT_TYPE = ProjectType() @click.command() @click.option( "--project", - "project_file_opt", + "project", type=PROJECT_TYPE, default=None, help="Either an absolute path to the project directory or a name of an already registered project. " @@ -200,7 +478,7 @@ PROJECT_TYPE = ProjectType() # Keep --project-file for backwards compatibility @click.option( "--project-file", - "project_file_opt", # Use same destination variable to avoid conflicts + "project", # Use same destination variable to avoid conflicts type=PROJECT_TYPE, default=None, help="[DEPRECATED] Use --project instead.", @@ -280,8 +558,14 @@ PROJECT_TYPE = ProjectType() default=None, help="Whether to trace the communication between Serena and the language servers. This is useful for debugging language server issues.", ) +@click.option( + "--tool-timeout", + type=float, + default=None, + help="Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration.", +) def start_mcp_server( - project_file_opt: str | None, + project: str | None, project_file_arg: str | None, context: str = DEFAULT_CONTEXT, modes: tuple[str, ...] = DEFAULT_MODES, @@ -292,6 +576,7 @@ def start_mcp_server( enable_gui_log_window: bool | None = None, log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, ) -> None: """Starts the Serena MCP server. By default, will not activate any project at startup. If you want to start with an already active project, use --project to pass the project name or path. @@ -301,18 +586,24 @@ def start_mcp_server( """ # Prioritize the positional argument if provided # This is for backward compatibility with the old CLI, should be removed in the future! - project_file = project_file_arg if project_file_arg is not None else project_file_opt + project_file = project_file_arg if project_file_arg is not None else project - mcp_server, agent = create_mcp_server_and_agent( - project=project_file, + mcp_factory: SerenaMCPFactory + if not USE_PROCESS_ISOLATION: + mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) + else: + mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) + + # Use process isolation by default to prevent asyncio event loop contamination + mcp_server = mcp_factory.create_mcp_server( host=host, port=port, - context=context, modes=modes, enable_web_dashboard=enable_web_dashboard, enable_gui_log_window=enable_gui_log_window, log_level=log_level, trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, ) # log after server creation such that the log appears in the GUI @@ -323,9 +614,6 @@ def start_mcp_server( f"Used path: {project_file}" ) - log.info( - f"Starting serena agent in MCP server with config:\n{agent.get_current_config_overview()}." - f"\n Log level: {agent.serena_config.log_level}" - ) + log.info("Starting MCP server ...") mcp_server.run(transport=transport) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py new file mode 100644 index 0000000..081dcc6 --- /dev/null +++ b/src/serena/process_isolated_agent.py @@ -0,0 +1,571 @@ +import asyncio +import contextlib +import logging +import multiprocessing +import os +import threading +import traceback +import webbrowser +from enum import StrEnum +from logging.handlers import QueueHandler +from multiprocessing.connection import Connection +from multiprocessing.sharedctypes import Synchronized +from multiprocessing.synchronize import Event as EventClass +from typing import Any, Literal, Self + +from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata + +from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry +from serena.config import SerenaAgentContext, SerenaAgentMode +from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI + +log = logging.getLogger(__name__) + +# Global synchronization primitives +_global_log_queue: multiprocessing.Queue = multiprocessing.Queue() +_dashboard_ready_event = multiprocessing.Event() +_dashboard_port_value = multiprocessing.Value("i", 0) +global_shutdown_event = multiprocessing.Event() + + +def request_global_shutdown() -> None: + """Signal the global shutdown event.""" + global_shutdown_event.set() + log.info("Global shutdown event set") + + +def _dashboard_worker( + tool_names: list[str], + log_q: "multiprocessing.Queue[Any]", + dashboard_ready_event: EventClass, + port_value: "Synchronized[int]", + shutdown_evt: EventClass, +) -> None: + """Entry point for the dashboard process.""" + # Route all logging to the shared queue + root = logging.getLogger() + root.handlers.clear() + root.setLevel(logging.DEBUG) + root.addHandler(QueueHandler(log_q)) + + async def _process_logs(api: SerenaDashboardAPI) -> None: + while not shutdown_evt.is_set(): + while not log_q.empty(): + record = log_q.get_nowait() + if record is None: + break + api.memory_log_handler.emit(record) + # Small delay to avoid busy waiting + await asyncio.sleep(0.1) + + async def _monitor_shutdown() -> None: + # Poll the multiprocessing Event in async context + # Check every 100ms until shutdown is requested + loop = asyncio.get_event_loop() + while True: + # Check in executor to avoid blocking + result = await loop.run_in_executor(None, shutdown_evt.is_set) + if result: + break + await asyncio.sleep(0.1) + + async def _async_main() -> None: + api = SerenaDashboardAPI( + memory_log_handler=MemoryLogHandler(), + tool_names=tool_names, + shutdown_callback=shutdown_evt.set, + ) + # Pick a free port and signal readiness + port = api._find_first_free_port(0x5EDA) + port_value.value = port + + # Start Flask server in a thread + def run_flask_server() -> None: + api._app.run(host="0.0.0.0", port=port, debug=False, use_reloader=False, threaded=True) + + server_thread = threading.Thread(target=run_flask_server, daemon=True) + server_thread.start() + + shutdown_task = asyncio.create_task(_monitor_shutdown()) + logging_loop_task = asyncio.create_task(_process_logs(api)) + dashboard_ready_event.set() + + done, pending = await asyncio.wait( + [shutdown_task, logging_loop_task], + return_when=asyncio.FIRST_COMPLETED, + ) + + # Cancel remaining tasks + for task in pending: + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task + + try: + asyncio.run(_async_main()) + except BaseException: + logging.exception("Dashboard worker crashed") + finally: + logging.info("Dashboard worker exiting") + + +def _shutdown_process(proc: multiprocessing.Process, timeout: float = 1.0) -> None: + """Helper to shutdown a process gracefully.""" + if proc.is_alive(): + proc.terminate() + proc.join(timeout=timeout) + if proc.is_alive(): + log.error("Process did not terminate gracefully, forcing kill") + proc.kill() + proc.join(timeout=timeout) + if proc.is_alive(): + log.error("Process did not respond to kill") + + +class ProcessIsolatedDashboard: + """Dashboard running in a separate process to avoid asyncio contamination.""" + + def __init__(self, tool_names: list[str]): + self.tool_names = tool_names + self.process: multiprocessing.Process | None = None + + def start(self, timeout: float = 10.0) -> None: + """Start the dashboard process.""" + if self.process is not None: + raise RuntimeError("Dashboard already started") + + self.process = multiprocessing.Process( + target=_dashboard_worker, + args=(self.tool_names, _global_log_queue, _dashboard_ready_event, _dashboard_port_value, global_shutdown_event), + daemon=True, + ) + self.process.start() + + if not _dashboard_ready_event.wait(timeout): + self.process.terminate() + self.process.join(timeout=1.0) + self.process = None + raise RuntimeError("Dashboard failed to start within timeout") + + port = _dashboard_port_value.value + log.info(f"Dashboard started on port {port}") + webbrowser.open(f"http://localhost:{port}/dashboard/index.html") + + def stop(self, timeout: float = 1.0) -> None: + """Signal shutdown and wait for the dashboard process to exit.""" + if self.process is None: + return + + log.info("Stopping dashboard process") + request_global_shutdown() + _shutdown_process(self.process, timeout=timeout) + self.process = None + + +class SerenaAgentWorker: + """Worker process that hosts the actual SerenaAgent.""" + + class RequestMethod(StrEnum): + INITIALIZE = "initialize" + TOOL_CALL = "tool_call" + GET_ACTIVE_TOOL_NAMES = "get_active_tool_names" + IS_LANGUAGE_SERVER_RUNNING = "is_language_server_running" + RESET_LANGUAGE_SERVER = "reset_language_server" + GET_EXPOSED_TOOL_NAMES = "get_exposed_tool_names" + SHUTDOWN = "shutdown" + + def __init__(self, conn: Connection): + self.conn = conn + self.agent: SerenaAgent | None = None + + def run(self, log_queue: "multiprocessing.Queue[str]") -> None: + """Main worker loop - runs in separate process.""" + qh = QueueHandler(log_queue) + root = logging.getLogger() + root.setLevel(logging.DEBUG) + root.addHandler(qh) + + log.info("SerenaAgent worker process started") + try: + while not global_shutdown_event.is_set(): + try: + # Use polling to avoid blocking indefinitely + if self.conn.poll(timeout=0.5): # Poll every 500ms + try: + request = self.conn.recv() + if request is None: # Explicit shutdown signal + break + + response = self._handle_request(request) + self.conn.send(response) + except EOFError: + # Connection closed - parent process terminated + log.info("Connection closed, worker shutting down") + break + except (BrokenPipeError, ConnectionResetError): + # Connection broken, can't communicate + log.info("Connection broken, worker shutting down") + break + except Exception as e: + log.error(f"Error processing request: {e}") + try: + response = { + "error": f"Worker process error: {e!s}", + "traceback": traceback.format_exc(), + } + self.conn.send(response) + except (EOFError, BrokenPipeError, ConnectionResetError): + # Connection is broken, can't send error response + log.info("Connection broken during error response, shutting down") + break + # Continue polling if no data available + + except Exception as e: + log.error(f"Error in worker main loop: {e}") + break + except Exception as e: + log.error(f"Fatal error in worker process: {e}") + finally: + self._cleanup() + log.info("SerenaAgent worker process stopped") + os._exit(0) # Exit without raising any further exceptions + + def _handle_request(self, request: dict[str, Any]) -> dict[str, Any]: + """Handle a single request.""" + try: + method = request["method"] + params = request.get("params", {}) + + match method: + case self.RequestMethod.INITIALIZE: + return self._initialize(params) + case self.RequestMethod.TOOL_CALL: + return self._tool_call(params) + case self.RequestMethod.GET_ACTIVE_TOOL_NAMES: + return self._get_active_tool_names() + case self.RequestMethod.IS_LANGUAGE_SERVER_RUNNING: + return self._is_language_server_running() + case self.RequestMethod.RESET_LANGUAGE_SERVER: + return self._reset_language_server() + case self.RequestMethod.GET_EXPOSED_TOOL_NAMES: + return self._get_exposed_tool_names() + case self.RequestMethod.SHUTDOWN: + return self.shutdown() + case _: + return {"error": f"Unknown method: {method}"} + except Exception as e: + return {"error": str(e), "traceback": traceback.format_exc()} + + def _initialize(self, params: dict[str, Any]) -> dict[str, Any]: + """Initialize the SerenaAgent.""" + if self.agent is not None: + return {"result": "SerenaAgent already initialized"} + try: + # Extract all possible initialization parameters + context_param = params.get("context") + project = params.get("project") + serena_config = SerenaConfig.from_json_dict(params["serena_config"]) + context = SerenaAgentContext.from_json_dict(context_param) if context_param is not None else None + modes = [SerenaAgentMode.from_json_dict(m) for m in params["modes"]] + log_level = params.get("log_level") + trace_lsp_communication = params.get("trace_lsp_communication") + tool_timeout = params.get("tool_timeout") + + self.agent = SerenaAgent( + project=project, + serena_config=serena_config, + context=context, + modes=modes, + enable_web_dashboard=False, + enable_gui_log_window=False, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) + return {"result": "SerenaAgent initialized successfully"} + except Exception as e: + return {"error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()} + + def _tool_call(self, params: dict[str, Any]) -> dict[str, Any]: + """Execute a tool call.""" + if self.agent is None: + return {"error": "SerenaAgent not initialized"} + + try: + tool_name = params["tool_name"] + tool_params = params["tool_params"] + + # Get the tool by name + tool = None + for tool_instance in self.agent._active_tools.values(): + if tool_instance.get_name_from_cls() == tool_name: + tool = tool_instance + break + + if tool is None: + return {"error": f"Tool '{tool_name}' not found or not active"} + + # Execute the tool + result = tool.apply_ex(**tool_params) + + return {"result": result} + + except Exception as e: + return {"error": str(e), "traceback": traceback.format_exc()} + + def _get_active_tool_names(self) -> dict[str, Any]: + """Get list of active tool names.""" + if self.agent is None: + return {"error": "SerenaAgent not initialized"} + + try: + tool_names = self.agent.get_active_tool_names() + return {"result": tool_names} + except Exception as e: + return {"error": str(e), "traceback": traceback.format_exc()} + + def _is_language_server_running(self) -> dict[str, Any]: + """Check if language server is running.""" + if self.agent is None: + return {"error": "SerenaAgent not initialized"} + + try: + is_running = self.agent.is_language_server_running() + return {"result": is_running} + except Exception as e: + return {"error": str(e), "traceback": traceback.format_exc()} + + def _reset_language_server(self) -> dict[str, Any]: + """Reset the language server.""" + if self.agent is None: + return {"error": "SerenaAgent not initialized"} + + try: + self.agent.reset_language_server() + return {"result": "Language server reset successfully"} + except Exception as e: + return {"error": str(e), "traceback": traceback.format_exc()} + + def _get_exposed_tool_names(self) -> dict[str, Any]: + """Get exposed tool names for MCP tool creation.""" + if self.agent is None: + return {"error": "SerenaAgent not initialized"} + + try: + tool_instances = self.agent.get_exposed_tool_instances() + # Return only tool names - metadata will be reconstructed from ToolRegistry + tool_names = [tool.get_name_from_cls() for tool in tool_instances] + return {"result": tool_names} + except Exception as e: + return {"error": str(e), "traceback": traceback.format_exc()} + + def shutdown(self) -> dict[str, Any]: + try: + log.info("Shutting down SerenaAgent worker process on request") + self._cleanup() + request_global_shutdown() + # Return successful response before exiting + return {"result": "Shutdown initiated"} + except Exception as e: + log.error(f"Error during shutdown: {e}") + return {"error": str(e), "traceback": traceback.format_exc()} + + def _cleanup(self) -> None: + """Clean up resources.""" + if self.agent is not None: + try: + if self.agent.is_language_server_running() and self.agent.language_server is not None: + self.agent.language_server.stop() + except Exception as e: + log.error(f"Error stopping language server: {e}") + self.agent = None + + +class ProcessIsolatedSerenaAgent: + """Process-isolated wrapper for SerenaAgent that prevents asyncio contamination.""" + + def __init__( + self, + project: str | None = None, + serena_config: SerenaConfigBase | None = None, + context: SerenaAgentContext | None = None, + modes: list[SerenaAgentMode] | None = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, + ): + self.project = project + self.serena_config = serena_config or SerenaConfig.from_config_file() + self.context = context + self.modes = modes or [] + self.log_level = log_level + self.trace_lsp_communication = trace_lsp_communication + self.tool_timeout = tool_timeout + + self.process: multiprocessing.Process | None = None + self.conn: Connection | None = None + + def start(self) -> None: + """Start the worker process.""" + if self.process is not None: + raise RuntimeError("ProcessIsolatedSerenaAgent already started") + + log.info("Starting process-isolated SerenaAgent") + + # Create communication pipe + parent_conn, child_conn = multiprocessing.Pipe() + self.conn = parent_conn # type: ignore + + # Create and start worker process, passing along the dashboard's queue if available + worker = SerenaAgentWorker(child_conn) # type: ignore + self.process = multiprocessing.Process(target=worker.run, args=[_global_log_queue]) + self.process.start() + + # Prepare initialization parameters, converting complex objects to dict if present + init_params = { + "project": self.project, + "serena_config": self.serena_config.to_json_dict(), + "context": self.context.to_json_dict() if self.context is not None else None, + "modes": [m.to_json_dict() for m in self.modes], + "log_level": self.log_level, + "trace_lsp_communication": self.trace_lsp_communication, + "tool_timeout": self.tool_timeout, + } + # Initialize the agent in the worker process + try: + self._make_request_with_result(SerenaAgentWorker.RequestMethod.INITIALIZE, init_params) + except Exception as e: + self.stop() + raise RuntimeError(f"Failed to initialize SerenaAgent: {e}") from e + + log.info("Process-isolated SerenaAgent started successfully") + + def stop(self) -> None: + """Stop the worker process.""" + if self.process is None: + return + log.info("Stopping SerenaAgent process") + try: + # Close connection to signal worker to shutdown + if self.conn is not None: + self.conn.close() + _shutdown_process(self.process, timeout=2.0) + self.process = None + except KeyboardInterrupt: + log.warning("Keyboard interrupt during shutdown - forcing termination") + if self.process and self.process.is_alive(): + self.process.kill() + self.process.join(timeout=1.0) + except Exception as e: + log.error(f"Error stopping worker process: {e}") + finally: + self.process = None + self.conn = None + + log.info("SerenaAgent stopped") + + def _make_request(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> dict[str, Any]: + """Make a request to the worker process.""" + if self.process is None or not self.process.is_alive(): + raise RuntimeError("Worker process is not running") + + if self.conn is None: + raise RuntimeError("Connection is not initialized") + + request = {"method": method, "params": params or {}} + + # Send request + try: + self.conn.send(request) + except (EOFError, BrokenPipeError) as e: + raise RuntimeError("Failed to send request: worker process may have crashed") from e + + # Wait for response with timeout + timeout = self.serena_config.tool_timeout + if self.conn.poll(timeout): + try: + return self.conn.recv() + except (EOFError, BrokenPipeError) as e: + raise RuntimeError("Failed to receive response: worker process may have crashed") from e + else: + raise TimeoutError(f"Request {method} timed out after {timeout} seconds") + + def _make_request_with_result(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> Any: + """Make a request and return the result, raising an exception if there's an error.""" + response = self._make_request(method, params) + if "error" in response: + raise RuntimeError(f"Request {method} failed: {response['error']}") + return response["result"] + + def tool_call(self, tool_name: str, **tool_params: Any) -> str: + """Call a tool in the worker process.""" + return self._make_request_with_result( + SerenaAgentWorker.RequestMethod.TOOL_CALL, {"tool_name": tool_name, "tool_params": tool_params} + ) + + def get_tool(self, tool_cls: type[Tool]) -> "ProcessIsolatedTool": + """Get a process-isolated tool that delegates to this agent.""" + tool_name = tool_cls.get_name_from_cls() + return ProcessIsolatedTool(self, tool_name) + + def get_active_tool_names(self) -> list[str]: + """Get list of active tool names.""" + return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_ACTIVE_TOOL_NAMES) + + def is_language_server_running(self) -> bool: + """Check if language server is running.""" + return self._make_request_with_result(SerenaAgentWorker.RequestMethod.IS_LANGUAGE_SERVER_RUNNING) + + def reset_language_server(self) -> None: + """Reset the language server.""" + self._make_request_with_result(SerenaAgentWorker.RequestMethod.RESET_LANGUAGE_SERVER) + + def shutdown_from_dashboard(self) -> None: + """Request shutdown from dashboard.""" + self._make_request_with_result(SerenaAgentWorker.RequestMethod.SHUTDOWN) + + def get_exposed_tool_names(self) -> list[str]: + """Get tool names for MCP tool creation.""" + return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_EXPOSED_TOOL_NAMES) + + def __enter__(self) -> Self: + self.start() + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.stop() + + +class ProcessIsolatedTool(ToolInterface): + """A clean tool wrapper that delegates to ProcessIsolatedSerenaAgent.""" + + def __init__(self, process_agent: ProcessIsolatedSerenaAgent, tool_name: str): + self.process_agent = process_agent + self._tool_name = tool_name + + @property + def _tool_class(self) -> type[Tool]: + return ToolRegistry.get_tool_class_by_name(self._tool_name) + + def get_name(self) -> str: + """Get the tool name for this process-isolated tool.""" + return self._tool_name + + def get_apply_docstring(self) -> str: + """Get the docstring for the apply method.""" + # in the actual tool, this is a classmethod + return self._tool_class.get_apply_docstring_from_cls() + + def get_apply_fn_metadata(self) -> FuncMetadata: + """Get the metadata for the apply method.""" + # in the actual tool, this is a classmethod + return self._tool_class.get_apply_fn_metadata_from_cls() + + def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str: + """Apply the tool with logging and exception handling.""" + try: + return self.process_agent.tool_call(self._tool_name, **kwargs) + except Exception as e: + if catch_exceptions: + return f"Error executing tool {self._tool_name}: {e!s}" + raise diff --git a/src/serena/resources/config/contexts/ide-assistant.yml b/src/serena/resources/config/contexts/ide-assistant.yml index 230e952..52f6ba0 100644 --- a/src/serena/resources/config/contexts/ide-assistant.yml +++ b/src/serena/resources/config/contexts/ide-assistant.yml @@ -1,8 +1,13 @@ description: Non-symbolic editing tools and general shell tool are excluded prompt: | - You are running in IDE assistant context where file operations and shell commands are handled by the IDE. - You should exclusively use symbolic tools for exploring and modifying the code, as the IDE handles - file-level operations. + You are running in IDE assistant context where file operations, basic (line-based) edits and reads, + and shell commands are handled by your own, internal tools. + The initial instructions and the current config inform you on which tools are available to you, + and how to use them. + Don't attempt to use any excluded tools, instead rely on your own internal tools + for achieving the basic file or shell operations. + However, if serena's tools can be used for achieving your task (see initial instructions), + you should prioritize them. excluded_tools: - create_text_file - read_file diff --git a/src/serena/resources/config/modes/editing.yml b/src/serena/resources/config/modes/editing.yml index cbf1251..a0ba5f7 100644 --- a/src/serena/resources/config/modes/editing.yml +++ b/src/serena/resources/config/modes/editing.yml @@ -23,11 +23,10 @@ prompt: | use `find_symbol` with the name path `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit, you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding to read the desired methods with `include_body=True`. - Note that you never need to add additional indentation, as all symbol editing tools will automatically add the indentation of the symbol that - you are replacing or inserting above or below. In particular, keep in mind the description of the `replace_symbol_body` tool. If you want to add some new code at the end of the file, you should + In particular, keep in mind the description of the `replace_symbol_body` tool. If you want to add some new code at the end of the file, you should use the `insert_after_symbol` tool with the last top-level symbol in the file. If you want to add an import, often a good strategy is to use `insert_before_symbol` with the first top-level symbol in the file. - You can unterstand relationships between symbols by using the `find_referencing_symbols` tool. If not explicitly requested otherwise by a user, + You can understand relationships between symbols by using the `find_referencing_symbols` tool. If not explicitly requested otherwise by a user, you make sure that when you edit a symbol, it is either done in a backward-compatible way, or you find and adjust the references as needed. The `find_referencing_symbols` tool will give you code snippets around the references, as well as symbolic information. You will generally be able to use the info from the snippets and the regex-based approach to adjust the references as well. @@ -52,7 +51,7 @@ prompt: | 1. If the snippet to be replaced is likely to be unique within the file, you perform the replacement by directly using the escaped version of the original. 2. If the snippet is probably not unique, and you want to replace all occurrences, you use the `allow_multiple_occurrences` flag. - 3. If the snippet is not unique, and you want to replace a specific occurence, you make use of the code surrounding the snippet + 3. If the snippet is not unique, and you want to replace a specific occurrence, you make use of the code surrounding the snippet to extend the regex with content before/after such that the regex will have exactly one match. 4. You generally assume that a snippet is unique, knowing that the tool will return an error on multiple matches. You only read more file content (for crafvarting a more specific regex) if such a failure unexpectedly occurs. @@ -102,7 +101,7 @@ prompt: | Generally, I remind you that you rely on the regex tool with providing you the correct feedback, no need for more verification! - IMPORTANT: REMEMBER TO USE WILDCARDS WEHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! + IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! excluded_tools: - replace_lines - insert_at_line diff --git a/src/serena/resources/config/prompt_templates/system_prompt.yml b/src/serena/resources/config/prompt_templates/system_prompt.yml index c524579..8f3dd50 100644 --- a/src/serena/resources/config/prompt_templates/system_prompt.yml +++ b/src/serena/resources/config/prompt_templates/system_prompt.yml @@ -39,7 +39,7 @@ prompts: use `find_symbol` with the name path `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit, you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding to read the desired methods with `include_body=True` - You can unterstand relationships between symbols by using the `find_referencing_symbols` tool. + You can understand relationships between symbols by using the `find_referencing_symbols` tool. You generally have access to memories and it may be useful for you to read them, but also only if they help you to answer the question or complete the task. You can infer which memories are relevant to the current task by reading diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index 2e1e1c3..edef951 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -1,4 +1,6 @@ -# language of the project (csharp, python, rust, java, typescript, javascript, go, cpp, or ruby) +# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) +# * For C, use cpp +# * For JavaScript, use typescript # Special requirements: # * csharp: Requires the presence of a .sln file in the project folder. language: python diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 7f75098..0f48bb5 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -1,7 +1,7 @@ import json import logging import os -from collections.abc import Iterator, Sequence +from collections.abc import Iterable, Iterator, Reversible, Sequence from contextlib import contextmanager from dataclasses import asdict, dataclass, field from difflib import SequenceMatcher @@ -9,9 +9,9 @@ from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Self, Union from sensai.util.string import ToStringMixin -from multilspy import SyncLanguageServer -from multilspy.language_server import ReferenceInSymbol as LSPReferenceInSymbol -from multilspy.multilspy_types import Position, SymbolKind, UnifiedSymbolInformation +from solidlsp import SolidLanguageServer +from solidlsp.ls import ReferenceInSymbol as LSPReferenceInSymbol +from solidlsp.ls_types import Position, SymbolKind, UnifiedSymbolInformation if TYPE_CHECKING: from .agent import SerenaAgent @@ -256,6 +256,13 @@ class Symbol(ToStringMixin): def symbol_kind(self) -> SymbolKind: return self.symbol_root["kind"] + def is_neighbouring_definition_separated_by_empty_line(self) -> bool: + """ + :return: whether a symbol definition of this symbol's kind is usually separated from the + previous/next definition by at least one empty line. + """ + return self.symbol_kind in (SymbolKind.Function, SymbolKind.Method, SymbolKind.Class, SymbolKind.Interface, SymbolKind.Struct) + @property def relative_path(self) -> str | None: location = self.symbol_root.get("location") @@ -488,15 +495,22 @@ class ReferenceInSymbol(ToStringMixin): class SymbolManager: - def __init__(self, lang_server: SyncLanguageServer, agent: Union["SerenaAgent", None] = None) -> None: + def __init__(self, lang_server: SolidLanguageServer, agent: Union["SerenaAgent", None] = None) -> None: """ :param lang_server: the language server to use for symbol retrieval as well as editing operations. :param agent: the agent to use (only needed for marking files as modified). You can pass None if you don't - need an agent to be avare of file modifications performed by the symbol manager. + need an agent to be aware of file modifications performed by the symbol manager. """ - self.lang_server = lang_server + self._lang_server = lang_server self.agent = agent + def set_language_server(self, lang_server: SolidLanguageServer) -> None: + """ + Set the language server to use for symbol retrieval and editing operations. + This is useful if you want to change the language server after initializing the SymbolManager. + """ + self._lang_server = lang_server + def find_by_name( self, name_path: str, @@ -512,7 +526,7 @@ class SymbolManager: to symbols within a specific file or directory. """ symbols: list[Symbol] = [] - symbol_roots = self.lang_server.request_full_symbol_tree(within_relative_path=within_relative_path, include_body=include_body) + symbol_roots = self._lang_server.request_full_symbol_tree(within_relative_path=within_relative_path, include_body=include_body) for root in symbol_roots: symbols.extend( Symbol(root).find( @@ -522,14 +536,14 @@ class SymbolManager: return symbols def get_document_symbols(self, relative_path: str) -> list[Symbol]: - symbol_dicts, roots = self.lang_server.request_document_symbols(relative_path, include_body=False) + symbol_dicts, roots = self._lang_server.request_document_symbols(relative_path, include_body=False) symbols = [Symbol(s) for s in symbol_dicts] return symbols def find_by_location(self, location: SymbolLocation) -> Symbol | None: if location.relative_path is None: return None - symbol_dicts, roots = self.lang_server.request_document_symbols(location.relative_path, include_body=False) + symbol_dicts, roots = self._lang_server.request_document_symbols(location.relative_path, include_body=False) for symbol_dict in symbol_dicts: symbol = Symbol(symbol_dict) if symbol.location == location: @@ -598,7 +612,7 @@ class SymbolManager: assert symbol_location.relative_path is not None assert symbol_location.line is not None assert symbol_location.column is not None - references = self.lang_server.request_referencing_symbols( + references = self._lang_server.request_referencing_symbols( relative_file_path=symbol_location.relative_path, line=symbol_location.line, column=symbol_location.column, @@ -618,9 +632,9 @@ class SymbolManager: @contextmanager def _edited_file(self, relative_path: str) -> Iterator[None]: - with self.lang_server.open_file(relative_path) as file_buffer: + with self._lang_server.open_file(relative_path) as file_buffer: yield - root_path = self.lang_server.language_server.repository_root_path + root_path = self._lang_server.language_server.repository_root_path abs_path = os.path.join(root_path, relative_path) with open(abs_path, "w", encoding="utf-8") as f: f.write(file_buffer.contents) @@ -641,7 +655,7 @@ class SymbolManager: def _get_code_file_content(self, relative_path: str) -> str: """Get the content of a file using the language server.""" - return self.lang_server.language_server.retrieve_full_file_content(relative_path) + return self._lang_server.language_server.retrieve_full_file_content(relative_path) def replace_body(self, name_path: str, relative_file_path: str, body: str, *, use_same_indentation: bool = True) -> None: """ @@ -682,26 +696,36 @@ class SymbolManager: if start_pos is None or end_pos is None: raise ValueError(f"Symbol at {location} does not have a defined body range.") start_line, start_col = start_pos["line"], start_pos["character"] + if use_same_indentation: indent = " " * start_col body_lines = body.splitlines() body = body_lines[0] + "\n" + "\n".join(indent + line for line in body_lines[1:]) - # make sure body always ends with at least one newline - if not body.endswith("\n"): - body += "\n" - self.lang_server.delete_text_between_positions(location.relative_path, start_pos, end_pos) - self.lang_server.insert_text_at_position(location.relative_path, start_line, start_col, body) + # make sure the replacement adds no additional newlines (before or after) - all newlines + # and whitespace before/after should remain the same, so we strip it entirely + body = body.strip() - def insert_after_symbol( - self, - name_path: str, - relative_file_path: str, - body: str, - *, - use_same_indentation: bool = True, - at_new_line: bool = True, - ) -> None: + self._lang_server.delete_text_between_positions(location.relative_path, start_pos, end_pos) + self._lang_server.insert_text_at_position(location.relative_path, start_line, start_col, body) + + @staticmethod + def _count_leading_newlines(text: Iterable) -> int: + cnt = 0 + for c in text: + if c == "\n": + cnt += 1 + elif c == "\r": + continue + else: + break + return cnt + + @classmethod + def _count_trailing_newlines(cls, text: Reversible) -> int: + return cls._count_leading_newlines(reversed(text)) + + def insert_after_symbol(self, name_path: str, relative_file_path: str, body: str, *, use_same_indentation: bool = True) -> None: """ Inserts content after the symbol with the given name in the given file. """ @@ -715,13 +739,9 @@ class SymbolManager: f"Found symbols at locations: \n" + json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2) ) symbol = symbol_candidates[-1] - return self.insert_after_symbol_at_location( - symbol.location, body, at_new_line=at_new_line, use_same_indentation=use_same_indentation - ) + return self.insert_after_symbol_at_location(symbol.location, body, use_same_indentation=use_same_indentation) - def insert_after_symbol_at_location( - self, location: SymbolLocation, body: str, *, at_new_line: bool = True, use_same_indentation: bool = True - ) -> None: + def insert_after_symbol_at_location(self, location: SymbolLocation, body: str, *, use_same_indentation: bool = True) -> None: """ Appends content after the given symbol @@ -743,12 +763,23 @@ class SymbolManager: if pos is None: raise ValueError(f"Symbol at {location} does not have a defined end position.") - line, col = pos["line"], pos["character"] - if at_new_line: - line += 1 - col = 0 - if not body.startswith("\n"): - body = "\n" + body + # start at the beginning of the next line + col = 0 + line = pos["line"] + 1 + # make sure a suitable number of leading empty lines is used (at least 0/1 depending on the symbol type, + # otherwise as many as the caller wanted to insert) + original_leading_newlines = self._count_leading_newlines(body) + body = body.lstrip("\r\n") + min_empty_lines = 0 + if symbol.is_neighbouring_definition_separated_by_empty_line(): + min_empty_lines = 1 + num_leading_empty_lines = max(min_empty_lines, original_leading_newlines) + if num_leading_empty_lines: + body = ("\n" * num_leading_empty_lines) + body + # make sure the one line break succeeding the original symbol, which we repurposed as prefix via + # `line += 1`, is replaced + body = body.rstrip("\r\n") + "\n" + if use_same_indentation: symbol_start_pos = symbol.body_start_position assert symbol_start_pos is not None, f"Symbol at {location=} does not have a defined start position." @@ -771,20 +802,11 @@ class SymbolManager: # > test test # > second line # > dataclass_instance.status = "active" # Reassign dataclass field - col = 0 with self._edited_symbol_location(location): - self.lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body) + self._lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body) - def insert_before_symbol( - self, - name_path: str, - relative_file_path: str, - body: str, - *, - at_new_line: bool = True, - use_same_indentation: bool = True, - ) -> None: + def insert_before_symbol(self, name_path: str, relative_file_path: str, body: str, *, use_same_indentation: bool = True) -> None: """ Inserts content before the symbol with the given name in the given file. """ @@ -798,11 +820,9 @@ class SymbolManager: f"Found symbols at locations: \n" + json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2) ) symbol = symbol_candidates[0] - self.insert_before_symbol_at_location(symbol.location, body, at_new_line=at_new_line, use_same_indentation=use_same_indentation) + self.insert_before_symbol_at_location(symbol.location, body, use_same_indentation=use_same_indentation) - def insert_before_symbol_at_location( - self, location: SymbolLocation, body: str, *, at_new_line: bool = True, use_same_indentation: bool = True - ) -> None: + def insert_before_symbol_at_location(self, location: SymbolLocation, body: str, *, use_same_indentation: bool = True) -> None: """ Inserts content before the given symbol @@ -813,21 +833,31 @@ class SymbolManager: symbol_start_pos = symbol.body_start_position if symbol_start_pos is None: raise ValueError(f"Symbol at {location} does not have a defined start position.") - line = symbol_start_pos["line"] - col = symbol_start_pos["character"] + if use_same_indentation: - indent = " " * (col) + indent = " " * (symbol_start_pos["character"]) body = "\n".join(indent + line for line in body.splitlines()) - # similar problems as in insert_after_symbol_at_location, see comment there - if at_new_line: - col = 0 - line -= 1 - if not body.endswith("\n"): - body += "\n" + # insert position is the start of line where the symbol is defined + line = symbol_start_pos["line"] + col = 0 + + original_trailing_empty_lines = self._count_trailing_newlines(body) - 1 + + # ensure eol is present at end + body = body.rstrip() + "\n" + + # add suitable number of trailing empty lines after the body (at least 0/1 depending on the symbol type, + # otherwise as many as the caller wanted to insert) + min_trailing_empty_lines = 0 + if symbol.is_neighbouring_definition_separated_by_empty_line(): + min_trailing_empty_lines = 1 + num_trailing_newlines = max(min_trailing_empty_lines, original_trailing_empty_lines) + body += "\n" * num_trailing_newlines + assert location.relative_path is not None - self.lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body) + self._lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body) def insert_at_line(self, relative_path: str, line: int, content: str) -> None: """ @@ -837,7 +867,7 @@ class SymbolManager: :param content: the content to insert """ with self._edited_file(relative_path): - self.lang_server.insert_text_at_position(relative_path, line, 0, content) + self._lang_server.insert_text_at_position(relative_path, line, 0, content) def delete_lines(self, relative_path: str, start_line: int, end_line: int) -> None: """ @@ -852,7 +882,7 @@ class SymbolManager: with self._edited_file(relative_path): start_pos = Position(line=start_line, character=start_col) end_pos = Position(line=end_line_for_delete, character=end_col) - self.lang_server.delete_text_between_positions(relative_path, start_pos, end_pos) + self._lang_server.delete_text_between_positions(relative_path, start_pos, end_pos) def delete_symbol_at_location(self, location: SymbolLocation) -> None: """ @@ -862,7 +892,7 @@ class SymbolManager: assert location.relative_path is not None assert symbol.body_start_position is not None assert symbol.body_end_position is not None - self.lang_server.delete_text_between_positions(location.relative_path, symbol.body_start_position, symbol.body_end_position) + self._lang_server.delete_text_between_positions(location.relative_path, symbol.body_start_position, symbol.body_end_position) def delete_symbol(self, name_path: str, relative_file_path: str) -> None: """ diff --git a/src/serena/text_utils.py b/src/serena/text_utils.py index 338b295..1b9deb2 100644 --- a/src/serena/text_utils.py +++ b/src/serena/text_utils.py @@ -1,3 +1,4 @@ +import fnmatch import logging import re from collections.abc import Callable @@ -6,8 +7,6 @@ from enum import StrEnum from typing import Any, Self from joblib import Parallel, delayed -from pathspec import PathSpec -from pathspec.patterns.gitwildmatch import GitWildMatchPattern log = logging.getLogger(__name__) @@ -153,19 +152,28 @@ def search_text( # Convert pattern to a compiled regex if it's a string if is_glob and isinstance(pattern, str): - # Convert glob pattern to regex - # Escape all regex special characters except * and ? - regex_special_chars = r"\^$.|+()[{" - escaped_pattern = "" - for char in pattern: - if char in regex_special_chars: - escaped_pattern += "\\" + char - elif char == "*": - escaped_pattern += ".*" - elif char == "?": - escaped_pattern += "." - else: - escaped_pattern += char + # Convert glob pattern with optional backslash escaping to regex + def glob_to_regex(glob_pat: str) -> str: + regex_parts: list[str] = [] + i = 0 + while i < len(glob_pat): + ch = glob_pat[i] + if ch == "*": + regex_parts.append(".*") + elif ch == "?": + regex_parts.append(".") + elif ch == "\\": + i += 1 + if i < len(glob_pat): + regex_parts.append(re.escape(glob_pat[i])) + else: + regex_parts.append("\\") + else: + regex_parts.append(re.escape(ch)) + i += 1 + return "".join(regex_parts) + + escaped_pattern = glob_to_regex(pattern) # For glob patterns, don't anchor with ^ and $ to allow partial line matches compiled_pattern = re.compile(escaped_pattern) elif isinstance(pattern, str): @@ -249,6 +257,52 @@ def default_file_reader(file_path: str) -> str: return f.read() +def glob_match(pattern: str, path: str) -> bool: + """ + Match a file path against a glob pattern. + + Supports standard glob patterns: + - * matches any number of characters except / + - ** matches any number of directories (zero or more) + - ? matches a single character except / + - [seq] matches any character in seq + + :param pattern: Glob pattern (e.g., 'src/**/*.py', '**agent.py') + :param path: File path to match against + :return: True if path matches pattern + """ + pattern = pattern.replace("\\", "/") # Normalize backslashes to forward slashes + path = path.replace("\\", "/") # Normalize path backslashes to forward slashes + + # Handle ** patterns that should match zero or more directories + if "**" in pattern: + # Method 1: Standard fnmatch (matches one or more directories) + regex1 = fnmatch.translate(pattern) + if re.match(regex1, path): + return True + + # Method 2: Handle zero-directory case by removing /** entirely + # Convert "src/**/test.py" to "src/test.py" + if "/**/" in pattern: + zero_dir_pattern = pattern.replace("/**/", "/") + regex2 = fnmatch.translate(zero_dir_pattern) + if re.match(regex2, path): + return True + + # Method 3: Handle leading ** case by removing **/ + # Convert "**/test.py" to "test.py" + if pattern.startswith("**/"): + zero_dir_pattern = pattern[3:] # Remove "**/" + regex3 = fnmatch.translate(zero_dir_pattern) + if re.match(regex3, path): + return True + + return False + else: + # Simple pattern without **, use fnmatch directly + return fnmatch.fnmatch(path, pattern) + + def search_files( file_paths: list[str], pattern: re.Pattern | str, @@ -272,15 +326,14 @@ def search_files( :return: List of MatchedConsecutiveLines objects """ # Pre-filter paths (done sequentially to avoid overhead) - include_spec = PathSpec.from_lines(GitWildMatchPattern, [paths_include_glob]) if paths_include_glob else None - exclude_spec = PathSpec.from_lines(GitWildMatchPattern, [paths_exclude_glob]) if paths_exclude_glob else None + # Use proper glob matching instead of gitignore patterns filtered_paths = [] for path in file_paths: - if include_spec and not include_spec.match_file(path): + if paths_include_glob and not glob_match(paths_include_glob, path): log.debug(f"Skipping {path}: does not match include pattern {paths_include_glob}") continue - if exclude_spec and exclude_spec.match_file(path): + if paths_exclude_glob and glob_match(paths_exclude_glob, path): log.debug(f"Skipping {path}: matches exclude pattern {paths_exclude_glob}") continue filtered_paths.append(path) diff --git a/src/serena/util/file_system.py b/src/serena/util/file_system.py index e7f773a..4e88a9a 100644 --- a/src/serena/util/file_system.py +++ b/src/serena/util/file_system.py @@ -1,3 +1,4 @@ +import glob import os from collections.abc import Callable from dataclasses import dataclass, field @@ -78,17 +79,14 @@ def find_all_non_ignored_files(repo_root: str) -> list[str]: @dataclass class GitignoreSpec: - """ - Represents a single gitignore file and its parsed patterns. - - :param file_path: Path to the gitignore file - :param patterns: List of adjusted patterns from the gitignore file - :param pathspec: Compiled PathSpec object for pattern matching - """ - file_path: str + """Path to the gitignore file.""" patterns: list[str] = field(default_factory=list) + """List of patterns from the gitignore file. + The patterns are adjusted based on the gitignore file location. + """ pathspec: PathSpec = field(init=False) + """Compiled PathSpec object for pattern matching.""" def __post_init__(self) -> None: """Initialize the PathSpec from patterns.""" @@ -137,18 +135,7 @@ class GitignoreParser: :return: List of absolute paths to .gitignore files """ - gitignore_files = [] - - for root, dirs, files in os.walk(self.repo_root): - # Skip .git directory - if ".git" in dirs: - dirs.remove(".git") - - if ".gitignore" in files: - gitignore_path = os.path.join(root, ".gitignore") - gitignore_files.append(gitignore_path) - - return gitignore_files + return glob.glob(self.repo_root + "/.gitignore") + glob.glob(self.repo_root + "/**/.gitignore") def _create_ignore_spec(self, gitignore_file_path: str) -> GitignoreSpec: """ @@ -228,7 +215,13 @@ class GitignoreParser: # Add the directory prefix but also allow matching in subdirectories adjusted_pattern = os.path.join(rel_dir, "**", line) else: - adjusted_pattern = line + if is_anchored: + # Anchored patterns in root should only match at root level + # Add leading slash back to indicate root-only matching + adjusted_pattern = "/" + line + else: + # Non-anchored patterns can match anywhere + adjusted_pattern = line # Re-add negation if needed if is_negation: @@ -254,9 +247,14 @@ class GitignoreParser: else: rel_path = path + abs_path = os.path.join(self.repo_root, rel_path) + # Normalize path separators rel_path = rel_path.replace(os.sep, "/") + if os.path.exists(abs_path) and os.path.isdir(abs_path) and not rel_path.endswith("/"): + rel_path = rel_path + "/" + # Check against each ignore spec for spec in self.ignore_specs: if spec.matches(rel_path): @@ -276,3 +274,14 @@ class GitignoreParser: """Reload all gitignore files from the repository.""" self.ignore_specs.clear() self._load_gitignore_files() + + +def match_path(path: str, path_spec: PathSpec) -> bool: + path = os.path.abspath(path) + normalized_path = str(path).replace(os.path.sep, "/") + + # pathspec can't handle the matching of directories if they don't end with a slash! + # see https://github.com/cpburnz/python-pathspec/issues/89 + if os.path.isdir(normalized_path) and not normalized_path.endswith("/"): + normalized_path = normalized_path + "/" + return path_spec.match_file(normalized_path) diff --git a/src/serena/util/git.py b/src/serena/util/git.py new file mode 100644 index 0000000..865ae59 --- /dev/null +++ b/src/serena/util/git.py @@ -0,0 +1,20 @@ +import logging + +from sensai.util.git import GitStatus + +from .shell import subprocess_check_output + +log = logging.getLogger(__name__) + + +def get_git_status() -> GitStatus | None: + try: + commit_hash = subprocess_check_output(["git", "rev-parse", "HEAD"]) + unstaged = bool(subprocess_check_output(["git", "diff", "--name-only"])) + staged = bool(subprocess_check_output(["git", "diff", "--staged", "--name-only"])) + untracked = bool(subprocess_check_output(["git", "ls-files", "--others", "--exclude-standard"])) + return GitStatus( + commit=commit_hash, has_unstaged_changes=unstaged, has_staged_uncommitted_changes=staged, has_untracked_files=untracked + ) + except: + return None diff --git a/src/serena/util/inspection.py b/src/serena/util/inspection.py index 509da8e..24c2171 100644 --- a/src/serena/util/inspection.py +++ b/src/serena/util/inspection.py @@ -3,8 +3,8 @@ import os from collections.abc import Generator from typing import TypeVar -from multilspy.multilspy_config import Language from serena.util.file_system import find_all_non_ignored_files +from solidlsp.ls_config import Language T = TypeVar("T") diff --git a/src/serena/util/shell.py b/src/serena/util/shell.py index d940384..c899b21 100644 --- a/src/serena/util/shell.py +++ b/src/serena/util/shell.py @@ -24,11 +24,14 @@ def execute_shell_command(command: str, cwd: str | None = None, capture_stderr: if cwd is None: cwd = os.getcwd() + is_windows = platform.system() == "Windows" process = subprocess.Popen( command, - shell=platform.system() != "Windows", + shell=not is_windows, + stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE if capture_stderr else None, + creationflags=subprocess.CREATE_NO_WINDOW if is_windows else 0, # type: ignore text=True, encoding="utf-8", errors="replace", @@ -37,3 +40,18 @@ def execute_shell_command(command: str, cwd: str | None = None, capture_stderr: stdout, stderr = process.communicate() return ShellCommandResult(stdout=stdout, stderr=stderr, return_code=process.returncode, cwd=cwd) + + +def subprocess_check_output(args: list[str], encoding: str = "utf-8", strip: bool = True, timeout: float | None = None) -> str: + kwargs = { + "stdin": subprocess.DEVNULL, + "stderr": subprocess.PIPE, + "timeout": timeout, + "env": os.environ.copy(), + } + if platform.system() == "Windows": + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW # type: ignore + output = subprocess.check_output(args, **kwargs).decode(encoding) # type: ignore + if strip: + output = output.strip() + return output diff --git a/src/solidlsp/__init__.py b/src/solidlsp/__init__.py new file mode 100644 index 0000000..0378e2d --- /dev/null +++ b/src/solidlsp/__init__.py @@ -0,0 +1,2 @@ +# ruff: noqa +from .ls import SolidLanguageServer diff --git a/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py similarity index 67% rename from src/multilspy/language_servers/clangd_language_server/clangd_language_server.py rename to src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py index 8cb4798..893c5cc 100644 --- a/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py @@ -2,32 +2,29 @@ Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. """ -import asyncio import json import logging import os -import stat import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator +import stat +import threading -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -class ClangdLanguageServer(LanguageServer): +class ClangdLanguageServer(SolidLanguageServer): """ Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. As the project gets bigger in size, building index will take time. Try running clangd multiple times to ensure index is built properly. Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ @@ -39,15 +36,18 @@ class ClangdLanguageServer(LanguageServer): ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path), "cpp", ) - self.server_ready = asyncio.Event() + self.server_ready = threading.Event() + self.service_ready_event = threading.Event() + self.initialize_searcher_command_available = threading.Event() + self.resolve_main_method_available = threading.Event() - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ Setup runtime dependencies for ClangdLanguageServer. """ platform_id = PlatformUtils.get_platform_id() - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f: d = json.load(f) del d["_description"] @@ -55,12 +55,12 @@ class ClangdLanguageServer(LanguageServer): "linux-x64", "win-x64", "osx-arm64", - ], "Unsupported platform: " + platform_id.value + ], ( + "Unsupported platform: " + platform_id.value + ) runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] + runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] assert len(runtime_dependencies) == 1 # Select dependency matching the current platform dependency = next((dep for dep in runtime_dependencies if dep["platformId"] == platform_id.value), None) @@ -74,9 +74,7 @@ class ClangdLanguageServer(LanguageServer): logger.log(f"Clangd executable not found at {clangd_executable_path}. Downloading from {clangd_url}", logging.INFO) os.makedirs(clangd_ls_dir, exist_ok=True) if dependency["archiveType"] == "zip": - FileUtils.download_and_extract_archive( - logger, clangd_url, clangd_ls_dir, dependency["archiveType"] - ) + FileUtils.download_and_extract_archive(logger, clangd_url, clangd_ls_dir, dependency["archiveType"]) else: raise RuntimeError(f"Unsupported archive type: {dependency['archiveType']}") if not os.path.exists(clangd_executable_path): @@ -92,7 +90,7 @@ class ClangdLanguageServer(LanguageServer): """ Returns the initialize params for the clangd Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json")) as f: d = json.load(f) del d["_description"] @@ -112,8 +110,7 @@ class ClangdLanguageServer(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["ClangdLanguageServer"]: + def _start_server(self): """ Starts the Clangd Language Server, waits for the server to be ready and yields the LanguageServer instance. @@ -126,7 +123,8 @@ class ClangdLanguageServer(LanguageServer): # Shutdown the LanguageServer on exit from scope # LanguageServer has been shutdown """ - async def register_capability_handler(params): + + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "workspace/executeCommand": @@ -134,24 +132,24 @@ class ClangdLanguageServer(LanguageServer): self.resolve_main_method_available.set() return - async def lang_status_handler(params): + def lang_status_handler(params): # TODO: Should we wait for # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} # Before proceeding? if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": self.service_ready_event.set() - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def check_experimental_status(params): + def check_experimental_status(params): if params["quiescent"] == True: self.server_ready.set() - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", register_capability_handler) @@ -163,31 +161,25 @@ class ClangdLanguageServer(LanguageServer): self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) - async with super().start_server(): - self.logger.log("Starting Clangd server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting Clangd server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = await self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": ['.', '<', '>', ':', '"', '/', '*'], - "resolveProvider": False, - } + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": [".", "<", ">", ":", '"', "/", "*"], + "resolveProvider": False, + } - self.server.notify.initialized({}) + self.server.notify.initialized({}) - self.completions_available.set() - # set ready flag - self.server_ready.set() - await self.server_ready.wait() - - yield self - - await self.server.shutdown() - await self.server.stop() + self.completions_available.set() + # set ready flag + self.server_ready.set() + self.server_ready.wait() diff --git a/src/multilspy/language_servers/clangd_language_server/initialize_params.json b/src/solidlsp/language_servers/clangd_language_server/initialize_params.json similarity index 100% rename from src/multilspy/language_servers/clangd_language_server/initialize_params.json rename to src/solidlsp/language_servers/clangd_language_server/initialize_params.json diff --git a/src/multilspy/language_servers/clangd_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/clangd_language_server/runtime_dependencies.json similarity index 100% rename from src/multilspy/language_servers/clangd_language_server/runtime_dependencies.json rename to src/solidlsp/language_servers/clangd_language_server/runtime_dependencies.json diff --git a/src/multilspy/language_servers/dart_language_server/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py similarity index 55% rename from src/multilspy/language_servers/dart_language_server/dart_language_server.py rename to src/solidlsp/language_servers/dart_language_server/dart_language_server.py index b814306..58d7101 100644 --- a/src/multilspy/language_servers/dart_language_server/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py @@ -1,17 +1,16 @@ -from contextlib import asynccontextmanager +import json import logging import os import pathlib -import shutil import stat -from typing import AsyncIterator -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -import json -from multilspy.multilspy_utils import FileUtils, PlatformUtils + +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -class DartLanguageServer(LanguageServer): +class DartLanguageServer(SolidLanguageServer): """ Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart. """ @@ -20,7 +19,6 @@ class DartLanguageServer(LanguageServer): """ Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - executable_path = self.setup_runtime_dependencies(logger) super().__init__( config, @@ -30,17 +28,15 @@ class DartLanguageServer(LanguageServer): "dart", ) - def setup_runtime_dependencies(self, logger: "MultilspyLogger") -> str: + def setup_runtime_dependencies(self, logger: "LanguageServerLogger") -> str: platform_id = PlatformUtils.get_platform_id() - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f: d = json.load(f) del d["_description"] runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] + runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] assert len(runtime_dependencies) == 1 dependency = runtime_dependencies[0] @@ -50,24 +46,18 @@ class DartLanguageServer(LanguageServer): if not os.path.exists(dart_ls_dir): os.makedirs(dart_ls_dir) - FileUtils.download_and_extract_archive( - logger, dependency["url"], dart_ls_dir, dependency["archiveType"] - ) - + FileUtils.download_and_extract_archive(logger, dependency["url"], dart_ls_dir, dependency["archiveType"]) assert os.path.exists(dart_executable_path) os.chmod(dart_executable_path, stat.S_IEXEC) return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2" - def _get_initialize_params(self, repository_absolute_path: str): """ Returns the initialize params for the Dart Language Server. """ - with open( - os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r" - ) as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json")) as f: d = json.load(f) del d["_description"] @@ -80,67 +70,50 @@ class DartLanguageServer(LanguageServer): d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path( - repository_absolute_path - ).as_uri() + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() assert d["workspaceFolders"][0]["name"] == "$name" d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["DartLanguageServer"]: + def _start_server(self): """ Start the language server and yield when the server is ready. """ - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def check_experimental_status(params): + def check_experimental_status(params): pass - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", do_nothing) self.server.on_notification("language/status", do_nothing) self.server.on_notification("window/logMessage", window_log_message) - self.server.on_request( - "workspace/executeClientCommand", execute_client_command_handler - ) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_notification("language/actionableNotification", do_nothing) - self.server.on_notification( - "experimental/serverStatus", check_experimental_status + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + self.logger.log("Starting dart-language-server server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log( + "Sending initialize request to dart-language-server", + logging.DEBUG, + ) + init_response = self.server.send_request("initialize", initialize_params) + self.logger.log( + f"Received initialize response from dart-language-server: {init_response}", + logging.INFO, ) - async with super().start_server(): - self.logger.log( - "Starting dart-language-server server process", logging.INFO - ) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request to dart-language-server", - logging.DEBUG, - ) - init_response = await self.server.send_request( - "initialize", initialize_params - ) - self.logger.log( - f"Received initialize response from dart-language-server: {init_response}", - logging.INFO, - ) - - self.server.notify.initialized({}) - - yield self - - await self.server.shutdown() - await self.server.stop() + self.server.notify.initialized({}) diff --git a/src/multilspy/language_servers/dart_language_server/initialize_params.json b/src/solidlsp/language_servers/dart_language_server/initialize_params.json similarity index 100% rename from src/multilspy/language_servers/dart_language_server/initialize_params.json rename to src/solidlsp/language_servers/dart_language_server/initialize_params.json diff --git a/src/multilspy/language_servers/dart_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/dart_language_server/runtime_dependencies.json similarity index 100% rename from src/multilspy/language_servers/dart_language_server/runtime_dependencies.json rename to src/solidlsp/language_servers/dart_language_server/runtime_dependencies.json diff --git a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py similarity index 71% rename from src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py rename to src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py index 6753bf1..7453bf1 100644 --- a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -2,7 +2,6 @@ Provides Java specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Java. """ -import asyncio import dataclasses import json import logging @@ -10,21 +9,19 @@ import os import pathlib import shutil import stat +import threading import uuid -from contextlib import asynccontextmanager -from typing import AsyncIterator +from pathlib import PurePath from overrides import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_settings import MultilspySettings -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils -from pathlib import PurePath +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings @dataclasses.dataclass @@ -43,24 +40,23 @@ class RuntimeDependencyPaths: intellisense_members_path: str -class EclipseJDTLS(LanguageServer): +class EclipseJDTLS(SolidLanguageServer): """ The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a new EclipseJDTLS instance initializing the language server settings appropriately. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - runtime_dependency_paths = self.setupRuntimeDependencies(logger, config) self.runtime_dependency_paths = runtime_dependency_paths # ws_dir is the workspace directory for the EclipseJDTLS server ws_dir = str( PurePath( - MultilspySettings.get_language_server_directory(), + SolidLSPSettings.get_language_server_directory(), "EclipseJDTLS", "workspaces", uuid.uuid4().hex, @@ -68,9 +64,7 @@ class EclipseJDTLS(LanguageServer): ) # shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces - shared_cache_location = str( - PurePath(MultilspySettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex") - ) + shared_cache_location = str(PurePath(SolidLSPSettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex")) jre_path = self.runtime_dependency_paths.jre_path lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path @@ -135,12 +129,12 @@ class EclipseJDTLS(LanguageServer): ] ) - self.service_ready_event = asyncio.Event() - self.intellicode_enable_command_available = asyncio.Event() - self.initialize_searcher_command_available = asyncio.Event() + self.service_ready_event = threading.Event() + self.intellicode_enable_command_available = threading.Event() + self.initialize_searcher_command_available = threading.Event() super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java") - + @override def is_ignored_dirname(self, dirname: str) -> bool: # Ignore common Java build directories from different build tools: @@ -150,22 +144,22 @@ class EclipseJDTLS(LanguageServer): # - IntelliJ IDEA: out, .idea # - General: classes, dist, lib return super().is_ignored_dirname(dirname) or dirname in [ - "target", # Maven - "build", # Gradle - "bin", # Eclipse - "out", # IntelliJ IDEA - "classes", # General - "dist", # General - "lib" # General + "target", # Maven + "build", # Gradle + "bin", # Eclipse + "out", # IntelliJ IDEA + "classes", # General + "dist", # General + "lib", # General ] - def setupRuntimeDependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> RuntimeDependencyPaths: + def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> RuntimeDependencyPaths: """ Setup runtime dependencies for EclipseJDTLS. """ platformId = PlatformUtils.get_platform_id() - with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), "r", encoding="utf-8") as f: + with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), encoding="utf-8") as f: runtimeDependencies = json.load(f) del runtimeDependencies["_description"] @@ -194,9 +188,7 @@ class EclipseJDTLS(LanguageServer): assert os.path.exists(gradle_path) dependency = runtimeDependencies["vscode-java"][platformId.value] - vscode_java_path = str( - PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"]) - ) + vscode_java_path = str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"])) os.makedirs(vscode_java_path, exist_ok=True) jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"])) jre_path = str(PurePath(vscode_java_path, dependency["jre_path"])) @@ -213,9 +205,7 @@ class EclipseJDTLS(LanguageServer): os.path.exists(jdtls_readonly_config_path), ] ): - FileUtils.download_and_extract_archive( - logger, dependency["url"], vscode_java_path, dependency["archiveType"] - ) + FileUtils.download_and_extract_archive(logger, dependency["url"], vscode_java_path, dependency["archiveType"]) os.chmod(jre_path, stat.S_IEXEC) @@ -240,9 +230,7 @@ class EclipseJDTLS(LanguageServer): os.path.exists(intellisense_members_path), ] ): - FileUtils.download_and_extract_archive( - logger, dependency["url"], intellicode_directory_path, dependency["archiveType"] - ) + FileUtils.download_and_extract_archive(logger, dependency["url"], intellicode_directory_path, dependency["archiveType"]) assert os.path.exists(intellicode_directory_path) assert os.path.exists(intellicode_jar_path) @@ -264,7 +252,7 @@ class EclipseJDTLS(LanguageServer): Returns the initialize parameters for the EclipseJDTLS server. """ # Look into https://github.com/eclipse/eclipse.jdt.ls/blob/master/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java to understand all the options available - with open(str(PurePath(os.path.dirname(__file__), "initialize_params.json")), "r", encoding="utf-8") as f: + with open(str(PurePath(os.path.dirname(__file__), "initialize_params.json")), encoding="utf-8") as f: d: InitializeParams = json.load(f) del d["_description"] @@ -300,47 +288,30 @@ class EclipseJDTLS(LanguageServer): d["initializationOptions"]["bundles"] = bundles assert d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] == [ - {"name": "JavaSE-17", "path": "static/vscode-java/extension/jre/17.0.8.1-linux-x86_64", "default": True} + {"name": "JavaSE-21", "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", "default": True} ] d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = [ - {"name": "JavaSE-17", "path": self.runtime_dependency_paths.jre_home_path, "default": True} + {"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True} ] for runtime in d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]: assert "name" in runtime assert "path" in runtime - assert os.path.exists( - runtime["path"] - ), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist" + assert os.path.exists(runtime["path"]), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist" assert d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["home"] == "abs(static/gradle-7.3.3)" - d["initializationOptions"]["settings"]["java"]["import"]["gradle"][ - "home" - ] = self.runtime_dependency_paths.gradle_path + d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["home"] = self.runtime_dependency_paths.gradle_path - d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["java"][ - "home" - ] = self.runtime_dependency_paths.jre_path + d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["java"]["home"] = self.runtime_dependency_paths.jre_path return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["EclipseJDTLS"]: + def _start_server(self): """ - Starts the Eclipse JDTLS Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - ``` + Starts the Eclipse JDTLS Language Server """ - async def register_capability_handler(params): + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "textDocument/completion": @@ -358,22 +329,22 @@ class EclipseJDTLS(LanguageServer): self.intellicode_enable_command_available.set() return - async def lang_status_handler(params): + def lang_status_handler(params): # TODO: Should we wait for # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} # Before proceeding? if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": self.service_ready_event.set() - async def execute_client_command_handler(params): + def execute_client_command_handler(params): assert params["command"] == "_java.reloadBundles.command" assert params["arguments"] == [] return [] - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - async def do_nothing(params): + def do_nothing(params): return self.server.on_request("client/registerCapability", register_capability_handler) @@ -384,42 +355,34 @@ class EclipseJDTLS(LanguageServer): self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_notification("language/actionableNotification", do_nothing) - async with super().start_server(): - self.logger.log("Starting EclipseJDTLS server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting EclipseJDTLS server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = await self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" not in init_response["capabilities"] - assert "executeCommandProvider" not in init_response["capabilities"] + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" not in init_response["capabilities"] + assert "executeCommandProvider" not in init_response["capabilities"] - self.server.notify.initialized({}) + self.server.notify.initialized({}) - self.server.notify.workspace_did_change_configuration( - {"settings": initialize_params["initializationOptions"]["settings"]} - ) + self.server.notify.workspace_did_change_configuration({"settings": initialize_params["initializationOptions"]["settings"]}) - await self.intellicode_enable_command_available.wait() + self.intellicode_enable_command_available.wait() - java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path - assert os.path.exists(java_intellisense_members_path) - intellicode_enable_result = await self.server.send.execute_command( - { - "command": "java.intellicode.enable", - "arguments": [True, java_intellisense_members_path], - } - ) - assert intellicode_enable_result + java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path + assert os.path.exists(java_intellisense_members_path) + intellicode_enable_result = self.server.send.execute_command( + { + "command": "java.intellicode.enable", + "arguments": [True, java_intellisense_members_path], + } + ) + assert intellicode_enable_result - # TODO: Add comments about why we wait here, and how this can be optimized - await self.service_ready_event.wait() - - yield self - - await self.server.shutdown() - await self.server.stop() + # TODO: Add comments about why we wait here, and how this can be optimized + self.service_ready_event.wait() diff --git a/src/multilspy/language_servers/eclipse_jdtls/initialize_params.json b/src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json similarity index 99% rename from src/multilspy/language_servers/eclipse_jdtls/initialize_params.json rename to src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json index 5c4cd52..b65b7b6 100644 --- a/src/multilspy/language_servers/eclipse_jdtls/initialize_params.json +++ b/src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json @@ -510,8 +510,8 @@ "workspaceCacheLimit": 90, "runtimes": [ { - "name": "JavaSE-17", - "path": "static/vscode-java/extension/jre/17.0.8.1-linux-x86_64", + "name": "JavaSE-21", + "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", "default": true } ] @@ -535,7 +535,7 @@ "version": null, "home": "abs(static/gradle-7.3.3)", "java": { - "home": "abs(static/launch_jres/17.0.6-linux-x86_64)" + "home": "abs(static/launch_jres/21.0.7-linux-x86_64)" }, "offline": { "enabled": false diff --git a/src/multilspy/language_servers/eclipse_jdtls/runtime_dependencies.json b/src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json similarity index 66% rename from src/multilspy/language_servers/eclipse_jdtls/runtime_dependencies.json rename to src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json index c224c69..1b00040 100644 --- a/src/multilspy/language_servers/eclipse_jdtls/runtime_dependencies.json +++ b/src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json @@ -9,53 +9,53 @@ }, "vscode-java": { "darwin-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-arm64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", "archiveType": "zip", "relative_extraction_path": "vscode-java" }, "osx-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-x64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", "archiveType": "zip", "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/17.0.8.1-macosx-x86_64", - "jre_path": "extension/jre/17.0.8.1-macosx-x86_64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.30.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar", + "jre_home_path": "extension/jre/21.0.7-macosx-aarch64", + "jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", "jdtls_readonly_config_path": "extension/server/config_mac_arm" }, "osx-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-x64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", "archiveType": "zip", "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/17.0.8.1-macosx-x86_64", - "jre_path": "extension/jre/17.0.8.1-macosx-x86_64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.30.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar", + "jre_home_path": "extension/jre/21.0.7-macosx-x86_64", + "jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", "jdtls_readonly_config_path": "extension/server/config_mac" }, "linux-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@linux-arm64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", "archiveType": "zip", "relative_extraction_path": "vscode-java" }, "linux-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@linux-x64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", "archiveType": "zip", "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/17.0.8.1-linux-x86_64", - "jre_path": "extension/jre/17.0.8.1-linux-x86_64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.30.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar", + "jre_home_path": "extension/jre/21.0.7-linux-x86_64", + "jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", "jdtls_readonly_config_path": "extension/server/config_linux" }, "win-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@win32-x64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", "archiveType": "zip", "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/17.0.8.1-win32-x86_64", - "jre_path": "extension/jre/17.0.8.1-win32-x86_64/bin/java.exe", - "lombok_jar_path": "extension/lombok/lombok-1.18.30.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar", + "jre_home_path": "extension/jre/21.0.7-win32-x86_64", + "jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", "jdtls_readonly_config_path": "extension/server/config_win" } }, diff --git a/src/multilspy/language_servers/gopls/gopls.py b/src/solidlsp/language_servers/gopls/gopls.py similarity index 59% rename from src/multilspy/language_servers/gopls/gopls.py rename to src/solidlsp/language_servers/gopls/gopls.py index d17901a..c3fadd7 100644 --- a/src/multilspy/language_servers/gopls/gopls.py +++ b/src/solidlsp/language_servers/gopls/gopls.py @@ -1,29 +1,24 @@ -import asyncio import json import logging import os import pathlib import subprocess -from contextlib import asynccontextmanager -from typing import AsyncIterator, List +import threading from overrides import override -from multilspy import multilspy_types -from multilspy.lsp_protocol_handler import lsp_types -from multilspy.multilspy_exceptions import MultilspyException -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import Error, ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -class Gopls(LanguageServer): +class Gopls(SolidLanguageServer): """ Provides Go specific instantiation of the LanguageServer class using gopls. """ - + @override def is_ignored_dirname(self, dirname: str) -> bool: # For Go projects, we should ignore: @@ -36,7 +31,7 @@ class Gopls(LanguageServer): def _get_go_version(): """Get the installed Go version or None if not found.""" try: - result = subprocess.run(['go', 'version'], capture_output=True, text=True) + result = subprocess.run(["go", "version"], capture_output=True, text=True, check=False) if result.returncode == 0: return result.stdout.strip() except FileNotFoundError: @@ -47,7 +42,7 @@ class Gopls(LanguageServer): def _get_gopls_version(): """Get the installed gopls version or None if not found.""" try: - result = subprocess.run(['gopls', 'version'], capture_output=True, text=True) + result = subprocess.run(["gopls", "version"], capture_output=True, text=True, check=False) if result.returncode == 0: return result.stdout.strip() except FileNotFoundError: @@ -62,8 +57,10 @@ class Gopls(LanguageServer): """ go_version = cls._get_go_version() if not go_version: - raise RuntimeError("Go is not installed. Please install Go from https://golang.org/doc/install and make sure it is added to your PATH.") - + raise RuntimeError( + "Go is not installed. Please install Go from https://golang.org/doc/install and make sure it is added to your PATH." + ) + gopls_version = cls._get_gopls_version() if not gopls_version: raise RuntimeError( @@ -71,12 +68,12 @@ class Gopls(LanguageServer): "Please install gopls as described in https://pkg.go.dev/golang.org/x/tools/gopls#section-readme\n\n" "After installation, make sure it is added to your PATH (it might be installed in a different location than Go)." ) - + return True - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): self.setup_runtime_dependency() - + super().__init__( config, logger, @@ -84,14 +81,14 @@ class Gopls(LanguageServer): ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path), "go", ) - self.server_ready = asyncio.Event() + self.server_ready = threading.Event() self.request_id = 0 def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the TypeScript Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -111,16 +108,16 @@ class Gopls(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["Gopls"]: + def _start_server(self): """Start gopls server process""" - async def register_capability_handler(params): + + def register_capability_handler(params): return - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - async def do_nothing(params): + def do_nothing(params): return self.server.on_request("client/registerCapability", register_capability_handler) @@ -128,30 +125,24 @@ class Gopls(LanguageServer): self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) - async with super().start_server(): - self.logger.log("Starting gopls server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting gopls server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = await self.server.send.initialize(initialize_params) - - # Verify server capabilities - assert "textDocumentSync" in init_response["capabilities"] - assert "completionProvider" in init_response["capabilities"] - assert "definitionProvider" in init_response["capabilities"] + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) - self.server.notify.initialized({}) - self.completions_available.set() + # Verify server capabilities + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] - # gopls server is typically ready immediately after initialization - self.server_ready.set() - await self.server_ready.wait() + self.server.notify.initialized({}) + self.completions_available.set() - yield self - - await self.server.shutdown() - await self.server.stop() + # gopls server is typically ready immediately after initialization + self.server_ready.set() + self.server_ready.wait() diff --git a/src/multilspy/language_servers/gopls/initialize_params.json b/src/solidlsp/language_servers/gopls/initialize_params.json similarity index 100% rename from src/multilspy/language_servers/gopls/initialize_params.json rename to src/solidlsp/language_servers/gopls/initialize_params.json diff --git a/src/multilspy/language_servers/intelephense/initialize_params.json b/src/solidlsp/language_servers/intelephense/initialize_params.json similarity index 100% rename from src/multilspy/language_servers/intelephense/initialize_params.json rename to src/solidlsp/language_servers/intelephense/initialize_params.json diff --git a/src/multilspy/language_servers/intelephense/intelephense.py b/src/solidlsp/language_servers/intelephense/intelephense.py similarity index 63% rename from src/multilspy/language_servers/intelephense/intelephense.py rename to src/solidlsp/language_servers/intelephense/intelephense.py index 33382cf..19eaa8b 100644 --- a/src/multilspy/language_servers/intelephense/intelephense.py +++ b/src/solidlsp/language_servers/intelephense/intelephense.py @@ -2,40 +2,38 @@ Provides PHP specific instantiation of the LanguageServer class using Intelephense. """ -import asyncio import json -import shutil import logging import os -import subprocess import pathlib -from contextlib import asynccontextmanager +import shutil +import subprocess from time import sleep -from typing import AsyncIterator from overrides import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import PlatformUtils, PlatformId +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import PlatformId, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -class Intelephense(LanguageServer): + +class Intelephense(SolidLanguageServer): """ Provides PHP specific instantiation of the LanguageServer class using Intelephense. """ - + @override def is_ignored_dirname(self, dirname: str) -> bool: # For PHP projects, we should ignore: # - vendor: third-party dependencies managed by Composer # - node_modules: if the project has JavaScript components # - cache: commonly used for caching - return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"] + return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"] - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ Setup runtime dependencies for Intelephense. """ @@ -52,17 +50,17 @@ class Intelephense(LanguageServer): ] assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy PHP at the moment" - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] runtime_dependencies = d.get("runtimeDependencies", []) intelephense_ls_dir = os.path.join(os.path.dirname(__file__), "static", "php-lsp") - + # Verify both node and npm are installed - is_node_installed = shutil.which('node') is not None + is_node_installed = shutil.which("node") is not None assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." - is_npm_installed = shutil.which('npm') is not None + is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." # Install intelephense if not already installed @@ -77,11 +75,12 @@ class Intelephense(LanguageServer): check=True, cwd=intelephense_ls_dir, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) else: # On Unix-like systems, run as non-root user import pwd + user = pwd.getpwuid(os.getuid()).pw_name subprocess.run( dependency["command"], @@ -90,33 +89,26 @@ class Intelephense(LanguageServer): user=user, cwd=intelephense_ls_dir, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) - + intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense") assert os.path.exists(intelephense_executable_path), "intelephense executable not found. Please install intelephense and try again." - + return f"{intelephense_executable_path} --stdio" - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): # Setup runtime dependencies before initializing intelephense_cmd = self.setup_runtime_dependencies(logger, config) - - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), - "php" - ) - self.server_ready = asyncio.Event() + + super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php") self.request_id = 0 def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the TypeScript Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -136,16 +128,16 @@ class Intelephense(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["Intelephense"]: + def _start_server(self): """Start Intelephense server process""" - async def register_capability_handler(params): + + def register_capability_handler(params): return - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - async def do_nothing(params): + def do_nothing(params): return self.server.on_request("client/registerCapability", register_capability_handler) @@ -153,51 +145,44 @@ class Intelephense(LanguageServer): self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) - async with super().start_server(): - self.logger.log("Starting Intelephense server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting Intelephense server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = await self.server.send.initialize(initialize_params) - self.logger.log( - "After sent initialize params", - logging.INFO, - ) - - # Verify server capabilities - assert "textDocumentSync" in init_response["capabilities"] - assert "completionProvider" in init_response["capabilities"] - assert "definitionProvider" in init_response["capabilities"] + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + self.logger.log( + "After sent initialize params", + logging.INFO, + ) - self.server.notify.initialized({}) - self.completions_available.set() + # Verify server capabilities + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] - # Intelephense server is typically ready immediately after initialization - self.server_ready.set() - await self.server_ready.wait() + self.server.notify.initialized({}) + self.completions_available.set() - yield self + # Intelephense server is typically ready immediately after initialization + # TODO: This is probably incorrect; the server does send an initialized notification, which we could wait for! - await self.server.shutdown() - await self.server.stop() - @override # For some reason, the LS may need longer to process this, so we just retry - async def _send_references_request(self, relative_file_path: str, line: int, column: int): + def _send_references_request(self, relative_file_path: str, line: int, column: int): # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is # despite the LS having processed requests already. I don't know what causes this, but sleeping # one second helps. It may be that sleeping only once is enough but that's hard to reliably test. # May be related to the time it takes to read the files or something like that. # The sleeping doesn't seem to be needed on all systems sleep(1) - return await super()._send_references_request(relative_file_path, line, column) - + return super()._send_references_request(relative_file_path, line, column) + @override - async def _send_definition_request(self, definition_params: DefinitionParams): + def _send_definition_request(self, definition_params: DefinitionParams): # TODO: same as above, also only a problem if the definition is in another file sleep(1) - return await super()._send_definition_request(definition_params) + return super()._send_definition_request(definition_params) diff --git a/src/multilspy/language_servers/intelephense/runtime_dependencies.json b/src/solidlsp/language_servers/intelephense/runtime_dependencies.json similarity index 100% rename from src/multilspy/language_servers/intelephense/runtime_dependencies.json rename to src/solidlsp/language_servers/intelephense/runtime_dependencies.json diff --git a/src/multilspy/language_servers/jedi_language_server/initialize_params.json b/src/solidlsp/language_servers/jedi_language_server/initialize_params.json similarity index 100% rename from src/multilspy/language_servers/jedi_language_server/initialize_params.json rename to src/solidlsp/language_servers/jedi_language_server/initialize_params.json diff --git a/src/multilspy/language_servers/jedi_language_server/jedi_server.py b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py similarity index 53% rename from src/multilspy/language_servers/jedi_language_server/jedi_server.py rename to src/solidlsp/language_servers/jedi_language_server/jedi_server.py index 90d4376..467d3ca 100644 --- a/src/multilspy/language_servers/jedi_language_server/jedi_server.py +++ b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py @@ -6,24 +6,22 @@ import json import logging import os import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator, Tuple from overrides import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -class JediServer(LanguageServer): +class JediServer(SolidLanguageServer): """ Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ @@ -34,7 +32,7 @@ class JediServer(LanguageServer): ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path), "python", ) - + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] @@ -43,7 +41,7 @@ class JediServer(LanguageServer): """ Returns the initialize params for the Jedi Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -63,33 +61,22 @@ class JediServer(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["JediServer"]: + def _start_server(self): """ - Starts the JEDI Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - ``` + Starts the JEDI Language Server """ - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def check_experimental_status(params): + def check_experimental_status(params): if params["quiescent"] == True: self.completions_available.set() - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", do_nothing) @@ -101,26 +88,20 @@ class JediServer(LanguageServer): self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) - async with super().start_server(): - self.logger.log("Starting jedi-language-server server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting jedi-language-server server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = await self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": [".", "'", '"'], - "resolveProvider": True, - } + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": [".", "'", '"'], + "resolveProvider": True, + } - self.server.notify.initialized({}) - - yield self - - await self.server.shutdown() - await self.server.stop() + self.server.notify.initialized({}) diff --git a/src/multilspy/language_servers/kotlin_language_server/initialize_params.json b/src/solidlsp/language_servers/kotlin_language_server/initialize_params.json similarity index 100% rename from src/multilspy/language_servers/kotlin_language_server/initialize_params.json rename to src/solidlsp/language_servers/kotlin_language_server/initialize_params.json diff --git a/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py similarity index 56% rename from src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py rename to src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py index 5edc0e7..6ddab2c 100644 --- a/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py @@ -2,23 +2,19 @@ Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. """ -import asyncio import dataclasses import json import logging import os -import stat import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator +import stat -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo @dataclasses.dataclass @@ -26,29 +22,30 @@ class KotlinRuntimeDependencyPaths: """ Stores the paths to the runtime dependencies of Kotlin Language Server """ + java_path: str java_home_path: str kotlin_executable_path: str -class KotlinLanguageServer(LanguageServer): +class KotlinLanguageServer(SolidLanguageServer): """ Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a Kotlin Language Server instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ runtime_dependency_paths = self.setup_runtime_dependencies(logger, config) self.runtime_dependency_paths = runtime_dependency_paths - + # Create command to execute the Kotlin Language Server script cmd = f'"{self.runtime_dependency_paths.kotlin_executable_path}"' - + # Set environment variables including JAVA_HOME proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path} - + super().__init__( config, logger, @@ -57,84 +54,82 @@ class KotlinLanguageServer(LanguageServer): "kotlin", ) - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> KotlinRuntimeDependencyPaths: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> KotlinRuntimeDependencyPaths: """ Setup runtime dependencies for Kotlin Language Server. """ platform_id = PlatformUtils.get_platform_id() # Verify platform support - assert platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-"), "Only Windows, Linux and macOS platforms are supported for Kotlin in multilspy at the moment" + assert ( + platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-") + ), "Only Windows, Linux and macOS platforms are supported for Kotlin in multilspy at the moment" # Load dependency information - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] - + kotlin_dependency = d["runtimeDependency"] java_dependency = d["java"][platform_id.value] # Setup paths for dependencies static_dir = os.path.join(os.path.dirname(__file__), "static") os.makedirs(static_dir, exist_ok=True) - + # Setup Java paths java_dir = os.path.join(static_dir, "java") os.makedirs(java_dir, exist_ok=True) - + java_home_path = os.path.join(java_dir, java_dependency["java_home_path"]) java_path = os.path.join(java_dir, java_dependency["java_path"]) - + # Download and extract Java if not exists if not os.path.exists(java_path): logger.log(f"Downloading Java for {platform_id.value}...", logging.INFO) - FileUtils.download_and_extract_archive( - logger, java_dependency["url"], java_dir, java_dependency["archiveType"] - ) + FileUtils.download_and_extract_archive(logger, java_dependency["url"], java_dir, java_dependency["archiveType"]) # Make Java executable if not platform_id.value.startswith("win-"): os.chmod(java_path, 0o755) - + assert os.path.exists(java_path), f"Java executable not found at {java_path}" - + # Setup Kotlin Language Server paths kotlin_ls_dir = os.path.join(static_dir, "server") - + # Get platform-specific executable script path if platform_id.value.startswith("win-"): kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server.bat") else: kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server") - + # Download and extract Kotlin Language Server if script doesn't exist if not os.path.exists(kotlin_script): logger.log("Downloading Kotlin Language Server...", logging.INFO) - FileUtils.download_and_extract_archive( - logger, kotlin_dependency["url"], static_dir, kotlin_dependency["archiveType"] - ) - + FileUtils.download_and_extract_archive(logger, kotlin_dependency["url"], static_dir, kotlin_dependency["archiveType"]) + # Make script executable on Unix platforms if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"): - os.chmod(kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) - + os.chmod( + kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH + ) + # Use script file if os.path.exists(kotlin_script): kotlin_executable_path = kotlin_script logger.log(f"Using Kotlin Language Server script at {kotlin_script}", logging.INFO) else: raise FileNotFoundError(f"Kotlin Language Server script not found at {kotlin_script}") - + return KotlinRuntimeDependencyPaths( - java_path=java_path, - java_home_path=java_home_path, - kotlin_executable_path=kotlin_executable_path + java_path=java_path, java_home_path=java_home_path, kotlin_executable_path=kotlin_executable_path ) def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Kotlin Language Server. """ - with open(str(pathlib.PurePath(os.path.dirname(__file__), "initialize_params.json")), "r", encoding="utf-8") as f: + with open(str(pathlib.PurePath(os.path.dirname(__file__), "initialize_params.json")), encoding="utf-8") as f: d: InitializeParams = json.load(f) del d["_description"] @@ -155,8 +150,8 @@ class KotlinLanguageServer(LanguageServer): d["initializationOptions"]["workspaceFolders"] = [pathlib.Path(repository_absolute_path).as_uri()] assert ( - d["workspaceFolders"] - == '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]' + d["workspaceFolders"] + == '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]' ) d["workspaceFolders"] = [ { @@ -167,28 +162,18 @@ class KotlinLanguageServer(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["KotlinLanguageServer"]: + def _start_server(self): + """ + Starts the Kotlin Language Server """ - Starts the Kotlin Language Server, waits for the server to be ready and yields the LanguageServer instance. - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - ``` - """ - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", do_nothing) @@ -199,36 +184,26 @@ class KotlinLanguageServer(LanguageServer): self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_notification("language/actionableNotification", do_nothing) - async with super().start_server(): - self.logger.log("Starting Kotlin server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting Kotlin server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = await self.server.send.initialize(initialize_params) + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) - capabilities = init_response["capabilities"] - assert "textDocumentSync" in capabilities, "Server must support textDocumentSync" - assert "hoverProvider" in capabilities, "Server must support hover" - assert "completionProvider" in capabilities, "Server must support code completion" - assert "signatureHelpProvider" in capabilities, "Server must support signature help" - assert "definitionProvider" in capabilities, "Server must support go to definition" - assert "referencesProvider" in capabilities, "Server must support find references" - assert "documentSymbolProvider" in capabilities, "Server must support document symbols" - assert "workspaceSymbolProvider" in capabilities, "Server must support workspace symbols" - assert "semanticTokensProvider" in capabilities, "Server must support semantic tokens" - - self.server.notify.initialized({}) - self.completions_available.set() + capabilities = init_response["capabilities"] + assert "textDocumentSync" in capabilities, "Server must support textDocumentSync" + assert "hoverProvider" in capabilities, "Server must support hover" + assert "completionProvider" in capabilities, "Server must support code completion" + assert "signatureHelpProvider" in capabilities, "Server must support signature help" + assert "definitionProvider" in capabilities, "Server must support go to definition" + assert "referencesProvider" in capabilities, "Server must support find references" + assert "documentSymbolProvider" in capabilities, "Server must support document symbols" + assert "workspaceSymbolProvider" in capabilities, "Server must support workspace symbols" + assert "semanticTokensProvider" in capabilities, "Server must support semantic tokens" - yield self - - try: - await self.server.shutdown() - except Exception as e: - self.logger.log(f"Error during Kotlin server shutdown: {str(e)}", logging.WARNING) - finally: - await self.server.stop() + self.server.notify.initialized({}) + self.completions_available.set() diff --git a/src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json similarity index 51% rename from src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json rename to src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json index a7b64b9..3568c9d 100644 --- a/src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json +++ b/src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json @@ -8,34 +8,34 @@ }, "java": { "win-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@win32-x64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", "archiveType": "zip", - "java_home_path": "extension/jre/17.0.8.1-win32-x86_64", - "java_path": "extension/jre/17.0.8.1-win32-x86_64/bin/java.exe" + "java_home_path": "extension/jre/21.0.7-win32-x86_64", + "java_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe" }, "linux-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@linux-x64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", "archiveType": "zip", - "java_home_path": "extension/jre/17.0.8.1-linux-x86_64", - "java_path": "extension/jre/17.0.8.1-linux-x86_64/bin/java" + "java_home_path": "extension/jre/21.0.7-linux-x86_64", + "java_path": "extension/jre/21.0.7-linux-x86_64/bin/java" }, "linux-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@linux-arm64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", "archiveType": "zip", - "java_home_path": "extension/jre/17.0.8.1-linux-aarch64", - "java_path": "extension/jre/17.0.8.1-linux-aarch64/bin/java" + "java_home_path": "extension/jre/21.0.7-linux-aarch64", + "java_path": "extension/jre/21.0.7-linux-aarch64/bin/java" }, "osx-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-x64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", "archiveType": "zip", - "java_home_path": "extension/jre/17.0.8.1-macosx-x86_64", - "java_path": "extension/jre/17.0.8.1-macosx-x86_64/bin/java" + "java_home_path": "extension/jre/21.0.7-macosx-x86_64", + "java_path": "extension/jre/21.0.7-macosx-x86_64/bin/java" }, "osx-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-arm64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", "archiveType": "zip", - "java_home_path": "extension/jre/17.0.8.1-macosx-aarch64", - "java_path": "extension/jre/17.0.8.1-macosx-aarch64/bin/java" + "java_home_path": "extension/jre/21.0.7-macosx-aarch64", + "java_path": "extension/jre/21.0.7-macosx-aarch64/bin/java" } } } diff --git a/src/multilspy/language_servers/omnisharp/initialize_params.json b/src/solidlsp/language_servers/omnisharp/initialize_params.json similarity index 100% rename from src/multilspy/language_servers/omnisharp/initialize_params.json rename to src/solidlsp/language_servers/omnisharp/initialize_params.json diff --git a/src/multilspy/language_servers/omnisharp/omnisharp.py b/src/solidlsp/language_servers/omnisharp/omnisharp.py similarity index 76% rename from src/multilspy/language_servers/omnisharp/omnisharp.py rename to src/solidlsp/language_servers/omnisharp/omnisharp.py index 08295a9..e66540c 100644 --- a/src/multilspy/language_servers/omnisharp/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp/omnisharp.py @@ -2,24 +2,23 @@ Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. """ -import asyncio import json import logging import os import pathlib import stat -from contextlib import asynccontextmanager -from typing import AsyncIterator, Iterable +import threading +from collections.abc import Iterable from overrides import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_exceptions import MultilspyException -from multilspy.multilspy_utils import FileUtils, PlatformUtils, PlatformId, DotnetVersion +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo def breadth_first_file_scan(root) -> Iterable[str]: @@ -29,7 +28,7 @@ def breadth_first_file_scan(root) -> Iterable[str]: """ dirs = [root] # while we has dirs to scan - while len(dirs): + while dirs: next_dirs = [] for parent in dirs: # scan each dir @@ -55,12 +54,12 @@ def find_least_depth_sln_file(root_dir) -> str | None: return None -class OmniSharp(LanguageServer): +class OmniSharp(SolidLanguageServer): """ Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates an OmniSharp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ @@ -69,7 +68,7 @@ class OmniSharp(LanguageServer): slnfilename = find_least_depth_sln_file(repository_root_path) if slnfilename is None: logger.log("No *.sln file found in repository", logging.ERROR) - raise MultilspyException("No SLN file found in repository") + raise LanguageServerException("No SLN file found in repository") cmd = " ".join( [ @@ -103,13 +102,12 @@ class OmniSharp(LanguageServer): "formattingOptions:indentationSize=4", ] ) - super().__init__( - config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp" - ) + super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp") + + self.server_ready = threading.Event() + self.definition_available = threading.Event() + self.references_available = threading.Event() - self.definition_available = asyncio.Event() - self.references_available = asyncio.Event() - @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj"] @@ -118,7 +116,7 @@ class OmniSharp(LanguageServer): """ Returns the initialize params for the Omnisharp Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -138,14 +136,14 @@ class OmniSharp(LanguageServer): return d - def setupRuntimeDependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> tuple[str, str]: + def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]: """ Setup runtime dependencies for OmniSharp. """ platform_id = PlatformUtils.get_platform_id() dotnet_version = PlatformUtils.get_dotnet_version() - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -156,7 +154,7 @@ class OmniSharp(LanguageServer): assert dotnet_version in [ DotnetVersion.V6, DotnetVersion.V7, - DotnetVersion.V8 + DotnetVersion.V8, ], "Only dotnet version 6 and 7 are supported in multilspy at the moment" # TODO: Do away with this assumption @@ -165,13 +163,11 @@ class OmniSharp(LanguageServer): dotnet_version = DotnetVersion.V6 runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] + runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] runtime_dependencies = [ dependency for dependency in runtime_dependencies - if not ("dotnet_version" in dependency) or dependency["dotnet_version"] == dotnet_version.value + if "dotnet_version" not in dependency or dependency["dotnet_version"] == dotnet_version.value ] assert len(runtime_dependencies) == 2 runtime_dependencies = { @@ -185,9 +181,7 @@ class OmniSharp(LanguageServer): omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "OmniSharp") if not os.path.exists(omnisharp_ls_dir): os.makedirs(omnisharp_ls_dir) - FileUtils.download_and_extract_archive( - logger, runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip" - ) + FileUtils.download_and_extract_archive(logger, runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip") omnisharp_executable_path = os.path.join(omnisharp_ls_dir, runtime_dependencies["OmniSharp"]["binaryName"]) assert os.path.exists(omnisharp_executable_path) os.chmod(omnisharp_executable_path, stat.S_IEXEC) @@ -195,32 +189,18 @@ class OmniSharp(LanguageServer): razor_omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "RazorOmnisharp") if not os.path.exists(razor_omnisharp_ls_dir): os.makedirs(razor_omnisharp_ls_dir) - FileUtils.download_and_extract_archive( - logger, runtime_dependencies["RazorOmnisharp"]["url"], razor_omnisharp_ls_dir, "zip" - ) - razor_omnisharp_dll_path = os.path.join( - razor_omnisharp_ls_dir, runtime_dependencies["RazorOmnisharp"]["dll_path"] - ) + FileUtils.download_and_extract_archive(logger, runtime_dependencies["RazorOmnisharp"]["url"], razor_omnisharp_ls_dir, "zip") + razor_omnisharp_dll_path = os.path.join(razor_omnisharp_ls_dir, runtime_dependencies["RazorOmnisharp"]["dll_path"]) assert os.path.exists(razor_omnisharp_dll_path) return omnisharp_executable_path, razor_omnisharp_dll_path - @asynccontextmanager - async def start_server(self) -> AsyncIterator["OmniSharp"]: + def _start_server(self): """ - Starts the Omnisharp Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown + Starts the Omnisharp Language Server """ - async def register_capability_handler(params): + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "textDocument/definition": @@ -230,7 +210,7 @@ class OmniSharp(LanguageServer): if registration["method"] == "textDocument/completion": self.completions_available.set() - async def lang_status_handler(params): + def lang_status_handler(params): # TODO: Should we wait for # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} # Before proceeding? @@ -238,20 +218,20 @@ class OmniSharp(LanguageServer): # self.service_ready_event.set() pass - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def check_experimental_status(params): + def check_experimental_status(params): if params["quiescent"] == True: self.server_ready.set() - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - async def workspace_configuration_handler(params): + def workspace_configuration_handler(params): # TODO: We do not know the appropriate way to handle this request. Should ideally contact the OmniSharp dev team return [ { @@ -373,37 +353,23 @@ class OmniSharp(LanguageServer): self.server.on_notification("experimental/serverStatus", check_experimental_status) self.server.on_request("workspace/configuration", workspace_configuration_handler) - async with super().start_server(): - self.logger.log("Starting OmniSharp server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting OmniSharp server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = await self.server.send.initialize(initialize_params) - self.server.notify.initialized({}) - with open(os.path.join(os.path.dirname(__file__), "workspace_did_change_configuration.json"), "r", encoding="utf-8") as f: - self.server.notify.workspace_did_change_configuration({ - "settings": json.load(f) - }) - assert "capabilities" in init_response - if ( - "definitionProvider" in init_response["capabilities"] - and init_response["capabilities"]["definitionProvider"] - ): - self.definition_available.set() - if ( - "referencesProvider" in init_response["capabilities"] - and init_response["capabilities"]["referencesProvider"] - ): - self.references_available.set() + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + self.server.notify.initialized({}) + with open(os.path.join(os.path.dirname(__file__), "workspace_did_change_configuration.json"), encoding="utf-8") as f: + self.server.notify.workspace_did_change_configuration({"settings": json.load(f)}) + assert "capabilities" in init_response + if "definitionProvider" in init_response["capabilities"] and init_response["capabilities"]["definitionProvider"]: + self.definition_available.set() + if "referencesProvider" in init_response["capabilities"] and init_response["capabilities"]["referencesProvider"]: + self.references_available.set() - await self.definition_available.wait() - await self.references_available.wait() - - yield self - - await self.server.shutdown() - await self.server.stop() + self.definition_available.wait() + self.references_available.wait() diff --git a/src/multilspy/language_servers/omnisharp/runtime_dependencies.json b/src/solidlsp/language_servers/omnisharp/runtime_dependencies.json similarity index 100% rename from src/multilspy/language_servers/omnisharp/runtime_dependencies.json rename to src/solidlsp/language_servers/omnisharp/runtime_dependencies.json diff --git a/src/multilspy/language_servers/omnisharp/workspace_did_change_configuration.json b/src/solidlsp/language_servers/omnisharp/workspace_did_change_configuration.json similarity index 100% rename from src/multilspy/language_servers/omnisharp/workspace_did_change_configuration.json rename to src/solidlsp/language_servers/omnisharp/workspace_did_change_configuration.json diff --git a/src/multilspy/language_servers/pyright_language_server/pyright_server.py b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py similarity index 62% rename from src/multilspy/language_servers/pyright_language_server/pyright_server.py rename to src/solidlsp/language_servers/pyright_language_server/pyright_server.py index cd6925e..893db94 100644 --- a/src/multilspy/language_servers/pyright_language_server/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py @@ -2,28 +2,28 @@ Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ -import json import logging import os import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator, Tuple +import re +import threading from overrides import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -class PyrightServer(LanguageServer): +class PyrightServer(SolidLanguageServer): """ Provides Python specific instantiation of the LanguageServer class using Pyright. Contains various configurations and settings specific to Python. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a PyrightServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. @@ -37,7 +37,11 @@ class PyrightServer(LanguageServer): ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path), "python", ) - + + # Event to signal when initial workspace analysis is complete + self.analysis_complete = threading.Event() + self.found_source_files = False + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] @@ -47,7 +51,7 @@ class PyrightServer(LanguageServer): Returns the initialize params for the Pyright Language Server. """ # Create basic initialization parameters - initialize_params: InitializeParams = { # type: ignore + initialize_params: InitializeParams = { # type: ignore "processId": os.getpid(), "rootPath": repository_absolute_path, "rootUri": pathlib.Path(repository_absolute_path).as_uri(), @@ -142,35 +146,57 @@ class PyrightServer(LanguageServer): return initialize_params - @asynccontextmanager - async def start_server(self) -> AsyncIterator["PyrightServer"]: + def _start_server(self): """ - Starts the Pyright Language Server, waits for the server to be ready and yields the LanguageServer instance. + Starts the Pyright Language Server and waits for initial workspace analysis to complete. + + This prevents zombie processes by ensuring Pyright has finished its initial background + tasks before we consider the server ready. Usage: ``` async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests + # LanguageServer has been initialized and workspace analysis is complete await lsp.request_definition(...) await lsp.request_references(...) # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown + # LanguageServer has been shutdown cleanly ``` """ - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def check_experimental_status(params): - if params["quiescent"] == True: + def window_log_message(msg): + """ + Monitor Pyright's log messages to detect when initial analysis is complete. + Pyright logs "Found X source files" when it finishes scanning the workspace. + """ + message_text = msg.get("message", "") + self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO) + + # Look for "Found X source files" which indicates workspace scanning is complete + # Unfortunately, pyright is unreliable and there seems to be no better way + if re.search(r"Found \d+ source files?", message_text): + self.logger.log("Pyright workspace scanning complete", logging.INFO) + self.found_source_files = True + self.analysis_complete.set() self.completions_available.set() - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + def check_experimental_status(params): + """ + Also listen for experimental/serverStatus as a backup signal + """ + if params.get("quiescent") == True: + self.logger.log("Received experimental/serverStatus with quiescent=true", logging.INFO) + if not self.found_source_files: + self.analysis_complete.set() + self.completions_available.set() + # Set up notification handlers self.server.on_request("client/registerCapability", do_nothing) self.server.on_notification("language/status", do_nothing) self.server.on_notification("window/logMessage", window_log_message) @@ -180,24 +206,34 @@ class PyrightServer(LanguageServer): self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) - async with super().start_server(): - self.logger.log("Starting pyright-langserver server process", logging.INFO) - await self.server.start() + self.logger.log("Starting pyright-langserver server process", logging.INFO) + self.server.start() - # Send proper initialization parameters - initialize_params = self._get_initialize_params(self.repository_root_path) + # Send proper initialization parameters + initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request from LSP client to pyright server and awaiting response", - logging.INFO, - ) - init_response = await self.server.send.initialize(initialize_params) - self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) + self.logger.log( + "Sending initialize request from LSP client to pyright server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) - # Verify that the server supports our required features - self.server.notify.initialized({}) + # Verify that the server supports our required features + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] - yield self + # Complete the initialization handshake + self.server.notify.initialized({}) - await self.server.shutdown() - await self.server.stop() + # Wait for Pyright to complete its initial workspace analysis + # This prevents zombie processes by ensuring background tasks finish + self.logger.log("Waiting for Pyright to complete initial workspace analysis...", logging.INFO) + if self.analysis_complete.wait(timeout=5.0): + self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) + else: + self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) + # Fallback: assume analysis is complete after timeout + self.analysis_complete.set() + self.completions_available.set() diff --git a/src/multilspy/language_servers/rust_analyzer/initialize_params.json b/src/solidlsp/language_servers/rust_analyzer/initialize_params.json similarity index 100% rename from src/multilspy/language_servers/rust_analyzer/initialize_params.json rename to src/solidlsp/language_servers/rust_analyzer/initialize_params.json diff --git a/src/multilspy/language_servers/rust_analyzer/runtime_dependencies.json b/src/solidlsp/language_servers/rust_analyzer/runtime_dependencies.json similarity index 100% rename from src/multilspy/language_servers/rust_analyzer/runtime_dependencies.json rename to src/solidlsp/language_servers/rust_analyzer/runtime_dependencies.json diff --git a/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py similarity index 57% rename from src/multilspy/language_servers/rust_analyzer/rust_analyzer.py rename to src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py index 6a2dcee..1f951e5 100644 --- a/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py +++ b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py @@ -2,32 +2,29 @@ Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. """ -import asyncio import json import logging import os -import stat import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator +import stat +import threading from overrides import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -class RustAnalyzer(LanguageServer): +class RustAnalyzer(SolidLanguageServer): """ Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ @@ -39,19 +36,22 @@ class RustAnalyzer(LanguageServer): ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path), "rust", ) - self.server_ready = asyncio.Event() - + self.server_ready = threading.Event() + self.service_ready_event = threading.Event() + self.initialize_searcher_command_available = threading.Event() + self.resolve_main_method_available = threading.Event() + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["target"] - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ Setup runtime dependencies for rust_analyzer. """ platform_id = PlatformUtils.get_platform_id() - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -61,9 +61,7 @@ class RustAnalyzer(LanguageServer): # ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment" runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] + runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] assert len(runtime_dependencies) == 1 dependency = runtime_dependencies[0] @@ -72,13 +70,9 @@ class RustAnalyzer(LanguageServer): if not os.path.exists(rustanalyzer_ls_dir): os.makedirs(rustanalyzer_ls_dir) if dependency["archiveType"] == "gz": - FileUtils.download_and_extract_archive( - logger, dependency["url"], rustanalyzer_executable_path, dependency["archiveType"] - ) + FileUtils.download_and_extract_archive(logger, dependency["url"], rustanalyzer_executable_path, dependency["archiveType"]) else: - FileUtils.download_and_extract_archive( - logger, dependency["url"], rustanalyzer_ls_dir, dependency["archiveType"] - ) + FileUtils.download_and_extract_archive(logger, dependency["url"], rustanalyzer_ls_dir, dependency["archiveType"]) assert os.path.exists(rustanalyzer_executable_path) os.chmod(rustanalyzer_executable_path, stat.S_IEXEC) @@ -88,7 +82,7 @@ class RustAnalyzer(LanguageServer): """ Returns the initialize params for the Rust Analyzer Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -108,22 +102,12 @@ class RustAnalyzer(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["RustAnalyzer"]: + def _start_server(self): """ - Starts the Rust Analyzer Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown + Starts the Rust Analyzer Language Server """ - async def register_capability_handler(params): + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "workspace/executeCommand": @@ -131,24 +115,24 @@ class RustAnalyzer(LanguageServer): self.resolve_main_method_available.set() return - async def lang_status_handler(params): + def lang_status_handler(params): # TODO: Should we wait for # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} # Before proceeding? if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": self.service_ready_event.set() - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def check_experimental_status(params): + def check_experimental_status(params): if params["quiescent"] == True: self.server_ready.set() - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", register_capability_handler) @@ -160,29 +144,23 @@ class RustAnalyzer(LanguageServer): self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) - async with super().start_server(): - self.logger.log("Starting RustAnalyzer server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting RustAnalyzer server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = await self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "resolveProvider": True, - "triggerCharacters": [":", ".", "'", "("], - "completionItem": {"labelDetailsSupport": True}, - } - self.server.notify.initialized({}) - self.completions_available.set() + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "resolveProvider": True, + "triggerCharacters": [":", ".", "'", "("], + "completionItem": {"labelDetailsSupport": True}, + } + self.server.notify.initialized({}) + self.completions_available.set() - await self.server_ready.wait() - - yield self - - await self.server.shutdown() - await self.server.stop() + self.server_ready.wait() diff --git a/src/multilspy/language_servers/solargraph/initialize_params.json b/src/solidlsp/language_servers/solargraph/initialize_params.json similarity index 100% rename from src/multilspy/language_servers/solargraph/initialize_params.json rename to src/solidlsp/language_servers/solargraph/initialize_params.json diff --git a/src/multilspy/language_servers/solargraph/runtime_dependencies.json b/src/solidlsp/language_servers/solargraph/runtime_dependencies.json similarity index 100% rename from src/multilspy/language_servers/solargraph/runtime_dependencies.json rename to src/solidlsp/language_servers/solargraph/runtime_dependencies.json diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/solidlsp/language_servers/solargraph/solargraph.py similarity index 61% rename from src/multilspy/language_servers/solargraph/solargraph.py rename to src/solidlsp/language_servers/solargraph/solargraph.py index 645f40a..116e04b 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/solidlsp/language_servers/solargraph/solargraph.py @@ -3,32 +3,29 @@ Provides Ruby specific instantiation of the LanguageServer class using Solargrap Contains various configurations and settings specific to Ruby. """ -import asyncio import json import logging import os +import pathlib import stat import subprocess -import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator, override +import threading +from typing import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils, PlatformId +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -class Solargraph(LanguageServer): +class Solargraph(SolidLanguageServer): """ Provides Ruby specific instantiation of the LanguageServer class using Solargraph. Contains various configurations and settings specific to Ruby. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a Solargraph instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. @@ -41,18 +38,20 @@ class Solargraph(LanguageServer): ProcessLaunchInfo(cmd=f"{solargraph_executable_path} stdio", cwd=repository_root_path), "ruby", ) - self.server_ready = asyncio.Event() - + self.server_ready = threading.Event() + self.service_ready_event = threading.Event() + self.initialize_searcher_command_available = threading.Event() + self.resolve_main_method_available = threading.Event() + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["vendor"] - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig, repository_root_path: str) -> str: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig, repository_root_path: str) -> str: """ Setup runtime dependencies for Solargraph. """ - - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -64,38 +63,40 @@ class Solargraph(LanguageServer): ruby_version = result.stdout.strip() logger.log(f"Ruby version: {ruby_version}", logging.INFO) except subprocess.CalledProcessError as e: - raise RuntimeError(f"Error checking for Ruby installation: {e.stderr}") - except FileNotFoundError: - raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") + raise RuntimeError(f"Error checking for Ruby installation: {e.stderr}") from e + except FileNotFoundError as e: + raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") from e # Check if solargraph is installed try: - result = subprocess.run(["gem", "list", "^solargraph$", "-i"], check=False, capture_output=True, text=True, cwd=repository_root_path) + result = subprocess.run( + ["gem", "list", "^solargraph$", "-i"], check=False, capture_output=True, text=True, cwd=repository_root_path + ) if result.stdout.strip() == "false": logger.log("Installing Solargraph...", logging.INFO) subprocess.run(dependency["installCommand"].split(), check=True, capture_output=True, cwd=repository_root_path) - + # Get the gem executable path directly result = subprocess.run(["gem", "which", "solargraph"], check=True, capture_output=True, text=True, cwd=repository_root_path) gem_path = result.stdout.strip() bin_dir = os.path.join(os.path.dirname(os.path.dirname(gem_path)), "bin") executable_path = os.path.join(bin_dir, "solargraph") - + if not os.path.exists(executable_path): raise RuntimeError(f"Solargraph executable not found at {executable_path}") - + # Ensure the executable has the right permissions os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IEXEC) return executable_path except subprocess.CalledProcessError as e: - raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") + raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") from e def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Solargraph Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -115,22 +116,12 @@ class Solargraph(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["Solargraph"]: + def _start_server(self): """ - Starts the Solargraph Language Server for Ruby, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown + Starts the Solargraph Language Server for Ruby """ - async def register_capability_handler(params): + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "workspace/executeCommand": @@ -138,20 +129,20 @@ class Solargraph(LanguageServer): self.resolve_main_method_available.set() return - async def lang_status_handler(params): + def lang_status_handler(params): # TODO: Should we wait for # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} # Before proceeding? if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": self.service_ready_event.set() - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", register_capability_handler) @@ -162,31 +153,25 @@ class Solargraph(LanguageServer): self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_notification("language/actionableNotification", do_nothing) - async with super().start_server(): - self.logger.log("Starting solargraph server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting solargraph server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - self.logger.log(f"Sending init params: {json.dumps(initialize_params, indent=4)}", logging.INFO) - init_response = await self.server.send.initialize(initialize_params) - self.logger.log(f"Received init response: {init_response}", logging.INFO) - assert init_response["capabilities"]["textDocumentSync"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "resolveProvider": True, - "triggerCharacters": [".", ":", "@"], - } - self.server.notify.initialized({}) - self.completions_available.set() + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + self.logger.log(f"Sending init params: {json.dumps(initialize_params, indent=4)}", logging.INFO) + init_response = self.server.send.initialize(initialize_params) + self.logger.log(f"Received init response: {init_response}", logging.INFO) + assert init_response["capabilities"]["textDocumentSync"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "resolveProvider": True, + "triggerCharacters": [".", ":", "@"], + } + self.server.notify.initialized({}) + self.completions_available.set() - self.server_ready.set() - await self.server_ready.wait() - - yield self - - await self.server.shutdown() - await self.server.stop() + self.server_ready.set() + self.server_ready.wait() diff --git a/src/multilspy/language_servers/typescript_language_server/initialize_params.json b/src/solidlsp/language_servers/typescript_language_server/initialize_params.json similarity index 100% rename from src/multilspy/language_servers/typescript_language_server/initialize_params.json rename to src/solidlsp/language_servers/typescript_language_server/initialize_params.json diff --git a/src/multilspy/language_servers/typescript_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/typescript_language_server/runtime_dependencies.json similarity index 100% rename from src/multilspy/language_servers/typescript_language_server/runtime_dependencies.json rename to src/solidlsp/language_servers/typescript_language_server/runtime_dependencies.json diff --git a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py similarity index 68% rename from src/multilspy/language_servers/typescript_language_server/typescript_language_server.py rename to src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py index 6657ebb..f220406 100644 --- a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py @@ -2,35 +2,33 @@ Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. """ -import asyncio import json import logging import os import pathlib import shutil import subprocess -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager +import threading from time import sleep from overrides import override -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import PlatformId, PlatformUtils +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import PlatformId, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo # Platform-specific imports -if os.name != 'nt': # Unix-like systems +if os.name != "nt": # Unix-like systems import pwd else: # Dummy pwd module for Windows class pwd: @staticmethod def getpwuid(uid): - return type('obj', (), {'pw_name': os.environ.get('USERNAME', 'unknown')})() + return type("obj", (), {"pw_name": os.environ.get("USERNAME", "unknown")})() # Conditionally import pwd module (Unix-only) @@ -38,12 +36,12 @@ if not PlatformUtils.get_platform_id().value.startswith("win"): import pwd -class TypeScriptLanguageServer(LanguageServer): +class TypeScriptLanguageServer(SolidLanguageServer): """ Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ @@ -55,7 +53,8 @@ class TypeScriptLanguageServer(LanguageServer): ProcessLaunchInfo(cmd=ts_lsp_executable_path, cwd=repository_root_path), "typescript", ) - self.server_ready = asyncio.Event() + self.server_ready = threading.Event() + self.initialize_searcher_command_available = threading.Event() @override def is_ignored_dirname(self, dirname: str) -> bool: @@ -66,7 +65,7 @@ class TypeScriptLanguageServer(LanguageServer): "coverage", ] - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ Setup runtime dependencies for TypeScript Language Server. """ @@ -92,9 +91,9 @@ class TypeScriptLanguageServer(LanguageServer): tsserver_executable_path = os.path.join(tsserver_ls_dir, "typescript-language-server") # Verify both node and npm are installed - is_node_installed = shutil.which('node') is not None + is_node_installed = shutil.which("node") is not None assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." - is_npm_installed = shutil.which('npm') is not None + is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." # Install typescript and typescript-language-server if not already installed @@ -109,7 +108,7 @@ class TypeScriptLanguageServer(LanguageServer): check=True, cwd=tsserver_ls_dir, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) else: # On Unix-like systems, run as non-root user @@ -121,12 +120,14 @@ class TypeScriptLanguageServer(LanguageServer): user=user, cwd=tsserver_ls_dir, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) - + tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server") - assert os.path.exists(tsserver_executable_path), "typescript-language-server executable not found. Please install typescript-language-server and try again." + assert os.path.exists( + tsserver_executable_path + ), "typescript-language-server executable not found. Please install typescript-language-server and try again." return f"{tsserver_executable_path} --stdio" def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: @@ -153,8 +154,7 @@ class TypeScriptLanguageServer(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["TypeScriptLanguageServer"]: + def _start_server(self): """ Starts the TypeScript Language Server, waits for the server to be ready and yields the LanguageServer instance. @@ -168,7 +168,7 @@ class TypeScriptLanguageServer(LanguageServer): # LanguageServer has been shutdown """ - async def register_capability_handler(params): + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "workspace/executeCommand": @@ -178,59 +178,64 @@ class TypeScriptLanguageServer(LanguageServer): # self.resolve_main_method_available.set() return - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + def check_experimental_status(params): + """ + Also listen for experimental/serverStatus as a backup signal + """ + if params.get("quiescent") == True: + self.server_ready.set() + self.completions_available.set() + self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) - async with super().start_server(): - self.logger.log("Starting TypeScript server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting TypeScript server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) - self.logger.log( - "Sending initialize request from LSP client to LSP server and awaiting response", - logging.INFO, - ) - init_response = await self.server.send.initialize(initialize_params) + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) - # TypeScript-specific capability checks - assert init_response["capabilities"]["textDocumentSync"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": ['.', '"', "'", '/', '@', '<'], - "resolveProvider": True - } + # TypeScript-specific capability checks + assert init_response["capabilities"]["textDocumentSync"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": [".", '"', "'", "/", "@", "<"], + "resolveProvider": True, + } - self.server.notify.initialized({}) - self.completions_available.set() - - # TypeScript server is typically ready immediately after initialization + self.server.notify.initialized({}) + if self.server_ready.wait(timeout=1.0): + self.logger.log("TypeScript server is ready", logging.INFO) + else: + self.logger.log("Timeout waiting for TypeScript server to become ready, proceeding anyway", logging.INFO) + # Fallback: assume server is ready after timeout self.server_ready.set() - await self.server_ready.wait() - - yield self - - await self.server.shutdown() - await self.server.stop() + self.completions_available.set() @override # For some reason, the LS may need longer to process this, so we just retry - async def _send_references_request(self, relative_file_path: str, line: int, column: int): + def _send_references_request(self, relative_file_path: str, line: int, column: int): # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is # despite the LS having processed requests already. I don't know what causes this, but sleeping # one second helps. It may be that sleeping only once is enough but that's hard to reliably test. # It may be that even this 1sec is not enough in larger TS projects, at some point we should find what # causes this and solve it. sleep(1) - return await super()._send_references_request(relative_file_path, line, column) + return super()._send_references_request(relative_file_path, line, column) diff --git a/src/multilspy/language_server.py b/src/solidlsp/ls.py similarity index 53% rename from src/multilspy/language_server.py rename to src/solidlsp/ls.py index 0fc968f..c26e32b 100644 --- a/src/multilspy/language_server.py +++ b/src/solidlsp/ls.py @@ -1,11 +1,3 @@ -""" -This file contains the main interface and the public API for multilspy. -The abstract class LanguageServer provides a factory method, creator that is -intended for creating instantiations of language specific clients. -The details of Language Specific configuration are not exposed to the user. -""" - -import asyncio import dataclasses import hashlib import json @@ -15,51 +7,46 @@ import pathlib import pickle import re import threading +from abc import ABC, abstractmethod from collections import defaultdict -from contextlib import asynccontextmanager, contextmanager +from collections.abc import Iterator +from contextlib import contextmanager from copy import copy from pathlib import Path, PurePath -from typing import AsyncIterator, Callable, Dict, Iterator, List, Optional, Tuple, Union, cast +from typing import Self, Union, cast import pathspec +import tqdm -from . import multilspy_types -from .lsp_protocol_handler import lsp_types as LSPTypes -from .lsp_protocol_handler.lsp_constants import LSPConstants -from .lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind -from .lsp_protocol_handler.server import ( +from serena.text_utils import MatchedConsecutiveLines, search_files +from solidlsp import ls_types +from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_handler import SolidLanguageServerHandler +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PathUtils, TextUtils +from solidlsp.lsp_protocol_handler import lsp_types +from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes +from solidlsp.lsp_protocol_handler.lsp_constants import LSPConstants +from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind +from solidlsp.lsp_protocol_handler.server import ( Error, - LanguageServerHandler, ProcessLaunchInfo, StringDict, ) -from .multilspy_config import Language, MultilspyConfig -from .multilspy_exceptions import MultilspyException -from .multilspy_logger import MultilspyLogger -from .multilspy_utils import FileUtils, PathUtils, TextUtils -from .type_helpers import ensure_all_methods_implemented -from .lsp_protocol_handler import lsp_types -# 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. -# It does however make sense to integrate many text-related utils into the language server -# since it caches (in-memory) file contents, so we can avoid reading from disk. -# Moreover, the way we want to use the language server (for retrieving actual content), -# it makes sense to have more content-related utils directly in it. -from serena.text_utils import LineType, MatchedConsecutiveLines, TextLine, search_files +GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, ls_types.UnifiedSymbolInformation] - -GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, multilspy_types.UnifiedSymbolInformation] - @dataclasses.dataclass(kw_only=True) class ReferenceInSymbol: """A symbol retrieved when requesting reference to a symbol, together with the location of the reference""" - symbol: multilspy_types.UnifiedSymbolInformation + + symbol: ls_types.UnifiedSymbolInformation line: int character: int - + + @dataclasses.dataclass class LSPFileBuffer: """ @@ -81,15 +68,13 @@ class LSPFileBuffer: # reference count of the file ref_count: int - # --------------------------------- MODIFICATIONS BY MISCHA --------------------------------- - content_hash: str = "" def __post_init__(self): - self.content_hash = hashlib.md5(self.contents.encode('utf-8')).hexdigest() + self.content_hash = hashlib.md5(self.contents.encode("utf-8")).hexdigest() -class LanguageServer: +class SolidLanguageServer(ABC): """ The LanguageServer class provides a language agnostic interface to the Language Server Protocol. It is used to communicate with Language Servers of different programming languages. @@ -101,10 +86,12 @@ class LanguageServer: A language-specific condition for directories that should always be ignored. For example, venv in Python and node_modules in JS/TS should be ignored always. """ - return dirname.startswith('.') + return dirname.startswith(".") @classmethod - def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, add_gitignore_content_to_config: bool = True) -> "LanguageServer": + def create( + cls, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, timeout: float | None = None + ) -> "SolidLanguageServer": """ Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. @@ -114,101 +101,92 @@ class LanguageServer: :param repository_root_path: The root path of the repository. :param config: The Multilspy configuration. :param logger: The logger to use. - :param add_gitignore_content_to_config: whether to add the content of the .gitignore file (if any found) to the config, so that - the paths ignored there are also ignored by the language server - + :param timeout: the timeout for requests to the language server. If None, no timeout will be used. :return LanguageServer: A language specific LanguageServer instance. """ - config = copy(config) # prevent mutation - if add_gitignore_content_to_config: - gitignore_path = os.path.join(repository_root_path, ".gitignore") - if not os.path.exists(gitignore_path): - logger.log( - f"Should ignore all files in gitignore not not .gitignore found at {gitignore_path}. Skipping.", - logging.WARNING - ) - gitignore_file_content = None - else: - if config.gitignore_file_content is not None: - raise ValueError( - f"Asked to add gitignore content to the config for {repository_root_path=} but there already is a non-empty entry" - ) - with open(gitignore_path, "r", encoding="utf-8") as f: - gitignore_file_content = f.read() - config.gitignore_file_content = gitignore_file_content + ls: SolidLanguageServer if config.code_language == Language.PYTHON: - from multilspy.language_servers.pyright_language_server.pyright_server import ( + from solidlsp.language_servers.pyright_language_server.pyright_server import ( PyrightServer, ) - return PyrightServer(config, logger, repository_root_path) - # It used to be jedi, but pyright is a bit faster, and also more actively maintained - # Keeping the previous code for reference - # from multilspy.language_servers.jedi_language_server.jedi_server import ( - # JediServer, - # ) + ls = PyrightServer(config, logger, repository_root_path) - # return JediServer(config, logger, repository_root_path) elif config.code_language == Language.JAVA: - from multilspy.language_servers.eclipse_jdtls.eclipse_jdtls import ( + from solidlsp.language_servers.eclipse_jdtls.eclipse_jdtls import ( EclipseJDTLS, ) - return EclipseJDTLS(config, logger, repository_root_path) + ls = EclipseJDTLS(config, logger, repository_root_path) + elif config.code_language == Language.KOTLIN: - from multilspy.language_servers.kotlin_language_server.kotlin_language_server import ( + from solidlsp.language_servers.kotlin_language_server.kotlin_language_server import ( KotlinLanguageServer, ) - return KotlinLanguageServer(config, logger, repository_root_path) + ls = KotlinLanguageServer(config, logger, repository_root_path) + elif config.code_language == Language.RUST: - from multilspy.language_servers.rust_analyzer.rust_analyzer import ( + from solidlsp.language_servers.rust_analyzer.rust_analyzer import ( RustAnalyzer, ) - return RustAnalyzer(config, logger, repository_root_path) - elif config.code_language == Language.CSHARP: - from multilspy.language_servers.omnisharp.omnisharp import OmniSharp + ls = RustAnalyzer(config, logger, repository_root_path) - return OmniSharp(config, logger, repository_root_path) - elif config.code_language in [Language.TYPESCRIPT, Language.JAVASCRIPT]: - from multilspy.language_servers.typescript_language_server.typescript_language_server import ( + elif config.code_language == Language.CSHARP: + from solidlsp.language_servers.omnisharp.omnisharp import OmniSharp + + ls = OmniSharp(config, logger, repository_root_path) + + elif config.code_language == Language.TYPESCRIPT: + from solidlsp.language_servers.typescript_language_server.typescript_language_server import ( TypeScriptLanguageServer, ) - return TypeScriptLanguageServer(config, logger, repository_root_path) + + ls = TypeScriptLanguageServer(config, logger, repository_root_path) + elif config.code_language == Language.GO: - from multilspy.language_servers.gopls.gopls import Gopls + from solidlsp.language_servers.gopls.gopls import Gopls + + ls = Gopls(config, logger, repository_root_path) - return Gopls(config, logger, repository_root_path) elif config.code_language == Language.RUBY: - from multilspy.language_servers.solargraph.solargraph import Solargraph + from solidlsp.language_servers.solargraph.solargraph import Solargraph + + ls = Solargraph(config, logger, repository_root_path) - return Solargraph(config, logger, repository_root_path) elif config.code_language == Language.DART: - from multilspy.language_servers.dart_language_server.dart_language_server import DartLanguageServer + from solidlsp.language_servers.dart_language_server.dart_language_server import DartLanguageServer + + ls = DartLanguageServer(config, logger, repository_root_path) - return DartLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.CPP: - from multilspy.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer + from solidlsp.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer + + ls = ClangdLanguageServer(config, logger, repository_root_path) - return ClangdLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.PHP: - from multilspy.language_servers.intelephense.intelephense import Intelephense + from solidlsp.language_servers.intelephense.intelephense import Intelephense + + ls = Intelephense(config, logger, repository_root_path) - return Intelephense(config, logger, repository_root_path) elif config.code_language == Language.CLOJURE: from multilspy.language_servers.clojure_lsp.clojure_lsp import ClojureLSP - return ClojureLSP(config, logger, repository_root_path) + ls = ClojureLSP(config, logger, repository_root_path) + else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) - raise MultilspyException(f"Language {config.code_language} is not supported") + raise LanguageServerException(f"Language {config.code_language} is not supported") + + ls.set_request_timeout(timeout) + return ls def __init__( self, - config: MultilspyConfig, - logger: MultilspyLogger, + config: LanguageServerConfig, + logger: LanguageServerLogger, repository_root_path: str, process_launch_info: ProcessLaunchInfo, language_id: str, @@ -226,63 +204,71 @@ class LanguageServer: The command must pass appropriate flags to the binary, so that it runs in the stdio mode, as opposed to HTTP, TCP modes supported by some language servers. """ - if type(self) == LanguageServer: - raise MultilspyException( - "LanguageServer is an abstract class and cannot be instantiated directly. Use LanguageServer.create method instead." - ) - self.logger = logger - self.server_started = False self.repository_root_path: str = repository_root_path - self.completions_available = asyncio.Event() + self.logger.log( + f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}", + logging.DEBUG, + ) + self.language_id = language_id + self.open_file_buffers: dict[str, LSPFileBuffer] = {} + self.language = Language(language_id) + + # load cache first to prevent any racing conditions due to asyncio stuff + self._document_symbols_cache: dict[ + str, tuple[str, tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]] + ] = {} + """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" + self._cache_lock = threading.Lock() + self._cache_has_changed: bool = False + self.load_cache() + + self.server_started = False + self.completions_available = threading.Event() if config.trace_lsp_communication: + def logging_fn(source: str, target: str, msg: StringDict | str): - self.logger.log(f"LSP: {source} -> {target}: {str(msg)}", logging.DEBUG) + self.logger.log(f"LSP: {source} -> {target}: {str(msg)[:90]}...", self.logger.logger.level) + else: logging_fn = None - # cmd is obtained from the child classes, which provide the language specific command to start the language server # LanguageServerHandler provides the functionality to start the language server and communicate with it - self.server = LanguageServerHandler( + self.logger.log( + f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG + ) + self.server = SolidLanguageServerHandler( process_launch_info, logger=logging_fn, start_independent_lsp_process=config.start_independent_lsp_process, ) - self.language_id = language_id - self.open_file_buffers: Dict[str, LSPFileBuffer] = {} - - # --------------------------------- MODIFICATIONS BY ORAIOS --------------------------------- - self._document_symbols_cache: dict[str, Tuple[str, Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]]] = {} - """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" - self.load_cache() - self._cache_has_changed: bool = False - self.language = Language(language_id) - # Set up the pathspec matcher for the ignored paths # for all absolute paths in ignored_paths, convert them to relative paths processed_patterns = [] for pattern in set(config.ignored_paths): # Normalize separators (pathspec expects forward slashes) - pattern = pattern.replace(os.path.sep, '/') + pattern = pattern.replace(os.path.sep, "/") processed_patterns.append(pattern) - # Combine explicitly passed patterns with the content of the .gitignore file - if config.gitignore_file_content is not None: - for line in config.gitignore_file_content.splitlines(): - if not line.startswith('#') and line.strip() != '': - processed_patterns.append(line.strip()) + self.logger.log(f"Processing {len(processed_patterns)} ignored paths from the config", logging.DEBUG) # Create a pathspec matcher from the processed patterns - self._ignore_spec = pathspec.PathSpec.from_lines( - pathspec.patterns.GitWildMatchPattern, - processed_patterns - ) + self._ignore_spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, processed_patterns) + + self._server_context = None + self._request_timeout: float | None = None + + def set_request_timeout(self, timeout: float | None) -> None: + """ + :param timeout: the timeout, in seconds, for requests to the language server. + """ + self.server.set_request_timeout(timeout) def get_ignore_spec(self) -> pathspec.PathSpec: """Returns the pathspec matcher for the paths that were configured to be ignored through - the multilspy config file and the .gitignore file. + the multilspy config. This is is a subset of the full language-specific ignore spec that determines which files are relevant for the language server. @@ -328,40 +314,57 @@ class LanguageServer: # Use pathspec for gitignore-style pattern matching # Normalize path separators for pathspec (it expects forward slashes) - normalized_path = str(rel_path).replace(os.path.sep, '/') + normalized_path = str(rel_path).replace(os.path.sep, "/") # pathspec can't handle the matching of directories if they don't end with a slash! # see https://github.com/cpburnz/python-pathspec/issues/89 - if os.path.isdir(os.path.join(self.repository_root_path, normalized_path)) and not normalized_path.endswith('/'): - normalized_path = normalized_path + '/' + if os.path.isdir(os.path.join(self.repository_root_path, normalized_path)) and not normalized_path.endswith("/"): + normalized_path = normalized_path + "/" # Use the pathspec matcher to check if the path matches any ignore pattern - if self._ignore_spec.match_file(normalized_path): + if self.get_ignore_spec().match_file(normalized_path): return True return False - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["LanguageServer"]: + def _shutdown(self, timeout: float = 5.0): """ - Starts the Language Server and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - ``` + A robust shutdown process designed to terminate cleanly on all platforms, including Windows, + by explicitly closing all I/O pipes. """ - self.server_started = True + if not self.server.is_running(): + self.logger.log("Server process not running, skipping shutdown.", logging.DEBUG) + return + + self.logger.log(f"Initiating final robust shutdown with a {timeout}s timeout...", logging.INFO) + process = self.server.process + + # --- Main Shutdown Logic --- + # Stage 1: Graceful Termination Request + # Send LSP shutdown and close stdin to signal no more input. + try: + self.server.shutdown() + if process.stdin and not process.stdin.is_closing(): + process.stdin.close() + except Exception: + pass # Ignore errors here, we are proceeding to terminate anyway. + + # Stage 2: Terminate and Concurrently Drain stdout/stderr + process.terminate() + + @contextmanager + def start_server(self) -> Iterator["SolidLanguageServer"]: + self.start() yield self - self.server_started = False + self.stop() - # TODO: Add support for more LSP features + def _start_server_process(self) -> None: + self.server_started = True + self._start_server() + + @abstractmethod + def _start_server(self): + pass @contextmanager def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]: @@ -375,7 +378,7 @@ class LanguageServer: "open_file called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) uri = pathlib.Path(absolute_file_path).as_uri() @@ -416,11 +419,9 @@ class LanguageServer: ) del self.open_file_buffers[uri] - def insert_text_at_position( - self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str - ) -> multilspy_types.Position: + def insert_text_at_position(self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str) -> ls_types.Position: """ - Insert text at the given line and column in the given file and return + Insert text at the given line and column in the given file and return the updated cursor position after inserting the text. :param relative_file_path: The relative path of the file to open. @@ -433,7 +434,7 @@ class LanguageServer: "insert_text_at_position called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) uri = pathlib.Path(absolute_file_path).as_uri() @@ -463,13 +464,13 @@ class LanguageServer: ], } ) - return multilspy_types.Position(line=new_l, character=new_c) + return ls_types.Position(line=new_l, character=new_c) def delete_text_between_positions( self, relative_file_path: str, - start: multilspy_types.Position, - end: multilspy_types.Position, + start: ls_types.Position, + end: ls_types.Position, ) -> str: """ Delete text between the given start and end positions in the given file and return the deleted text. @@ -479,7 +480,7 @@ class LanguageServer: "insert_text_at_position called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) uri = pathlib.Path(absolute_file_path).as_uri() @@ -489,7 +490,9 @@ class LanguageServer: file_buffer = self.open_file_buffers[uri] file_buffer.version += 1 - new_contents, deleted_text = TextUtils.delete_text_between_positions(file_buffer.contents, start_line=start["line"], start_col=start["character"], end_line=end["line"], end_col=end["character"]) + new_contents, deleted_text = TextUtils.delete_text_between_positions( + file_buffer.contents, start_line=start["line"], start_col=start["character"], end_line=end["line"], end_col=end["character"] + ) file_buffer.contents = new_contents self.server.notify.did_change_text_document( { @@ -502,12 +505,10 @@ class LanguageServer: ) return deleted_text - async def _send_definition_request(self, definition_params: DefinitionParams) -> Union[Definition, List[LocationLink], None]: - return await self.server.send.definition(definition_params) - - async def request_definition( - self, relative_file_path: str, line: int, column: int - ) -> List[multilspy_types.Location]: + def _send_definition_request(self, definition_params: DefinitionParams) -> Definition | list[LocationLink] | None: + return self.server.send.definition(definition_params) + + def request_definition(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]: """ Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server for the symbol at the given line and column in the given file. Wait for the response and return the result. @@ -518,52 +519,52 @@ class LanguageServer: :return List[multilspy_types.Location]: A list of locations where the symbol is defined """ - if not self.server_started: self.logger.log( "request_definition called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") with self.open_file(relative_file_path): # sending request to the language server and waiting for response - definition_params = cast(DefinitionParams, { - LSPConstants.TEXT_DOCUMENT: { - LSPConstants.URI: pathlib.Path( - str(PurePath(self.repository_root_path, relative_file_path)) - ).as_uri() + definition_params = cast( + DefinitionParams, + { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri() + }, + LSPConstants.POSITION: { + LSPConstants.LINE: line, + LSPConstants.CHARACTER: column, + }, }, - LSPConstants.POSITION: { - LSPConstants.LINE: line, - LSPConstants.CHARACTER: column, - }, - }) - response = await self._send_definition_request(definition_params) + ) + response = self._send_definition_request(definition_params) - ret: List[multilspy_types.Location] = [] + ret: list[ls_types.Location] = [] if isinstance(response, list): # response is either of type Location[] or LocationLink[] for item in response: assert isinstance(item, dict) if LSPConstants.URI in item and LSPConstants.RANGE in item: - new_item: multilspy_types.Location = {} + new_item: ls_types.Location = {} new_item.update(item) new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) - ret.append(multilspy_types.Location(new_item)) + ret.append(ls_types.Location(new_item)) elif ( LSPConstants.ORIGIN_SELECTION_RANGE in item and LSPConstants.TARGET_URI in item and LSPConstants.TARGET_RANGE in item and LSPConstants.TARGET_SELECTION_RANGE in item ): - new_item: multilspy_types.Location = {} + new_item: ls_types.Location = {} new_item["uri"] = item[LSPConstants.TARGET_URI] new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) new_item["range"] = item[LSPConstants.TARGET_SELECTION_RANGE] - ret.append(multilspy_types.Location(**new_item)) + ret.append(ls_types.Location(**new_item)) else: assert False, f"Unexpected response from Language Server: {item}" elif isinstance(response, dict): @@ -571,11 +572,11 @@ class LanguageServer: assert LSPConstants.URI in response assert LSPConstants.RANGE in response - new_item: multilspy_types.Location = {} + new_item: ls_types.Location = {} new_item.update(response) new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) - ret.append(multilspy_types.Location(**new_item)) + ret.append(ls_types.Location(**new_item)) elif response is None: # Some language servers return None when they cannot find a definition # This is expected for certain symbol types like generics or types with incomplete information @@ -589,8 +590,8 @@ class LanguageServer: return ret # Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses - async def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[lsp_types.Location] | None: - return await self.server.send.references( + def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None: + return self.server.send.references( { "textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))}, "position": {"line": line, "character": column}, @@ -598,9 +599,7 @@ class LanguageServer: } ) - async def request_references( - self, relative_file_path: str, line: int, column: int - ) -> List[multilspy_types.Location]: + def request_references(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]: """ Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server to find references to the symbol at the given line and column in the given file. Wait for the response and return the result. @@ -612,21 +611,19 @@ class LanguageServer: :return: A list of locations where the symbol is referenced (excluding ignored directories) """ - if not self.server_started: self.logger.log( "request_references called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") - + raise LanguageServerException("Language Server not started") with self.open_file(relative_file_path): try: - response = await self._send_references_request(relative_file_path, line=line, column=column) + response = self._send_references_request(relative_file_path, line=line, column=column) except Exception as e: # Catch LSP internal error (-32603) and raise a more informative exception - if isinstance(e, Error) and getattr(e, 'code', None) == -32603: + if isinstance(e, Error) and getattr(e, "code", None) == -32603: raise RuntimeError( f"LSP internal error (-32603) when requesting references for {relative_file_path}:{line}:{column}. " "This often occurs when requesting references for a symbol not referenced in the expected way. " @@ -635,7 +632,7 @@ class LanguageServer: if response is None: return [] - ret: List[multilspy_types.Location] = [] + ret: list[ls_types.Location] = [] assert isinstance(response, list), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}" for item in response: assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}" @@ -648,17 +645,17 @@ class LanguageServer: self.logger.log(f"Ignoring reference in {rel_path} since it should be ignored", logging.DEBUG) continue - new_item: multilspy_types.Location = {} + new_item: ls_types.Location = {} new_item.update(item) new_item["absolutePath"] = str(abs_path) new_item["relativePath"] = str(rel_path) - ret.append(multilspy_types.Location(**new_item)) + ret.append(ls_types.Location(**new_item)) return ret - async def request_references_with_content( + def request_references_with_content( self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0 - ) -> List[MatchedConsecutiveLines]: + ) -> list[MatchedConsecutiveLines]: """ Like request_references, but returns the content of the lines containing the references, not just the locations. @@ -670,8 +667,11 @@ class LanguageServer: :return: A list of MatchedConsecutiveLines objects, one for each reference. """ - references = await self.request_references(relative_file_path, line, column) - return [self.retrieve_content_around_line(ref["relativePath"], ref["range"]["start"]["line"], context_lines_before, context_lines_after) for ref in references] + references = self.request_references(relative_file_path, line, column) + return [ + self.retrieve_content_around_line(ref["relativePath"], ref["range"]["start"]["line"], context_lines_before, context_lines_after) + for ref in references + ] def retrieve_full_file_content(self, relative_file_path: str) -> str: """ @@ -680,7 +680,9 @@ class LanguageServer: with self.open_file(relative_file_path) as file_data: return file_data.contents - def retrieve_content_around_line(self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0) -> MatchedConsecutiveLines: + def retrieve_content_around_line( + self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0 + ) -> MatchedConsecutiveLines: """ Retrieve the content of the given file around the given line. @@ -693,12 +695,17 @@ class LanguageServer: """ with self.open_file(relative_file_path) as file_data: file_contents = file_data.contents - return MatchedConsecutiveLines.from_file_contents(file_contents, line=line, context_lines_before=context_lines_before, context_lines_after=context_lines_after, source_file_path=relative_file_path) + return MatchedConsecutiveLines.from_file_contents( + file_contents, + line=line, + context_lines_before=context_lines_before, + context_lines_after=context_lines_after, + source_file_path=relative_file_path, + ) - - async def request_completions( + def request_completions( self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False - ) -> List[multilspy_types.CompletionItem]: + ) -> list[ls_types.CompletionItem]: """ Raise a [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) request to the Language Server to find completions at the given line and column in the given file. Wait for the response and return the result. @@ -710,39 +717,35 @@ class LanguageServer: :return List[multilspy_types.CompletionItem]: A list of completions """ with self.open_file(relative_file_path): - open_file_buffer = self.open_file_buffers[ - pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() - ] + open_file_buffer = self.open_file_buffers[pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()] completion_params: LSPTypes.CompletionParams = { "position": {"line": line, "character": column}, "textDocument": {"uri": open_file_buffer.uri}, "context": {"triggerKind": LSPTypes.CompletionTriggerKind.Invoked}, } - response: Union[List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None] = None + response: list[LSPTypes.CompletionItem] | LSPTypes.CompletionList | None = None num_retries = 0 while response is None or (response["isIncomplete"] and num_retries < 30): - await self.completions_available.wait() - response: Union[ - List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None - ] = await self.server.send.completion(completion_params) + self.completions_available.wait() + response: list[LSPTypes.CompletionItem] | LSPTypes.CompletionList | None = self.server.send.completion(completion_params) if isinstance(response, list): response = {"items": response, "isIncomplete": False} num_retries += 1 # TODO: Understand how to appropriately handle `isIncomplete` - if response is None or (response["isIncomplete"] and not(allow_incomplete)): + if response is None or (response["isIncomplete"] and not (allow_incomplete)): return [] if "items" in response: response = response["items"] - response: List[LSPTypes.CompletionItem] = response + response = cast(list[LSPTypes.CompletionItem], response) # TODO: Handle the case when the completion is a keyword items = [item for item in response if item["kind"] != LSPTypes.CompletionItemKind.Keyword] - completions_list: List[multilspy_types.CompletionItem] = [] + completions_list: list[ls_types.CompletionItem] = [] for item in items: assert "insertText" in item or "textEdit" in item @@ -750,7 +753,7 @@ class LanguageServer: completion_item = {} if "detail" in item: completion_item["detail"] = item["detail"] - + if "label" in item: completion_item["completionText"] = item["label"] completion_item["kind"] = item["kind"] @@ -770,11 +773,10 @@ class LanguageServer: item["textEdit"]["range"]["start"]["line"] == new_dot_lineno, item["textEdit"]["range"]["start"]["character"] == new_dot_colno, item["textEdit"]["range"]["start"]["line"] == item["textEdit"]["range"]["end"]["line"], - item["textEdit"]["range"]["start"]["character"] - == item["textEdit"]["range"]["end"]["character"], + item["textEdit"]["range"]["start"]["character"] == item["textEdit"]["range"]["end"]["character"], ) ) - + completion_item["completionText"] = item["textEdit"]["newText"] completion_item["kind"] = item["kind"] elif "textEdit" in item and "insert" in item["textEdit"]: @@ -782,15 +784,14 @@ class LanguageServer: else: assert False - completion_item = multilspy_types.CompletionItem(**completion_item) + completion_item = ls_types.CompletionItem(**completion_item) completions_list.append(completion_item) - return [ - json.loads(json_repr) - for json_repr in set([json.dumps(item, sort_keys=True) for item in completions_list]) - ] + return [json.loads(json_repr) for json_repr in set(json.dumps(item, sort_keys=True) for item in completions_list)] - async def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: + def request_document_symbols( + self, relative_file_path: str, include_body: bool = False + ) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]: """ Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server to find symbols in the given file. Wait for the response and return the result. @@ -808,49 +809,49 @@ class LanguageServer: # Should be fixed in the future, it's a small performance optimization cache_key = f"{relative_file_path}-{include_body}" with self.open_file(relative_file_path) as file_data: - file_hash_and_result = self._document_symbols_cache.get(cache_key) - if file_hash_and_result is not None: - file_hash, result = file_hash_and_result - if file_hash == file_data.content_hash: - self.logger.log(f"Returning cached document symbols for {relative_file_path}", logging.DEBUG) - return result + with self._cache_lock: + file_hash_and_result = self._document_symbols_cache.get(cache_key) + if file_hash_and_result is not None: + file_hash, result = file_hash_and_result + if file_hash == file_data.content_hash: + self.logger.log(f"Returning cached document symbols for {relative_file_path}", logging.DEBUG) + return result + else: + self.logger.log(f"Content for {relative_file_path} has changed. Will overwrite in-memory cache", logging.DEBUG) else: - self.logger.log(f"Content for {relative_file_path} has changed. Will overwrite in-memory cache", logging.DEBUG) - else: - self.logger.log(f"No cache hit for symbols with {include_body=} in {relative_file_path}", logging.DEBUG) + self.logger.log(f"No cache hit for symbols with {include_body=} in {relative_file_path}", logging.DEBUG) self.logger.log(f"Requesting document symbols for {relative_file_path} from the Language Server", logging.DEBUG) - response = await self.server.send.document_symbol( - { - "textDocument": { - "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() - } - } + response = self.server.send.document_symbol( + {"textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()}} + ) + self.logger.log( + f"Received {len(response) if response is not None else None} document symbols for {relative_file_path} from the Language Server", + logging.DEBUG, ) - self.logger.log(f"Received {len(response) if response is not None else None} document symbols for {relative_file_path} from the Language Server", logging.DEBUG) def turn_item_into_symbol_with_children(item: GenericDocumentSymbol): - item = cast(multilspy_types.UnifiedSymbolInformation, item) + item = cast(ls_types.UnifiedSymbolInformation, item) absolute_path = os.path.join(self.repository_root_path, relative_file_path) - + # handle missing entries in location if "location" not in item: uri = pathlib.Path(absolute_path).as_uri() assert "range" in item - tree_location = multilspy_types.Location( + tree_location = ls_types.Location( uri=uri, - range=item['range'], + range=item["range"], absolutePath=absolute_path, relativePath=relative_file_path, ) - item['location'] = tree_location + item["location"] = tree_location location = item["location"] if "absolutePath" not in location: location["absolutePath"] = absolute_path if "relativePath" not in location: location["relativePath"] = relative_file_path if include_body: - item['body'] = self.retrieve_symbol_body(item) + item["body"] = self.retrieve_symbol_body(item) # handle missing selectionRange if "selectionRange" not in item: if "range" in item: @@ -862,9 +863,9 @@ class LanguageServer: child["parent"] = item item[LSPConstants.CHILDREN] = children - flat_all_symbol_list: List[multilspy_types.UnifiedSymbolInformation] = [] + flat_all_symbol_list: list[ls_types.UnifiedSymbolInformation] = [] assert isinstance(response, list), f"Unexpected response from Language Server: {response}" - root_nodes: List[multilspy_types.UnifiedSymbolInformation] = [] + root_nodes: list[ls_types.UnifiedSymbolInformation] = [] for root_item in response: if "range" not in root_item and "location" not in root_item: if root_item["kind"] in [SymbolKind.File, SymbolKind.Module]: @@ -874,9 +875,9 @@ class LanguageServer: # so we cast and rename the var after the mutating call to turn_item_into_symbol_with_children # which turned and item into a "symbol" turn_item_into_symbol_with_children(root_item) - root_symbol = cast(multilspy_types.UnifiedSymbolInformation, root_item) + root_symbol = cast(ls_types.UnifiedSymbolInformation, root_item) root_symbol["parent"] = None - + root_nodes.append(root_symbol) assert isinstance(root_symbol, dict) assert LSPConstants.NAME in root_symbol @@ -884,10 +885,10 @@ class LanguageServer: if LSPConstants.CHILDREN in root_symbol: # TODO: l_tree should be a list of TreeRepr. Define the following function to return TreeRepr as well - - def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> List[multilspy_types.UnifiedSymbolInformation]: - node = cast(multilspy_types.UnifiedSymbolInformation, node) - l: List[multilspy_types.UnifiedSymbolInformation] = [] + + def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> list[ls_types.UnifiedSymbolInformation]: + node = cast(ls_types.UnifiedSymbolInformation, node) + l: list[ls_types.UnifiedSymbolInformation] = [] turn_item_into_symbol_with_children(node) assert LSPConstants.CHILDREN in node children = node[LSPConstants.CHILDREN] @@ -895,20 +896,23 @@ class LanguageServer: for child in children: l.extend(visit_tree_nodes_and_build_tree_repr(child)) return l - + flat_all_symbol_list.extend(visit_tree_nodes_and_build_tree_repr(root_symbol)) else: - flat_all_symbol_list.append(multilspy_types.UnifiedSymbolInformation(**root_symbol)) + flat_all_symbol_list.append(ls_types.UnifiedSymbolInformation(**root_symbol)) result = flat_all_symbol_list, root_nodes self.logger.log(f"Caching document symbols for {relative_file_path}", logging.DEBUG) - self._document_symbols_cache[cache_key] = (file_data.content_hash, result) - self._cache_has_changed = True + with self._cache_lock: + self._document_symbols_cache[cache_key] = (file_data.content_hash, result) + self._cache_has_changed = True return result - - async def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: + + def request_full_symbol_tree( + self, within_relative_path: str | None = None, include_body: bool = False + ) -> list[ls_types.UnifiedSymbolInformation]: """ - Will go through all files in the project or within a relative path and build a tree of symbols. + Will go through all files in the project or within a relative path and build a tree of symbols. Note: this may be slow the first time it is called, especially if `within_relative_path` is not used to restrict the search. For each file, a symbol of kind File (2) will be created. For directories, a symbol of kind Package (4) will be created. @@ -925,21 +929,23 @@ class LanguageServer: :return: A list of root symbols representing the top-level packages/modules in the project. """ - if within_relative_path is not None: within_abs_path = os.path.join(self.repository_root_path, within_relative_path) if not os.path.exists(within_abs_path): raise FileNotFoundError(f"File or directory not found: {within_abs_path}") if os.path.isfile(within_abs_path): if self.is_ignored_path(within_relative_path): - self.logger.log(f"You passed a file explicitly, but it is ignored. This is probably an error. File: {within_relative_path}", logging.ERROR) + self.logger.log( + f"You passed a file explicitly, but it is ignored. This is probably an error. File: {within_relative_path}", + logging.ERROR, + ) return [] else: - _, root_nodes = await self.request_document_symbols(within_relative_path, include_body=include_body) + _, root_nodes = self.request_document_symbols(within_relative_path, include_body=include_body) return root_nodes # Helper function to recursively process directories - async def process_directory(rel_dir_path: str) -> List[multilspy_types.UnifiedSymbolInformation]: + def process_directory(rel_dir_path: str) -> list[ls_types.UnifiedSymbolInformation]: abs_dir_path = self.repository_root_path if rel_dir_path == "." else os.path.join(self.repository_root_path, rel_dir_path) abs_dir_path = os.path.realpath(abs_dir_path) @@ -954,16 +960,16 @@ class LanguageServer: return [] # Create package symbol for directory - package_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore + package_symbol = ls_types.UnifiedSymbolInformation( # type: ignore name=os.path.basename(abs_dir_path), - kind=multilspy_types.SymbolKind.Package, - location=multilspy_types.Location( + kind=ls_types.SymbolKind.Package, + location=ls_types.Location( uri=str(pathlib.Path(abs_dir_path).as_uri()), range={"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}, absolutePath=str(abs_dir_path), relativePath=str(Path(abs_dir_path).resolve().relative_to(self.repository_root_path)), ), - children=[] + children=[], ) result.append(package_symbol) @@ -975,24 +981,24 @@ class LanguageServer: continue if os.path.isdir(contained_dir_or_file_abs_path): - child_symbols = await process_directory(contained_dir_or_file_rel_path) + child_symbols = process_directory(contained_dir_or_file_rel_path) package_symbol["children"].extend(child_symbols) for child in child_symbols: child["parent"] = package_symbol - + elif os.path.isfile(contained_dir_or_file_abs_path): - _, file_root_nodes = await self.request_document_symbols(contained_dir_or_file_rel_path, include_body=include_body) - + _, file_root_nodes = self.request_document_symbols(contained_dir_or_file_rel_path, include_body=include_body) + # Create file symbol, link with children file_rel_path = str(Path(contained_dir_or_file_abs_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 + file_symbol = ls_types.UnifiedSymbolInformation( # type: ignore name=os.path.splitext(contained_dir_or_file_name)[0], - kind=multilspy_types.SymbolKind.File, + kind=ls_types.SymbolKind.File, range=fileRange, selectionRange=fileRange, - location=multilspy_types.Location( + location=ls_types.Location( uri=str(pathlib.Path(contained_dir_or_file_abs_path).as_uri()), range=fileRange, absolutePath=str(contained_dir_or_file_abs_path), @@ -1008,7 +1014,7 @@ class LanguageServer: package_symbol["children"].append(file_symbol) # TODO: Not sure if this is actually still needed given recent changes to relative path handling - def fix_relative_path(nodes: List[multilspy_types.UnifiedSymbolInformation]): + def fix_relative_path(nodes: list[ls_types.UnifiedSymbolInformation]): for node in nodes: if "location" in node and "relativePath" in node["location"]: path = Path(node["location"]["relativePath"]) @@ -1027,46 +1033,45 @@ class LanguageServer: # Start from the root or the specified directory start_rel_path = within_relative_path or "." - return await process_directory(start_rel_path) + return process_directory(start_rel_path) @staticmethod - def _get_range_from_file_content(file_content: str) -> multilspy_types.Range: + def _get_range_from_file_content(file_content: str) -> ls_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) - ) + return ls_types.Range(start=ls_types.Position(line=0, character=0), end=ls_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]]]: + def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, ls_types.SymbolKind, int, int]]]: """ An overview of the given directory. Maps relative paths of all contained files to info about top-level symbols in the file (name, kind, line, column). """ - symbol_tree = await self.request_full_symbol_tree(relative_dir_path) + symbol_tree = self.request_full_symbol_tree(relative_dir_path) # Initialize result dictionary - result: dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]] = defaultdict(list) + result: dict[str, list[tuple[str, ls_types.SymbolKind, int, int]]] = defaultdict(list) # Helper function to process a symbol and its children - def process_symbol(symbol: multilspy_types.UnifiedSymbolInformation): - if symbol["kind"] == multilspy_types.SymbolKind.File: + def process_symbol(symbol: ls_types.UnifiedSymbolInformation): + if symbol["kind"] == ls_types.SymbolKind.File: # For file symbols, process their children (top-level symbols) for child in symbol["children"]: assert "location" in child assert "selectionRange" in child path = Path(child["location"]["absolutePath"]).resolve().relative_to(self.repository_root_path) - result[str(path)].append(( - child["name"], - child["kind"], - child["selectionRange"]["start"]["line"], - child["selectionRange"]["start"]["character"] - )) + result[str(path)].append( + ( + child["name"], + child["kind"], + child["selectionRange"]["start"]["line"], + child["selectionRange"]["start"]["character"], + ) + ) # For package/directory symbols, process their children for child in symbol["children"]: process_symbol(child) @@ -1076,28 +1081,23 @@ class LanguageServer: process_symbol(root) return result - async def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]: + def request_document_overview(self, relative_file_path: str) -> list[tuple[str, ls_types.SymbolKind, int, int]]: """ An overview of the given file. Returns the list of tuples (name, kind, line, column) of all top-level symbols in the file. """ - _, document_roots = await self.request_document_symbols(relative_file_path) + _, document_roots = self.request_document_symbols(relative_file_path) result = [] for root in document_roots: try: result.append( - ( root["name"], - root["kind"], - root["selectionRange"]["start"]["line"], - root["selectionRange"]["start"]["character"],) + (root["name"], root["kind"], root["selectionRange"]["start"]["line"], root["selectionRange"]["start"]["character"]) ) except KeyError as e: - raise KeyError( - f"Could not process symbol of name {root.get('name', 'unknown')} in {relative_file_path=}" - ) from e + raise KeyError(f"Could not process symbol of name {root.get('name', 'unknown')} in {relative_file_path=}") from e return result - async def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: + def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, ls_types.SymbolKind, int, int]]]: """ An overview of all symbols in the given file or directory. @@ -1109,12 +1109,12 @@ class LanguageServer: raise FileNotFoundError(f"File or directory not found: {abs_path}") if abs_path.is_file(): - symbols_overview = await self.request_document_overview(within_relative_path) + symbols_overview = self.request_document_overview(within_relative_path) return {within_relative_path: symbols_overview} else: - return await self.request_dir_overview(within_relative_path) + return self.request_dir_overview(within_relative_path) - async def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]: + def request_hover(self, relative_file_path: str, line: int, column: int) -> ls_types.Hover | None: """ Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server to find the hover information at the given line and column in the given file. Wait for the response and return the result. @@ -1126,28 +1126,24 @@ class LanguageServer: :return None """ with self.open_file(relative_file_path): - response = await self.server.send.hover( + response = self.server.send.hover( { - "textDocument": { - "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() - }, + "textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()}, "position": { "line": line, "character": column, }, } ) - + if response is None: return None assert isinstance(response, dict) - return multilspy_types.Hover(**response) + return ls_types.Hover(**response) - # ----------------------------- FROM HERE ON MODIFICATIONS BY MISCHA -------------------- - - def retrieve_symbol_body(self, symbol: multilspy_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation) -> str: + def retrieve_symbol_body(self, symbol: ls_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation) -> str: """ Load the body of the given symbol. If the body is already contained in the symbol, just return it. """ @@ -1161,36 +1157,37 @@ class LanguageServer: assert "relativePath" in symbol["location"] symbol_file = self.retrieve_full_file_content(symbol["location"]["relativePath"]) symbol_lines = symbol_file.split("\n") - symbol_body = "\n".join(symbol_lines[symbol_start_line:symbol_end_line+1]) + symbol_body = "\n".join(symbol_lines[symbol_start_line : symbol_end_line + 1]) # remove leading indentation symbol_start_column = symbol["location"]["range"]["start"]["character"] symbol_body = symbol_body[symbol_start_column:] return symbol_body - - async def request_parsed_files(self) -> list[str]: + def request_parsed_files(self) -> list[str]: """Retrieves relative paths of all files analyzed by the Language Server.""" if not self.server_started: self.logger.log( "request_parsed_files called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") rel_file_paths = [] - for root, dirs, files in os.walk(self.repository_root_path): - # Don't go into directories that are ignored by modifying dirs inplace - # Explanation for the + "/" part: - # pathspec can't handle the matching of directories if they don't end with a slash! - # see https://github.com/cpburnz/python-pathspec/issues/89 - dirs[:] = [d for d in dirs if not self.is_ignored_path(os.path.join(root, d) + "/")] + for root, dirs, files in os.walk(self.repository_root_path, followlinks=True): + dirs[:] = [d for d in dirs if not self.is_ignored_path(os.path.join(root, d))] for file in files: - rel_file_path = os.path.join(root, file) - if not self.is_ignored_path(rel_file_path): - rel_file_paths.append(rel_file_path) + rel_file_path = os.path.relpath(os.path.join(root, file), start=self.repository_root_path) + try: + if not self.is_ignored_path(rel_file_path): + rel_file_paths.append(rel_file_path) + except FileNotFoundError: + self.logger.log( + f"File {rel_file_path} not found (possibly due it being a symlink), skipping it in request_parsed_files", + logging.WARNING, + ) return rel_file_paths - async def search_files_for_pattern( + def search_files_for_pattern( self, pattern: re.Pattern | str, context_lines_before: int = 0, @@ -1211,7 +1208,7 @@ class LanguageServer: if isinstance(pattern, str): pattern = re.compile(pattern) - relative_file_paths = await self.request_parsed_files() + relative_file_paths = self.request_parsed_files() return search_files( relative_file_paths, pattern, @@ -1219,10 +1216,10 @@ class LanguageServer: context_lines_before=context_lines_before, context_lines_after=context_lines_after, paths_include_glob=paths_include_glob, - paths_exclude_glob=paths_exclude_glob + paths_exclude_glob=paths_exclude_glob, ) - async def request_referencing_symbols( + def request_referencing_symbols( self, relative_file_path: str, line: int, @@ -1231,7 +1228,7 @@ class LanguageServer: include_self: bool = False, include_body: bool = False, include_file_symbols: bool = False, - ) -> List[ReferenceInSymbol]: + ) -> list[ReferenceInSymbol]: """ Finds all symbols that reference the symbol at the given location. This is similar to request_references but filters to only include symbols @@ -1255,10 +1252,10 @@ class LanguageServer: "request_referencing_symbols called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") # First, get all references to the symbol - references = await self.request_references(relative_file_path, line, column) + references = self.request_references(relative_file_path, line, column) if not references: return [] @@ -1272,9 +1269,7 @@ class LanguageServer: with self.open_file(ref_path) as file_data: # Get the containing symbol for this reference - containing_symbol = await self.request_containing_symbol( - ref_path, ref_line, ref_col, include_body=include_body - ) + containing_symbol = self.request_containing_symbol(ref_path, ref_line, ref_col, include_body=include_body) if containing_symbol is None: # TODO: HORRIBLE HACK! I don't know how to do it better for now... # THIS IS BOUND TO BREAK IN MANY CASES! IT IS ALSO SPECIFIC TO PYTHON! @@ -1292,9 +1287,9 @@ class LanguageServer: ref_text = file_data.contents.split("\n")[ref_line] if "." in ref_text: containing_symbol_name = ref_text.split(".")[0] - all_symbols, _ = await self.request_document_symbols(ref_path) + all_symbols, _ = self.request_document_symbols(ref_path) for symbol in all_symbols: - if symbol["name"] == containing_symbol_name and symbol["kind"] == multilspy_types.SymbolKind.Variable: + if symbol["name"] == containing_symbol_name and symbol["kind"] == ls_types.SymbolKind.Variable: containing_symbol = copy(symbol) containing_symbol["location"] = ref containing_symbol["range"] = ref["range"] @@ -1304,10 +1299,10 @@ class LanguageServer: 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 + logging.WARNING, ) fileRange = self._get_range_from_file_content(file_data.contents) - location = multilspy_types.Location( + location = ls_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)), @@ -1320,8 +1315,8 @@ class LanguageServer: else: body = "" - containing_symbol = multilspy_types.UnifiedSymbolInformation( - kind=multilspy_types.SymbolKind.File, + containing_symbol = ls_types.UnifiedSymbolInformation( + kind=ls_types.SymbolKind.File, range=fileRange, selectionRange=fileRange, location=location, @@ -1329,7 +1324,7 @@ class LanguageServer: children=[], body=body, ) - if containing_symbol is None or not include_file_symbols and containing_symbol["kind"] == multilspy_types.SymbolKind.File: + if containing_symbol is None or (not include_file_symbols and containing_symbol["kind"] == ls_types.SymbolKind.File): continue assert "location" in containing_symbol @@ -1345,23 +1340,23 @@ class LanguageServer: if include_self: result.append(ReferenceInSymbol(symbol=containing_symbol, line=ref_line, character=ref_col)) continue - else: - self.logger.log(f"Found self-reference for {incoming_symbol['name']}, skipping it since {include_self=}", logging.DEBUG) - continue + self.logger.log(f"Found self-reference for {incoming_symbol['name']}, skipping it since {include_self=}", logging.DEBUG) + continue # checking whether reference is an import # This is neither really safe nor elegant, but if we don't do it, # there is no way to distinguish between definitions and imports as import is not a symbol-type # and we get the type referenced symbol resulting from imports... - if (not include_imports \ - and incoming_symbol is not None \ - and containing_symbol["name"] == incoming_symbol["name"] \ - and containing_symbol["kind"] == incoming_symbol["kind"] \ + if ( + not include_imports + and incoming_symbol is not None + and containing_symbol["name"] == incoming_symbol["name"] + and containing_symbol["kind"] == incoming_symbol["kind"] ): self.logger.log( - f"Found import of referenced symbol {incoming_symbol['name']}" + f"Found import of referenced symbol {incoming_symbol['name']}" f"in {containing_symbol['location']['relativePath']}, skipping", - logging.DEBUG + logging.DEBUG, ) continue @@ -1369,14 +1364,14 @@ class LanguageServer: return result - async def request_containing_symbol( + def request_containing_symbol( self, relative_file_path: str, line: int, - column: Optional[int] = None, + column: int | None = None, strict: bool = False, include_body: bool = False, - ) -> multilspy_types.UnifiedSymbolInformation | None: + ) -> ls_types.UnifiedSymbolInformation | None: """ Finds the first symbol containing the position for the given file. For Python, container symbols are considered to be those with kinds corresponding to @@ -1405,9 +1400,7 @@ class LanguageServer: """ # checking if the line is empty, unfortunately ugly and duplicating code, but I don't want to refactor with self.open_file(relative_file_path): - absolute_file_path = str( - PurePath(self.repository_root_path, relative_file_path) - ) + absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) content = FileUtils.read_file(self.logger, absolute_file_path) if content.split("\n")[line].strip() == "": self.logger.log( @@ -1416,7 +1409,7 @@ class LanguageServer: ) return None - symbols, _ = await self.request_document_symbols(relative_file_path) + symbols, _ = self.request_document_symbols(relative_file_path) # make jedi and pyright api compatible # the former has no location, the later has no range @@ -1424,7 +1417,7 @@ class LanguageServer: for symbol in symbols: if "location" not in symbol: range = symbol["range"] - location = multilspy_types.Location( + location = ls_types.Location( uri=f"file:/{absolute_file_path}", range=range, absolutePath=absolute_file_path, @@ -1439,13 +1432,9 @@ class LanguageServer: location["uri"] = Path(absolute_file_path).as_uri() # Allowed container kinds, currently only for Python - container_symbol_kinds = { - multilspy_types.SymbolKind.Method, - multilspy_types.SymbolKind.Function, - multilspy_types.SymbolKind.Class - } + container_symbol_kinds = {ls_types.SymbolKind.Method, ls_types.SymbolKind.Function, ls_types.SymbolKind.Class} - def is_position_in_range(line: int, range_d: multilspy_types.Range) -> bool: + def is_position_in_range(line: int, range_d: ls_types.Range) -> bool: start = range_d["start"] end = range_d["end"] @@ -1462,11 +1451,11 @@ class LanguageServer: # Only consider containers that are not one-liners (otherwise we may get imports) candidate_containers = [ - s for s in symbols if s["kind"] in container_symbol_kinds and s["location"]["range"]["start"]["line"] != s["location"]["range"]["end"]["line"] - ] - var_containers = [ - s for s in symbols if s["kind"] == multilspy_types.SymbolKind.Variable + s + for s in symbols + if s["kind"] in container_symbol_kinds and s["location"]["range"]["start"]["line"] != s["location"]["range"]["end"]["line"] ] + var_containers = [s for s in symbols if s["kind"] == ls_types.SymbolKind.Variable] candidate_containers.extend(var_containers) if not candidate_containers: @@ -1489,7 +1478,9 @@ class LanguageServer: else: return None - async def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None: + def request_container_of_symbol( + self, symbol: ls_types.UnifiedSymbolInformation, include_body: bool = False + ) -> ls_types.UnifiedSymbolInformation | None: """ Finds the container of the given symbol if there is one. If the parent attribute is present, the parent is returned without further searching. @@ -1501,7 +1492,7 @@ class LanguageServer: if "parent" in symbol: return symbol["parent"] assert "location" in symbol, f"Symbol {symbol} has no location and no parent attribute" - return await self.request_containing_symbol( + return self.request_containing_symbol( symbol["location"]["relativePath"], symbol["location"]["range"]["start"]["line"], symbol["location"]["range"]["start"]["character"], @@ -1509,13 +1500,13 @@ class LanguageServer: include_body=include_body, ) - async def request_defining_symbol( + def request_defining_symbol( self, relative_file_path: str, line: int, column: int, include_body: bool = False, - ) -> Optional[multilspy_types.UnifiedSymbolInformation]: + ) -> ls_types.UnifiedSymbolInformation | None: """ Finds the symbol that defines the symbol at the given location. @@ -1533,10 +1524,10 @@ class LanguageServer: "request_defining_symbol called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") # Get the definition location(s) - definitions = await self.request_definition(relative_file_path, line, column) + definitions = self.request_definition(relative_file_path, line, column) if not definitions: return None @@ -1547,49 +1538,73 @@ class LanguageServer: def_col = definition["range"]["start"]["character"] # Find the symbol at or containing this location - defining_symbol = await self.request_containing_symbol( - def_path, def_line, def_col, strict=False, include_body=include_body - ) + defining_symbol = self.request_containing_symbol(def_path, def_line, def_col, strict=False, include_body=include_body) return defining_symbol @property - def _cache_path(self) -> Path: - return Path(self.repository_root_path) / ".serena" / "cache" / "document_symbols_cache_v20-05-25.pkl" + def cache_path(self) -> Path: + """ + The path to the cache file for the document symbols. + """ + return Path(self.repository_root_path) / ".serena" / "cache" / self.language_id / "document_symbols_cache_v23-06-25.pkl" + + def index_repository(self, progress_bar: bool = True, save_after_n_files: int = 10) -> None: + """Will go through the entire repository and "index" all files, meaning save their symbols to the cache. + + :param progress_bar: Whether to show a progress bar while indexing the repository. + :param save_after_n_files: How many files to process before saving a checkpoint of the cache. + """ + parsed_files = self.request_parsed_files() + files_processed = 0 + pbar = tqdm.tqdm(parsed_files, disable=not progress_bar) + for relative_file_path in pbar: + pbar.set_description(f"Indexing ({os.path.basename(relative_file_path)})") + self.request_document_symbols(relative_file_path, include_body=False) + self.request_document_symbols(relative_file_path, include_body=True) + files_processed += 1 + if files_processed % save_after_n_files == 0: + self.save_cache() + self.save_cache() def save_cache(self): - if self._cache_has_changed: - self.logger.log(f"Saving updated document symbols cache to {self._cache_path}", logging.INFO) - self._cache_path.parent.mkdir(parents=True, exist_ok=True) + with self._cache_lock: + if not self._cache_has_changed: + self.logger.log("No changes to document symbols cache, skipping save", logging.DEBUG) + return + + self.logger.log(f"Saving updated document symbols cache to {self.cache_path}", logging.INFO) + self.cache_path.parent.mkdir(parents=True, exist_ok=True) try: - with open(self._cache_path, "wb") as f: + with open(self.cache_path, "wb") as f: pickle.dump(self._document_symbols_cache, f) + self._cache_has_changed = False except Exception as e: self.logger.log( - f"Failed to save document symbols cache to {self._cache_path}: {e}. " - "Note: this may have resulted in a corrupted cache file.", logging.ERROR - ) - else: - self.logger.log(f"No changes to document symbols cache, skipping save", logging.DEBUG) - self._cache_has_changed = False + f"Failed to save document symbols cache to {self.cache_path}: {e}. " + "Note: this may have resulted in a corrupted cache file.", + logging.ERROR, + ) def load_cache(self): - if not self._cache_path.exists(): + if not self.cache_path.exists(): return - self.logger.log(f"Loading document symbols cache from {self._cache_path}", logging.INFO) - with open(self._cache_path, "rb") as f: + + with self._cache_lock: + self.logger.log(f"Loading document symbols cache from {self.cache_path}", logging.INFO) try: - self._document_symbols_cache = pickle.load(f) + with open(self.cache_path, "rb") as f: + self._document_symbols_cache = pickle.load(f) + self.logger.log(f"Loaded {len(self._document_symbols_cache)} document symbols from cache.", logging.INFO) except Exception as e: # cache often becomes corrupt, so just skip loading it self.logger.log( - f"Failed to load document symbols cache from {self._cache_path}: {e}. Possible cause: the cache file is corrupted. " - "Check for any errors related to saving the cache in the logs.", - logging.ERROR - ) + f"Failed to load document symbols cache from {self.cache_path}: {e}. Possible cause: the cache file is corrupted. " + "Check for any errors related to saving the cache in the logs.", + logging.ERROR, + ) - - async def request_workspace_symbol(self, query: str) -> Union[List[multilspy_types.UnifiedSymbolInformation], None]: + def request_workspace_symbol(self, query: str) -> list[ls_types.UnifiedSymbolInformation] | None: """ Raise a [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) request to the Language Server to find symbols across the whole workspace. Wait for the response and return the result. @@ -1598,13 +1613,13 @@ class LanguageServer: :return: A list of matching symbols """ - response = await self.server.send.workspace_symbol({"query": query}) + response = self.server.send.workspace_symbol({"query": query}) if response is None: return None assert isinstance(response, list) - ret: List[multilspy_types.UnifiedSymbolInformation] = [] + ret: list[ls_types.UnifiedSymbolInformation] = [] for item in response: assert isinstance(item, dict) @@ -1612,533 +1627,29 @@ class LanguageServer: assert LSPConstants.KIND in item assert LSPConstants.LOCATION in item - ret.append(multilspy_types.UnifiedSymbolInformation(**item)) + ret.append(ls_types.UnifiedSymbolInformation(**item)) return ret -@ensure_all_methods_implemented(LanguageServer) -class SyncLanguageServer: - """ - The SyncLanguageServer class provides a language agnostic interface to the Language Server Protocol. - It is used to communicate with Language Servers of different programming languages. - """ - - def __init__(self, language_server: LanguageServer, timeout: Optional[int] = None): - """ - :param language_server: the async language server being wrapped - :param timeout: the timeout, in seconds, to use for requests to the language server. - """ - self.language_server = language_server - self.loop = None - self.loop_thread = None - self.timeout = timeout - - self._server_context = None - - @classmethod - def create( - cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, add_gitignore_content_to_config=True, - timeout: Optional[int] = None - ) -> "SyncLanguageServer": - """ - Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. - - If language is Java, then ensure that jdk-17.0.6 or higher is installed, `java` is in PATH, and JAVA_HOME is set to the installation directory. - - :param repository_root_path: The root path of the repository (must be absolute). - :param config: The Multilspy configuration. - :param logger: The logger to use. - :param add_gitignore_content_to_config: whether to add the content of the .gitignore file (if any found) to the config, so that - the paths ignored there are also ignored by the language server - :param timeout: the timeout, in seconds, to use for requests; if None, use no timeout - - :return SyncLanguageServer: A language specific LanguageServer instance. - """ - return SyncLanguageServer(LanguageServer.create(config, logger, repository_root_path, add_gitignore_content_to_config=add_gitignore_content_to_config), timeout=timeout) - - @property - def repository_root_path(self) -> str: - return self.language_server.repository_root_path - - @contextmanager - def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]: - """ - Open a file in the Language Server. This is required before making any requests to the Language Server. - - :param relative_file_path: The relative path of the file to open. - """ - with self.language_server.open_file(relative_file_path) as file_buffer: - yield file_buffer - - def insert_text_at_position( - self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str - ) -> multilspy_types.Position: - """ - Insert text at the given line and column in the given file and return - the updated cursor position after inserting the text. - - :param relative_file_path: The relative path of the file to open. - :param line: The line number at which text should be inserted. - :param column: The column number at which text should be inserted. - :param text_to_be_inserted: The text to insert. - """ - return self.language_server.insert_text_at_position(relative_file_path, line, column, text_to_be_inserted) - - def delete_text_between_positions( - self, - relative_file_path: str, - start: multilspy_types.Position, - end: multilspy_types.Position, - ) -> str: - """ - Delete text between the given start and end positions in the given file and return the deleted text. - """ - return self.language_server.delete_text_between_positions(relative_file_path, start, end) - - @contextmanager - def start_server(self) -> Iterator["SyncLanguageServer"]: - """ - Starts the language server process and connects to it. - - :return: None - """ - self.loop = asyncio.new_event_loop() - self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True) - self.loop_thread.start() - ctx = self.language_server.start_server() - asyncio.run_coroutine_threadsafe(ctx.__aenter__(), loop=self.loop).result() - yield self - asyncio.run_coroutine_threadsafe(ctx.__aexit__(None, None, None), loop=self.loop).result() - self.loop.call_soon_threadsafe(self.loop.stop) - self.loop_thread.join() - - def request_definition(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]: - """ - Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server - for the symbol at the given line and column in the given file. Wait for the response and return the result. - - :param relative_file_path: The relative path of the file that has the symbol for which definition should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol - - :return List[multilspy_types.Location]: A list of locations where the symbol is defined - """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_definition(file_path, line, column), self.loop - ).result(timeout=self.timeout) - return result - - def request_references(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]: - """ - Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server - to find references to the symbol at the given line and column in the given file. Wait for the response and return the result. - - :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol - - :return List[multilspy_types.Location]: A list of locations where the symbol is referenced - """ - try: - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_references(file_path, line, column), self.loop - ).result(timeout=self.timeout) - except Exception as e: - from multilspy.lsp_protocol_handler.server import Error - if isinstance(e, Error) and getattr(e, 'code', None) == -32603: - raise RuntimeError( - f"LSP internal error (-32603) when requesting references for {file_path}:{line}:{column}. " - "This often occurs when requesting references for a symbol not referenced in the expected way. " - ) from e - raise - return result - - - def request_references_with_content( - self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0 - ) -> List[MatchedConsecutiveLines]: - """ - Like request_references, but returns the content of the lines containing the references, not just the locations. - - :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol - :param context_lines_before: The number of lines to include in the context before the line containing the reference - :param context_lines_after: The number of lines to include in the context after the line containing the reference - - :return: A list of MatchedConsecutiveLines objects, one for each reference. - """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_references_with_content(relative_file_path, line, column, context_lines_before, context_lines_after), self.loop - ).result() - return result - - def request_completions( - self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False - ) -> List[multilspy_types.CompletionItem]: - """ - Raise a [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) request to the Language Server - to find completions at the given line and column in the given file. Wait for the response and return the result. - - :param relative_file_path: The relative path of the file that has the symbol for which completions should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol - - :return List[multilspy_types.CompletionItem]: A list of completions - """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_completions(relative_file_path, line, column, allow_incomplete), - self.loop, - ).result(timeout=self.timeout) - return result - - def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: - """ - Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server - to find symbols in the given file. Wait for the response and return the result. - - :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. Each symbol in hierarchy starting from the roots has a children attribute. - """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_document_symbols(relative_file_path, include_body), self.loop - ).result() - return result - - def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: - """ - Will go through all files in the project and build a tree of symbols. Note: this may be slow the first time it is called. - - For each file, a symbol of kind Module (3) will be created. For directories, a symbol of kind Package (4) will be created. - All symbols will have a children attribute, thereby representing the tree structure of all symbols in the project - that are within the repository. - Will ignore directories starting with '.', language-specific defaults - and user-configured directories (e.g. from .gitignore). - - :param within_relative_path: pass a relative path to only consider symbols within this path. - If a file is passed, only the symbols within this file will be considered. - If a directory is passed, all files within this directory will be considered. - If None, the entire codebase will be considered. - :param include_body: whether to include the body of the symbols in the result. - - :return: A list of root symbols representing the top-level packages/modules in the project. - """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_full_symbol_tree(within_relative_path, include_body), self.loop - ).result(timeout=self.timeout) - return result - - 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. - - Maps relative paths of all contained files to info about top-level symbols in the file - (name, kind, line, column). - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_dir_overview(relative_dir_path), self.loop - ).result(timeout=self.timeout) - return result - - def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]: - """ - An overview of the given file. - - Returns the list of tuples (name, kind, line, column) of all top-level symbols in the file. - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_document_overview(relative_file_path), self.loop - ).result(timeout=self.timeout) - return result - - def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: - """ - An overview of all symbols in the given file or directory. - - :param within_relative_path: the relative path to the file or directory to get the overview of. - :return: A mapping of all relative paths analyzed to lists of tuples (name, kind, line, column) of all top-level symbols in the corresponding file. - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_overview(within_relative_path), self.loop - ).result() - return result - - def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]: - """ - Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server - to find the hover information at the given line and column in the given file. Wait for the response and return the result. - - :param relative_file_path: The relative path of the file that has the hover information - :param line: The line number of the symbol - :param column: The column number of the symbol - - :return None - """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_hover(relative_file_path, line, column), self.loop - ).result(timeout=self.timeout) - return result - - def request_workspace_symbol(self, query: str) -> Union[List[multilspy_types.UnifiedSymbolInformation], None]: - """ - Raise a [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) request to the Language Server - to find symbols across the whole workspace. Wait for the response and return the result. - - :param query: The query string to filter symbols by - - :return Union[List[multilspy_types.UnifiedSymbolInformation], None]: A list of matching symbols - """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_workspace_symbol(query), self.loop - ).result(timeout=self.timeout) - return result - - # ----------------------------- FROM HERE ON MODIFICATIONS BY MISCHA -------------------- - - def retrieve_symbol_body(self, symbol: multilspy_types.UnifiedSymbolInformation) -> str: - """ - Load the body of the given symbol. If the body is already contained in the symbol, just return it. - - :param symbol: The symbol to retrieve the body of. - :return: The body of the symbol. - """ - return self.language_server.retrieve_symbol_body(symbol) - - def request_parsed_files(self) -> list[str]: - """Retrieves relative paths of all files analyzed by the Language Server.""" - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_parsed_files(), self.loop - ).result() - return result - - def request_referencing_symbols( - 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[ReferenceInSymbol]: - """ - Finds all symbols that reference the symbol at the given location. - This is similar to request_references but filters to only include symbols - (functions, methods, classes, etc.) that reference the target symbol. - - :param relative_file_path: The relative path to the file. - :param line: The 0-indexed line number. - :param column: The 0-indexed column number. - :param include_imports: whether to also include imports as references. - Unfortunately, the LSP does not have an import type, so the references corresponding to imports - will not be easily distinguishable from definitions. - :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 objects containing the symbol and the location of the reference. - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_referencing_symbols( - relative_file_path, - line, - column, - include_imports=include_imports, - include_self=include_self, - include_body=include_body, - include_file_symbols=include_file_symbols, - ), - self.loop - ).result(timeout=self.timeout) - return result - - def request_containing_symbol( - self, relative_file_path: str, line: int, - column: Optional[int] = None, strict: bool = False, - include_body: bool = False, - ) -> multilspy_types.UnifiedSymbolInformation | None: - """ - Finds the first symbol containing the position for the given file. - For Python, container symbols are considered to be those with kinds corresponding to - functions, methods, or classes (typically: Function (12), Method (6), Class (5)). - - The method operates as follows: - - Request the document symbols for the file. - - Filter symbols to those that start at or before the given line. - - From these, first look for symbols whose range contains the (line, column). - - If one or more symbols contain the position, return the one with the greatest starting position - (i.e. the innermost container). - - If none (strictly) contain the position, return the symbol with the greatest starting position - among those above the given line. - - If no container candidates are found, return None. - - :param relative_file_path: The relative path to the Python file. - :param line: The 0-indexed line number. - :param column: The 0-indexed column (also called character). If not passed, the lookup will be based - only on the line. - :param strict: If True, the position must be strictly within the range of the symbol. - Setting to true is useful for example for finding the parent of a symbol, as with strict=False, - and the line pointing to a symbol itself, the containing symbol will be the symbol itself - (and not the parent). - :param include_body: whether to include the body of the symbol in the result. - :return: The container symbol (if found) or None. - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_containing_symbol(relative_file_path, line, column=column, strict=strict, include_body=include_body), self.loop - ).result(timeout=self.timeout) - return result - - def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None: - """ - Finds the container of the given symbol if there is one. - - :param symbol: The symbol to find the container of. - :param include_body: whether to include the body of the symbol in the result. - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_container_of_symbol(symbol, include_body=include_body), self.loop - ).result(timeout=self.timeout) - return result - - def request_defining_symbol( - self, relative_file_path: str, line: int, column: int, - include_body: bool = False, - ) -> Optional[multilspy_types.UnifiedSymbolInformation]: - """ - Finds the symbol that defines the symbol at the given location. - - This method first finds the definition of the symbol at the given position, - then retrieves the full symbol information for that definition. - - :param relative_file_path: The relative path to the file. - :param line: The 0-indexed line number. - :param column: The 0-indexed column number. - :param include_body: whether to include the body of the symbol in the result. - :return: The symbol information for the definition, or None if not found. - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_defining_symbol(relative_file_path, line, column, include_body=include_body), self.loop - ).result(timeout=self.timeout) - return result - - def retrieve_full_file_content(self, relative_file_path: str) -> str: - """ - Retrieve the full content of the given file. - """ - return self.language_server.retrieve_full_file_content(relative_file_path) - - def retrieve_content_around_line(self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0) -> MatchedConsecutiveLines: - """ - Retrieve the content of the given file around the given line. - - :param relative_file_path: The relative path of the file to retrieve the content from - :param line: The line number to retrieve the content around - :param context_lines_before: The number of lines to retrieve before the given line - :param context_lines_after: The number of lines to retrieve after the given line - :return MatchedConsecutiveLines: A container with the desired lines. - """ - return self.language_server.retrieve_content_around_line(relative_file_path, line, context_lines_before, context_lines_after) - - def search_files_for_pattern( - self, - pattern: re.Pattern | str, - context_lines_before: int = 0, - context_lines_after: int = 0, - paths_include_glob: str | None = None, - paths_exclude_glob: str | None = None, - ) -> list[MatchedConsecutiveLines]: - """ - Search for a pattern across all files analyzed by the Language Server. - - :param pattern: Regular expression pattern to search for, either as a compiled Pattern or string - :param context_lines_before: Number of lines of context to include before each match - :param context_lines_after: Number of lines of context to include after each match - :param paths_include_glob: Glob pattern to filter which files to include in the search - :param paths_exclude_glob: Glob pattern to filter which files to exclude from the search. Takes precedence over paths_include_glob. - :return: List of matched consecutive lines with context - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.search_files_for_pattern(pattern, context_lines_before, context_lines_after, paths_include_glob, paths_exclude_glob), self.loop - ).result(timeout=self.timeout) - return result - - def start(self) -> "SyncLanguageServer": + def start(self) -> "SolidLanguageServer": """ Starts the language server process and connects to it. Call shutdown when ready. :return: self for method chaining """ - self.loop = asyncio.new_event_loop() - self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True) - self.loop_thread.start() - self._server_context = self.language_server.start_server() - asyncio.run_coroutine_threadsafe(self._server_context.__aenter__(), loop=self.loop).result() + self.logger.log( + f"Starting language server with language {self.language_server.language} for {self.language_server.repository_root_path}", + logging.INFO, + ) + self._server_context = self._start_server_process() + return self + + def stop(self, shutdown_timeout: float = 2.0) -> None: + self._shutdown(timeout=shutdown_timeout) + + @property + def language_server(self) -> Self: return self def is_running(self) -> bool: - """ - Check if the language server is running. - """ - return self.loop is not None and self.loop_thread is not None and self.loop_thread.is_alive() - - def stop(self) -> None: - """ - Shuts down the language server process and cleans up resources. - - If the language server is not running, this method will log a warning and do nothing. - """ - self.save_cache() - if not self.is_running(): - self.language_server.logger.log("Language server not running, skipping shutdown.", logging.INFO) - return - - assert self.loop - asyncio.run_coroutine_threadsafe(self._server_context.__aexit__(None, None, None), loop=self.loop).result() - self.loop.call_soon_threadsafe(self.loop.stop) - self.loop_thread.join() - self.loop = None - self.loop_thread = None - - def save_cache(self): - """ - Save the cache to a file. - """ - self.language_server.save_cache() - - def load_cache(self): - """ - Load the cache from a file. - """ - self.language_server.load_cache() - - def is_ignored_dirname(self, dirname: str) -> bool: - """ - A language-specific condition for directories that should be ignored always. For example, venv - in Python and node_modules in JS/TS should be ignored always. - """ - return self.language_server.is_ignored_dirname(dirname) - - def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool: - """ - Whether the given path should be ignored. - """ - return self.language_server.is_ignored_path(relative_path, ignore_unsupported_files=ignore_unsupported_files) - - def get_ignore_spec(self) -> pathspec.PathSpec: - """Returns the pathspec matcher for the paths that were configured to be ignored through - the multilspy config file and the .gitignore file. - - This is is a subset of the full language-specific ignore spec that determines - which files are relevant for the language server. - - This matcher is useful for operations outside of the language server, - such as when searching for relevant non-language files in the project. - """ - return self.language_server.get_ignore_spec() + return self.server.is_running() diff --git a/src/multilspy/multilspy_config.py b/src/solidlsp/ls_config.py similarity index 80% rename from src/multilspy/multilspy_config.py rename to src/solidlsp/ls_config.py index 3d34bae..c612612 100644 --- a/src/multilspy/multilspy_config.py +++ b/src/solidlsp/ls_config.py @@ -1,10 +1,10 @@ """ -Configuration parameters for Multilspy. +Configuration objects for language servers """ + import fnmatch -from enum import Enum -from typing import List from dataclasses import dataclass, field +from enum import Enum class FilenameMatcher: @@ -32,7 +32,6 @@ class Language(str, Enum): JAVA = "java" KOTLIN = "kotlin" TYPESCRIPT = "typescript" - JAVASCRIPT = "javascript" GO = "go" RUBY = "ruby" DART = "dart" @@ -50,9 +49,13 @@ class Language(str, Enum): case self.JAVA: return FilenameMatcher("*.java") case self.TYPESCRIPT: - return FilenameMatcher("*.ts", "*.js", "*.jsx", "*.tsx") - case self.JAVASCRIPT: - return FilenameMatcher("*.js", "*.jsx") + # see https://github.com/oraios/serena/issues/204 + path_patterns = [] + for prefix in ["c", "m", ""]: + for postfix in ["x", ""]: + for base_pattern in ["ts", "js"]: + path_patterns.append(f"*.{prefix}{base_pattern}{postfix}") + return FilenameMatcher(*path_patterns) case self.CSHARP: return FilenameMatcher("*.cs") case self.RUST: @@ -76,17 +79,16 @@ class Language(str, Enum): @dataclass -class MultilspyConfig: +class LanguageServerConfig: """ Configuration parameters """ + code_language: Language trace_lsp_communication: bool = False start_independent_lsp_process: bool = True ignored_paths: list[str] = field(default_factory=list) """Paths, dirs or glob-like patterns. The matching will follow the same logic as for .gitignore entries""" - gitignore_file_content: str | None = None - """Optional content of the gitignore file. If passed, will be used in addition to the explicitly passed ignored_paths for deciding which paths to ignore.""" @classmethod def from_dict(cls, env: dict): @@ -94,7 +96,5 @@ class MultilspyConfig: Create a MultilspyConfig instance from a dictionary """ import inspect - return cls(**{ - k: v for k, v in env.items() - if k in inspect.signature(cls).parameters - }) + + return cls(**{k: v for k, v in env.items() if k in inspect.signature(cls).parameters}) diff --git a/src/multilspy/multilspy_exceptions.py b/src/solidlsp/ls_exceptions.py similarity index 77% rename from src/multilspy/multilspy_exceptions.py rename to src/solidlsp/ls_exceptions.py index d645d13..0484da6 100644 --- a/src/multilspy/multilspy_exceptions.py +++ b/src/solidlsp/ls_exceptions.py @@ -2,7 +2,8 @@ This module contains the exceptions raised by the Multilspy framework. """ -class MultilspyException(Exception): + +class LanguageServerException(Exception): """ Exceptions raised by the Multilspy framework. """ @@ -11,4 +12,4 @@ class MultilspyException(Exception): """ Initializes the exception with the given message. """ - super().__init__(message) \ No newline at end of file + super().__init__(message) diff --git a/src/multilspy/lsp_protocol_handler/server.py b/src/solidlsp/ls_handler.py similarity index 55% rename from src/multilspy/lsp_protocol_handler/server.py rename to src/solidlsp/ls_handler.py index 85e8ac5..65a5931 100644 --- a/src/multilspy/lsp_protocol_handler/server.py +++ b/src/solidlsp/ls_handler.py @@ -1,149 +1,63 @@ -""" -This file provides the implementation of the JSON-RPC client, that launches and -communicates with the language server. - -The initial implementation of this file was obtained from -https://github.com/predragnikolic/OLSP under the MIT License with the following terms: - -MIT License - -Copyright (c) 2023 Предраг Николић - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - import asyncio -import dataclasses import json import logging import os +import subprocess +import threading +import time +from collections.abc import Callable +from dataclasses import dataclass +from queue import Queue +from typing import Any + import psutil -from typing import Any, Callable, Dict, List, Optional, Union -from .lsp_requests import LspNotification, LspRequest -from .lsp_types import ErrorCodes -from ..multilspy_exceptions import MultilspyException +from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_request import LanguageServerRequest +from solidlsp.lsp_protocol_handler.lsp_requests import LspNotification +from solidlsp.lsp_protocol_handler.lsp_types import ErrorCodes +from solidlsp.lsp_protocol_handler.server import ( + ENCODING, + Error, + MessageType, + PayloadLike, + ProcessLaunchInfo, + StringDict, + content_length, + create_message, + make_error_response, + make_notification, + make_request, + make_response, +) -StringDict = Dict[str, Any] -PayloadLike = Union[List[StringDict], StringDict, None] -CONTENT_LENGTH = "Content-Length: " -ENCODING = "utf-8" log = logging.getLogger(__name__) -@dataclasses.dataclass -class ProcessLaunchInfo: - """ - This class is used to store the information required to launch a process. - """ - - # The command to launch the process - cmd: str - - # The environment variables to set for the process - env: Dict[str, str] = dataclasses.field(default_factory=dict) - - # The working directory for the process - cwd: str = os.getcwd() - - -class Error(Exception): - def __init__(self, code: ErrorCodes, message: str) -> None: - super().__init__(message) - self.code = code - - def to_lsp(self) -> StringDict: - return {"code": self.code, "message": super().__str__()} - - @classmethod - def from_lsp(cls, d: StringDict) -> "Error": - return Error(d["code"], d["message"]) - - def __str__(self) -> str: - return f"{super().__str__()} ({self.code})" - - -def make_response(request_id: Any, params: PayloadLike) -> StringDict: - return {"jsonrpc": "2.0", "id": request_id, "result": params} - - -def make_error_response(request_id: Any, err: Error) -> StringDict: - return {"jsonrpc": "2.0", "id": request_id, "error": err.to_lsp()} - - -def make_notification(method: str, params: PayloadLike) -> StringDict: - return {"jsonrpc": "2.0", "method": method, "params": params} - - -def make_request(method: str, request_id: Any, params: PayloadLike) -> StringDict: - return {"jsonrpc": "2.0", "method": method, "id": request_id, "params": params} - - -class StopLoopException(Exception): - pass - - -def create_message(payload: PayloadLike): - body = json.dumps(payload, check_circular=False, ensure_ascii=False, separators=(",", ":")).encode(ENCODING) - return ( - f"Content-Length: {len(body)}\r\n".encode(ENCODING), - "Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n".encode(ENCODING), - body, - ) - - -class MessageType: - error = 1 - warning = 2 - info = 3 - log = 4 - - class Request: + + @dataclass + class Result: + payload: PayloadLike | None = None + error: Error | None = None + + def is_error(self) -> bool: + return self.error is not None + def __init__(self) -> None: - self.cv = asyncio.Condition() - self.result: Optional[PayloadLike] = None - self.error: Optional[Error] = None + self._result_queue = Queue() - async def on_result(self, params: PayloadLike) -> None: - self.result = params - async with self.cv: - self.cv.notify() + def on_result(self, params: PayloadLike) -> None: + self._result_queue.put(Request.Result(payload=params)) - async def on_error(self, err: Error) -> None: - self.error = err - async with self.cv: - self.cv.notify() + def on_error(self, err: Error) -> None: + self._result_queue.put(Request.Result(error=err)) + + def get_result(self, timeout: float | None = None) -> Result: + return self._result_queue.get(timeout=timeout) -def content_length(line: bytes) -> Optional[int]: - if line.startswith(b"Content-Length: "): - _, value = line.split(b"Content-Length: ") - value = value.strip() - try: - return int(value) - except ValueError: - raise ValueError("Invalid Content-Length header: {}".format(value)) - return None - - -class LanguageServerHandler: +class SolidLanguageServerHandler: """ This class provides the implementation of Python client for the Language Server Protocol. A class that launches the language server and communicates with it @@ -179,21 +93,17 @@ class LanguageServerHandler: language server process in an independent process group. Default is `True`. Setting it to `False` means that the language server process will be in the same process group as the the current process, and any SIGINT and SIGTERM signals will be sent to both processes. + """ def __init__( self, process_launch_info: ProcessLaunchInfo, - logger: Optional[Callable[[str, str, StringDict | str], None]] = None, + logger: Callable[[str, str, StringDict | str], None] | None = None, start_independent_lsp_process=True, + request_timeout: float | None = None, ) -> None: - """ - Params: - cmd: A string that represents the command to launch the language server process. - logger: An optional function that takes two strings (source and destination) and - a payload dictionary, and logs the communication between the client and the server. - """ - self.send = LspRequest(self.send_request) + self.send = LanguageServerRequest(self.send_request) self.notify = LspNotification(self.send_notification) self.process_launch_info = process_launch_info @@ -201,7 +111,7 @@ class LanguageServerHandler: self._received_shutdown = False self.request_id = 1 - self._response_handlers: Dict[Any, Request] = {} + self._response_handlers: dict[Any, Request] = {} self.on_request_handlers = {} self.on_notification_handlers = {} self.logger = logger @@ -209,8 +119,27 @@ class LanguageServerHandler: self.task_counter = 0 self.loop = None self.start_independent_lsp_process = start_independent_lsp_process + self._request_timeout = request_timeout - async def start(self) -> None: + # Add thread locks for shared resources to prevent race conditions + self._stdin_lock = threading.Lock() + self._request_id_lock = threading.Lock() + self._response_handlers_lock = threading.Lock() + self._tasks_lock = threading.Lock() + + def set_request_timeout(self, timeout: float | None) -> None: + """ + :param timeout: the timeout, in seconds, for all requests sent to the language server. + """ + self._request_timeout = timeout + + def is_running(self) -> bool: + """ + Checks if the language server process is currently running. + """ + return self.process is not None and self.process.returncode is None + + def start(self) -> None: """ Starts the language server process and creates a task to continuously read from its stdout to handle communications from the server to the client @@ -219,81 +148,62 @@ class LanguageServerHandler: child_proc_env.update(self.process_launch_info.env) log.info("Starting language server process via command: %s", self.process_launch_info.cmd) - self.process = await asyncio.create_subprocess_shell( + self.process = subprocess.Popen( self.process_launch_info.cmd, - stdout=asyncio.subprocess.PIPE, - stdin=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, env=child_proc_env, cwd=self.process_launch_info.cwd, start_new_session=self.start_independent_lsp_process, + shell=True, ) # Check if process terminated immediately if self.process.returncode is not None: log.error("Language server has already terminated/could not be started") # Process has already terminated - stderr_data = await self.process.stderr.read() - error_message = stderr_data.decode('utf-8', errors='replace') + stderr_data = self.process.stderr.read() + error_message = stderr_data.decode("utf-8", errors="replace") raise RuntimeError(f"Process terminated immediately with code {self.process.returncode}. Error: {error_message}") - self.loop = asyncio.get_event_loop() - self.tasks[self.task_counter] = self.loop.create_task(self.run_forever()) - self.task_counter += 1 - self.tasks[self.task_counter] = self.loop.create_task(self.run_forever_stderr()) - self.task_counter += 1 + # start threads to read stdout and stderr of the process + threading.Thread( + target=self.run_forever, + name="LSP-stdout-reader", + daemon=True, + ).start() + threading.Thread( + target=self.run_forever_stderr, + name="LSP-stderr-reader", + daemon=True, + ).start() - async def stop(self) -> None: + def stop(self) -> None: """ Sends the terminate signal to the language server process and waits for it to exit, with a timeout, killing it if necessary """ - # First cancel all tasks - await self._cancel_pending_tasks() - process = self.process self.process = None - - if not process: - return - - # Clean up the process - await self._cleanup_process(process) + if process: + self._cleanup_process(process) - async def _cancel_pending_tasks(self): - """Cancel all pending tasks and wait for them to complete or timeout.""" - pending_tasks = [] - for task in self.tasks.values(): - if not task.done(): - task.cancel() - pending_tasks.append(task) - - if pending_tasks: - try: - await asyncio.wait_for(asyncio.gather(*pending_tasks, return_exceptions=True), timeout=5.0) - except (asyncio.TimeoutError, Exception): - pass - - self.tasks = {} - - async def _cleanup_process(self, process): + def _cleanup_process(self, process): """Clean up a process: close stdin, terminate/kill process, close stdout/stderr.""" # Close stdin first to prevent deadlocks # See: https://bugs.python.org/issue35539 self._safely_close_pipe(process.stdin) - + # Terminate/kill the process if it's still running if process.returncode is None: - await self._terminate_or_kill_process(process) - + self._terminate_or_kill_process(process) + # Close stdout and stderr pipes after process has exited # This is essential to prevent "I/O operation on closed pipe" errors and # "Event loop is closed" errors during garbage collection # See: https://bugs.python.org/issue41320 and https://github.com/python/cpython/issues/88050 self._safely_close_pipe(process.stdout) self._safely_close_pipe(process.stderr) - - # Small delay to ensure OS has released file handles - await asyncio.sleep(0.5) def _safely_close_pipe(self, pipe): """Safely close a pipe, ignoring any exceptions.""" @@ -303,14 +213,16 @@ class LanguageServerHandler: except Exception: pass - async def _terminate_or_kill_process(self, process): + def _terminate_or_kill_process(self, process): """Try to terminate the process gracefully, then forcefully if necessary.""" # First try to terminate the process tree gracefully self._signal_process_tree(process, terminate=True) - + + # TODO + """ # Wait for the process to exit (with timeout) try: - await asyncio.wait_for(process.wait(), timeout=10) + asyncio.wait_for(process.wait(), timeout=10) except (asyncio.TimeoutError, Exception): # If termination failed, forcefully kill the process tree self._signal_process_tree(process, terminate=False) @@ -319,18 +231,19 @@ class LanguageServerHandler: await asyncio.wait_for(process.wait(), timeout=2) except Exception: pass + """ def _signal_process_tree(self, process, terminate=True): """Send signal (terminate or kill) to the process and all its children.""" signal_method = "terminate" if terminate else "kill" - + # Try to get the parent process parent = None try: parent = psutil.Process(process.pid) except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): pass - + # If we have the parent process and it's running, signal the entire tree if parent and parent.is_running(): # Signal children first @@ -339,7 +252,7 @@ class LanguageServerHandler: getattr(child, signal_method)() except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): pass - + # Then signal the parent try: getattr(parent, signal_method)() @@ -352,19 +265,25 @@ class LanguageServerHandler: except Exception: pass - - async def shutdown(self) -> None: + def shutdown(self) -> None: """ Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit """ - await self.send.shutdown() + self._log("Sending shutdown request to server") + self.send.shutdown() + self._log("Received shutdown response from server") self._received_shutdown = True + self._log("Sending exit notification to server") self.notify.exit() + self._log("Sent exit notification to server") + # TODO + """ if self.process and self.process.stdout: self.process.stdout.set_exception(StopLoopException()) # This yields the control to the event loop to allow the exception to be handled # in the run_forever and run_forever_stderr methods await asyncio.sleep(0) + """ def _log(self, message: str | StringDict) -> None: """ @@ -373,14 +292,33 @@ class LanguageServerHandler: if self.logger is not None: self.logger("client", "logger", message) - async def run_forever(self) -> bool: + @staticmethod + def _read_bytes_from_process(process, stream, num_bytes): + """Read exactly num_bytes from process stdout""" + if process.poll() is not None: + # Process has terminated, check if we can still read + pass + + data = b"" + while len(data) < num_bytes: + chunk = stream.read(num_bytes - len(data)) + if not chunk: + if process.poll() is not None: + raise EOFError(f"Process terminated. Expected {num_bytes} bytes, got {len(data)}") + # Process still running but no data available yet + time.sleep(0.01) # Small delay + continue + data += chunk + return data + + def run_forever(self) -> bool: """ Continuously read from the language server process stdout and handle the messages invoking the registered response and notification handlers """ try: - while self.process and self.process.stdout and not self.process.stdout.at_eof(): - line = await self.process.stdout.readline() + while self.process and self.process.stdout and self.process.stdout.readable(): + line = self.process.stdout.readline() if not line: continue try: @@ -390,44 +328,43 @@ class LanguageServerHandler: if num_bytes is None: continue while line and line.strip(): - line = await self.process.stdout.readline() + line = self.process.stdout.readline() if not line: continue - body = await self.process.stdout.readexactly(num_bytes) + body = self._read_bytes_from_process(self.process, self.process.stdout, num_bytes) - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(self._handle_body(body)) - self.task_counter += 1 - except (BrokenPipeError, ConnectionResetError, StopLoopException): + self._handle_body(body) + except (BrokenPipeError, ConnectionResetError): pass return self._received_shutdown - async def run_forever_stderr(self) -> None: + def run_forever_stderr(self) -> None: """ Continuously read from the language server process stderr and log the messages """ try: - while self.process and self.process.stderr and not self.process.stderr.at_eof(): - line = await self.process.stderr.readline() + while self.process and self.process.stderr and self.process.stderr.readable(): + line = self.process.stderr.readline() if not line: continue - self._log("LSP stderr: " + line.decode(ENCODING, errors='replace')) - except (BrokenPipeError, ConnectionResetError, StopLoopException): + self._log("LSP stderr: " + line.decode(ENCODING, errors="replace")) + except (BrokenPipeError, ConnectionResetError): pass - async def _handle_body(self, body: bytes) -> None: + def _handle_body(self, body: bytes) -> None: """ Parse the body text received from the language server process and invoke the appropriate handler """ try: - await self._receive_payload(json.loads(body)) - except IOError as ex: + self._receive_payload(json.loads(body)) + except OSError as ex: self._log(f"malformed {ENCODING}: {ex}") except UnicodeDecodeError as ex: self._log(f"malformed {ENCODING}: {ex}") except json.JSONDecodeError as ex: self._log(f"malformed JSON: {ex}") - async def _receive_payload(self, payload: StringDict) -> None: + def _receive_payload(self, payload: StringDict) -> None: """ Determine if the payload received from server is for a request, response, or notification and invoke the appropriate handler """ @@ -436,70 +373,64 @@ class LanguageServerHandler: try: if "method" in payload: if "id" in payload: - await self._request_handler(payload) + self._request_handler(payload) else: - await self._notification_handler(payload) + self._notification_handler(payload) elif "id" in payload: - await self._response_handler(payload) + self._response_handler(payload) else: self._log(f"Unknown payload type: {payload}") except Exception as err: self._log(f"Error handling server payload: {err}") - def send_notification(self, method: str, params: Optional[dict] = None) -> None: + def send_notification(self, method: str, params: dict | None = None) -> None: """ Send notification pertaining to the given method to the server with the given parameters """ - self._send_payload_sync(make_notification(method, params)) + self._send_payload(make_notification(method, params)) def send_response(self, request_id: Any, params: PayloadLike) -> None: """ Send response to the given request id to the server with the given parameters """ - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task( - self._send_payload(make_response(request_id, params)) - ) - self.task_counter += 1 + self._send_payload(make_response(request_id, params)) def send_error_response(self, request_id: Any, err: Error) -> None: """ Send error response to the given request id to the server with the given error """ - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task( - self._send_payload(make_error_response(request_id, err)) - ) - self.task_counter += 1 + # Use lock to prevent race conditions on tasks and task_counter + self._send_payload(make_error_response(request_id, err)) - async def send_request(self, method: str, params: Optional[dict] = None) -> PayloadLike: + def send_request(self, method: str, params: dict | None = None) -> PayloadLike: """ Send request to the server, register the request id, and wait for the response """ request = Request() - request_id = self.request_id - self.request_id += 1 - self._response_handlers[request_id] = request - async with request.cv: - await self._send_payload(make_request(method, request_id, params)) - self._log(f"Waiting for asyncio condition for request {method} with params:\n{params}") - await request.cv.wait() - self._log(f"Finished waiting, processing result") - if isinstance(request.error, Error): - raise MultilspyException(f"Could not process request {method} with params:\n{params}.\n Language server error: {request.error}") from request.error - self._log(f"Returning non-error result, which is:\n{request.result}") - return request.result - def _send_payload_sync(self, payload: StringDict) -> None: - """ - Send the payload to the server by writing to its stdin synchronously - """ - if not self.process or not self.process.stdin: - return - msg = create_message(payload) - if self.logger: - self.logger("client", "server", payload) - self.process.stdin.writelines(msg) + # Use lock to prevent race conditions on request_id and _response_handlers + with self._request_id_lock: + request_id = self.request_id + self.request_id += 1 - async def _send_payload(self, payload: StringDict) -> None: + with self._response_handlers_lock: + self._response_handlers[request_id] = request + + self._send_payload(make_request(method, request_id, params)) + + self._log(f"Waiting for response to request {method} with params:\n{params}") + result = request.get_result(timeout=self._request_timeout) + + self._log("Processing result") + if result.is_error(): + raise LanguageServerException( + f"Could not process request {method} with params:\n{params}.\n Language server error: {result.error}" + ) from result.error + + self._log(f"Returning non-error result, which is:\n{result.payload}") + return result.payload + + def _send_payload(self, payload: StringDict) -> None: """ Send the payload to the server by writing to its stdin asynchronously. """ @@ -507,8 +438,17 @@ class LanguageServerHandler: return self._log(payload) msg = create_message(payload) - self.process.stdin.writelines(msg) - await self.process.stdin.drain() + + # Use lock to prevent concurrent writes to stdin that cause buffer corruption + with self._stdin_lock: + try: + self.process.stdin.writelines(msg) + self.process.stdin.flush() + except (BrokenPipeError, ConnectionResetError, OSError) as e: + # Log the error but don't raise to prevent cascading failures + if self.logger: + self.logger("client", "logger", f"Failed to write to stdin: {e}") + return def on_request(self, method: str, cb) -> None: """ @@ -522,19 +462,21 @@ class LanguageServerHandler: """ self.on_notification_handlers[method] = cb - async def _response_handler(self, response: StringDict) -> None: + def _response_handler(self, response: StringDict) -> None: """ Handle the response received from the server for a request, using the id to determine the request """ - request = self._response_handlers.pop(response["id"]) - if "result" in response and "error" not in response: - await request.on_result(response["result"]) - elif "result" not in response and "error" in response: - await request.on_error(Error.from_lsp(response["error"])) - else: - await request.on_error(Error(ErrorCodes.InvalidRequest, "")) + with self._response_handlers_lock: + request = self._response_handlers.pop(response["id"]) - async def _request_handler(self, response: StringDict) -> None: + if "result" in response and "error" not in response: + request.on_result(response["result"]) + elif "result" not in response and "error" in response: + request.on_error(Error.from_lsp(response["error"])) + else: + request.on_error(Error(ErrorCodes.InvalidRequest, "")) + + def _request_handler(self, response: StringDict) -> None: """ Handle the request received from the server: call the appropriate callback function and return the result """ @@ -547,18 +489,18 @@ class LanguageServerHandler: request_id, Error( ErrorCodes.MethodNotFound, - "method '{}' not handled on client.".format(method), + f"method '{method}' not handled on client.", ), ) return try: - self.send_response(request_id, await handler(params)) + self.send_response(request_id, handler(params)) except Error as ex: self.send_error_response(request_id, ex) except Exception as ex: self.send_error_response(request_id, Error(ErrorCodes.InternalError, str(ex))) - async def _notification_handler(self, response: StringDict) -> None: + def _notification_handler(self, response: StringDict) -> None: """ Handle the notification received from the server: call the appropriate callback function """ @@ -569,7 +511,7 @@ class LanguageServerHandler: self._log(f"unhandled {method}") return try: - await handler(params) + handler(params) except asyncio.CancelledError: return except Exception as ex: diff --git a/src/multilspy/multilspy_logger.py b/src/solidlsp/ls_logger.py similarity index 86% rename from src/multilspy/multilspy_logger.py rename to src/solidlsp/ls_logger.py index e604e1a..6bdf12c 100644 --- a/src/multilspy/multilspy_logger.py +++ b/src/solidlsp/ls_logger.py @@ -1,9 +1,11 @@ """ Multilspy logger module. """ + import inspect import logging from datetime import datetime + from pydantic import BaseModel @@ -20,7 +22,7 @@ class LogLine(BaseModel): message: str -class MultilspyLogger: +class LanguageServerLogger: """ Logger class """ @@ -30,11 +32,10 @@ class MultilspyLogger: self.logger.setLevel(log_level) self.json_format = json_format - def log(self, debug_message: str, level: int, sanitized_error_message: str = "") -> None: + def log(self, debug_message: str, level: int, sanitized_error_message: str = "", stacklevel: int = 2) -> None: """ - Log the debug and santized messages using the logger + Log the debug and sanitized messages using the logger """ - debug_message = debug_message.replace("'", '"').replace("\n", " ") sanitized_error_message = sanitized_error_message.replace("'", '"').replace("\n", " ") @@ -59,6 +60,7 @@ class MultilspyLogger: self.logger.log( level=level, msg=debug_log_line.json(), + stacklevel=stacklevel, ) else: - self.logger.log(level, debug_message) + self.logger.log(level, debug_message, stacklevel=stacklevel) diff --git a/src/solidlsp/ls_request.py b/src/solidlsp/ls_request.py new file mode 100644 index 0000000..209a3f0 --- /dev/null +++ b/src/solidlsp/ls_request.py @@ -0,0 +1,377 @@ +from typing import Union + +from solidlsp.lsp_protocol_handler import lsp_types + + +class LanguageServerRequest: + def __init__(self, send_request): + self.send_request = send_request + + def implementation(self, params: lsp_types.ImplementationParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: + """A request to resolve the implementation locations of a symbol at a given text + document position. The request's parameter is of type [TextDocumentPositionParams] + (#TextDocumentPositionParams) the response is of type {@link Definition} or a + Thenable that resolves to such. + """ + return self.send_request("textDocument/implementation", params) + + def type_definition( + self, params: lsp_types.TypeDefinitionParams + ) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: + """A request to resolve the type definition locations of a symbol at a given text + document position. The request's parameter is of type [TextDocumentPositionParams] + (#TextDocumentPositionParams) the response is of type {@link Definition} or a + Thenable that resolves to such. + """ + return self.send_request("textDocument/typeDefinition", params) + + def document_color(self, params: lsp_types.DocumentColorParams) -> list["lsp_types.ColorInformation"]: + """A request to list all color symbols found in a given text document. The request's + parameter is of type {@link DocumentColorParams} the + response is of type {@link ColorInformation ColorInformation[]} or a Thenable + that resolves to such. + """ + return self.send_request("textDocument/documentColor", params) + + def color_presentation(self, params: lsp_types.ColorPresentationParams) -> list["lsp_types.ColorPresentation"]: + """A request to list all presentation for a color. The request's + parameter is of type {@link ColorPresentationParams} the + response is of type {@link ColorInformation ColorInformation[]} or a Thenable + that resolves to such. + """ + return self.send_request("textDocument/colorPresentation", params) + + def folding_range(self, params: lsp_types.FoldingRangeParams) -> list["lsp_types.FoldingRange"] | None: + """A request to provide folding ranges in a document. The request's + parameter is of type {@link FoldingRangeParams}, the + response is of type {@link FoldingRangeList} or a Thenable + that resolves to such. + """ + return self.send_request("textDocument/foldingRange", params) + + def declaration(self, params: lsp_types.DeclarationParams) -> Union["lsp_types.Declaration", list["lsp_types.LocationLink"], None]: + """A request to resolve the type definition locations of a symbol at a given text + document position. The request's parameter is of type [TextDocumentPositionParams] + (#TextDocumentPositionParams) the response is of type {@link Declaration} + or a typed array of {@link DeclarationLink} or a Thenable that resolves + to such. + """ + return self.send_request("textDocument/declaration", params) + + def selection_range(self, params: lsp_types.SelectionRangeParams) -> list["lsp_types.SelectionRange"] | None: + """A request to provide selection ranges in a document. The request's + parameter is of type {@link SelectionRangeParams}, the + response is of type {@link SelectionRange SelectionRange[]} or a Thenable + that resolves to such. + """ + return self.send_request("textDocument/selectionRange", params) + + def prepare_call_hierarchy(self, params: lsp_types.CallHierarchyPrepareParams) -> list["lsp_types.CallHierarchyItem"] | None: + """A request to result a `CallHierarchyItem` in a document at a given position. + Can be used as an input to an incoming or outgoing call hierarchy. + + @since 3.16.0 + """ + return self.send_request("textDocument/prepareCallHierarchy", params) + + def incoming_calls(self, params: lsp_types.CallHierarchyIncomingCallsParams) -> list["lsp_types.CallHierarchyIncomingCall"] | None: + """A request to resolve the incoming calls for a given `CallHierarchyItem`. + + @since 3.16.0 + """ + return self.send_request("callHierarchy/incomingCalls", params) + + def outgoing_calls(self, params: lsp_types.CallHierarchyOutgoingCallsParams) -> list["lsp_types.CallHierarchyOutgoingCall"] | None: + """A request to resolve the outgoing calls for a given `CallHierarchyItem`. + + @since 3.16.0 + """ + return self.send_request("callHierarchy/outgoingCalls", params) + + def semantic_tokens_full(self, params: lsp_types.SemanticTokensParams) -> Union["lsp_types.SemanticTokens", None]: + """@since 3.16.0""" + return self.send_request("textDocument/semanticTokens/full", params) + + def semantic_tokens_delta( + self, params: lsp_types.SemanticTokensDeltaParams + ) -> Union["lsp_types.SemanticTokens", "lsp_types.SemanticTokensDelta", None]: + """@since 3.16.0""" + return self.send_request("textDocument/semanticTokens/full/delta", params) + + def semantic_tokens_range(self, params: lsp_types.SemanticTokensRangeParams) -> Union["lsp_types.SemanticTokens", None]: + """@since 3.16.0""" + return self.send_request("textDocument/semanticTokens/range", params) + + def linked_editing_range(self, params: lsp_types.LinkedEditingRangeParams) -> Union["lsp_types.LinkedEditingRanges", None]: + """A request to provide ranges that can be edited together. + + @since 3.16.0 + """ + return self.send_request("textDocument/linkedEditingRange", params) + + def will_create_files(self, params: lsp_types.CreateFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: + """The will create files request is sent from the client to the server before files are actually + created as long as the creation is triggered from within the client. + + @since 3.16.0 + """ + return self.send_request("workspace/willCreateFiles", params) + + def will_rename_files(self, params: lsp_types.RenameFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: + """The will rename files request is sent from the client to the server before files are actually + renamed as long as the rename is triggered from within the client. + + @since 3.16.0 + """ + return self.send_request("workspace/willRenameFiles", params) + + def will_delete_files(self, params: lsp_types.DeleteFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: + """The did delete files notification is sent from the client to the server when + files were deleted from within the client. + + @since 3.16.0 + """ + return self.send_request("workspace/willDeleteFiles", params) + + def moniker(self, params: lsp_types.MonikerParams) -> list["lsp_types.Moniker"] | None: + """A request to get the moniker of a symbol at a given text document position. + The request parameter is of type {@link TextDocumentPositionParams}. + The response is of type {@link Moniker Moniker[]} or `null`. + """ + return self.send_request("textDocument/moniker", params) + + def prepare_type_hierarchy(self, params: lsp_types.TypeHierarchyPrepareParams) -> list["lsp_types.TypeHierarchyItem"] | None: + """A request to result a `TypeHierarchyItem` in a document at a given position. + Can be used as an input to a subtypes or supertypes type hierarchy. + + @since 3.17.0 + """ + return self.send_request("textDocument/prepareTypeHierarchy", params) + + def type_hierarchy_supertypes(self, params: lsp_types.TypeHierarchySupertypesParams) -> list["lsp_types.TypeHierarchyItem"] | None: + """A request to resolve the supertypes for a given `TypeHierarchyItem`. + + @since 3.17.0 + """ + return self.send_request("typeHierarchy/supertypes", params) + + def type_hierarchy_subtypes(self, params: lsp_types.TypeHierarchySubtypesParams) -> list["lsp_types.TypeHierarchyItem"] | None: + """A request to resolve the subtypes for a given `TypeHierarchyItem`. + + @since 3.17.0 + """ + return self.send_request("typeHierarchy/subtypes", params) + + def inline_value(self, params: lsp_types.InlineValueParams) -> list["lsp_types.InlineValue"] | None: + """A request to provide inline values in a document. The request's parameter is of + type {@link InlineValueParams}, the response is of type + {@link InlineValue InlineValue[]} or a Thenable that resolves to such. + + @since 3.17.0 + """ + return self.send_request("textDocument/inlineValue", params) + + def inlay_hint(self, params: lsp_types.InlayHintParams) -> list["lsp_types.InlayHint"] | None: + """A request to provide inlay hints in a document. The request's parameter is of + type {@link InlayHintsParams}, the response is of type + {@link InlayHint InlayHint[]} or a Thenable that resolves to such. + + @since 3.17.0 + """ + return self.send_request("textDocument/inlayHint", params) + + def resolve_inlay_hint(self, params: lsp_types.InlayHint) -> "lsp_types.InlayHint": + """A request to resolve additional properties for an inlay hint. + The request's parameter is of type {@link InlayHint}, the response is + of type {@link InlayHint} or a Thenable that resolves to such. + + @since 3.17.0 + """ + return self.send_request("inlayHint/resolve", params) + + def text_document_diagnostic(self, params: lsp_types.DocumentDiagnosticParams) -> "lsp_types.DocumentDiagnosticReport": + """The document diagnostic request definition. + + @since 3.17.0 + """ + return self.send_request("textDocument/diagnostic", params) + + def workspace_diagnostic(self, params: lsp_types.WorkspaceDiagnosticParams) -> "lsp_types.WorkspaceDiagnosticReport": + """The workspace diagnostic request definition. + + @since 3.17.0 + """ + return self.send_request("workspace/diagnostic", params) + + def initialize(self, params: lsp_types.InitializeParams) -> "lsp_types.InitializeResult": + """The initialize request is sent from the client to the server. + It is sent once as the request after starting up the server. + The requests parameter is of type {@link InitializeParams} + the response if of type {@link InitializeResult} of a Thenable that + resolves to such. + """ + return self.send_request("initialize", params) + + def shutdown(self) -> None: + """A shutdown request is sent from the client to the server. + It is sent once when the client decides to shutdown the + server. The only notification that is sent after a shutdown request + is the exit event. + """ + return self.send_request("shutdown") + + def will_save_wait_until(self, params: lsp_types.WillSaveTextDocumentParams) -> list["lsp_types.TextEdit"] | None: + """A document will save request is sent from the client to the server before + the document is actually saved. The request can return an array of TextEdits + which will be applied to the text document before it is saved. Please note that + clients might drop results if computing the text edits took too long or if a + server constantly fails on this request. This is done to keep the save fast and + reliable. + """ + return self.send_request("textDocument/willSaveWaitUntil", params) + + def completion(self, params: lsp_types.CompletionParams) -> Union[list["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]: + """Request to request completion at a given text document position. The request's + parameter is of type {@link TextDocumentPosition} the response + is of type {@link CompletionItem CompletionItem[]} or {@link CompletionList} + or a Thenable that resolves to such. + + The request can delay the computation of the {@link CompletionItem.detail `detail`} + and {@link CompletionItem.documentation `documentation`} properties to the `completionItem/resolve` + request. However, properties that are needed for the initial sorting and filtering, like `sortText`, + `filterText`, `insertText`, and `textEdit`, must not be changed during resolve. + """ + return self.send_request("textDocument/completion", params) + + def resolve_completion_item(self, params: lsp_types.CompletionItem) -> "lsp_types.CompletionItem": + """Request to resolve additional information for a given completion item.The request's + parameter is of type {@link CompletionItem} the response + is of type {@link CompletionItem} or a Thenable that resolves to such. + """ + return self.send_request("completionItem/resolve", params) + + def hover(self, params: lsp_types.HoverParams) -> Union["lsp_types.Hover", None]: + """Request to request hover information at a given text document position. The request's + parameter is of type {@link TextDocumentPosition} the response is of + type {@link Hover} or a Thenable that resolves to such. + """ + return self.send_request("textDocument/hover", params) + + def signature_help(self, params: lsp_types.SignatureHelpParams) -> Union["lsp_types.SignatureHelp", None]: + return self.send_request("textDocument/signatureHelp", params) + + def definition(self, params: lsp_types.DefinitionParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: + """A request to resolve the definition location of a symbol at a given text + document position. The request's parameter is of type [TextDocumentPosition] + (#TextDocumentPosition) the response is of either type {@link Definition} + or a typed array of {@link DefinitionLink} or a Thenable that resolves + to such. + """ + return self.send_request("textDocument/definition", params) + + def references(self, params: lsp_types.ReferenceParams) -> list["lsp_types.Location"] | None: + """A request to resolve project-wide references for the symbol denoted + by the given text document position. The request's parameter is of + type {@link ReferenceParams} the response is of type + {@link Location Location[]} or a Thenable that resolves to such. + """ + return self.send_request("textDocument/references", params) + + def document_highlight(self, params: lsp_types.DocumentHighlightParams) -> list["lsp_types.DocumentHighlight"] | None: + """Request to resolve a {@link DocumentHighlight} for a given + text document position. The request's parameter is of type [TextDocumentPosition] + (#TextDocumentPosition) the request response is of type [DocumentHighlight[]] + (#DocumentHighlight) or a Thenable that resolves to such. + """ + return self.send_request("textDocument/documentHighlight", params) + + def document_symbol( + self, params: lsp_types.DocumentSymbolParams + ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.DocumentSymbol"] | None: + """A request to list all symbols found in a given text document. The request's + parameter is of type {@link TextDocumentIdentifier} the + response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable + that resolves to such. + """ + return self.send_request("textDocument/documentSymbol", params) + + def code_action(self, params: lsp_types.CodeActionParams) -> list[Union["lsp_types.Command", "lsp_types.CodeAction"]] | None: + """A request to provide commands for the given text document and range.""" + return self.send_request("textDocument/codeAction", params) + + def resolve_code_action(self, params: lsp_types.CodeAction) -> "lsp_types.CodeAction": + """Request to resolve additional information for a given code action.The request's + parameter is of type {@link CodeAction} the response + is of type {@link CodeAction} or a Thenable that resolves to such. + """ + return self.send_request("codeAction/resolve", params) + + def workspace_symbol( + self, params: lsp_types.WorkspaceSymbolParams + ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.WorkspaceSymbol"] | None: + """A request to list project-wide symbols matching the query string given + by the {@link WorkspaceSymbolParams}. The response is + of type {@link SymbolInformation SymbolInformation[]} or a Thenable that + resolves to such. + + @since 3.17.0 - support for WorkspaceSymbol in the returned data. Clients + need to advertise support for WorkspaceSymbols via the client capability + `workspace.symbol.resolveSupport`. + """ + return self.send_request("workspace/symbol", params) + + def resolve_workspace_symbol(self, params: lsp_types.WorkspaceSymbol) -> "lsp_types.WorkspaceSymbol": + """A request to resolve the range inside the workspace + symbol's location. + + @since 3.17.0 + """ + return self.send_request("workspaceSymbol/resolve", params) + + def code_lens(self, params: lsp_types.CodeLensParams) -> list["lsp_types.CodeLens"] | None: + """A request to provide code lens for the given text document.""" + return self.send_request("textDocument/codeLens", params) + + def resolve_code_lens(self, params: lsp_types.CodeLens) -> "lsp_types.CodeLens": + """A request to resolve a command for a given code lens.""" + return self.send_request("codeLens/resolve", params) + + def document_link(self, params: lsp_types.DocumentLinkParams) -> list["lsp_types.DocumentLink"] | None: + """A request to provide document links""" + return self.send_request("textDocument/documentLink", params) + + def resolve_document_link(self, params: lsp_types.DocumentLink) -> "lsp_types.DocumentLink": + """Request to resolve additional information for a given document link. The request's + parameter is of type {@link DocumentLink} the response + is of type {@link DocumentLink} or a Thenable that resolves to such. + """ + return self.send_request("documentLink/resolve", params) + + def formatting(self, params: lsp_types.DocumentFormattingParams) -> list["lsp_types.TextEdit"] | None: + """A request to to format a whole document.""" + return self.send_request("textDocument/formatting", params) + + def range_formatting(self, params: lsp_types.DocumentRangeFormattingParams) -> list["lsp_types.TextEdit"] | None: + """A request to to format a range in a document.""" + return self.send_request("textDocument/rangeFormatting", params) + + def on_type_formatting(self, params: lsp_types.DocumentOnTypeFormattingParams) -> list["lsp_types.TextEdit"] | None: + """A request to format a document on type.""" + return self.send_request("textDocument/onTypeFormatting", params) + + def rename(self, params: lsp_types.RenameParams) -> Union["lsp_types.WorkspaceEdit", None]: + """A request to rename a symbol.""" + return self.send_request("textDocument/rename", params) + + def prepare_rename(self, params: lsp_types.PrepareRenameParams) -> Union["lsp_types.PrepareRenameResult", None]: + """A request to test and perform the setup necessary for a rename. + + @since 3.16 - support for default behavior + """ + return self.send_request("textDocument/prepareRename", params) + + def execute_command(self, params: lsp_types.ExecuteCommandParams) -> Union["lsp_types.LSPAny", None]: + """A request send from the client to the server to execute a command. The request might return + a workspace edit which the client will apply to the workspace. + """ + return self.send_request("workspace/executeCommand", params) diff --git a/src/multilspy/multilspy_types.py b/src/solidlsp/ls_types.py similarity index 91% rename from src/multilspy/multilspy_types.py rename to src/solidlsp/ls_types.py index dad0116..3d4d465 100644 --- a/src/multilspy/multilspy_types.py +++ b/src/solidlsp/ls_types.py @@ -4,16 +4,19 @@ Defines wrapper objects around the types returned by LSP to ensure decoupling be from __future__ import annotations -from enum import IntEnum, Enum -from typing_extensions import NotRequired, TypedDict, List, Dict, Union +from enum import Enum, IntEnum +from typing import NotRequired, Union + +from typing_extensions import TypedDict URI = str DocumentUri = str Uint = int RegExp = str + class Position(TypedDict): - """Position in a text document expressed as zero-based line and character + r"""Position in a text document expressed as zero-based line and character offset. Prior to 3.17 the offsets were always based on a UTF-16 string representation. So a string of the form `a𐐀b` the character offset of the character `a` is 0, the character offset of `𐐀` is 1 and the character @@ -39,7 +42,8 @@ class Position(TypedDict): Positions are line end character agnostic. So you can not specify a position that denotes `\r|\n` or `\n|` where `|` represents the character offset. - @since 3.17.0 - support for negotiated position encoding.""" + @since 3.17.0 - support for negotiated position encoding. + """ line: Uint """ Line position in a document (zero-based). @@ -67,7 +71,8 @@ class Range(TypedDict): start: { line: 5, character: 23 } end : { line 6, character : 0 } } - ```""" + ``` + """ start: Position """ The range's start position. """ @@ -77,12 +82,14 @@ class Range(TypedDict): class Location(TypedDict): """Represents a location inside a resource, such as a line - inside a text file.""" + inside a text file. + """ uri: DocumentUri range: Range absolutePath: str - relativePath: Union[str, None] + relativePath: str | None + class CompletionItemKind(IntEnum): """The kind of a completion entry.""" @@ -113,9 +120,11 @@ class CompletionItemKind(IntEnum): Operator = 24 TypeParameter = 25 + class CompletionItem(TypedDict): """A completion item represents a text snippet that is - proposed to complete text that is being typed.""" + proposed to complete text that is being typed. + """ completionText: str """ The completionText of this completion item. @@ -131,6 +140,7 @@ class CompletionItem(TypedDict): """ A human-readable string with additional information about this item, like type or symbol information. """ + class SymbolKind(IntEnum): """A symbol kind.""" @@ -161,17 +171,21 @@ class SymbolKind(IntEnum): Operator = 25 TypeParameter = 26 + class SymbolTag(IntEnum): """Symbol tags are extra annotations that tweak the rendering of a symbol. - @since 3.16""" + @since 3.16 + """ Deprecated = 1 """ Render a symbol as obsolete, usually using a strike-out. """ + class UnifiedSymbolInformation(TypedDict): """Represents information about programming constructs like variables, classes, - interfaces etc.""" + interfaces etc. + """ deprecated: NotRequired[bool] """ Indicates if this symbol is deprecated. @@ -191,7 +205,7 @@ class UnifiedSymbolInformation(TypedDict): """ The name of this symbol. """ kind: SymbolKind """ The kind of this symbol. """ - tags: NotRequired[List[SymbolTag]] + tags: NotRequired[list[SymbolTag]] """ Tags for this symbol. @since 3.16.0 """ @@ -207,7 +221,7 @@ class UnifiedSymbolInformation(TypedDict): detail: NotRequired[str] """ More detail for this symbol, e.g the signature of a function. """ - + range: NotRequired[Range] """ The range enclosing this symbol not including leading/trailing whitespace but everything else like comments. This information is typically used to determine if the clients cursor is @@ -215,37 +229,40 @@ class UnifiedSymbolInformation(TypedDict): selectionRange: NotRequired[Range] """ The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. Must be contained by the `range`. """ - + body: NotRequired[str] """ The body of the symbol. """ - - children: List[UnifiedSymbolInformation] + + children: list[UnifiedSymbolInformation] """ The children of the symbol. Added to be compatible with `lsp_types.DocumentSymbol`, since it is sometimes useful to have the children of the symbol as a user-facing feature.""" - + parent: NotRequired[UnifiedSymbolInformation | None] """The parent of the symbol, if there is any. Added with Serena, not part of the LSP. All symbols except the root packages will have a parent. """ - + class MarkupKind(Enum): """Describes the content type that a client supports in various result literals like `Hover`, `ParameterInfo` or `CompletionItem`. Please note that `MarkupKinds` must not start with a `$`. This kinds - are reserved for internal usage.""" + are reserved for internal usage. + """ PlainText = "plaintext" """ Plain text is supported as a content format """ Markdown = "markdown" """ Markdown is supported as a content format """ + class __MarkedString_Type_1(TypedDict): language: str value: str + MarkedString = Union[str, "__MarkedString_Type_1"] """ MarkedString can be used to render human readable text. It is either a markdown string or a code-block that provides a language and a code snippet. The language identifier @@ -260,8 +277,9 @@ ${value} Note that markdown strings will be sanitized - that means html will be escaped. @deprecated use MarkupContent instead. """ + class MarkupContent(TypedDict): - """A `MarkupContent` literal represents a string value which content is interpreted base on its + r"""A `MarkupContent` literal represents a string value which content is interpreted base on its kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds. If the kind is `markdown` then the value can contain fenced code blocks like in GitHub issues. @@ -282,18 +300,20 @@ class MarkupContent(TypedDict): ``` *Please Note* that clients might sanitize the return markdown. A client could decide to - remove HTML from the markdown to avoid script execution.""" + remove HTML from the markdown to avoid script execution. + """ - kind: "MarkupKind" + kind: MarkupKind """ The type of the Markup """ value: str """ The content itself """ + class Hover(TypedDict): """The result of a hover request.""" - contents: Union["MarkupContent", "MarkedString", List["MarkedString"]] + contents: MarkupContent | MarkedString | list[MarkedString] """ The hover's content """ - range: NotRequired["Range"] + range: NotRequired[Range] """ An optional range inside the text document that is used to - visualize the hover, e.g. by changing the background color. """ \ No newline at end of file + visualize the hover, e.g. by changing the background color. """ diff --git a/src/multilspy/multilspy_utils.py b/src/solidlsp/ls_utils.py similarity index 68% rename from src/multilspy/multilspy_utils.py rename to src/solidlsp/ls_utils.py index c649ba9..7aa610e 100644 --- a/src/multilspy/multilspy_utils.py +++ b/src/solidlsp/ls_utils.py @@ -5,27 +5,31 @@ This file contains various utility functions like I/O operations, handling paths import gzip import logging import os -from typing import Tuple, Union -import requests -import shutil -import uuid - import platform +import shutil import subprocess +import uuid from enum import Enum +from pathlib import Path, PurePath -from multilspy.multilspy_exceptions import MultilspyException -from pathlib import PurePath, Path -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_types import UnifiedSymbolInformation +import requests + +from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_types import UnifiedSymbolInformation + + +class InvalidTextLocationError(Exception): + pass class TextUtils: """ Utilities for text operations. """ + @staticmethod - def get_line_col_from_index(text: str, index: int) -> Tuple[int, int]: + def get_line_col_from_index(text: str, index: int) -> tuple[int, int]: """ Returns the zero-indexed line and column number of the given index in the given text """ @@ -33,7 +37,7 @@ class TextUtils: c = 0 idx = 0 while idx < index: - if text[idx] == '\n': + if text[idx] == "\n": l += 1 c = 0 else: @@ -41,7 +45,7 @@ class TextUtils: idx += 1 return l, c - + @staticmethod def get_index_from_line_col(text: str, line: int, col: int) -> int: """ @@ -49,46 +53,57 @@ class TextUtils: """ idx = 0 while line > 0: - assert idx < len(text), (idx, len(text), text) + if idx >= len(text): + raise InvalidTextLocationError if text[idx] == "\n": line -= 1 idx += 1 idx += col return idx - + @staticmethod - def _get_updated_position_from_line_and_column_and_edit(l: int, c: int, text_to_be_inserted: str) -> Tuple[int, int]: + def _get_updated_position_from_line_and_column_and_edit(l: int, c: int, text_to_be_inserted: str) -> tuple[int, int]: """ Utility function to get the position of the cursor after inserting text at a given line and column. """ - num_newlines_in_gen_text = text_to_be_inserted.count('\n') + num_newlines_in_gen_text = text_to_be_inserted.count("\n") if num_newlines_in_gen_text > 0: l += num_newlines_in_gen_text - c = len(text_to_be_inserted.split('\n')[-1]) + c = len(text_to_be_inserted.split("\n")[-1]) else: c += len(text_to_be_inserted) return (l, c) - + @staticmethod - def delete_text_between_positions(text: str, start_line: int, start_col: int, end_line: int, end_col: int) -> Tuple[str, str]: + def delete_text_between_positions(text: str, start_line: int, start_col: int, end_line: int, end_col: int) -> tuple[str, str]: """ Deletes the text between the given start and end positions. Returns the modified text and the deleted text. """ del_start_idx = TextUtils.get_index_from_line_col(text, start_line, start_col) del_end_idx = TextUtils.get_index_from_line_col(text, end_line, end_col) - + deleted_text = text[del_start_idx:del_end_idx] new_text = text[:del_start_idx] + text[del_end_idx:] return new_text, deleted_text - + @staticmethod - def insert_text_at_position(text: str, line: int, col: int, text_to_be_inserted: str) -> Tuple[str, int, int]: + def insert_text_at_position(text: str, line: int, col: int, text_to_be_inserted: str) -> tuple[str, int, int]: """ Inserts the given text at the given line and column. Returns the modified text and the new line and column. """ - change_index = TextUtils.get_index_from_line_col(text, line, col) + try: + change_index = TextUtils.get_index_from_line_col(text, line, col) + except InvalidTextLocationError: + num_lines_in_text = text.count("\n") + 1 + max_line = num_lines_in_text - 1 + if line == max_line + 1 and col == 0: # trying to insert at new line after full text + # insert at end, adding missing newline + change_index = len(text) + text_to_be_inserted = "\n" + text_to_be_inserted + else: + raise new_text = text[:change_index] + text_to_be_inserted + text[change_index:] new_l, new_c = TextUtils._get_updated_position_from_line_and_column_and_edit(line, col, text_to_be_inserted) return new_text, new_l, new_c @@ -98,6 +113,7 @@ class PathUtils: """ Utilities for platform-agnostic path operations. """ + @staticmethod def uri_to_path(uri: str) -> str: """ @@ -106,14 +122,15 @@ class PathUtils: This method was obtained from https://stackoverflow.com/a/61922504 """ try: - from urllib.parse import urlparse, unquote + from urllib.parse import unquote, urlparse from urllib.request import url2pathname except ImportError: - # backwards compatability - from urlparse import urlparse + # backwards compatibility from urllib import unquote, url2pathname + + from urlparse import urlparse parsed = urlparse(uri) - host = "{0}{0}{mnt}{0}".format(os.path.sep, mnt=parsed.netloc) + host = f"{os.path.sep}{os.path.sep}{parsed.netloc}{os.path.sep}" return os.path.normpath(os.path.join(host, url2pathname(unquote(parsed.path)))) @staticmethod @@ -126,10 +143,10 @@ class PathUtils: @staticmethod def is_glob_pattern(pattern: str) -> bool: """Check if a pattern contains glob-specific characters.""" - return any(c in pattern for c in '*?[]!') + return any(c in pattern for c in "*?[]!") @staticmethod - def get_relative_path(path: str, base_path: str) -> Union[str, None]: + def get_relative_path(path: str, base_path: str) -> str | None: """ Gets relative path if it's possible (paths should be on the same drive), returns `None` otherwise. @@ -145,22 +162,22 @@ class FileUtils: """ @staticmethod - def read_file(logger: MultilspyLogger, file_path: str) -> str: + def read_file(logger: LanguageServerLogger, file_path: str) -> str: """ Reads the file at the given path and returns the contents as a string. """ if not os.path.exists(file_path): logger.log(f"File read '{file_path}' failed: File does not exist.", logging.ERROR) - raise MultilspyException(f"File read '{file_path}' failed: File does not exist.") + raise LanguageServerException(f"File read '{file_path}' failed: File does not exist.") try: - with open(file_path, "r", encoding="utf-8") as inp_file: + with open(file_path, encoding="utf-8") as inp_file: return inp_file.read() except Exception as exc: logger.log(f"File read '{file_path}' failed to read with encoding 'utf-8': {exc}", logging.ERROR) - raise MultilspyException("File read failed.") from None - + raise LanguageServerException("File read failed.") from None + @staticmethod - def download_file(logger: MultilspyLogger, url: str, target_path: str) -> None: + def download_file(logger: LanguageServerLogger, url: str, target_path: str) -> None: """ Downloads the file from the given URL to the given {target_path} """ @@ -168,15 +185,15 @@ class FileUtils: response = requests.get(url, stream=True, timeout=60) if response.status_code != 200: logger.log(f"Error downloading file '{url}': {response.status_code} {response.text}", logging.ERROR) - raise MultilspyException("Error downoading file.") + raise LanguageServerException("Error downloading file.") with open(target_path, "wb") as f: shutil.copyfileobj(response.raw, f) except Exception as exc: logger.log(f"Error downloading file '{url}': {exc}", logging.ERROR) - raise MultilspyException("Error downoading file.") from None + raise LanguageServerException("Error downloading file.") from None @staticmethod - def download_and_extract_archive(logger: MultilspyLogger, url: str, target_path: str, archive_type: str) -> None: + def download_and_extract_archive(logger: LanguageServerLogger, url: str, target_path: str, archive_type: str) -> None: """ Downloads the archive from the given URL having format {archive_type} and extracts it to the given {target_path} """ @@ -201,10 +218,10 @@ class FileUtils: shutil.copyfileobj(f_in, f_out) else: logger.log(f"Unknown archive type '{archive_type}' for extraction", logging.ERROR) - raise MultilspyException(f"Unknown archive type '{archive_type}'") + raise LanguageServerException(f"Unknown archive type '{archive_type}'") except Exception as exc: logger.log(f"Error extracting archive '{tmp_file_name}' obtained from '{url}': {exc}", logging.ERROR) - raise MultilspyException("Error extracting archive.") from exc + raise LanguageServerException("Error extracting archive.") from exc finally: for tmp_file_name in tmp_files: if os.path.exists(tmp_file_name): @@ -215,6 +232,7 @@ class PlatformId(str, Enum): """ multilspy supported platforms """ + WIN_x86 = "win-x86" WIN_x64 = "win-x64" WIN_arm64 = "win-arm64" @@ -232,6 +250,7 @@ class DotnetVersion(str, Enum): """ multilspy supported dotnet versions """ + V4 = "4" V6 = "6" V7 = "7" @@ -260,11 +279,11 @@ class PlatformUtils: platform_id = system_map[system] + "-" + machine_map[machine] if system == "Linux" and bitness == "64bit": libc = platform.libc_ver()[0] - if libc != 'glibc': + if libc != "glibc": platform_id += "-" + libc return PlatformId(platform_id) else: - raise MultilspyException(f"Unknown platform: {system=}, {machine=}, {bitness=}") + raise LanguageServerException(f"Unknown platform: {system=}, {machine=}, {bitness=}") @staticmethod def _determine_windows_machine_type(): @@ -274,13 +293,13 @@ class PlatformUtils: class SYSTEM_INFO(ctypes.Structure): class _U(ctypes.Union): class _S(ctypes.Structure): - _fields_ = [("wProcessorArchitecture", wintypes.WORD), - ("wReserved", wintypes.WORD)] - _fields_ = [("dwOemId", wintypes.DWORD), - ("s", _S)] + _fields_ = [("wProcessorArchitecture", wintypes.WORD), ("wReserved", wintypes.WORD)] + + _fields_ = [("dwOemId", wintypes.DWORD), ("s", _S)] _anonymous_ = ("s",) - _fields_ = [("u", _U), + _fields_ = [ + ("u", _U), ("dwPageSize", wintypes.DWORD), ("lpMinimumApplicationAddress", wintypes.LPVOID), ("lpMaximumApplicationAddress", wintypes.LPVOID), @@ -289,22 +308,22 @@ class PlatformUtils: ("dwProcessorType", wintypes.DWORD), ("dwAllocationGranularity", wintypes.DWORD), ("wProcessorLevel", wintypes.WORD), - ("wProcessorRevision", wintypes.WORD)] + ("wProcessorRevision", wintypes.WORD), + ] _anonymous_ = ("u",) sys_info = SYSTEM_INFO() ctypes.windll.kernel32.GetNativeSystemInfo(ctypes.byref(sys_info)) arch_map = { - 9: 'AMD64', - 5: 'ARM', - 12: 'arm64', - 6: 'Intel Itanium-based', - 0: 'i386', + 9: "AMD64", + 5: "ARM", + 12: "arm64", + 6: "Intel Itanium-based", + 0: "i386", } - return arch_map.get(sys_info.wProcessorArchitecture, f'Unknown ({sys_info.wProcessorArchitecture})') - + return arch_map.get(sys_info.wProcessorArchitecture, f"Unknown ({sys_info.wProcessorArchitecture})") @staticmethod def get_dotnet_version() -> DotnetVersion: @@ -313,29 +332,39 @@ class PlatformUtils: """ try: result = subprocess.run(["dotnet", "--list-runtimes"], capture_output=True, check=True) - version = '' - for line in result.stdout.decode('utf-8').split('\n'): - if line.startswith('Microsoft.NETCore.App'): - version = line.split(' ')[1] - break - if version == '': - raise MultilspyException("dotnet not found on the system") - if version.startswith("8"): - return DotnetVersion.V8 - elif version.startswith("7"): - return DotnetVersion.V7 - elif version.startswith("6"): - return DotnetVersion.V6 - elif version.startswith("4"): - return DotnetVersion.V4 - else: - raise MultilspyException("Unknown dotnet version: " + version) + available_version_cmd_output = [] + for line in result.stdout.decode("utf-8").split("\n"): + if line.startswith("Microsoft.NETCore.App"): + version_cmd_output = line.split(" ")[1] + available_version_cmd_output.append(version_cmd_output) + + if not available_version_cmd_output: + raise LanguageServerException("dotnet not found on the system") + + # Check for supported versions in order of preference (latest first) + for version_cmd_output in available_version_cmd_output: + if version_cmd_output.startswith("8"): + return DotnetVersion.V8 + for version_cmd_output in available_version_cmd_output: + if version_cmd_output.startswith("7"): + return DotnetVersion.V7 + for version_cmd_output in available_version_cmd_output: + if version_cmd_output.startswith("6"): + return DotnetVersion.V6 + for version_cmd_output in available_version_cmd_output: + if version_cmd_output.startswith("4"): + return DotnetVersion.V4 + + # If no supported version found, raise exception with all available versions + raise LanguageServerException( + f"No supported dotnet version found. Available versions: {', '.join(available_version_cmd_output)}. Supported versions: 4, 6, 7, 8" + ) except (FileNotFoundError, subprocess.CalledProcessError): try: result = subprocess.run(["mono", "--version"], capture_output=True, check=True) return DotnetVersion.VMONO except (FileNotFoundError, subprocess.CalledProcessError): - raise MultilspyException("dotnet or mono not found on the system") + raise LanguageServerException("dotnet or mono not found on the system") class SymbolUtils: diff --git a/src/multilspy/lsp_protocol_handler/lsp_constants.py b/src/solidlsp/lsp_protocol_handler/lsp_constants.py similarity index 98% rename from src/multilspy/lsp_protocol_handler/lsp_constants.py rename to src/solidlsp/lsp_protocol_handler/lsp_constants.py index 149af38..329c003 100644 --- a/src/multilspy/lsp_protocol_handler/lsp_constants.py +++ b/src/solidlsp/lsp_protocol_handler/lsp_constants.py @@ -2,6 +2,7 @@ This module contains constants used in the LSP protocol. """ + class LSPConstants: """ This class contains constants used in the LSP protocol. @@ -31,7 +32,7 @@ class LSPConstants: # key used to represent the language a document is in - "java", "csharp", etc. LANGUAGE_ID = "languageId" - # key used to represent the version of a document (a shared value betwen the client and server) + # key used to represent the version of a document (a shared value between the client and server) VERSION = "version" # key used to represent the text of a document being sent from the client to the server on open diff --git a/src/multilspy/lsp_protocol_handler/lsp_requests.py b/src/solidlsp/lsp_protocol_handler/lsp_requests.py similarity index 69% rename from src/multilspy/lsp_protocol_handler/lsp_requests.py rename to src/solidlsp/lsp_protocol_handler/lsp_requests.py index 852df21..383e456 100644 --- a/src/multilspy/lsp_protocol_handler/lsp_requests.py +++ b/src/solidlsp/lsp_protocol_handler/lsp_requests.py @@ -29,8 +29,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from typing import List, Union -from multilspy.lsp_protocol_handler import lsp_types +from typing import Union + +from solidlsp.lsp_protocol_handler import lsp_types + class LspRequest: def __init__(self, send_request): @@ -38,96 +40,94 @@ class LspRequest: async def implementation( self, params: lsp_types.ImplementationParams - ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + ) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the implementation locations of a symbol at a given text document position. The request's parameter is of type [TextDocumentPositionParams] (#TextDocumentPositionParams) the response is of type {@link Definition} or a - Thenable that resolves to such.""" + Thenable that resolves to such. + """ return await self.send_request("textDocument/implementation", params) async def type_definition( self, params: lsp_types.TypeDefinitionParams - ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + ) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the type definition locations of a symbol at a given text document position. The request's parameter is of type [TextDocumentPositionParams] (#TextDocumentPositionParams) the response is of type {@link Definition} or a - Thenable that resolves to such.""" + Thenable that resolves to such. + """ return await self.send_request("textDocument/typeDefinition", params) - async def document_color( - self, params: lsp_types.DocumentColorParams - ) -> List["lsp_types.ColorInformation"]: + async def document_color(self, params: lsp_types.DocumentColorParams) -> list["lsp_types.ColorInformation"]: """A request to list all color symbols found in a given text document. The request's parameter is of type {@link DocumentColorParams} the response is of type {@link ColorInformation ColorInformation[]} or a Thenable - that resolves to such.""" + that resolves to such. + """ return await self.send_request("textDocument/documentColor", params) - async def color_presentation( - self, params: lsp_types.ColorPresentationParams - ) -> List["lsp_types.ColorPresentation"]: + async def color_presentation(self, params: lsp_types.ColorPresentationParams) -> list["lsp_types.ColorPresentation"]: """A request to list all presentation for a color. The request's parameter is of type {@link ColorPresentationParams} the response is of type {@link ColorInformation ColorInformation[]} or a Thenable - that resolves to such.""" + that resolves to such. + """ return await self.send_request("textDocument/colorPresentation", params) - async def folding_range( - self, params: lsp_types.FoldingRangeParams - ) -> Union[List["lsp_types.FoldingRange"], None]: + async def folding_range(self, params: lsp_types.FoldingRangeParams) -> list["lsp_types.FoldingRange"] | None: """A request to provide folding ranges in a document. The request's parameter is of type {@link FoldingRangeParams}, the response is of type {@link FoldingRangeList} or a Thenable - that resolves to such.""" + that resolves to such. + """ return await self.send_request("textDocument/foldingRange", params) async def declaration( self, params: lsp_types.DeclarationParams - ) -> Union["lsp_types.Declaration", List["lsp_types.LocationLink"], None]: + ) -> Union["lsp_types.Declaration", list["lsp_types.LocationLink"], None]: """A request to resolve the type definition locations of a symbol at a given text document position. The request's parameter is of type [TextDocumentPositionParams] (#TextDocumentPositionParams) the response is of type {@link Declaration} or a typed array of {@link DeclarationLink} or a Thenable that resolves - to such.""" + to such. + """ return await self.send_request("textDocument/declaration", params) - async def selection_range( - self, params: lsp_types.SelectionRangeParams - ) -> Union[List["lsp_types.SelectionRange"], None]: + async def selection_range(self, params: lsp_types.SelectionRangeParams) -> list["lsp_types.SelectionRange"] | None: """A request to provide selection ranges in a document. The request's parameter is of type {@link SelectionRangeParams}, the response is of type {@link SelectionRange SelectionRange[]} or a Thenable - that resolves to such.""" + that resolves to such. + """ return await self.send_request("textDocument/selectionRange", params) - async def prepare_call_hierarchy( - self, params: lsp_types.CallHierarchyPrepareParams - ) -> Union[List["lsp_types.CallHierarchyItem"], None]: + async def prepare_call_hierarchy(self, params: lsp_types.CallHierarchyPrepareParams) -> list["lsp_types.CallHierarchyItem"] | None: """A request to result a `CallHierarchyItem` in a document at a given position. Can be used as an input to an incoming or outgoing call hierarchy. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("textDocument/prepareCallHierarchy", params) async def incoming_calls( self, params: lsp_types.CallHierarchyIncomingCallsParams - ) -> Union[List["lsp_types.CallHierarchyIncomingCall"], None]: + ) -> list["lsp_types.CallHierarchyIncomingCall"] | None: """A request to resolve the incoming calls for a given `CallHierarchyItem`. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("callHierarchy/incomingCalls", params) async def outgoing_calls( self, params: lsp_types.CallHierarchyOutgoingCallsParams - ) -> Union[List["lsp_types.CallHierarchyOutgoingCall"], None]: + ) -> list["lsp_types.CallHierarchyOutgoingCall"] | None: """A request to resolve the outgoing calls for a given `CallHierarchyItem`. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("callHierarchy/outgoingCalls", params) - async def semantic_tokens_full( - self, params: lsp_types.SemanticTokensParams - ) -> Union["lsp_types.SemanticTokens", None]: + async def semantic_tokens_full(self, params: lsp_types.SemanticTokensParams) -> Union["lsp_types.SemanticTokens", None]: """@since 3.16.0""" return await self.send_request("textDocument/semanticTokens/full", params) @@ -137,157 +137,143 @@ class LspRequest: """@since 3.16.0""" return await self.send_request("textDocument/semanticTokens/full/delta", params) - async def semantic_tokens_range( - self, params: lsp_types.SemanticTokensRangeParams - ) -> Union["lsp_types.SemanticTokens", None]: + async def semantic_tokens_range(self, params: lsp_types.SemanticTokensRangeParams) -> Union["lsp_types.SemanticTokens", None]: """@since 3.16.0""" return await self.send_request("textDocument/semanticTokens/range", params) - async def linked_editing_range( - self, params: lsp_types.LinkedEditingRangeParams - ) -> Union["lsp_types.LinkedEditingRanges", None]: + async def linked_editing_range(self, params: lsp_types.LinkedEditingRangeParams) -> Union["lsp_types.LinkedEditingRanges", None]: """A request to provide ranges that can be edited together. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("textDocument/linkedEditingRange", params) - async def will_create_files( - self, params: lsp_types.CreateFilesParams - ) -> Union["lsp_types.WorkspaceEdit", None]: + async def will_create_files(self, params: lsp_types.CreateFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The will create files request is sent from the client to the server before files are actually created as long as the creation is triggered from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("workspace/willCreateFiles", params) - async def will_rename_files( - self, params: lsp_types.RenameFilesParams - ) -> Union["lsp_types.WorkspaceEdit", None]: + async def will_rename_files(self, params: lsp_types.RenameFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The will rename files request is sent from the client to the server before files are actually renamed as long as the rename is triggered from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("workspace/willRenameFiles", params) - async def will_delete_files( - self, params: lsp_types.DeleteFilesParams - ) -> Union["lsp_types.WorkspaceEdit", None]: + async def will_delete_files(self, params: lsp_types.DeleteFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The did delete files notification is sent from the client to the server when files were deleted from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("workspace/willDeleteFiles", params) - async def moniker( - self, params: lsp_types.MonikerParams - ) -> Union[List["lsp_types.Moniker"], None]: + async def moniker(self, params: lsp_types.MonikerParams) -> list["lsp_types.Moniker"] | None: """A request to get the moniker of a symbol at a given text document position. The request parameter is of type {@link TextDocumentPositionParams}. - The response is of type {@link Moniker Moniker[]} or `null`.""" + The response is of type {@link Moniker Moniker[]} or `null`. + """ return await self.send_request("textDocument/moniker", params) - async def prepare_type_hierarchy( - self, params: lsp_types.TypeHierarchyPrepareParams - ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + async def prepare_type_hierarchy(self, params: lsp_types.TypeHierarchyPrepareParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to result a `TypeHierarchyItem` in a document at a given position. Can be used as an input to a subtypes or supertypes type hierarchy. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("textDocument/prepareTypeHierarchy", params) async def type_hierarchy_supertypes( self, params: lsp_types.TypeHierarchySupertypesParams - ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + ) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to resolve the supertypes for a given `TypeHierarchyItem`. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("typeHierarchy/supertypes", params) - async def type_hierarchy_subtypes( - self, params: lsp_types.TypeHierarchySubtypesParams - ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + async def type_hierarchy_subtypes(self, params: lsp_types.TypeHierarchySubtypesParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to resolve the subtypes for a given `TypeHierarchyItem`. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("typeHierarchy/subtypes", params) - async def inline_value( - self, params: lsp_types.InlineValueParams - ) -> Union[List["lsp_types.InlineValue"], None]: + async def inline_value(self, params: lsp_types.InlineValueParams) -> list["lsp_types.InlineValue"] | None: """A request to provide inline values in a document. The request's parameter is of type {@link InlineValueParams}, the response is of type {@link InlineValue InlineValue[]} or a Thenable that resolves to such. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("textDocument/inlineValue", params) - async def inlay_hint( - self, params: lsp_types.InlayHintParams - ) -> Union[List["lsp_types.InlayHint"], None]: + async def inlay_hint(self, params: lsp_types.InlayHintParams) -> list["lsp_types.InlayHint"] | None: """A request to provide inlay hints in a document. The request's parameter is of type {@link InlayHintsParams}, the response is of type {@link InlayHint InlayHint[]} or a Thenable that resolves to such. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("textDocument/inlayHint", params) - async def resolve_inlay_hint( - self, params: lsp_types.InlayHint - ) -> "lsp_types.InlayHint": + async def resolve_inlay_hint(self, params: lsp_types.InlayHint) -> "lsp_types.InlayHint": """A request to resolve additional properties for an inlay hint. The request's parameter is of type {@link InlayHint}, the response is of type {@link InlayHint} or a Thenable that resolves to such. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("inlayHint/resolve", params) - async def text_document_diagnostic( - self, params: lsp_types.DocumentDiagnosticParams - ) -> "lsp_types.DocumentDiagnosticReport": + async def text_document_diagnostic(self, params: lsp_types.DocumentDiagnosticParams) -> "lsp_types.DocumentDiagnosticReport": """The document diagnostic request definition. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("textDocument/diagnostic", params) - async def workspace_diagnostic( - self, params: lsp_types.WorkspaceDiagnosticParams - ) -> "lsp_types.WorkspaceDiagnosticReport": + async def workspace_diagnostic(self, params: lsp_types.WorkspaceDiagnosticParams) -> "lsp_types.WorkspaceDiagnosticReport": """The workspace diagnostic request definition. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("workspace/diagnostic", params) - async def initialize( - self, params: lsp_types.InitializeParams - ) -> "lsp_types.InitializeResult": + async def initialize(self, params: lsp_types.InitializeParams) -> "lsp_types.InitializeResult": """The initialize request is sent from the client to the server. It is sent once as the request after starting up the server. The requests parameter is of type {@link InitializeParams} the response if of type {@link InitializeResult} of a Thenable that - resolves to such.""" + resolves to such. + """ return await self.send_request("initialize", params) async def shutdown(self) -> None: """A shutdown request is sent from the client to the server. It is sent once when the client decides to shutdown the server. The only notification that is sent after a shutdown request - is the exit event.""" + is the exit event. + """ return await self.send_request("shutdown") - async def will_save_wait_until( - self, params: lsp_types.WillSaveTextDocumentParams - ) -> Union[List["lsp_types.TextEdit"], None]: + async def will_save_wait_until(self, params: lsp_types.WillSaveTextDocumentParams) -> list["lsp_types.TextEdit"] | None: """A document will save request is sent from the client to the server before the document is actually saved. The request can return an array of TextEdits which will be applied to the text document before it is saved. Please note that clients might drop results if computing the text edits took too long or if a server constantly fails on this request. This is done to keep the save fast and - reliable.""" + reliable. + """ return await self.send_request("textDocument/willSaveWaitUntil", params) async def completion( self, params: lsp_types.CompletionParams - ) -> Union[List["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]: + ) -> Union[list["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]: """Request to request completion at a given text document position. The request's parameter is of type {@link TextDocumentPosition} the response is of type {@link CompletionItem CompletionItem[]} or {@link CompletionList} @@ -300,85 +286,72 @@ class LspRequest: """ return await self.send_request("textDocument/completion", params) - async def resolve_completion_item( - self, params: lsp_types.CompletionItem - ) -> "lsp_types.CompletionItem": + async def resolve_completion_item(self, params: lsp_types.CompletionItem) -> "lsp_types.CompletionItem": """Request to resolve additional information for a given completion item.The request's parameter is of type {@link CompletionItem} the response - is of type {@link CompletionItem} or a Thenable that resolves to such.""" + is of type {@link CompletionItem} or a Thenable that resolves to such. + """ return await self.send_request("completionItem/resolve", params) - async def hover( - self, params: lsp_types.HoverParams - ) -> Union["lsp_types.Hover", None]: + async def hover(self, params: lsp_types.HoverParams) -> Union["lsp_types.Hover", None]: """Request to request hover information at a given text document position. The request's parameter is of type {@link TextDocumentPosition} the response is of - type {@link Hover} or a Thenable that resolves to such.""" + type {@link Hover} or a Thenable that resolves to such. + """ return await self.send_request("textDocument/hover", params) - async def signature_help( - self, params: lsp_types.SignatureHelpParams - ) -> Union["lsp_types.SignatureHelp", None]: + async def signature_help(self, params: lsp_types.SignatureHelpParams) -> Union["lsp_types.SignatureHelp", None]: return await self.send_request("textDocument/signatureHelp", params) - async def definition( - self, params: lsp_types.DefinitionParams - ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + async def definition(self, params: lsp_types.DefinitionParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the definition location of a symbol at a given text document position. The request's parameter is of type [TextDocumentPosition] (#TextDocumentPosition) the response is of either type {@link Definition} or a typed array of {@link DefinitionLink} or a Thenable that resolves - to such.""" + to such. + """ return await self.send_request("textDocument/definition", params) - async def references( - self, params: lsp_types.ReferenceParams - ) -> Union[List["lsp_types.Location"], None]: + async def references(self, params: lsp_types.ReferenceParams) -> list["lsp_types.Location"] | None: """A request to resolve project-wide references for the symbol denoted by the given text document position. The request's parameter is of type {@link ReferenceParams} the response is of type - {@link Location Location[]} or a Thenable that resolves to such.""" + {@link Location Location[]} or a Thenable that resolves to such. + """ return await self.send_request("textDocument/references", params) - async def document_highlight( - self, params: lsp_types.DocumentHighlightParams - ) -> Union[List["lsp_types.DocumentHighlight"], None]: + async def document_highlight(self, params: lsp_types.DocumentHighlightParams) -> list["lsp_types.DocumentHighlight"] | None: """Request to resolve a {@link DocumentHighlight} for a given text document position. The request's parameter is of type [TextDocumentPosition] (#TextDocumentPosition) the request response is of type [DocumentHighlight[]] - (#DocumentHighlight) or a Thenable that resolves to such.""" + (#DocumentHighlight) or a Thenable that resolves to such. + """ return await self.send_request("textDocument/documentHighlight", params) async def document_symbol( self, params: lsp_types.DocumentSymbolParams - ) -> Union[ - List["lsp_types.SymbolInformation"], List["lsp_types.DocumentSymbol"], None - ]: + ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.DocumentSymbol"] | None: """A request to list all symbols found in a given text document. The request's parameter is of type {@link TextDocumentIdentifier} the response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable - that resolves to such.""" + that resolves to such. + """ return await self.send_request("textDocument/documentSymbol", params) - async def code_action( - self, params: lsp_types.CodeActionParams - ) -> Union[List[Union["lsp_types.Command", "lsp_types.CodeAction"]], None]: + async def code_action(self, params: lsp_types.CodeActionParams) -> list[Union["lsp_types.Command", "lsp_types.CodeAction"]] | None: """A request to provide commands for the given text document and range.""" return await self.send_request("textDocument/codeAction", params) - async def resolve_code_action( - self, params: lsp_types.CodeAction - ) -> "lsp_types.CodeAction": + async def resolve_code_action(self, params: lsp_types.CodeAction) -> "lsp_types.CodeAction": """Request to resolve additional information for a given code action.The request's parameter is of type {@link CodeAction} the response - is of type {@link CodeAction} or a Thenable that resolves to such.""" + is of type {@link CodeAction} or a Thenable that resolves to such. + """ return await self.send_request("codeAction/resolve", params) async def workspace_symbol( self, params: lsp_types.WorkspaceSymbolParams - ) -> Union[ - List["lsp_types.SymbolInformation"], List["lsp_types.WorkspaceSymbol"], None - ]: + ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.WorkspaceSymbol"] | None: """A request to list project-wide symbols matching the query string given by the {@link WorkspaceSymbolParams}. The response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable that @@ -390,78 +363,60 @@ class LspRequest: """ return await self.send_request("workspace/symbol", params) - async def resolve_workspace_symbol( - self, params: lsp_types.WorkspaceSymbol - ) -> "lsp_types.WorkspaceSymbol": + async def resolve_workspace_symbol(self, params: lsp_types.WorkspaceSymbol) -> "lsp_types.WorkspaceSymbol": """A request to resolve the range inside the workspace symbol's location. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("workspaceSymbol/resolve", params) - async def code_lens( - self, params: lsp_types.CodeLensParams - ) -> Union[List["lsp_types.CodeLens"], None]: + async def code_lens(self, params: lsp_types.CodeLensParams) -> list["lsp_types.CodeLens"] | None: """A request to provide code lens for the given text document.""" return await self.send_request("textDocument/codeLens", params) - async def resolve_code_lens( - self, params: lsp_types.CodeLens - ) -> "lsp_types.CodeLens": + async def resolve_code_lens(self, params: lsp_types.CodeLens) -> "lsp_types.CodeLens": """A request to resolve a command for a given code lens.""" return await self.send_request("codeLens/resolve", params) - async def document_link( - self, params: lsp_types.DocumentLinkParams - ) -> Union[List["lsp_types.DocumentLink"], None]: + async def document_link(self, params: lsp_types.DocumentLinkParams) -> list["lsp_types.DocumentLink"] | None: """A request to provide document links""" return await self.send_request("textDocument/documentLink", params) - async def resolve_document_link( - self, params: lsp_types.DocumentLink - ) -> "lsp_types.DocumentLink": + async def resolve_document_link(self, params: lsp_types.DocumentLink) -> "lsp_types.DocumentLink": """Request to resolve additional information for a given document link. The request's parameter is of type {@link DocumentLink} the response - is of type {@link DocumentLink} or a Thenable that resolves to such.""" + is of type {@link DocumentLink} or a Thenable that resolves to such. + """ return await self.send_request("documentLink/resolve", params) - async def formatting( - self, params: lsp_types.DocumentFormattingParams - ) -> Union[List["lsp_types.TextEdit"], None]: + async def formatting(self, params: lsp_types.DocumentFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to to format a whole document.""" return await self.send_request("textDocument/formatting", params) - async def range_formatting( - self, params: lsp_types.DocumentRangeFormattingParams - ) -> Union[List["lsp_types.TextEdit"], None]: + async def range_formatting(self, params: lsp_types.DocumentRangeFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to to format a range in a document.""" return await self.send_request("textDocument/rangeFormatting", params) - async def on_type_formatting( - self, params: lsp_types.DocumentOnTypeFormattingParams - ) -> Union[List["lsp_types.TextEdit"], None]: + async def on_type_formatting(self, params: lsp_types.DocumentOnTypeFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to format a document on type.""" return await self.send_request("textDocument/onTypeFormatting", params) - async def rename( - self, params: lsp_types.RenameParams - ) -> Union["lsp_types.WorkspaceEdit", None]: + async def rename(self, params: lsp_types.RenameParams) -> Union["lsp_types.WorkspaceEdit", None]: """A request to rename a symbol.""" return await self.send_request("textDocument/rename", params) - async def prepare_rename( - self, params: lsp_types.PrepareRenameParams - ) -> Union["lsp_types.PrepareRenameResult", None]: + async def prepare_rename(self, params: lsp_types.PrepareRenameParams) -> Union["lsp_types.PrepareRenameResult", None]: """A request to test and perform the setup necessary for a rename. - @since 3.16 - support for default behavior""" + @since 3.16 - support for default behavior + """ return await self.send_request("textDocument/prepareRename", params) - async def execute_command( - self, params: lsp_types.ExecuteCommandParams - ) -> Union["lsp_types.LSPAny", None]: + async def execute_command(self, params: lsp_types.ExecuteCommandParams) -> Union["lsp_types.LSPAny", None]: """A request send from the client to the server to execute a command. The request might return - a workspace edit which the client will apply to the workspace.""" + a workspace edit which the client will apply to the workspace. + """ return await self.send_request("workspace/executeCommand", params) @@ -469,92 +424,87 @@ class LspNotification: def __init__(self, send_notification): self.send_notification = send_notification - def did_change_workspace_folders( - self, params: lsp_types.DidChangeWorkspaceFoldersParams - ) -> None: + def did_change_workspace_folders(self, params: lsp_types.DidChangeWorkspaceFoldersParams) -> None: """The `workspace/didChangeWorkspaceFolders` notification is sent from the client to the server when the workspace - folder configuration changes.""" + folder configuration changes. + """ return self.send_notification("workspace/didChangeWorkspaceFolders", params) - def cancel_work_done_progress( - self, params: lsp_types.WorkDoneProgressCancelParams - ) -> None: + def cancel_work_done_progress(self, params: lsp_types.WorkDoneProgressCancelParams) -> None: """The `window/workDoneProgress/cancel` notification is sent from the client to the server to cancel a progress - initiated on the server side.""" + initiated on the server side. + """ return self.send_notification("window/workDoneProgress/cancel", params) def did_create_files(self, params: lsp_types.CreateFilesParams) -> None: """The did create files notification is sent from the client to the server when files were created from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_notification("workspace/didCreateFiles", params) def did_rename_files(self, params: lsp_types.RenameFilesParams) -> None: """The did rename files notification is sent from the client to the server when files were renamed from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_notification("workspace/didRenameFiles", params) def did_delete_files(self, params: lsp_types.DeleteFilesParams) -> None: """The will delete files request is sent from the client to the server before files are actually deleted as long as the deletion is triggered from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_notification("workspace/didDeleteFiles", params) - def did_open_notebook_document( - self, params: lsp_types.DidOpenNotebookDocumentParams - ) -> None: + def did_open_notebook_document(self, params: lsp_types.DidOpenNotebookDocumentParams) -> None: """A notification sent when a notebook opens. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_notification("notebookDocument/didOpen", params) - def did_change_notebook_document( - self, params: lsp_types.DidChangeNotebookDocumentParams - ) -> None: + def did_change_notebook_document(self, params: lsp_types.DidChangeNotebookDocumentParams) -> None: return self.send_notification("notebookDocument/didChange", params) - def did_save_notebook_document( - self, params: lsp_types.DidSaveNotebookDocumentParams - ) -> None: + def did_save_notebook_document(self, params: lsp_types.DidSaveNotebookDocumentParams) -> None: """A notification sent when a notebook document is saved. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_notification("notebookDocument/didSave", params) - def did_close_notebook_document( - self, params: lsp_types.DidCloseNotebookDocumentParams - ) -> None: + def did_close_notebook_document(self, params: lsp_types.DidCloseNotebookDocumentParams) -> None: """A notification sent when a notebook closes. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_notification("notebookDocument/didClose", params) def initialized(self, params: lsp_types.InitializedParams) -> None: """The initialized notification is sent from the client to the server after the client is fully initialized and the server - is allowed to send requests from the server to the client.""" + is allowed to send requests from the server to the client. + """ return self.send_notification("initialized", params) def exit(self) -> None: """The exit event is sent from the client to the server to - ask the server to exit its process.""" + ask the server to exit its process. + """ return self.send_notification("exit") - def workspace_did_change_configuration( - self, params: lsp_types.DidChangeConfigurationParams - ) -> None: + def workspace_did_change_configuration(self, params: lsp_types.DidChangeConfigurationParams) -> None: """The configuration change notification is sent from the client to the server when the client's configuration has changed. The notification contains - the changed configuration as defined by the language client.""" + the changed configuration as defined by the language client. + """ return self.send_notification("workspace/didChangeConfiguration", params) - def did_open_text_document( - self, params: lsp_types.DidOpenTextDocumentParams - ) -> None: + def did_open_text_document(self, params: lsp_types.DidOpenTextDocumentParams) -> None: """The document open notification is sent from the client to the server to signal newly opened text documents. The document's truth is now managed by the client and the server must not try to read the document's truth using the document's @@ -562,47 +512,43 @@ class LspNotification: mean that its content is presented in an editor. An open notification must not be sent more than once without a corresponding close notification send before. This means open and close notification must be balanced and the max open count - is one.""" + is one. + """ return self.send_notification("textDocument/didOpen", params) - def did_change_text_document( - self, params: lsp_types.DidChangeTextDocumentParams - ) -> None: + def did_change_text_document(self, params: lsp_types.DidChangeTextDocumentParams) -> None: """The document change notification is sent from the client to the server to signal - changes to a text document.""" + changes to a text document. + """ return self.send_notification("textDocument/didChange", params) - def did_close_text_document( - self, params: lsp_types.DidCloseTextDocumentParams - ) -> None: + def did_close_text_document(self, params: lsp_types.DidCloseTextDocumentParams) -> None: """The document close notification is sent from the client to the server when the document got closed in the client. The document's truth now exists where the document's uri points to (e.g. if the document's uri is a file uri the truth now exists on disk). As with the open notification the close notification is about managing the document's content. Receiving a close notification doesn't mean that the document was open in an editor before. A close - notification requires a previous open notification to be sent.""" + notification requires a previous open notification to be sent. + """ return self.send_notification("textDocument/didClose", params) - def did_save_text_document( - self, params: lsp_types.DidSaveTextDocumentParams - ) -> None: + def did_save_text_document(self, params: lsp_types.DidSaveTextDocumentParams) -> None: """The document save notification is sent from the client to the server when - the document got saved in the client.""" + the document got saved in the client. + """ return self.send_notification("textDocument/didSave", params) - def will_save_text_document( - self, params: lsp_types.WillSaveTextDocumentParams - ) -> None: + def will_save_text_document(self, params: lsp_types.WillSaveTextDocumentParams) -> None: """A document will save notification is sent from the client to the server before - the document is actually saved.""" + the document is actually saved. + """ return self.send_notification("textDocument/willSave", params) - def did_change_watched_files( - self, params: lsp_types.DidChangeWatchedFilesParams - ) -> None: + def did_change_watched_files(self, params: lsp_types.DidChangeWatchedFilesParams) -> None: """The watched files notification is sent from the client to the server when - the client detects changes to file watched by the language client.""" + the client detects changes to file watched by the language client. + """ return self.send_notification("workspace/didChangeWatchedFiles", params) def set_trace(self, params: lsp_types.SetTraceParams) -> None: diff --git a/src/multilspy/lsp_protocol_handler/lsp_types.py b/src/solidlsp/lsp_protocol_handler/lsp_types.py similarity index 93% rename from src/multilspy/lsp_protocol_handler/lsp_types.py rename to src/solidlsp/lsp_protocol_handler/lsp_types.py index 4ec467e..5488c35 100644 --- a/src/multilspy/lsp_protocol_handler/lsp_types.py +++ b/src/solidlsp/lsp_protocol_handler/lsp_types.py @@ -30,8 +30,9 @@ SOFTWARE. """ from enum import Enum, IntEnum, IntFlag -from typing import Dict, List, Literal, Union -from typing_extensions import NotRequired, TypedDict +from typing import Literal, NotRequired, Union + +from typing_extensions import TypedDict URI = str DocumentUri = str @@ -44,7 +45,8 @@ class SemanticTokenTypes(Enum): an clients can specify additional token types via the corresponding client capabilities. - @since 3.16.0""" + @since 3.16.0 + """ Namespace = "namespace" Type = "type" @@ -79,7 +81,8 @@ class SemanticTokenModifiers(Enum): an clients can specify additional token types via the corresponding client capabilities. - @since 3.16.0""" + @since 3.16.0 + """ Declaration = "declaration" Definition = "definition" @@ -96,7 +99,8 @@ class SemanticTokenModifiers(Enum): class DocumentDiagnosticReportKind(Enum): """The document diagnostic report kinds. - @since 3.17.0""" + @since 3.17.0 + """ Full = "full" """ A diagnostic report with a full @@ -191,8 +195,8 @@ class SymbolKind(IntEnum): Event = 24 Operator = 25 TypeParameter = 26 - - @classmethod + + @classmethod def from_int(cls, value: int) -> "SymbolKind": for symbol_kind in cls: if symbol_kind.value == value: @@ -203,7 +207,8 @@ class SymbolKind(IntEnum): class SymbolTag(IntEnum): """Symbol tags are extra annotations that tweak the rendering of a symbol. - @since 3.16""" + @since 3.16 + """ Deprecated = 1 """ Render a symbol as obsolete, usually using a strike-out. """ @@ -212,7 +217,8 @@ class SymbolTag(IntEnum): class UniquenessLevel(Enum): """Moniker uniqueness level to define scope of the moniker. - @since 3.16.0""" + @since 3.16.0 + """ Document = "document" """ The moniker is only unique inside a document """ @@ -229,7 +235,8 @@ class UniquenessLevel(Enum): class MonikerKind(Enum): """The moniker kind. - @since 3.16.0""" + @since 3.16.0 + """ Import = "import" """ The moniker represent a symbol that is imported into a project """ @@ -243,7 +250,8 @@ class MonikerKind(Enum): class InlayHintKind(IntEnum): """Inlay hint kinds. - @since 3.17.0""" + @since 3.17.0 + """ Type = 1 """ An inlay hint that for a type annotation. """ @@ -266,7 +274,8 @@ class MessageType(IntEnum): class TextDocumentSyncKind(IntEnum): """Defines how the host (editor) should sync - document changes to the language server.""" + document changes to the language server. + """ None_ = 0 """ Documents should not be synced at all. """ @@ -325,7 +334,8 @@ class CompletionItemTag(IntEnum): """Completion item tags are extra annotations that tweak the rendering of a completion item. - @since 3.15.0""" + @since 3.15.0 + """ Deprecated = 1 """ Render a completion as obsolete, usually using a strike-out. """ @@ -333,7 +343,8 @@ class CompletionItemTag(IntEnum): class InsertTextFormat(IntEnum): """Defines whether the insert text in a completion item should be interpreted as - plain text or a snippet.""" + plain text or a snippet. + """ PlainText = 1 """ The primary text to be inserted is treated as a plain string. """ @@ -352,7 +363,8 @@ class InsertTextMode(IntEnum): """How whitespace and indentation is handled during completion item insertion. - @since 3.16.0""" + @since 3.16.0 + """ AsIs = 1 """ The insertion or replace strings is taken as it is. If the @@ -449,7 +461,8 @@ class MarkupKind(Enum): result literals like `Hover`, `ParameterInfo` or `CompletionItem`. Please note that `MarkupKinds` must not start with a `$`. This kinds - are reserved for internal usage.""" + are reserved for internal usage. + """ PlainText = "plaintext" """ Plain text is supported as a content format """ @@ -460,7 +473,8 @@ class MarkupKind(Enum): class PositionEncodingKind(Enum): """A set of predefined position encoding kinds. - @since 3.17.0""" + @since 3.17.0 + """ UTF8 = "utf-8" """ Character offsets count UTF-8 code units. """ @@ -513,7 +527,8 @@ class DiagnosticSeverity(IntEnum): class DiagnosticTag(IntEnum): """The diagnostic tags. - @since 3.15.0""" + @since 3.15.0 + """ Unnecessary = 1 """ Unused or unnecessary code. @@ -542,7 +557,8 @@ class CompletionTriggerKind(IntEnum): class SignatureHelpTriggerKind(IntEnum): """How a signature help was triggered. - @since 3.15.0""" + @since 3.15.0 + """ Invoked = 1 """ Signature help was invoked manually by the user or by a command. """ @@ -555,7 +571,8 @@ class SignatureHelpTriggerKind(IntEnum): class CodeActionTriggerKind(IntEnum): """The reason why code actions were requested. - @since 3.17.0""" + @since 3.17.0 + """ Invoked = 1 """ Code actions were explicitly requested by the user or by an extension. """ @@ -570,7 +587,8 @@ class FileOperationPatternKind(Enum): """A pattern kind describing if a glob pattern matches a file a folder or both. - @since 3.16.0""" + @since 3.16.0 + """ File = "file" """ The pattern matches a file only. """ @@ -581,7 +599,8 @@ class FileOperationPatternKind(Enum): class NotebookCellKind(IntEnum): """A notebook cell kind. - @since 3.17.0""" + @since 3.17.0 + """ Markup = 1 """ A markup-cell is formatted source that is used for display. """ @@ -624,7 +643,7 @@ class TokenFormat(Enum): Relative = "relative" -Definition = Union["Location", List["Location"]] +Definition = Union["Location", list["Location"]] """ The definition of a symbol represented as one or many {@link Location locations}. For most programming languages there is only one location at which a symbol is defined. @@ -638,7 +657,7 @@ DefinitionLink = "LocationLink" Provides additional metadata over normal {@link Location location} definitions, including the range of the defining symbol """ -LSPArray = List["LSPAny"] +LSPArray = list["LSPAny"] """ LSP arrays. @since 3.17.0 """ @@ -650,7 +669,7 @@ convenience it is allowed and assumed that all these properties are optional as well. @since 3.17.0 """ -Declaration = Union["Location", List["Location"]] +Declaration = Union["Location", list["Location"]] """ The declaration of a symbol representation as one or many {@link Location locations}. """ DeclarationLink = "LocationLink" @@ -662,9 +681,7 @@ the declaring symbol. Servers should prefer returning `DeclarationLink` over `Declaration` if supported by the client. """ -InlineValue = Union[ - "InlineValueText", "InlineValueVariableLookup", "InlineValueEvaluatableExpression" -] +InlineValue = Union["InlineValueText", "InlineValueVariableLookup", "InlineValueEvaluatableExpression"] """ Inline value information can be provided by different means: - directly as a text value (class InlineValueText). - as a name to use for a variable lookup (class InlineValueVariableLookup) @@ -673,9 +690,7 @@ The InlineValue types combines all inline value types into one type. @since 3.17.0 """ -DocumentDiagnosticReport = Union[ - "RelatedFullDocumentDiagnosticReport", "RelatedUnchangedDocumentDiagnosticReport" -] +DocumentDiagnosticReport = Union["RelatedFullDocumentDiagnosticReport", "RelatedUnchangedDocumentDiagnosticReport"] """ The result of a document diagnostic pull request. A report can either be a full report containing all diagnostics for the requested document or an unchanged report indicating that nothing @@ -684,14 +699,12 @@ pull request. @since 3.17.0 """ -PrepareRenameResult = Union[ - "Range", "__PrepareRenameResult_Type_1", "__PrepareRenameResult_Type_2" -] +PrepareRenameResult = Union["Range", "__PrepareRenameResult_Type_1", "__PrepareRenameResult_Type_2"] -DocumentSelector = List["DocumentFilter"] +DocumentSelector = list["DocumentFilter"] """ A document selector is the combination of one or many document filters. -@sample `let sel:DocumentSelector = [{ language: 'typescript' }, { language: 'json', pattern: '**∕tsconfig.json' }]`; +@sample `let sel:DocumentSelector = [{ language: 'typescript' }, { language: 'json', pattern: '**/tsconfig.json' }]`; The use of a string as a document filter is deprecated @since 3.16.0. """ @@ -708,9 +721,7 @@ WorkspaceDocumentDiagnosticReport = Union[ @since 3.17.0 """ -TextDocumentContentChangeEvent = Union[ - "__TextDocumentContentChangeEvent_Type_1", "__TextDocumentContentChangeEvent_Type_2" -] +TextDocumentContentChangeEvent = Union["__TextDocumentContentChangeEvent_Type_1", "__TextDocumentContentChangeEvent_Type_2"] """ An event describing a change to a text document. If only a text is provided it is considered to be the full content of the document. """ @@ -734,7 +745,7 @@ a notebook cell document. @since 3.17.0 - proposed support for NotebookCellTextDocumentFilter. """ -LSPObject = Dict[str, "LSPAny"] +LSPObject = dict[str, "LSPAny"] """ LSP object definition. @since 3.17.0 """ @@ -756,7 +767,7 @@ Glob patterns can have the following syntax: - `*` to match one or more characters in a path segment - `?` to match on one character in a path segment - `**` to match any number of path segments, including none -- `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) +- `{}` to group sub patterns into an OR expression. (e.g. `**\u200b/*.{ts,js}` matches all TypeScript and JavaScript files) - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) @@ -781,7 +792,7 @@ Pattern = str - `*` to match one or more characters in a path segment - `?` to match on one character in a path segment - `**` to match any number of path segments, including none -- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) +- `{}` to group conditions (e.g. `**\u200b/*.{ts,js}` matches all TypeScript and JavaScript files) - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) @@ -802,7 +813,8 @@ class ImplementationParams(TypedDict): class Location(TypedDict): """Represents a location inside a resource, such as a line - inside a text file.""" + inside a text file. + """ uri: "DocumentUri" range: "Range" @@ -858,7 +870,7 @@ class DidChangeWorkspaceFoldersParams(TypedDict): class ConfigurationParams(TypedDict): """The parameters of a configuration request.""" - items: List["ConfigurationItem"] + items: list["ConfigurationItem"] class DocumentColorParams(TypedDict): @@ -916,7 +928,7 @@ class ColorPresentation(TypedDict): """ An {@link TextEdit edit} which is applied to a document when selecting this presentation for the color. When `falsy` the {@link ColorPresentation.label label} is used. """ - additionalTextEdits: NotRequired[List["TextEdit"]] + additionalTextEdits: NotRequired[list["TextEdit"]] """ An optional array of additional {@link TextEdit text edits} that are applied when selecting this color presentation. Edits must not overlap with the main {@link ColorPresentation.textEdit edit} nor with themselves. """ @@ -1007,7 +1019,7 @@ class SelectionRangeParams(TypedDict): textDocument: "TextDocumentIdentifier" """ The text document. """ - positions: List["Position"] + positions: list["Position"] """ The positions inside the text document. """ workDoneToken: NotRequired["ProgressToken"] """ An optional token that a server can use to report work done progress. """ @@ -1018,7 +1030,8 @@ class SelectionRangeParams(TypedDict): class SelectionRange(TypedDict): """A selection range represents a part of a selection hierarchy. A selection range - may have a parent selection range that contains it.""" + may have a parent selection range that contains it. + """ range: "Range" """ The {@link Range range} of this selection range. """ @@ -1048,7 +1061,8 @@ class WorkDoneProgressCancelParams(TypedDict): class CallHierarchyPrepareParams(TypedDict): """The parameter of a `textDocument/prepareCallHierarchy` request. - @since 3.16.0""" + @since 3.16.0 + """ textDocument: "TextDocumentIdentifier" """ The text document. """ @@ -1062,13 +1076,14 @@ class CallHierarchyItem(TypedDict): """Represents programming constructs like functions or constructors in the context of call hierarchy. - @since 3.16.0""" + @since 3.16.0 + """ name: str """ The name of this item. """ kind: "SymbolKind" """ The kind of this item. """ - tags: NotRequired[List["SymbolTag"]] + tags: NotRequired[list["SymbolTag"]] """ Tags for this item. """ detail: NotRequired[str] """ More detail for this item, e.g. the signature of a function. """ @@ -1087,7 +1102,8 @@ class CallHierarchyItem(TypedDict): class CallHierarchyRegistrationOptions(TypedDict): """Call hierarchy options used during static or dynamic registration. - @since 3.16.0""" + @since 3.16.0 + """ documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null @@ -1100,7 +1116,8 @@ class CallHierarchyRegistrationOptions(TypedDict): class CallHierarchyIncomingCallsParams(TypedDict): """The parameter of a `callHierarchy/incomingCalls` request. - @since 3.16.0""" + @since 3.16.0 + """ item: "CallHierarchyItem" workDoneToken: NotRequired["ProgressToken"] @@ -1117,7 +1134,7 @@ CallHierarchyIncomingCall = TypedDict( "from": "CallHierarchyItem", # The ranges at which the calls appear. This is relative to the caller # denoted by {@link CallHierarchyIncomingCall.from `this.from`}. - "fromRanges": List["Range"], + "fromRanges": list["Range"], }, ) """ Represents an incoming call, e.g. a caller of a method or constructor. @@ -1128,7 +1145,8 @@ CallHierarchyIncomingCall = TypedDict( class CallHierarchyOutgoingCallsParams(TypedDict): """The parameter of a `callHierarchy/outgoingCalls` request. - @since 3.16.0""" + @since 3.16.0 + """ item: "CallHierarchyItem" workDoneToken: NotRequired["ProgressToken"] @@ -1141,11 +1159,12 @@ class CallHierarchyOutgoingCallsParams(TypedDict): class CallHierarchyOutgoingCall(TypedDict): """Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc. - @since 3.16.0""" + @since 3.16.0 + """ to: "CallHierarchyItem" """ The item that is called. """ - fromRanges: List["Range"] + fromRanges: list["Range"] """ The range at which this item is called. This is the range relative to the caller, e.g the item passed to {@link CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls `provideCallHierarchyOutgoingCalls`} and not {@link CallHierarchyOutgoingCall.to `this.to`}. """ @@ -1171,14 +1190,14 @@ class SemanticTokens(TypedDict): the client will include the result id in the next semantic token request. A server can then instead of computing all semantic tokens again simply send a delta. """ - data: List[Uint] + data: list[Uint] """ The actual tokens. """ class SemanticTokensPartialResult(TypedDict): """@since 3.16.0""" - data: List[Uint] + data: list[Uint] class SemanticTokensRegistrationOptions(TypedDict): @@ -1189,7 +1208,7 @@ class SemanticTokensRegistrationOptions(TypedDict): the document selector provided on the client side will be used. """ legend: "SemanticTokensLegend" """ The legend used by the server """ - range: NotRequired[Union[bool, dict]] + range: NotRequired[bool | dict] """ Server supports providing semantic tokens for a specific range of a document. """ full: NotRequired[Union[bool, "__SemanticTokensOptions_full_Type_1"]] @@ -1218,14 +1237,14 @@ class SemanticTokensDelta(TypedDict): """@since 3.16.0""" resultId: NotRequired[str] - edits: List["SemanticTokensEdit"] + edits: list["SemanticTokensEdit"] """ The semantic token edits to transform a previous result into a new result. """ class SemanticTokensDeltaPartialResult(TypedDict): """@since 3.16.0""" - edits: List["SemanticTokensEdit"] + edits: list["SemanticTokensEdit"] class SemanticTokensRangeParams(TypedDict): @@ -1245,7 +1264,8 @@ class SemanticTokensRangeParams(TypedDict): class ShowDocumentParams(TypedDict): """Params to show a document. - @since 3.16.0""" + @since 3.16.0 + """ uri: "URI" """ The document uri to show. """ @@ -1268,7 +1288,8 @@ class ShowDocumentParams(TypedDict): class ShowDocumentResult(TypedDict): """The result of a showDocument request. - @since 3.16.0""" + @since 3.16.0 + """ success: bool """ A boolean indicating if the show was successful. """ @@ -1286,9 +1307,10 @@ class LinkedEditingRangeParams(TypedDict): class LinkedEditingRanges(TypedDict): """The result of a linked editing range request. - @since 3.16.0""" + @since 3.16.0 + """ - ranges: List["Range"] + ranges: list["Range"] """ A list of ranges that can be edited together. The ranges must have identical length and contain identical text content. The ranges cannot overlap. """ wordPattern: NotRequired[str] @@ -1310,9 +1332,10 @@ class CreateFilesParams(TypedDict): """The parameters sent in notifications/requests for user-initiated creation of files. - @since 3.16.0""" + @since 3.16.0 + """ - files: List["FileCreate"] + files: list["FileCreate"] """ An array of all files/folders created in this operation. """ @@ -1328,13 +1351,12 @@ class WorkspaceEdit(TypedDict): An invalid sequence (e.g. (1) delete file a.txt and (2) insert text into file a.txt) will cause failure of the operation. How the client recovers from the failure is described by - the client capability: `workspace.workspaceEdit.failureHandling`""" + the client capability: `workspace.workspaceEdit.failureHandling` + """ - changes: NotRequired[Dict["DocumentUri", List["TextEdit"]]] + changes: NotRequired[dict["DocumentUri", list["TextEdit"]]] """ Holds changes to existing resources. """ - documentChanges: NotRequired[ - List[Union["TextDocumentEdit", "CreateFile", "RenameFile", "DeleteFile"]] - ] + documentChanges: NotRequired[list[Union["TextDocumentEdit", "CreateFile", "RenameFile", "DeleteFile"]]] """ Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes are either an array of `TextDocumentEdit`s to express changes to n different text documents where each text document edit addresses a specific version of a text document. Or it can contain @@ -1345,9 +1367,7 @@ class WorkspaceEdit(TypedDict): If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s using the `changes` property are supported. """ - changeAnnotations: NotRequired[ - Dict["ChangeAnnotationIdentifier", "ChangeAnnotation"] - ] + changeAnnotations: NotRequired[dict["ChangeAnnotationIdentifier", "ChangeAnnotation"]] """ A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and delete file / folder operations. @@ -1359,9 +1379,10 @@ class WorkspaceEdit(TypedDict): class FileOperationRegistrationOptions(TypedDict): """The options to register for file operations. - @since 3.16.0""" + @since 3.16.0 + """ - filters: List["FileOperationFilter"] + filters: list["FileOperationFilter"] """ The actual filters. """ @@ -1369,9 +1390,10 @@ class RenameFilesParams(TypedDict): """The parameters sent in notifications/requests for user-initiated renames of files. - @since 3.16.0""" + @since 3.16.0 + """ - files: List["FileRename"] + files: list["FileRename"] """ An array of all files/folders renamed in this operation. When a folder is renamed, only the folder will be included, and not its children. """ @@ -1380,9 +1402,10 @@ class DeleteFilesParams(TypedDict): """The parameters sent in notifications/requests for user-initiated deletes of files. - @since 3.16.0""" + @since 3.16.0 + """ - files: List["FileDelete"] + files: list["FileDelete"] """ An array of all files/folders deleted in this operation. """ @@ -1401,7 +1424,8 @@ class MonikerParams(TypedDict): class Moniker(TypedDict): """Moniker definition to match LSIF 0.5 moniker definition. - @since 3.16.0""" + @since 3.16.0 + """ scheme: str """ The scheme of the moniker. For example tsc or .Net """ @@ -1423,7 +1447,8 @@ class MonikerRegistrationOptions(TypedDict): class TypeHierarchyPrepareParams(TypedDict): """The parameter of a `textDocument/prepareTypeHierarchy` request. - @since 3.17.0""" + @since 3.17.0 + """ textDocument: "TextDocumentIdentifier" """ The text document. """ @@ -1440,7 +1465,7 @@ class TypeHierarchyItem(TypedDict): """ The name of this item. """ kind: "SymbolKind" """ The kind of this item. """ - tags: NotRequired[List["SymbolTag"]] + tags: NotRequired[list["SymbolTag"]] """ Tags for this item. """ detail: NotRequired[str] """ More detail for this item, e.g. the signature of a function. """ @@ -1463,7 +1488,8 @@ class TypeHierarchyItem(TypedDict): class TypeHierarchyRegistrationOptions(TypedDict): """Type hierarchy options used during static or dynamic registration. - @since 3.17.0""" + @since 3.17.0 + """ documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null @@ -1476,7 +1502,8 @@ class TypeHierarchyRegistrationOptions(TypedDict): class TypeHierarchySupertypesParams(TypedDict): """The parameter of a `typeHierarchy/supertypes` request. - @since 3.17.0""" + @since 3.17.0 + """ item: "TypeHierarchyItem" workDoneToken: NotRequired["ProgressToken"] @@ -1489,7 +1516,8 @@ class TypeHierarchySupertypesParams(TypedDict): class TypeHierarchySubtypesParams(TypedDict): """The parameter of a `typeHierarchy/subtypes` request. - @since 3.17.0""" + @since 3.17.0 + """ item: "TypeHierarchyItem" workDoneToken: NotRequired["ProgressToken"] @@ -1502,7 +1530,8 @@ class TypeHierarchySubtypesParams(TypedDict): class InlineValueParams(TypedDict): """A parameter literal used in inline value requests. - @since 3.17.0""" + @since 3.17.0 + """ textDocument: "TextDocumentIdentifier" """ The text document. """ @@ -1518,7 +1547,8 @@ class InlineValueParams(TypedDict): class InlineValueRegistrationOptions(TypedDict): """Inline value options used during static or dynamic registration. - @since 3.17.0""" + @since 3.17.0 + """ documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null @@ -1531,7 +1561,8 @@ class InlineValueRegistrationOptions(TypedDict): class InlayHintParams(TypedDict): """A parameter literal used in inlay hint requests. - @since 3.17.0""" + @since 3.17.0 + """ textDocument: "TextDocumentIdentifier" """ The text document. """ @@ -1544,11 +1575,12 @@ class InlayHintParams(TypedDict): class InlayHint(TypedDict): """Inlay hint information. - @since 3.17.0""" + @since 3.17.0 + """ position: "Position" """ The position of this hint. """ - label: Union[str, List["InlayHintLabelPart"]] + label: str | list["InlayHintLabelPart"] """ The label of this hint. A human readable string or an array of InlayHintLabelPart label parts. @@ -1556,7 +1588,7 @@ class InlayHint(TypedDict): kind: NotRequired["InlayHintKind"] """ The kind of this hint. Can be omitted in which case the client should fall back to a reasonable default. """ - textEdits: NotRequired[List["TextEdit"]] + textEdits: NotRequired[list["TextEdit"]] """ Optional text edits that are performed when accepting this inlay hint. *Note* that edits are expected to change the document so that the inlay @@ -1584,7 +1616,8 @@ class InlayHint(TypedDict): class InlayHintRegistrationOptions(TypedDict): """Inlay hint options used during static or dynamic registration. - @since 3.17.0""" + @since 3.17.0 + """ resolveProvider: NotRequired[bool] """ The server provides support to resolve additional @@ -1600,7 +1633,8 @@ class InlayHintRegistrationOptions(TypedDict): class DocumentDiagnosticParams(TypedDict): """Parameters of the document diagnostic request. - @since 3.17.0""" + @since 3.17.0 + """ textDocument: "TextDocumentIdentifier" """ The text document. """ @@ -1618,9 +1652,10 @@ class DocumentDiagnosticParams(TypedDict): class DocumentDiagnosticReportPartialResult(TypedDict): """A partial result for a document diagnostic report. - @since 3.17.0""" + @since 3.17.0 + """ - relatedDocuments: Dict[ + relatedDocuments: dict[ "DocumentUri", Union["FullDocumentDiagnosticReport", "UnchangedDocumentDiagnosticReport"], ] @@ -1629,7 +1664,8 @@ class DocumentDiagnosticReportPartialResult(TypedDict): class DiagnosticServerCancellationData(TypedDict): """Cancellation data returned from a diagnostic request. - @since 3.17.0""" + @since 3.17.0 + """ retriggerRequest: bool @@ -1637,7 +1673,8 @@ class DiagnosticServerCancellationData(TypedDict): class DiagnosticRegistrationOptions(TypedDict): """Diagnostic registration options. - @since 3.17.0""" + @since 3.17.0 + """ documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null @@ -1660,11 +1697,12 @@ class DiagnosticRegistrationOptions(TypedDict): class WorkspaceDiagnosticParams(TypedDict): """Parameters of the workspace diagnostic request. - @since 3.17.0""" + @since 3.17.0 + """ identifier: NotRequired[str] """ The additional identifier provided during registration. """ - previousResultIds: List["PreviousResultId"] + previousResultIds: list["PreviousResultId"] """ The currently known diagnostic reports with their previous result ids. """ workDoneToken: NotRequired["ProgressToken"] @@ -1677,27 +1715,30 @@ class WorkspaceDiagnosticParams(TypedDict): class WorkspaceDiagnosticReport(TypedDict): """A workspace diagnostic report. - @since 3.17.0""" + @since 3.17.0 + """ - items: List["WorkspaceDocumentDiagnosticReport"] + items: list["WorkspaceDocumentDiagnosticReport"] class WorkspaceDiagnosticReportPartialResult(TypedDict): """A partial result for a workspace diagnostic report. - @since 3.17.0""" + @since 3.17.0 + """ - items: List["WorkspaceDocumentDiagnosticReport"] + items: list["WorkspaceDocumentDiagnosticReport"] class DidOpenNotebookDocumentParams(TypedDict): """The params sent in an open notebook document notification. - @since 3.17.0""" + @since 3.17.0 + """ notebookDocument: "NotebookDocument" """ The notebook document that got opened. """ - cellTextDocuments: List["TextDocumentItem"] + cellTextDocuments: list["TextDocumentItem"] """ The text documents that represent the content of a notebook cell. """ @@ -1705,7 +1746,8 @@ class DidOpenNotebookDocumentParams(TypedDict): class DidChangeNotebookDocumentParams(TypedDict): """The params sent in a change notebook document notification. - @since 3.17.0""" + @since 3.17.0 + """ notebookDocument: "VersionedNotebookDocumentIdentifier" """ The notebook document that did change. The version number points @@ -1731,7 +1773,8 @@ class DidChangeNotebookDocumentParams(TypedDict): class DidSaveNotebookDocumentParams(TypedDict): """The params sent in a save notebook document notification. - @since 3.17.0""" + @since 3.17.0 + """ notebookDocument: "NotebookDocumentIdentifier" """ The notebook document that got saved. """ @@ -1740,25 +1783,26 @@ class DidSaveNotebookDocumentParams(TypedDict): class DidCloseNotebookDocumentParams(TypedDict): """The params sent in a close notebook document notification. - @since 3.17.0""" + @since 3.17.0 + """ notebookDocument: "NotebookDocumentIdentifier" """ The notebook document that got closed. """ - cellTextDocuments: List["TextDocumentIdentifier"] + cellTextDocuments: list["TextDocumentIdentifier"] """ The text documents that represent the content of a notebook cell that got closed. """ class RegistrationParams(TypedDict): - registrations: List["Registration"] + registrations: list["Registration"] class UnregistrationParams(TypedDict): - unregisterations: List["Unregistration"] + unregisterations: list["Unregistration"] class InitializeParams(TypedDict): - processId: Union[int, None] + processId: int | None """ The process Id of the parent process that started the server. @@ -1777,7 +1821,7 @@ class InitializeParams(TypedDict): (See https://en.wikipedia.org/wiki/IETF_language_tag) @since 3.16.0 """ - rootPath: NotRequired[Union[str, None]] + rootPath: NotRequired[str | None] """ The rootPath of the workspace. Is null if no folder is open. @@ -1794,7 +1838,7 @@ class InitializeParams(TypedDict): """ User provided initialization options. """ trace: NotRequired["TraceValues"] """ The initial trace setting. If omitted trace is disabled ('off'). """ - workspaceFolders: NotRequired[Union[List["WorkspaceFolder"], None]] + workspaceFolders: NotRequired[list["WorkspaceFolder"] | None] """ The workspace folders configured in the client when the server starts. This property is only available if the client supports workspace folders. @@ -1817,7 +1861,8 @@ class InitializeResult(TypedDict): class InitializeError(TypedDict): """The data type of the ResponseError if the - initialize request fails.""" + initialize request fails. + """ retry: bool """ Indicates whether the client execute the following retry logic: @@ -1838,7 +1883,7 @@ class DidChangeConfigurationParams(TypedDict): class DidChangeConfigurationRegistrationOptions(TypedDict): - section: NotRequired[Union[str, List[str]]] + section: NotRequired[str | list[str]] class ShowMessageParams(TypedDict): @@ -1855,7 +1900,7 @@ class ShowMessageRequestParams(TypedDict): """ The message type. See {@link MessageType} """ message: str """ The actual message. """ - actions: NotRequired[List["MessageActionItem"]] + actions: NotRequired[list["MessageActionItem"]] """ The message action items to present. """ @@ -1887,7 +1932,7 @@ class DidChangeTextDocumentParams(TypedDict): """ The document that did change. The version number points to the version after all provided content changes have been applied. """ - contentChanges: List["TextDocumentContentChangeEvent"] + contentChanges: list["TextDocumentContentChangeEvent"] """ The actual content changes. The content changes describe single state changes to the document. So if there are two content changes c1 (at array index 0) and c2 (at array index 1) for a document in state S then c1 moves the document from @@ -1961,14 +2006,14 @@ class TextEdit(TypedDict): class DidChangeWatchedFilesParams(TypedDict): """The watched files change notification's parameters.""" - changes: List["FileEvent"] + changes: list["FileEvent"] """ The actual file events. """ class DidChangeWatchedFilesRegistrationOptions(TypedDict): """Describe options to be used when registered for text document change events.""" - watchers: List["FileSystemWatcher"] + watchers: list["FileSystemWatcher"] """ The watchers to register. """ @@ -1981,7 +2026,7 @@ class PublishDiagnosticsParams(TypedDict): """ Optional the version number of the document the diagnostics are published for. @since 3.15.0 """ - diagnostics: List["Diagnostic"] + diagnostics: list["Diagnostic"] """ An array of diagnostic information items. """ @@ -2004,7 +2049,8 @@ class CompletionParams(TypedDict): class CompletionItem(TypedDict): """A completion item represents a text snippet that is - proposed to complete text that is being typed.""" + proposed to complete text that is being typed. + """ label: str """ The label of this completion item. @@ -2021,7 +2067,7 @@ class CompletionItem(TypedDict): kind: NotRequired["CompletionItemKind"] """ The kind of this completion item. Based of the kind an icon is chosen by the editor. """ - tags: NotRequired[List["CompletionItemTag"]] + tags: NotRequired[list["CompletionItemTag"]] """ Tags for this completion item. @since 3.15.0 """ @@ -2104,7 +2150,7 @@ class CompletionItem(TypedDict): property is used as a text. @since 3.17.0 """ - additionalTextEdits: NotRequired[List["TextEdit"]] + additionalTextEdits: NotRequired[list["TextEdit"]] """ An optional array of additional {@link TextEdit text edits} that are applied when selecting this completion. Edits must not overlap (including the same insert position) with the main {@link CompletionItem.textEdit edit} nor with themselves. @@ -2112,7 +2158,7 @@ class CompletionItem(TypedDict): Additional text edits should be used to change text unrelated to the current cursor position (for example adding an import statement at the top of the file if the completion item will insert an unqualified type). """ - commitCharacters: NotRequired[List[str]] + commitCharacters: NotRequired[list[str]] """ An optional set of characters that when pressed while this completion is active will accept it first and then type that character. *Note* that all commit characters should have `length=1` and that superfluous characters will be ignored. """ @@ -2127,7 +2173,8 @@ class CompletionItem(TypedDict): class CompletionList(TypedDict): """Represents a collection of {@link CompletionItem completion items} to be presented - in the editor.""" + in the editor. + """ isIncomplete: bool """ This list it not complete. Further typing results in recomputing this list. @@ -2148,7 +2195,7 @@ class CompletionList(TypedDict): capability. @since 3.17.0 """ - items: List["CompletionItem"] + items: list["CompletionItem"] """ The completion items. """ @@ -2158,7 +2205,7 @@ class CompletionRegistrationOptions(TypedDict): documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null the document selector provided on the client side will be used. """ - triggerCharacters: NotRequired[List[str]] + triggerCharacters: NotRequired[list[str]] """ Most tools trigger completion request automatically without explicitly requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they do so when the user starts to type an identifier. For example if the user types `c` in a JavaScript file @@ -2167,7 +2214,7 @@ class CompletionRegistrationOptions(TypedDict): If code complete should automatically be trigger on characters not being valid inside an identifier (for example `.` in JavaScript) list them in `triggerCharacters`. """ - allCommitCharacters: NotRequired[List[str]] + allCommitCharacters: NotRequired[list[str]] """ The list of all possible characters that commit a completion. This field can be used if clients don't support individual commit characters per completion item. See `ClientCapabilities.textDocument.completion.completionItem.commitCharactersSupport` @@ -2200,7 +2247,7 @@ class HoverParams(TypedDict): class Hover(TypedDict): """The result of a hover request.""" - contents: Union["MarkupContent", "MarkedString", List["MarkedString"]] + contents: Union["MarkupContent", "MarkedString", list["MarkedString"]] """ The hover's content """ range: NotRequired["Range"] """ An optional range inside the text document that is used to @@ -2234,16 +2281,17 @@ class SignatureHelpParams(TypedDict): class SignatureHelp(TypedDict): """Signature help represents the signature of something callable. There can be multiple signature but only one - active and only one active parameter.""" + active and only one active parameter. + """ - signatures: List["SignatureInformation"] + signatures: list["SignatureInformation"] """ One or more signatures. """ activeSignature: NotRequired[Uint] """ The active signature. If omitted or the value lies outside the range of `signatures` the value defaults to zero or is ignored if the `SignatureHelp` has no signatures. - Whenever possible implementors should make an active decision about + Whenever possible implementers should make an active decision about the active signature and shouldn't rely on a default value. In future version of the protocol this property might become @@ -2264,9 +2312,9 @@ class SignatureHelpRegistrationOptions(TypedDict): documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null the document selector provided on the client side will be used. """ - triggerCharacters: NotRequired[List[str]] + triggerCharacters: NotRequired[list[str]] """ List of characters that trigger signature help automatically. """ - retriggerCharacters: NotRequired[List[str]] + retriggerCharacters: NotRequired[list[str]] """ List of characters that re-trigger signature help. These trigger characters are only active when signature help is already showing. All trigger characters @@ -2337,7 +2385,8 @@ class DocumentHighlightParams(TypedDict): class DocumentHighlight(TypedDict): """A document highlight is a range inside a text document which deserves special attention. Usually a document highlight is visualized by changing - the background color of its range.""" + the background color of its range. + """ range: "Range" """ The range this highlight applies to. """ @@ -2367,7 +2416,8 @@ class DocumentSymbolParams(TypedDict): class SymbolInformation(TypedDict): """Represents information about programming constructs like variables, classes, - interfaces etc.""" + interfaces etc. + """ deprecated: NotRequired[bool] """ Indicates if this symbol is deprecated. @@ -2387,7 +2437,7 @@ class SymbolInformation(TypedDict): """ The name of this symbol. """ kind: "SymbolKind" """ The kind of this symbol. """ - tags: NotRequired[List["SymbolTag"]] + tags: NotRequired[list["SymbolTag"]] """ Tags for this symbol. @since 3.16.0 """ @@ -2402,7 +2452,8 @@ class DocumentSymbol(TypedDict): """Represents programming constructs like variables, classes, interfaces etc. that appear in a document. Document symbols can be hierarchical and they have two ranges: one that encloses its definition and one that points to - its most interesting range, e.g. the range of an identifier.""" + its most interesting range, e.g. the range of an identifier. + """ name: str """ The name of this symbol. Will be displayed in the user interface and therefore must not be @@ -2411,7 +2462,7 @@ class DocumentSymbol(TypedDict): """ More detail for this symbol, e.g the signature of a function. """ kind: "SymbolKind" """ The kind of this symbol. """ - tags: NotRequired[List["SymbolTag"]] + tags: NotRequired[list["SymbolTag"]] """ Tags for this document symbol. @since 3.16.0 """ @@ -2461,13 +2512,14 @@ class Command(TypedDict): """Represents a reference to a command. Provides a title which will be used to represent a command in the UI and, optionally, an array of arguments which will be passed to the command handler - function when invoked.""" + function when invoked. + """ title: str """ Title of the command, like `save`. """ command: str """ The identifier of the actual command handler. """ - arguments: NotRequired[List["LSPAny"]] + arguments: NotRequired[list["LSPAny"]] """ Arguments that the command handler should be invoked with. """ @@ -2485,7 +2537,7 @@ class CodeAction(TypedDict): """ The kind of the code action. Used to filter code actions. """ - diagnostics: NotRequired[List["Diagnostic"]] + diagnostics: NotRequired[list["Diagnostic"]] """ The diagnostics that this code action resolves. """ isPreferred: NotRequired[bool] """ Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted @@ -2530,7 +2582,7 @@ class CodeActionRegistrationOptions(TypedDict): documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null the document selector provided on the client side will be used. """ - codeActionKinds: NotRequired[List["CodeActionKind"]] + codeActionKinds: NotRequired[list["CodeActionKind"]] """ CodeActionKinds that this server may return. The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server @@ -2560,7 +2612,8 @@ class WorkspaceSymbol(TypedDict): See also SymbolInformation. - @since 3.17.0""" + @since 3.17.0 + """ location: Union["Location", "__WorkspaceSymbol_location_Type_1"] """ The location of the symbol. Whether a server is allowed to @@ -2575,7 +2628,7 @@ class WorkspaceSymbol(TypedDict): """ The name of this symbol. """ kind: "SymbolKind" """ The kind of this symbol. """ - tags: NotRequired[List["SymbolTag"]] + tags: NotRequired[list["SymbolTag"]] """ Tags for this symbol. @since 3.16.0 """ @@ -2613,7 +2666,8 @@ class CodeLens(TypedDict): source text, like the number of references, a way to run tests, etc. A code lens is _unresolved_ when no command is associated to it. For performance - reasons the creation of a code lens and resolving should be done in two stages.""" + reasons the creation of a code lens and resolving should be done in two stages. + """ range: "Range" """ The range in which this code lens is valid. Should only span a single line. """ @@ -2649,7 +2703,8 @@ class DocumentLinkParams(TypedDict): class DocumentLink(TypedDict): """A document link is a range in a text document that links to an internal or external resource, like another - text document or a web site.""" + text document or a web site. + """ range: "Range" """ The range this link applies to. """ @@ -2744,7 +2799,7 @@ class DocumentOnTypeFormattingRegistrationOptions(TypedDict): the document selector provided on the client side will be used. """ firstTriggerCharacter: str """ A character on which formatting should be triggered, like `{`. """ - moreTriggerCharacter: NotRequired[List[str]] + moreTriggerCharacter: NotRequired[list[str]] """ More trigger characters. """ @@ -2789,7 +2844,7 @@ class ExecuteCommandParams(TypedDict): command: str """ The identifier of the actual command handler. """ - arguments: NotRequired[List["LSPAny"]] + arguments: NotRequired[list["LSPAny"]] """ Arguments that the command should be invoked with. """ workDoneToken: NotRequired["ProgressToken"] """ An optional token that a server can use to report work done progress. """ @@ -2798,7 +2853,7 @@ class ExecuteCommandParams(TypedDict): class ExecuteCommandRegistrationOptions(TypedDict): """Registration options for a {@link ExecuteCommandRequest}.""" - commands: List[str] + commands: list[str] """ The commands to be executed on the server """ @@ -2816,7 +2871,8 @@ class ApplyWorkspaceEditParams(TypedDict): class ApplyWorkspaceEditResult(TypedDict): """The result returned from the apply workspace edit request. - @since 3.17 renamed from ApplyWorkspaceEditResponse""" + @since 3.17 renamed from ApplyWorkspaceEditResponse + """ applied: bool """ Indicates whether the edit was applied or not. """ @@ -2895,7 +2951,7 @@ class LogTraceParams(TypedDict): class CancelParams(TypedDict): - id: Union[int, str] + id: int | str """ The request id to cancel. """ @@ -2908,7 +2964,8 @@ class ProgressParams(TypedDict): class TextDocumentPositionParams(TypedDict): """A parameter literal used in requests to pass a text document and a position inside that - document.""" + document. + """ textDocument: "TextDocumentIdentifier" """ The text document. """ @@ -2929,7 +2986,8 @@ class PartialResultParams(TypedDict): class LocationLink(TypedDict): """Represents the connection of two locations. Provides additional metadata over normal {@link Location locations}, - including an origin range.""" + including an origin range. + """ originSelectionRange: NotRequired["Range"] """ Span of the origin of this link. @@ -2958,7 +3016,8 @@ class Range(TypedDict): start: { line: 5, character: 23 } end : { line 6, character : 0 } } - ```""" + ``` + """ start: "Position" """ The range's start position. """ @@ -2972,7 +3031,8 @@ class ImplementationOptions(TypedDict): class StaticRegistrationOptions(TypedDict): """Static registration options to be returned in the initialize - request.""" + request. + """ id: NotRequired[str] """ The id used to register the request. The id can be used to deregister @@ -2986,9 +3046,9 @@ class TypeDefinitionOptions(TypedDict): class WorkspaceFoldersChangeEvent(TypedDict): """The workspace folder change event.""" - added: List["WorkspaceFolder"] + added: list["WorkspaceFolder"] """ The array of added workspace folders """ - removed: List["WorkspaceFolder"] + removed: list["WorkspaceFolder"] """ The array of the removed workspace folders """ @@ -3032,7 +3092,7 @@ class DeclarationOptions(TypedDict): class Position(TypedDict): - """Position in a text document expressed as zero-based line and character + r"""Position in a text document expressed as zero-based line and character offset. Prior to 3.17 the offsets were always based on a UTF-16 string representation. So a string of the form `a𐐀b` the character offset of the character `a` is 0, the character offset of `𐐀` is 1 and the character @@ -3058,7 +3118,8 @@ class Position(TypedDict): Positions are line end character agnostic. So you can not specify a position that denotes `\r|\n` or `\n|` where `|` represents the character offset. - @since 3.17.0 - support for negotiated position encoding.""" + @since 3.17.0 - support for negotiated position encoding. + """ line: Uint """ Line position in a document (zero-based). @@ -3082,7 +3143,8 @@ class SelectionRangeOptions(TypedDict): class CallHierarchyOptions(TypedDict): """Call hierarchy options used during static registration. - @since 3.16.0""" + @since 3.16.0 + """ workDoneProgress: NotRequired[bool] @@ -3092,7 +3154,7 @@ class SemanticTokensOptions(TypedDict): legend: "SemanticTokensLegend" """ The legend used by the server """ - range: NotRequired[Union[bool, dict]] + range: NotRequired[bool | dict] """ Server supports providing semantic tokens for a specific range of a document. """ full: NotRequired[Union[bool, "__SemanticTokensOptions_full_Type_2"]] @@ -3107,7 +3169,7 @@ class SemanticTokensEdit(TypedDict): """ The start offset of the edit. """ deleteCount: Uint """ The count of elements to remove. """ - data: NotRequired[List[Uint]] + data: NotRequired[list[Uint]] """ The elements to insert. """ @@ -3118,7 +3180,8 @@ class LinkedEditingRangeOptions(TypedDict): class FileCreate(TypedDict): """Represents information on a file/folder create. - @since 3.16.0""" + @since 3.16.0 + """ uri: str """ A file:// URI for the location of the file/folder being created. """ @@ -3128,11 +3191,12 @@ class TextDocumentEdit(TypedDict): """Describes textual changes on a text document. A TextDocumentEdit describes all changes on a document version Si and after they are applied move the document to version Si+1. So the creator of a TextDocumentEdit doesn't need to sort the array of edits or do any - kind of ordering. However the edits must be non overlapping.""" + kind of ordering. However the edits must be non overlapping. + """ textDocument: "OptionalVersionedTextDocumentIdentifier" """ The text document to change. """ - edits: List[Union["TextEdit", "AnnotatedTextEdit"]] + edits: list[Union["TextEdit", "AnnotatedTextEdit"]] """ The edits to be applied. @since 3.16.0 - support for AnnotatedTextEdit. This is guarded using a @@ -3189,7 +3253,8 @@ class DeleteFile(TypedDict): class ChangeAnnotation(TypedDict): """Additional information that describes document changes. - @since 3.16.0""" + @since 3.16.0 + """ label: str """ A human-readable string describing the actual change. The string @@ -3206,7 +3271,8 @@ class FileOperationFilter(TypedDict): """A filter to describe in which file operation requests or notifications the server is interested in receiving. - @since 3.16.0""" + @since 3.16.0 + """ scheme: NotRequired[str] """ A Uri scheme like `file` or `untitled`. """ @@ -3217,7 +3283,8 @@ class FileOperationFilter(TypedDict): class FileRename(TypedDict): """Represents information on a file/folder rename. - @since 3.16.0""" + @since 3.16.0 + """ oldUri: str """ A file:// URI for the original location of the file/folder being renamed. """ @@ -3228,7 +3295,8 @@ class FileRename(TypedDict): class FileDelete(TypedDict): """Represents information on a file/folder delete. - @since 3.16.0""" + @since 3.16.0 + """ uri: str """ A file:// URI for the location of the file/folder being deleted. """ @@ -3241,7 +3309,8 @@ class MonikerOptions(TypedDict): class TypeHierarchyOptions(TypedDict): """Type hierarchy options used during static registration. - @since 3.17.0""" + @since 3.17.0 + """ workDoneProgress: NotRequired[bool] @@ -3259,7 +3328,8 @@ class InlineValueContext(TypedDict): class InlineValueText(TypedDict): """Provide inline value as text. - @since 3.17.0""" + @since 3.17.0 + """ range: "Range" """ The document range for which the inline value applies. """ @@ -3272,7 +3342,8 @@ class InlineValueVariableLookup(TypedDict): If only a range is specified, the variable name will be extracted from the underlying document. An optional variable name can be used to override the extracted name. - @since 3.17.0""" + @since 3.17.0 + """ range: "Range" """ The document range for which the inline value applies. @@ -3288,7 +3359,8 @@ class InlineValueEvaluatableExpression(TypedDict): If only a range is specified, the expression will be extracted from the underlying document. An optional expression can be used to override the extracted expression. - @since 3.17.0""" + @since 3.17.0 + """ range: "Range" """ The document range for which the inline value applies. @@ -3300,7 +3372,8 @@ class InlineValueEvaluatableExpression(TypedDict): class InlineValueOptions(TypedDict): """Inline value options used during static registration. - @since 3.17.0""" + @since 3.17.0 + """ workDoneProgress: NotRequired[bool] @@ -3309,7 +3382,8 @@ class InlayHintLabelPart(TypedDict): """An inlay hint label part allows for interactive and composite labels of inlay hints. - @since 3.17.0""" + @since 3.17.0 + """ value: str """ The value of this label part. """ @@ -3337,7 +3411,7 @@ class InlayHintLabelPart(TypedDict): class MarkupContent(TypedDict): - """A `MarkupContent` literal represents a string value which content is interpreted base on its + r"""A `MarkupContent` literal represents a string value which content is interpreted base on its kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds. If the kind is `markdown` then the value can contain fenced code blocks like in GitHub issues. @@ -3358,7 +3432,8 @@ class MarkupContent(TypedDict): ``` *Please Note* that clients might sanitize the return markdown. A client could decide to - remove HTML from the markdown to avoid script execution.""" + remove HTML from the markdown to avoid script execution. + """ kind: "MarkupKind" """ The type of the Markup """ @@ -3369,7 +3444,8 @@ class MarkupContent(TypedDict): class InlayHintOptions(TypedDict): """Inlay hint options used during static registration. - @since 3.17.0""" + @since 3.17.0 + """ resolveProvider: NotRequired[bool] """ The server provides support to resolve additional @@ -3380,10 +3456,11 @@ class InlayHintOptions(TypedDict): class RelatedFullDocumentDiagnosticReport(TypedDict): """A full diagnostic report with a set of related documents. - @since 3.17.0""" + @since 3.17.0 + """ relatedDocuments: NotRequired[ - Dict[ + dict[ "DocumentUri", Union["FullDocumentDiagnosticReport", "UnchangedDocumentDiagnosticReport"], ] @@ -3401,17 +3478,18 @@ class RelatedFullDocumentDiagnosticReport(TypedDict): """ An optional result id. If provided it will be sent on the next diagnostic request for the same document. """ - items: List["Diagnostic"] + items: list["Diagnostic"] """ The actual items. """ class RelatedUnchangedDocumentDiagnosticReport(TypedDict): """An unchanged diagnostic report with a set of related documents. - @since 3.17.0""" + @since 3.17.0 + """ relatedDocuments: NotRequired[ - Dict[ + dict[ "DocumentUri", Union["FullDocumentDiagnosticReport", "UnchangedDocumentDiagnosticReport"], ] @@ -3436,7 +3514,8 @@ class RelatedUnchangedDocumentDiagnosticReport(TypedDict): class FullDocumentDiagnosticReport(TypedDict): """A diagnostic report with a full set of problems. - @since 3.17.0""" + @since 3.17.0 + """ kind: Literal["full"] """ A full document diagnostic report. """ @@ -3444,7 +3523,7 @@ class FullDocumentDiagnosticReport(TypedDict): """ An optional result id. If provided it will be sent on the next diagnostic request for the same document. """ - items: List["Diagnostic"] + items: list["Diagnostic"] """ The actual items. """ @@ -3452,7 +3531,8 @@ class UnchangedDocumentDiagnosticReport(TypedDict): """A diagnostic report indicating that the last returned report is still accurate. - @since 3.17.0""" + @since 3.17.0 + """ kind: Literal["unchanged"] """ A document diagnostic report indicating @@ -3467,7 +3547,8 @@ class UnchangedDocumentDiagnosticReport(TypedDict): class DiagnosticOptions(TypedDict): """Diagnostic options. - @since 3.17.0""" + @since 3.17.0 + """ identifier: NotRequired[str] """ An optional identifier under which the diagnostics are @@ -3485,7 +3566,8 @@ class DiagnosticOptions(TypedDict): class PreviousResultId(TypedDict): """A previous result id in a workspace pull request. - @since 3.17.0""" + @since 3.17.0 + """ uri: "DocumentUri" """ The URI for which the client knowns a @@ -3497,7 +3579,8 @@ class PreviousResultId(TypedDict): class NotebookDocument(TypedDict): """A notebook document. - @since 3.17.0""" + @since 3.17.0 + """ uri: "URI" """ The notebook document's uri. """ @@ -3511,13 +3594,14 @@ class NotebookDocument(TypedDict): document. Note: should always be an object literal (e.g. LSPObject) """ - cells: List["NotebookCell"] + cells: list["NotebookCell"] """ The cells of a notebook. """ class TextDocumentItem(TypedDict): """An item to transfer a text document from the client to the - server.""" + server. + """ uri: "DocumentUri" """ The text document's uri. """ @@ -3533,7 +3617,8 @@ class TextDocumentItem(TypedDict): class VersionedNotebookDocumentIdentifier(TypedDict): """A versioned notebook document identifier. - @since 3.17.0""" + @since 3.17.0 + """ version: int """ The version number of this notebook document. """ @@ -3544,7 +3629,8 @@ class VersionedNotebookDocumentIdentifier(TypedDict): class NotebookDocumentChangeEvent(TypedDict): """A change event for a notebook document. - @since 3.17.0""" + @since 3.17.0 + """ metadata: NotRequired["LSPObject"] """ The changed meta data if any. @@ -3557,7 +3643,8 @@ class NotebookDocumentChangeEvent(TypedDict): class NotebookDocumentIdentifier(TypedDict): """A literal to identify a notebook document in the client. - @since 3.17.0""" + @since 3.17.0 + """ uri: "URI" """ The notebook document's uri. """ @@ -3586,7 +3673,7 @@ class Unregistration(TypedDict): class WorkspaceFoldersInitializeParams(TypedDict): - workspaceFolders: NotRequired[Union[List["WorkspaceFolder"], None]] + workspaceFolders: NotRequired[list["WorkspaceFolder"] | None] """ The workspace folders configured in the client when the server starts. This property is only available if the client supports workspace folders. @@ -3598,7 +3685,8 @@ class WorkspaceFoldersInitializeParams(TypedDict): class ServerCapabilities(TypedDict): """Defines the capabilities provided by a language - server.""" + server. + """ positionEncoding: NotRequired["PositionEncodingKind"] """ The position encoding the server picked from the encodings offered @@ -3610,15 +3698,11 @@ class ServerCapabilities(TypedDict): If omitted it defaults to 'utf-16'. @since 3.17.0 """ - textDocumentSync: NotRequired[ - Union["TextDocumentSyncOptions", "TextDocumentSyncKind"] - ] + textDocumentSync: NotRequired[Union["TextDocumentSyncOptions", "TextDocumentSyncKind"]] """ Defines how text documents are synced. Is either a detailed structure defining each notification or for backwards compatibility the TextDocumentSyncKind number. """ - notebookDocumentSync: NotRequired[ - Union["NotebookDocumentSyncOptions", "NotebookDocumentSyncRegistrationOptions"] - ] + notebookDocumentSync: NotRequired[Union["NotebookDocumentSyncOptions", "NotebookDocumentSyncRegistrationOptions"]] """ Defines how notebook documents are synced. @since 3.17.0 """ @@ -3628,19 +3712,13 @@ class ServerCapabilities(TypedDict): """ The server provides hover support. """ signatureHelpProvider: NotRequired["SignatureHelpOptions"] """ The server provides signature help support. """ - declarationProvider: NotRequired[ - Union[bool, "DeclarationOptions", "DeclarationRegistrationOptions"] - ] + declarationProvider: NotRequired[Union[bool, "DeclarationOptions", "DeclarationRegistrationOptions"]] """ The server provides Goto Declaration support. """ definitionProvider: NotRequired[Union[bool, "DefinitionOptions"]] """ The server provides goto definition support. """ - typeDefinitionProvider: NotRequired[ - Union[bool, "TypeDefinitionOptions", "TypeDefinitionRegistrationOptions"] - ] + typeDefinitionProvider: NotRequired[Union[bool, "TypeDefinitionOptions", "TypeDefinitionRegistrationOptions"]] """ The server provides Goto Type Definition support. """ - implementationProvider: NotRequired[ - Union[bool, "ImplementationOptions", "ImplementationRegistrationOptions"] - ] + implementationProvider: NotRequired[Union[bool, "ImplementationOptions", "ImplementationRegistrationOptions"]] """ The server provides Goto Implementation support. """ referencesProvider: NotRequired[Union[bool, "ReferenceOptions"]] """ The server provides find references support. """ @@ -3656,17 +3734,13 @@ class ServerCapabilities(TypedDict): """ The server provides code lens. """ documentLinkProvider: NotRequired["DocumentLinkOptions"] """ The server provides document link support. """ - colorProvider: NotRequired[ - Union[bool, "DocumentColorOptions", "DocumentColorRegistrationOptions"] - ] + colorProvider: NotRequired[Union[bool, "DocumentColorOptions", "DocumentColorRegistrationOptions"]] """ The server provides color provider support. """ workspaceSymbolProvider: NotRequired[Union[bool, "WorkspaceSymbolOptions"]] """ The server provides workspace symbol support. """ documentFormattingProvider: NotRequired[Union[bool, "DocumentFormattingOptions"]] """ The server provides document formatting. """ - documentRangeFormattingProvider: NotRequired[ - Union[bool, "DocumentRangeFormattingOptions"] - ] + documentRangeFormattingProvider: NotRequired[Union[bool, "DocumentRangeFormattingOptions"]] """ The server provides document range formatting. """ documentOnTypeFormattingProvider: NotRequired["DocumentOnTypeFormattingOptions"] """ The server provides document formatting on typing. """ @@ -3674,63 +3748,41 @@ class ServerCapabilities(TypedDict): """ The server provides rename support. RenameOptions may only be specified if the client states that it supports `prepareSupport` in its initial `initialize` request. """ - foldingRangeProvider: NotRequired[ - Union[bool, "FoldingRangeOptions", "FoldingRangeRegistrationOptions"] - ] + foldingRangeProvider: NotRequired[Union[bool, "FoldingRangeOptions", "FoldingRangeRegistrationOptions"]] """ The server provides folding provider support. """ - selectionRangeProvider: NotRequired[ - Union[bool, "SelectionRangeOptions", "SelectionRangeRegistrationOptions"] - ] + selectionRangeProvider: NotRequired[Union[bool, "SelectionRangeOptions", "SelectionRangeRegistrationOptions"]] """ The server provides selection range support. """ executeCommandProvider: NotRequired["ExecuteCommandOptions"] """ The server provides execute command support. """ - callHierarchyProvider: NotRequired[ - Union[bool, "CallHierarchyOptions", "CallHierarchyRegistrationOptions"] - ] + callHierarchyProvider: NotRequired[Union[bool, "CallHierarchyOptions", "CallHierarchyRegistrationOptions"]] """ The server provides call hierarchy support. @since 3.16.0 """ - linkedEditingRangeProvider: NotRequired[ - Union[ - bool, "LinkedEditingRangeOptions", "LinkedEditingRangeRegistrationOptions" - ] - ] + linkedEditingRangeProvider: NotRequired[Union[bool, "LinkedEditingRangeOptions", "LinkedEditingRangeRegistrationOptions"]] """ The server provides linked editing range support. @since 3.16.0 """ - semanticTokensProvider: NotRequired[ - Union["SemanticTokensOptions", "SemanticTokensRegistrationOptions"] - ] + semanticTokensProvider: NotRequired[Union["SemanticTokensOptions", "SemanticTokensRegistrationOptions"]] """ The server provides semantic tokens support. @since 3.16.0 """ - monikerProvider: NotRequired[ - Union[bool, "MonikerOptions", "MonikerRegistrationOptions"] - ] + monikerProvider: NotRequired[Union[bool, "MonikerOptions", "MonikerRegistrationOptions"]] """ The server provides moniker support. @since 3.16.0 """ - typeHierarchyProvider: NotRequired[ - Union[bool, "TypeHierarchyOptions", "TypeHierarchyRegistrationOptions"] - ] + typeHierarchyProvider: NotRequired[Union[bool, "TypeHierarchyOptions", "TypeHierarchyRegistrationOptions"]] """ The server provides type hierarchy support. @since 3.17.0 """ - inlineValueProvider: NotRequired[ - Union[bool, "InlineValueOptions", "InlineValueRegistrationOptions"] - ] + inlineValueProvider: NotRequired[Union[bool, "InlineValueOptions", "InlineValueRegistrationOptions"]] """ The server provides inline values. @since 3.17.0 """ - inlayHintProvider: NotRequired[ - Union[bool, "InlayHintOptions", "InlayHintRegistrationOptions"] - ] + inlayHintProvider: NotRequired[Union[bool, "InlayHintOptions", "InlayHintRegistrationOptions"]] """ The server provides inlay hints. @since 3.17.0 """ - diagnosticProvider: NotRequired[ - Union["DiagnosticOptions", "DiagnosticRegistrationOptions"] - ] + diagnosticProvider: NotRequired[Union["DiagnosticOptions", "DiagnosticRegistrationOptions"]] """ The server has support for pull model diagnostics. @since 3.17.0 """ @@ -3778,14 +3830,15 @@ class FileSystemWatcher(TypedDict): class Diagnostic(TypedDict): """Represents a diagnostic, such as a compiler error or warning. Diagnostic objects - are only valid in the scope of a resource.""" + are only valid in the scope of a resource. + """ range: "Range" """ The range at which the message applies """ severity: NotRequired["DiagnosticSeverity"] """ The diagnostic's severity. Can be omitted. If omitted it is up to the client to interpret diagnostics as error, warning, info or hint. """ - code: NotRequired[Union[int, str]] + code: NotRequired[int | str] """ The diagnostic's code, which usually appear in the user interface. """ codeDescription: NotRequired["CodeDescription"] """ An optional property to describe the error code. @@ -3798,11 +3851,11 @@ class Diagnostic(TypedDict): appears in the user interface. """ message: str """ The diagnostic's message. It usually appears in the user interface """ - tags: NotRequired[List["DiagnosticTag"]] + tags: NotRequired[list["DiagnosticTag"]] """ Additional metadata about the diagnostic. @since 3.15.0 """ - relatedInformation: NotRequired[List["DiagnosticRelatedInformation"]] + relatedInformation: NotRequired[list["DiagnosticRelatedInformation"]] """ An array of related diagnostic information, e.g. when symbol-names within a scope collide all definitions can be marked via this property. """ data: NotRequired["LSPAny"] @@ -3825,7 +3878,8 @@ class CompletionContext(TypedDict): class CompletionItemLabelDetails(TypedDict): """Additional details for a completion item label. - @since 3.17.0""" + @since 3.17.0 + """ detail: NotRequired[str] """ An optional string which is rendered less prominently directly after {@link CompletionItem.label label}, @@ -3838,7 +3892,8 @@ class CompletionItemLabelDetails(TypedDict): class InsertReplaceEdit(TypedDict): """A special text edit to provide an insert and a replace operation. - @since 3.16.0""" + @since 3.16.0 + """ newText: str """ The string to be inserted. """ @@ -3851,7 +3906,7 @@ class InsertReplaceEdit(TypedDict): class CompletionOptions(TypedDict): """Completion options.""" - triggerCharacters: NotRequired[List[str]] + triggerCharacters: NotRequired[list[str]] """ Most tools trigger completion request automatically without explicitly requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they do so when the user starts to type an identifier. For example if the user types `c` in a JavaScript file @@ -3860,7 +3915,7 @@ class CompletionOptions(TypedDict): If code complete should automatically be trigger on characters not being valid inside an identifier (for example `.` in JavaScript) list them in `triggerCharacters`. """ - allCommitCharacters: NotRequired[List[str]] + allCommitCharacters: NotRequired[list[str]] """ The list of all possible characters that commit a completion. This field can be used if clients don't support individual commit characters per completion item. See `ClientCapabilities.textDocument.completion.completionItem.commitCharactersSupport` @@ -3889,7 +3944,8 @@ class HoverOptions(TypedDict): class SignatureHelpContext(TypedDict): """Additional information about the context in which a signature help request was triggered. - @since 3.15.0""" + @since 3.15.0 + """ triggerKind: "SignatureHelpTriggerKind" """ Action that caused signature help to be triggered. """ @@ -3912,7 +3968,8 @@ class SignatureHelpContext(TypedDict): class SignatureInformation(TypedDict): """Represents the signature of something callable. A signature can have a label, like a function-name, a doc-comment, and - a set of parameters.""" + a set of parameters. + """ label: str """ The label of this signature. Will be shown in @@ -3920,7 +3977,7 @@ class SignatureInformation(TypedDict): documentation: NotRequired[Union[str, "MarkupContent"]] """ The human-readable doc-comment of this signature. Will be shown in the UI but can be omitted. """ - parameters: NotRequired[List["ParameterInformation"]] + parameters: NotRequired[list["ParameterInformation"]] """ The parameters of this signature. """ activeParameter: NotRequired[Uint] """ The index of the active parameter. @@ -3933,9 +3990,9 @@ class SignatureInformation(TypedDict): class SignatureHelpOptions(TypedDict): """Server Capabilities for a {@link SignatureHelpRequest}.""" - triggerCharacters: NotRequired[List[str]] + triggerCharacters: NotRequired[list[str]] """ List of characters that trigger signature help automatically. """ - retriggerCharacters: NotRequired[List[str]] + retriggerCharacters: NotRequired[list[str]] """ List of characters that re-trigger signature help. These trigger characters are only active when signature help is already showing. All trigger characters @@ -3953,7 +4010,8 @@ class DefinitionOptions(TypedDict): class ReferenceContext(TypedDict): """Value-object that contains additional information when - requesting references.""" + requesting references. + """ includeDeclaration: bool """ Include the declaration of the current symbol. """ @@ -3978,7 +4036,7 @@ class BaseSymbolInformation(TypedDict): """ The name of this symbol. """ kind: "SymbolKind" """ The kind of this symbol. """ - tags: NotRequired[List["SymbolTag"]] + tags: NotRequired[list["SymbolTag"]] """ Tags for this symbol. @since 3.16.0 """ @@ -4002,15 +4060,16 @@ class DocumentSymbolOptions(TypedDict): class CodeActionContext(TypedDict): """Contains additional diagnostic information about the context in which - a {@link CodeActionProvider.provideCodeActions code action} is run.""" + a {@link CodeActionProvider.provideCodeActions code action} is run. + """ - diagnostics: List["Diagnostic"] + diagnostics: list["Diagnostic"] """ An array of diagnostics known on the client side overlapping the range provided to the `textDocument/codeAction` request. They are provided so that the server knows which errors are currently presented to the user for the given range. There is no guarantee that these accurately reflect the error state of the resource. The primary parameter to compute code actions is the provided range. """ - only: NotRequired[List["CodeActionKind"]] + only: NotRequired[list["CodeActionKind"]] """ Requested kind of actions to return. Actions not of this kind are filtered out by the client before being shown. So servers @@ -4024,7 +4083,7 @@ class CodeActionContext(TypedDict): class CodeActionOptions(TypedDict): """Provider options for a {@link CodeActionRequest}.""" - codeActionKinds: NotRequired[List["CodeActionKind"]] + codeActionKinds: NotRequired[list["CodeActionKind"]] """ CodeActionKinds that this server may return. The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server @@ -4102,7 +4161,7 @@ class DocumentOnTypeFormattingOptions(TypedDict): firstTriggerCharacter: str """ A character on which formatting should be triggered, like `{`. """ - moreTriggerCharacter: NotRequired[List[str]] + moreTriggerCharacter: NotRequired[list[str]] """ More trigger characters. """ @@ -4119,7 +4178,7 @@ class RenameOptions(TypedDict): class ExecuteCommandOptions(TypedDict): """The server capabilities of a {@link ExecuteCommandRequest}.""" - commands: List[str] + commands: list[str] """ The commands to be executed on the server """ workDoneProgress: NotRequired[bool] @@ -4127,16 +4186,16 @@ class ExecuteCommandOptions(TypedDict): class SemanticTokensLegend(TypedDict): """@since 3.16.0""" - tokenTypes: List[str] + tokenTypes: list[str] """ The token types a server uses. """ - tokenModifiers: List[str] + tokenModifiers: list[str] """ The token modifiers a server uses. """ class OptionalVersionedTextDocumentIdentifier(TypedDict): """A text document identifier to optionally denote a specific version of a text document.""" - version: Union[int, None] + version: int | None """ The version number of this document. If a versioned text document identifier is sent from the server to the client and the file is not open in the editor (the server has not received an open notification before) the server can send @@ -4149,7 +4208,8 @@ class OptionalVersionedTextDocumentIdentifier(TypedDict): class AnnotatedTextEdit(TypedDict): """A special text edit with an additional change annotation. - @since 3.16.0.""" + @since 3.16.0. + """ annotationId: "ChangeAnnotationIdentifier" """ The actual identifier of the change annotation """ @@ -4203,14 +4263,15 @@ class FileOperationPattern(TypedDict): """A pattern to describe in which file operation requests or notifications the server is interested in receiving. - @since 3.16.0""" + @since 3.16.0 + """ glob: str """ The glob pattern to match. Glob patterns can have the following syntax: - `*` to match one or more characters in a path segment - `?` to match on one character in a path segment - `**` to match any number of path segments, including none - - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) + - `{}` to group sub patterns into an OR expression. (e.g. `**\u200b/*.{ts,js}` matches all TypeScript and JavaScript files) - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) """ matches: NotRequired["FileOperationPatternKind"] @@ -4224,11 +4285,12 @@ class FileOperationPattern(TypedDict): class WorkspaceFullDocumentDiagnosticReport(TypedDict): """A full document diagnostic report for a workspace diagnostic result. - @since 3.17.0""" + @since 3.17.0 + """ uri: "DocumentUri" """ The URI for which diagnostic information is reported. """ - version: Union[int, None] + version: int | None """ The version number for which the diagnostics are reported. If the document is not marked as open `null` can be provided. """ kind: Literal["full"] @@ -4237,18 +4299,19 @@ class WorkspaceFullDocumentDiagnosticReport(TypedDict): """ An optional result id. If provided it will be sent on the next diagnostic request for the same document. """ - items: List["Diagnostic"] + items: list["Diagnostic"] """ The actual items. """ class WorkspaceUnchangedDocumentDiagnosticReport(TypedDict): """An unchanged document diagnostic report for a workspace diagnostic result. - @since 3.17.0""" + @since 3.17.0 + """ uri: "DocumentUri" """ The URI for which diagnostic information is reported. """ - version: Union[int, None] + version: int | None """ The version number for which the diagnostics are reported. If the document is not marked as open `null` can be provided. """ kind: Literal["unchanged"] @@ -4268,7 +4331,8 @@ class NotebookCell(TypedDict): cells and can therefore be used to uniquely identify a notebook cell or the cell's text document. - @since 3.17.0""" + @since 3.17.0 + """ kind: "NotebookCellKind" """ The cell's kind """ @@ -4288,13 +4352,14 @@ class NotebookCellArrayChange(TypedDict): """A change describing how to move a `NotebookCell` array from state S to S'. - @since 3.17.0""" + @since 3.17.0 + """ start: Uint """ The start oftest of the cell that changed. """ deleteCount: Uint """ The deleted cells """ - cells: NotRequired[List["NotebookCell"]] + cells: NotRequired[list["NotebookCell"]] """ The new cells, if any """ @@ -4350,9 +4415,10 @@ class NotebookDocumentSyncOptions(TypedDict): document that contain at least one matching cell will be synced. - @since 3.17.0""" + @since 3.17.0 + """ - notebookSelector: List[ + notebookSelector: list[ Union[ "__NotebookDocumentSyncOptions_notebookSelector_Type_1", "__NotebookDocumentSyncOptions_notebookSelector_Type_2", @@ -4367,9 +4433,10 @@ class NotebookDocumentSyncOptions(TypedDict): class NotebookDocumentSyncRegistrationOptions(TypedDict): """Registration options specific to a notebook. - @since 3.17.0""" + @since 3.17.0 + """ - notebookSelector: List[ + notebookSelector: list[ Union[ "__NotebookDocumentSyncOptions_notebookSelector_Type_3", "__NotebookDocumentSyncOptions_notebookSelector_Type_4", @@ -4387,7 +4454,7 @@ class NotebookDocumentSyncRegistrationOptions(TypedDict): class WorkspaceFoldersServerCapabilities(TypedDict): supported: NotRequired[bool] """ The server has support for workspace folders """ - changeNotifications: NotRequired[Union[str, bool]] + changeNotifications: NotRequired[str | bool] """ Whether the server wants to receive workspace folder change notifications. @@ -4400,7 +4467,8 @@ class WorkspaceFoldersServerCapabilities(TypedDict): class FileOperationOptions(TypedDict): """Options for notifications/requests for user operations on files. - @since 3.16.0""" + @since 3.16.0 + """ didCreate: NotRequired["FileOperationRegistrationOptions"] """ The server is interested in receiving didCreateFiles notifications. """ @@ -4419,7 +4487,8 @@ class FileOperationOptions(TypedDict): class CodeDescription(TypedDict): """Structure to capture a description for an error code. - @since 3.16.0""" + @since 3.16.0 + """ href: "URI" """ An URI to open with more information about the diagnostic error. """ @@ -4428,7 +4497,8 @@ class CodeDescription(TypedDict): class DiagnosticRelatedInformation(TypedDict): """Represents a related message and source code location for a diagnostic. This should be used to point to code locations that cause or related to a diagnostics, e.g when duplicating - a symbol in a scope.""" + a symbol in a scope. + """ location: "Location" """ The location of this related diagnostic information. """ @@ -4438,9 +4508,10 @@ class DiagnosticRelatedInformation(TypedDict): class ParameterInformation(TypedDict): """Represents a parameter of a callable-signature. A parameter can - have a label and a doc-comment.""" + have a label and a doc-comment. + """ - label: Union[str, List[Union[Uint, Uint]]] + label: str | list[Uint | Uint] """ The label of this parameter information. Either a string or an inclusive start and exclusive end offsets within its containing @@ -4458,7 +4529,8 @@ class NotebookCellTextDocumentFilter(TypedDict): """A notebook cell text document filter denotes a cell text document by different properties. - @since 3.17.0""" + @since 3.17.0 + """ notebook: Union[str, "NotebookDocumentFilter"] """ A filter that matches against the notebook @@ -4475,7 +4547,8 @@ class NotebookCellTextDocumentFilter(TypedDict): class FileOperationPatternOptions(TypedDict): """Matching options for the file operation pattern. - @since 3.16.0""" + @since 3.16.0 + """ ignoreCase: NotRequired[bool] """ The pattern should be matched ignoring casing. """ @@ -4644,7 +4717,8 @@ class TextDocumentClientCapabilities(TypedDict): class NotebookDocumentClientCapabilities(TypedDict): """Capabilities specific to the notebook document support. - @since 3.17.0""" + @since 3.17.0 + """ synchronization: "NotebookDocumentSyncClientCapabilities" """ Capabilities specific to notebook document synchronization @@ -4676,11 +4750,10 @@ class WindowClientCapabilities(TypedDict): class GeneralClientCapabilities(TypedDict): """General client capabilities. - @since 3.16.0""" + @since 3.16.0 + """ - staleRequestSupport: NotRequired[ - "__GeneralClientCapabilities_staleRequestSupport_Type_1" - ] + staleRequestSupport: NotRequired["__GeneralClientCapabilities_staleRequestSupport_Type_1"] """ Client capability that signals how the client handles stale requests (e.g. a request for which the client will not process the response @@ -4695,7 +4768,7 @@ class GeneralClientCapabilities(TypedDict): """ Client capabilities specific to the client's markdown parser. @since 3.16.0 """ - positionEncodings: NotRequired[List["PositionEncodingKind"]] + positionEncodings: NotRequired[list["PositionEncodingKind"]] """ The position encodings supported by the client. Client and server have to agree on the same position encoding to ensure that offsets (e.g. character position in a line) are interpreted the same on both @@ -4721,7 +4794,8 @@ class RelativePattern(TypedDict): relatively to a base URI. The common value for a `baseUri` is a workspace folder root, but it can be another absolute URI as well. - @since 3.17.0""" + @since 3.17.0 + """ baseUri: Union["WorkspaceFolder", "URI"] """ A workspace folder or a base URI to which this pattern will be matched @@ -4733,7 +4807,7 @@ class RelativePattern(TypedDict): class WorkspaceEditClientCapabilities(TypedDict): documentChanges: NotRequired[bool] """ The client supports versioned document changes in `WorkspaceEdit`s """ - resourceOperations: NotRequired[List["ResourceOperationKind"]] + resourceOperations: NotRequired[list["ResourceOperationKind"]] """ The resource operations the client supports. Clients should at least support 'create', 'rename' and 'delete' files and folders. @@ -4751,9 +4825,7 @@ class WorkspaceEditClientCapabilities(TypedDict): character. @since 3.16.0 """ - changeAnnotationSupport: NotRequired[ - "__WorkspaceEditClientCapabilities_changeAnnotationSupport_Type_1" - ] + changeAnnotationSupport: NotRequired["__WorkspaceEditClientCapabilities_changeAnnotationSupport_Type_1"] """ Whether the client in general supports change annotations on text edits, create file, rename file and delete file changes. @@ -4789,9 +4861,7 @@ class WorkspaceSymbolClientCapabilities(TypedDict): Clients supporting tags have to handle unknown tags gracefully. @since 3.16.0 """ - resolveSupport: NotRequired[ - "__WorkspaceSymbolClientCapabilities_resolveSupport_Type_1" - ] + resolveSupport: NotRequired["__WorkspaceSymbolClientCapabilities_resolveSupport_Type_1"] """ The client support partial workspace symbols. The client will send the request `workspaceSymbol/resolve` to the server to resolve additional properties. @@ -4838,7 +4908,8 @@ class FileOperationClientCapabilities(TypedDict): These events do not come from the file system, they come from user operations like renaming a file in the UI. - @since 3.16.0""" + @since 3.16.0 + """ dynamicRegistration: NotRequired[bool] """ Whether the client supports dynamic registration for file requests/notifications. """ @@ -4859,7 +4930,8 @@ class FileOperationClientCapabilities(TypedDict): class InlineValueWorkspaceClientCapabilities(TypedDict): """Client workspace capabilities specific to inline values. - @since 3.17.0""" + @since 3.17.0 + """ refreshSupport: NotRequired[bool] """ Whether the client implementation supports a refresh request sent from the @@ -4874,7 +4946,8 @@ class InlineValueWorkspaceClientCapabilities(TypedDict): class InlayHintWorkspaceClientCapabilities(TypedDict): """Client workspace capabilities specific to inlay hints. - @since 3.17.0""" + @since 3.17.0 + """ refreshSupport: NotRequired[bool] """ Whether the client implementation supports a refresh request sent from @@ -4889,7 +4962,8 @@ class InlayHintWorkspaceClientCapabilities(TypedDict): class DiagnosticWorkspaceClientCapabilities(TypedDict): """Workspace client capabilities specific to diagnostic pull requests. - @since 3.17.0""" + @since 3.17.0 + """ refreshSupport: NotRequired[bool] """ Whether the client implementation supports a refresh request sent from @@ -4922,9 +4996,7 @@ class CompletionClientCapabilities(TypedDict): completionItem: NotRequired["__CompletionClientCapabilities_completionItem_Type_1"] """ The client supports the following `CompletionItem` specific capabilities. """ - completionItemKind: NotRequired[ - "__CompletionClientCapabilities_completionItemKind_Type_1" - ] + completionItemKind: NotRequired["__CompletionClientCapabilities_completionItemKind_Type_1"] insertTextMode: NotRequired["InsertTextMode"] """ Defines how the client handles whitespace and indentation when accepting a completion item that uses multi line @@ -4944,7 +5016,7 @@ class CompletionClientCapabilities(TypedDict): class HoverClientCapabilities(TypedDict): dynamicRegistration: NotRequired[bool] """ Whether hover supports dynamic registration. """ - contentFormat: NotRequired[List["MarkupKind"]] + contentFormat: NotRequired[list["MarkupKind"]] """ Client supports the following content formats for the content property. The order describes the preferred format of the client. """ @@ -4954,9 +5026,7 @@ class SignatureHelpClientCapabilities(TypedDict): dynamicRegistration: NotRequired[bool] """ Whether signature help supports dynamic registration. """ - signatureInformation: NotRequired[ - "__SignatureHelpClientCapabilities_signatureInformation_Type_1" - ] + signatureInformation: NotRequired["__SignatureHelpClientCapabilities_signatureInformation_Type_1"] """ The client supports the following `SignatureInformation` specific properties. """ contextSupport: NotRequired[bool] @@ -5058,9 +5128,7 @@ class CodeActionClientCapabilities(TypedDict): dynamicRegistration: NotRequired[bool] """ Whether code action supports dynamic registration. """ - codeActionLiteralSupport: NotRequired[ - "__CodeActionClientCapabilities_codeActionLiteralSupport_Type_1" - ] + codeActionLiteralSupport: NotRequired["__CodeActionClientCapabilities_codeActionLiteralSupport_Type_1"] """ The client support code action literals of type `CodeAction` as a valid response of the `textDocument/codeAction` request. If the property is not set the request can only return `Command` literals. @@ -5180,9 +5248,7 @@ class FoldingRangeClientCapabilities(TypedDict): """ If set, the client signals that it only supports folding complete lines. If set, client will ignore specified `startCharacter` and `endCharacter` properties in a FoldingRange. """ - foldingRangeKind: NotRequired[ - "__FoldingRangeClientCapabilities_foldingRangeKind_Type_1" - ] + foldingRangeKind: NotRequired["__FoldingRangeClientCapabilities_foldingRangeKind_Type_1"] """ Specific options for the folding range kind. @since 3.17.0 """ @@ -5251,11 +5317,11 @@ class SemanticTokensClientCapabilities(TypedDict): `request.range` are both set to true but the server only provides a range provider the client might not render a minimap correctly or might even decide to not show any semantic tokens at all. """ - tokenTypes: List[str] + tokenTypes: list[str] """ The token types that the client supports. """ - tokenModifiers: List[str] + tokenModifiers: list[str] """ The token modifiers that the client supports. """ - formats: List["TokenFormat"] + formats: list["TokenFormat"] """ The token formats the clients supports. """ overlappingTokenSupport: NotRequired[bool] """ Whether the client supports tokens that can overlap each other. """ @@ -5284,7 +5350,8 @@ class SemanticTokensClientCapabilities(TypedDict): class LinkedEditingRangeClientCapabilities(TypedDict): """Client capabilities for the linked editing range request. - @since 3.16.0""" + @since 3.16.0 + """ dynamicRegistration: NotRequired[bool] """ Whether implementation supports dynamic registration. If this is set to `true` @@ -5295,7 +5362,8 @@ class LinkedEditingRangeClientCapabilities(TypedDict): class MonikerClientCapabilities(TypedDict): """Client capabilities specific to the moniker request. - @since 3.16.0""" + @since 3.16.0 + """ dynamicRegistration: NotRequired[bool] """ Whether moniker supports dynamic registration. If this is set to `true` @@ -5315,7 +5383,8 @@ class TypeHierarchyClientCapabilities(TypedDict): class InlineValueClientCapabilities(TypedDict): """Client capabilities specific to inline values. - @since 3.17.0""" + @since 3.17.0 + """ dynamicRegistration: NotRequired[bool] """ Whether implementation supports dynamic registration for inline value providers. """ @@ -5324,7 +5393,8 @@ class InlineValueClientCapabilities(TypedDict): class InlayHintClientCapabilities(TypedDict): """Inlay hint client capabilities. - @since 3.17.0""" + @since 3.17.0 + """ dynamicRegistration: NotRequired[bool] """ Whether inlay hints support dynamic registration. """ @@ -5336,7 +5406,8 @@ class InlayHintClientCapabilities(TypedDict): class DiagnosticClientCapabilities(TypedDict): """Client capabilities specific to diagnostic pull requests. - @since 3.17.0""" + @since 3.17.0 + """ dynamicRegistration: NotRequired[bool] """ Whether implementation supports dynamic registration. If this is set to `true` @@ -5349,7 +5420,8 @@ class DiagnosticClientCapabilities(TypedDict): class NotebookDocumentSyncClientCapabilities(TypedDict): """Notebook specific client capabilities. - @since 3.17.0""" + @since 3.17.0 + """ dynamicRegistration: NotRequired[bool] """ Whether implementation supports dynamic registration. If this is @@ -5363,16 +5435,15 @@ class NotebookDocumentSyncClientCapabilities(TypedDict): class ShowMessageRequestClientCapabilities(TypedDict): """Show message request client capabilities""" - messageActionItem: NotRequired[ - "__ShowMessageRequestClientCapabilities_messageActionItem_Type_1" - ] + messageActionItem: NotRequired["__ShowMessageRequestClientCapabilities_messageActionItem_Type_1"] """ Capabilities specific to the `MessageActionItem` type. """ class ShowDocumentClientCapabilities(TypedDict): """Client capabilities for the showDocument request. - @since 3.16.0""" + @since 3.16.0 + """ support: bool """ The client has support for the showDocument @@ -5382,7 +5453,8 @@ class ShowDocumentClientCapabilities(TypedDict): class RegularExpressionsClientCapabilities(TypedDict): """Client capabilities specific to regular expressions. - @since 3.16.0""" + @since 3.16.0 + """ engine: str """ The engine's name. """ @@ -5393,13 +5465,14 @@ class RegularExpressionsClientCapabilities(TypedDict): class MarkdownClientCapabilities(TypedDict): """Client capabilities specific to the used markdown parser. - @since 3.16.0""" + @since 3.16.0 + """ parser: str """ The name of the parser. """ version: NotRequired[str] """ The version of the parser. """ - allowedTags: NotRequired[List[str]] + allowedTags: NotRequired[list[str]] """ A list of HTML tags that the client allows / supports in Markdown. @@ -5412,10 +5485,8 @@ class __CodeActionClientCapabilities_codeActionLiteralSupport_Type_1(TypedDict): set. """ -class __CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind_Type_1( - TypedDict -): - valueSet: List["CodeActionKind"] +class __CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind_Type_1(TypedDict): + valueSet: list["CodeActionKind"] """ The code action kind values the client supports. When this property exists the client also guarantees that it will handle values outside its set gracefully and falls back @@ -5423,7 +5494,7 @@ class __CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind_Typ class __CodeActionClientCapabilities_resolveSupport_Type_1(TypedDict): - properties: List[str] + properties: list[str] """ The properties that a client can resolve lazily. """ @@ -5435,7 +5506,7 @@ class __CodeAction_disabled_Type_1(TypedDict): class __CompletionClientCapabilities_completionItemKind_Type_1(TypedDict): - valueSet: NotRequired[List["CompletionItemKind"]] + valueSet: NotRequired[list["CompletionItemKind"]] """ The completion item kind values the client supports. When this property exists the client also guarantees that it will handle values outside its set gracefully and falls back @@ -5456,16 +5527,14 @@ class __CompletionClientCapabilities_completionItem_Type_1(TypedDict): that is typing in one will update others too. """ commitCharactersSupport: NotRequired[bool] """ Client supports commit characters on a completion item. """ - documentationFormat: NotRequired[List["MarkupKind"]] + documentationFormat: NotRequired[list["MarkupKind"]] """ Client supports the following content formats for the documentation property. The order describes the preferred format of the client. """ deprecatedSupport: NotRequired[bool] """ Client supports the deprecated property on a completion item. """ preselectSupport: NotRequired[bool] """ Client supports the preselect property on a completion item. """ - tagSupport: NotRequired[ - "__CompletionClientCapabilities_completionItem_tagSupport_Type_1" - ] + tagSupport: NotRequired["__CompletionClientCapabilities_completionItem_tagSupport_Type_1"] """ Client supports the tag property on a completion item. Clients supporting tags have to handle unknown tags gracefully. Clients especially need to preserve unknown tags when sending a completion item back to the server in @@ -5477,17 +5546,13 @@ class __CompletionClientCapabilities_completionItem_Type_1(TypedDict): completion item is inserted in the text or should replace text. @since 3.16.0 """ - resolveSupport: NotRequired[ - "__CompletionClientCapabilities_completionItem_resolveSupport_Type_1" - ] + resolveSupport: NotRequired["__CompletionClientCapabilities_completionItem_resolveSupport_Type_1"] """ Indicates which properties a client can resolve lazily on a completion item. Before version 3.16.0 only the predefined properties `documentation` and `details` could be resolved lazily. @since 3.16.0 """ - insertTextModeSupport: NotRequired[ - "__CompletionClientCapabilities_completionItem_insertTextModeSupport_Type_1" - ] + insertTextModeSupport: NotRequired["__CompletionClientCapabilities_completionItem_insertTextModeSupport_Type_1"] """ The client supports the `insertTextMode` property on a completion item to override the whitespace handling mode as defined by the client (see `insertTextMode`). @@ -5500,24 +5565,22 @@ class __CompletionClientCapabilities_completionItem_Type_1(TypedDict): @since 3.17.0 """ -class __CompletionClientCapabilities_completionItem_insertTextModeSupport_Type_1( - TypedDict -): - valueSet: List["InsertTextMode"] +class __CompletionClientCapabilities_completionItem_insertTextModeSupport_Type_1(TypedDict): + valueSet: list["InsertTextMode"] class __CompletionClientCapabilities_completionItem_resolveSupport_Type_1(TypedDict): - properties: List[str] + properties: list[str] """ The properties that a client can resolve lazily. """ class __CompletionClientCapabilities_completionItem_tagSupport_Type_1(TypedDict): - valueSet: List["CompletionItemTag"] + valueSet: list["CompletionItemTag"] """ The tags supported by the client. """ class __CompletionClientCapabilities_completionList_Type_1(TypedDict): - itemDefaults: NotRequired[List[str]] + itemDefaults: NotRequired[list[str]] """ The client supports the following itemDefaults on a completion list. @@ -5529,13 +5592,11 @@ class __CompletionClientCapabilities_completionList_Type_1(TypedDict): class __CompletionList_itemDefaults_Type_1(TypedDict): - commitCharacters: NotRequired[List[str]] + commitCharacters: NotRequired[list[str]] """ A default commit character set. @since 3.17.0 """ - editRange: NotRequired[ - Union["Range", "__CompletionList_itemDefaults_editRange_Type_1"] - ] + editRange: NotRequired[Union["Range", "__CompletionList_itemDefaults_editRange_Type_1"]] """ A default edit range. @since 3.17.0 """ @@ -5577,7 +5638,7 @@ class __CompletionOptions_completionItem_Type_2(TypedDict): class __DocumentSymbolClientCapabilities_symbolKind_Type_1(TypedDict): - valueSet: NotRequired[List["SymbolKind"]] + valueSet: NotRequired[list["SymbolKind"]] """ The symbol kind values the client supports. When this property exists the client also guarantees that it will handle values outside its set gracefully and falls back @@ -5589,12 +5650,12 @@ class __DocumentSymbolClientCapabilities_symbolKind_Type_1(TypedDict): class __DocumentSymbolClientCapabilities_tagSupport_Type_1(TypedDict): - valueSet: List["SymbolTag"] + valueSet: list["SymbolTag"] """ The tags supported by the client. """ class __FoldingRangeClientCapabilities_foldingRangeKind_Type_1(TypedDict): - valueSet: NotRequired[List["FoldingRangeKind"]] + valueSet: NotRequired[list["FoldingRangeKind"]] """ The folding range kind values the client supports. When this property exists the client also guarantees that it will handle values outside its set gracefully and falls back @@ -5612,7 +5673,7 @@ class __FoldingRangeClientCapabilities_foldingRange_Type_1(TypedDict): class __GeneralClientCapabilities_staleRequestSupport_Type_1(TypedDict): cancel: bool """ The client will actively cancel the request. """ - retryOnContentModified: List[str] + retryOnContentModified: list[str] """ The list of requests for which the client will retry the request if it receives a response with error code `ContentModified` """ @@ -5626,7 +5687,7 @@ class __InitializeResult_serverInfo_Type_1(TypedDict): class __InlayHintClientCapabilities_resolveSupport_Type_1(TypedDict): - properties: List[str] + properties: list[str] """ The properties that a client can resolve lazily. """ @@ -5639,27 +5700,25 @@ class __NotebookDocumentChangeEvent_cells_Type_1(TypedDict): structure: NotRequired["__NotebookDocumentChangeEvent_cells_structure_Type_1"] """ Changes to the cell structure to add or remove cells. """ - data: NotRequired[List["NotebookCell"]] + data: NotRequired[list["NotebookCell"]] """ Changes to notebook cells properties like its kind, execution summary or metadata. """ - textContent: NotRequired[ - List["__NotebookDocumentChangeEvent_cells_textContent_Type_1"] - ] + textContent: NotRequired[list["__NotebookDocumentChangeEvent_cells_textContent_Type_1"]] """ Changes to the text content of notebook cells. """ class __NotebookDocumentChangeEvent_cells_structure_Type_1(TypedDict): array: "NotebookCellArrayChange" """ The change to the cell array. """ - didOpen: NotRequired[List["TextDocumentItem"]] + didOpen: NotRequired[list["TextDocumentItem"]] """ Additional opened cell text documents. """ - didClose: NotRequired[List["TextDocumentIdentifier"]] + didClose: NotRequired[list["TextDocumentIdentifier"]] """ Additional closed cell text documents. """ class __NotebookDocumentChangeEvent_cells_textContent_Type_1(TypedDict): document: "VersionedTextDocumentIdentifier" - changes: List["TextDocumentContentChangeEvent"] + changes: list["TextDocumentContentChangeEvent"] class __NotebookDocumentFilter_Type_1(TypedDict): @@ -5694,9 +5753,7 @@ class __NotebookDocumentSyncOptions_notebookSelector_Type_1(TypedDict): """ The notebook to be synced If a string value is provided it matches against the notebook type. '*' matches every notebook. """ - cells: NotRequired[ - List["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_1"] - ] + cells: NotRequired[list["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_1"]] """ The cells of the matching notebook to be synced. """ @@ -5705,7 +5762,7 @@ class __NotebookDocumentSyncOptions_notebookSelector_Type_2(TypedDict): """ The notebook to be synced If a string value is provided it matches against the notebook type. '*' matches every notebook. """ - cells: List["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_2"] + cells: list["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_2"] """ The cells of the matching notebook to be synced. """ @@ -5714,9 +5771,7 @@ class __NotebookDocumentSyncOptions_notebookSelector_Type_3(TypedDict): """ The notebook to be synced If a string value is provided it matches against the notebook type. '*' matches every notebook. """ - cells: NotRequired[ - List["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_3"] - ] + cells: NotRequired[list["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_3"]] """ The cells of the matching notebook to be synced. """ @@ -5725,7 +5780,7 @@ class __NotebookDocumentSyncOptions_notebookSelector_Type_4(TypedDict): """ The notebook to be synced If a string value is provided it matches against the notebook type. '*' matches every notebook. """ - cells: List["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_4"] + cells: list["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_4"] """ The cells of the matching notebook to be synced. """ @@ -5755,17 +5810,15 @@ class __PrepareRenameResult_Type_2(TypedDict): class __PublishDiagnosticsClientCapabilities_tagSupport_Type_1(TypedDict): - valueSet: List["DiagnosticTag"] + valueSet: list["DiagnosticTag"] """ The tags supported by the client. """ class __SemanticTokensClientCapabilities_requests_Type_1(TypedDict): - range: NotRequired[Union[bool, dict]] + range: NotRequired[bool | dict] """ The client will send the `textDocument/semanticTokens/range` request if the server provides a corresponding handler. """ - full: NotRequired[ - Union[bool, "__SemanticTokensClientCapabilities_requests_full_Type_1"] - ] + full: NotRequired[Union[bool, "__SemanticTokensClientCapabilities_requests_full_Type_1"]] """ The client will send the `textDocument/semanticTokens/full` request if the server provides a corresponding handler. """ @@ -5805,12 +5858,10 @@ class __ShowMessageRequestClientCapabilities_messageActionItem_Type_1(TypedDict) class __SignatureHelpClientCapabilities_signatureInformation_Type_1(TypedDict): - documentationFormat: NotRequired[List["MarkupKind"]] + documentationFormat: NotRequired[list["MarkupKind"]] """ Client supports the following content formats for the documentation property. The order describes the preferred format of the client. """ - parameterInformation: NotRequired[ - "__SignatureHelpClientCapabilities_signatureInformation_parameterInformation_Type_1" - ] + parameterInformation: NotRequired["__SignatureHelpClientCapabilities_signatureInformation_parameterInformation_Type_1"] """ Client capabilities specific to parameter information. """ activeParameterSupport: NotRequired[bool] """ The client supports the `activeParameter` property on `SignatureInformation` @@ -5819,9 +5870,7 @@ class __SignatureHelpClientCapabilities_signatureInformation_Type_1(TypedDict): @since 3.16.0 """ -class __SignatureHelpClientCapabilities_signatureInformation_parameterInformation_Type_1( - TypedDict -): +class __SignatureHelpClientCapabilities_signatureInformation_parameterInformation_Type_1(TypedDict): labelOffsetSupport: NotRequired[bool] """ The client supports processing label offsets instead of a simple label string. @@ -5880,13 +5929,13 @@ class __WorkspaceEditClientCapabilities_changeAnnotationSupport_Type_1(TypedDict class __WorkspaceSymbolClientCapabilities_resolveSupport_Type_1(TypedDict): - properties: List[str] + properties: list[str] """ The properties that a client can resolve lazily. Usually `location.range` """ class __WorkspaceSymbolClientCapabilities_symbolKind_Type_1(TypedDict): - valueSet: NotRequired[List["SymbolKind"]] + valueSet: NotRequired[list["SymbolKind"]] """ The symbol kind values the client supports. When this property exists the client also guarantees that it will handle values outside its set gracefully and falls back @@ -5898,7 +5947,7 @@ class __WorkspaceSymbolClientCapabilities_symbolKind_Type_1(TypedDict): class __WorkspaceSymbolClientCapabilities_tagSupport_Type_1(TypedDict): - valueSet: List["SymbolTag"] + valueSet: list["SymbolTag"] """ The tags supported by the client. """ diff --git a/src/solidlsp/lsp_protocol_handler/server.py b/src/solidlsp/lsp_protocol_handler/server.py new file mode 100644 index 0000000..5045312 --- /dev/null +++ b/src/solidlsp/lsp_protocol_handler/server.py @@ -0,0 +1,122 @@ +""" +This file provides the implementation of the JSON-RPC client, that launches and +communicates with the language server. + +The initial implementation of this file was obtained from +https://github.com/predragnikolic/OLSP under the MIT License with the following terms: + +MIT License + +Copyright (c) 2023 Предраг Николић + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import dataclasses +import json +import logging +import os +from typing import Any, Union + +from .lsp_types import ErrorCodes + +StringDict = dict[str, Any] +PayloadLike = Union[list[StringDict], StringDict, None] +CONTENT_LENGTH = "Content-Length: " +ENCODING = "utf-8" +log = logging.getLogger(__name__) + + +@dataclasses.dataclass +class ProcessLaunchInfo: + """ + This class is used to store the information required to launch a process. + """ + + # The command to launch the process + cmd: str + + # The environment variables to set for the process + env: dict[str, str] = dataclasses.field(default_factory=dict) + + # The working directory for the process + cwd: str = os.getcwd() + + +class Error(Exception): + def __init__(self, code: ErrorCodes, message: str) -> None: + super().__init__(message) + self.code = code + + def to_lsp(self) -> StringDict: + return {"code": self.code, "message": super().__str__()} + + @classmethod + def from_lsp(cls, d: StringDict) -> "Error": + return Error(d["code"], d["message"]) + + def __str__(self) -> str: + return f"{super().__str__()} ({self.code})" + + +def make_response(request_id: Any, params: PayloadLike) -> StringDict: + return {"jsonrpc": "2.0", "id": request_id, "result": params} + + +def make_error_response(request_id: Any, err: Error) -> StringDict: + return {"jsonrpc": "2.0", "id": request_id, "error": err.to_lsp()} + + +def make_notification(method: str, params: PayloadLike) -> StringDict: + return {"jsonrpc": "2.0", "method": method, "params": params} + + +def make_request(method: str, request_id: Any, params: PayloadLike) -> StringDict: + return {"jsonrpc": "2.0", "method": method, "id": request_id, "params": params} + + +class StopLoopException(Exception): + pass + + +def create_message(payload: PayloadLike): + body = json.dumps(payload, check_circular=False, ensure_ascii=False, separators=(",", ":")).encode(ENCODING) + return ( + f"Content-Length: {len(body)}\r\n".encode(ENCODING), + "Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n".encode(ENCODING), + body, + ) + + +class MessageType: + error = 1 + warning = 2 + info = 3 + log = 4 + + +def content_length(line: bytes) -> int | None: + if line.startswith(b"Content-Length: "): + _, value = line.split(b"Content-Length: ") + value = value.strip() + try: + return int(value) + except ValueError: + raise ValueError(f"Invalid Content-Length header: {value}") + return None diff --git a/src/multilspy/multilspy_settings.py b/src/solidlsp/settings.py similarity index 84% rename from src/multilspy/multilspy_settings.py rename to src/solidlsp/settings.py index 5d7ebfa..38b3010 100644 --- a/src/multilspy/multilspy_settings.py +++ b/src/solidlsp/settings.py @@ -1,14 +1,12 @@ """ -Defines the settings for multilspy. +Defines settings for Solid-LSP """ import os import pathlib -class MultilspySettings: - """ - Provides the various settings for multilspy. - """ + +class SolidLSPSettings: @staticmethod def get_language_server_directory() -> str: """Returns the directory for language servers""" diff --git a/test/conftest.py b/test/conftest.py index a5ca841..6632f3a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,10 +1,15 @@ +import logging from pathlib import Path import pytest +from sensai.util.logging import configure -from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language, MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger +from serena.util.file_system import GitignoreParser +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger + +configure(level=logging.DEBUG) @pytest.fixture(scope="session") @@ -22,13 +27,25 @@ def get_repo_path(language: Language) -> Path: return Path(__file__).parent / "resources" / "repos" / language / "test_repo" -def create_ls(language: Language, repo_path: str): - config = MultilspyConfig(code_language=language) - logger = MultilspyLogger() - return SyncLanguageServer.create(config, logger, repo_path) +def create_ls( + language: Language, + repo_path: str | None = None, + ignored_paths: list[str] | None = None, + trace_lsp_communication: bool = False, + log_level: int = logging.INFO, +) -> SolidLanguageServer: + ignored_paths = ignored_paths or [] + if repo_path is None: + repo_path = str(get_repo_path(language)) + gitignore_parser = GitignoreParser(str(repo_path)) + for spec in gitignore_parser.get_ignore_specs(): + ignored_paths.extend(spec.patterns) + config = LanguageServerConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication) + logger = LanguageServerLogger(log_level=log_level) + return SolidLanguageServer.create(config, logger, repo_path) -def create_default_ls(language: Language) -> SyncLanguageServer: +def create_default_ls(language: Language) -> SolidLanguageServer: repo_path = str(get_repo_path(language)) return create_ls(language, repo_path) diff --git a/test/resources/repos/java/test_repo/pom.xml b/test/resources/repos/java/test_repo/pom.xml index 7df64e1..672f990 100644 --- a/test/resources/repos/java/test_repo/pom.xml +++ b/test/resources/repos/java/test_repo/pom.xml @@ -8,7 +8,22 @@ jar Java Test Repo - 17 - 17 + 21 + 21 + 3.13.0 + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.plugin.version} + + 21 + 21 + + + + diff --git a/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py b/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py new file mode 100644 index 0000000..1e6da93 --- /dev/null +++ b/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py @@ -0,0 +1,141 @@ +""" +Example demonstrating user management with the test_repo module. + +This example showcases: +- Creating and managing users +- Using various object types and relationships +- Type annotations and complex Python patterns +""" + +import logging +from dataclasses import dataclass +from typing import Any + +from test_repo.models import User, create_user_object +from test_repo.services import UserService + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@dataclass +class UserStats: + """Statistics about user activity.""" + + user_id: str + login_count: int = 0 + last_active_days: int = 0 + engagement_score: float = 0.0 + + def is_active(self) -> bool: + """Check if the user is considered active.""" + return self.last_active_days < 30 + + +class UserManager: + """Example class demonstrating complex user management.""" + + def __init__(self, service: UserService): + self.service = service + self.active_users: dict[str, User] = {} + self.user_stats: dict[str, UserStats] = {} + + def register_user(self, name: str, email: str, roles: list[str] | None = None) -> User: + """Register a new user.""" + logger.info(f"Registering new user: {name} ({email})") + user = self.service.create_user(name=name, email=email, roles=roles) + self.active_users[user.id] = user + self.user_stats[user.id] = UserStats(user_id=user.id) + return user + + def get_user(self, user_id: str) -> User | None: + """Get a user by ID.""" + if user_id in self.active_users: + return self.active_users[user_id] + + # Try to fetch from service + user = self.service.get_user(user_id) + if user: + self.active_users[user.id] = user + return user + + def update_user_stats(self, user_id: str, login_count: int, days_since_active: int) -> None: + """Update statistics for a user.""" + if user_id not in self.user_stats: + self.user_stats[user_id] = UserStats(user_id=user_id) + + stats = self.user_stats[user_id] + stats.login_count = login_count + stats.last_active_days = days_since_active + + # Calculate engagement score based on activity + engagement = (100 - min(days_since_active, 100)) * 0.8 + engagement += min(login_count, 20) * 0.2 + stats.engagement_score = engagement + + def get_active_users(self) -> list[User]: + """Get all active users.""" + active_user_ids = [user_id for user_id, stats in self.user_stats.items() if stats.is_active()] + return [self.active_users[user_id] for user_id in active_user_ids if user_id in self.active_users] + + def get_user_by_email(self, email: str) -> User | None: + """Find a user by their email address.""" + for user in self.active_users.values(): + if user.email == email: + return user + return None + + +# Example function demonstrating type annotations +def process_user_data(users: list[User], include_inactive: bool = False, transform_func: callable | None = None) -> dict[str, Any]: + """Process user data with optional transformations.""" + result: dict[str, Any] = {"users": [], "total": 0, "admin_count": 0} + + for user in users: + if transform_func: + user_data = transform_func(user.to_dict()) + else: + user_data = user.to_dict() + + result["users"].append(user_data) + result["total"] += 1 + + if "admin" in user.roles: + result["admin_count"] += 1 + + return result + + +def main(): + """Main function demonstrating the usage of UserManager.""" + # Initialize service and manager + service = UserService() + manager = UserManager(service) + + # Register some users + admin = manager.register_user("Admin User", "admin@example.com", ["admin"]) + user1 = manager.register_user("Regular User", "user@example.com", ["user"]) + user2 = manager.register_user("Another User", "another@example.com", ["user"]) + + # Update some stats + manager.update_user_stats(admin.id, 100, 5) + manager.update_user_stats(user1.id, 50, 10) + manager.update_user_stats(user2.id, 10, 45) # Inactive user + + # Get active users + active_users = manager.get_active_users() + logger.info(f"Active users: {len(active_users)}") + + # Process user data + user_data = process_user_data(active_users, transform_func=lambda u: {**u, "full_name": u.get("name", "")}) + + logger.info(f"Processed {user_data['total']} users, {user_data['admin_count']} admins") + + # Example of calling create_user directly + external_user = create_user_object(id="ext123", name="External User", email="external@example.org", roles=["external"]) + logger.info(f"Created external user: {external_user.name}") + + +if __name__ == "__main__": + main() diff --git a/test/serena/__snapshots__/test_symbol_editing.ambr b/test/serena/__snapshots__/test_symbol_editing.ambr index 2b698a2..327a5ef 100644 --- a/test/serena/__snapshots__/test_symbol_editing.ambr +++ b/test/serena/__snapshots__/test_symbol_editing.ambr @@ -1,713 +1,925 @@ -# serializer version: 1 -# name: test_delete_symbol[test_case0] - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_delete_symbol[test_case1] - ''' - - - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - helperFunction(); - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case0-after] - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - new_module_var = "Inserted after typed_module_var" - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - def modify_instance_var(self): - # Reassign instance variable - self.instance_var = "Modified instance value" - self.reassignable_instance_var = 200 # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case0-before] - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - new_module_var = "Inserted after typed_module_var" - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - def modify_instance_var(self): - # Reassign instance variable - self.instance_var = "Modified instance value" - self.reassignable_instance_var = 200 # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case1-after] - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - def modify_instance_var(self): - # Reassign instance variable - self.instance_var = "Modified instance value" - self.reassignable_instance_var = 200 # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - def new_inserted_function(): - print("This is a new function inserted before another.") - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case1-before] - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - def modify_instance_var(self): - # Reassign instance variable - self.instance_var = "Modified instance value" - self.reassignable_instance_var = 200 # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - def new_inserted_function(): - print("This is a new function inserted before another.") - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case2-after] - ''' - export class DemoClass { - value: number; - constructor(value: number) { - this.value = value; - } - printValue() { - console.log(this.value); - } - } - - function newFunctionAfterClass(): void { - console.log("This function is after DemoClass."); - } - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - helperFunction(); - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case2-before] - ''' - function newFunctionAfterClass(): void { - console.log("This function is after DemoClass."); - } - export class DemoClass { - value: number; - constructor(value: number) { - this.value = value; - } - printValue() { - console.log(this.value); - } - } - - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - helperFunction(); - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case3-after] - ''' - export class DemoClass { - value: number; - constructor(value: number) { - this.value = value; - } - printValue() { - console.log(this.value); - } - } - - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - function newInsertedFunction(): void { - console.log("This is a new function inserted before another."); - } - helperFunction(); - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case3-before] - ''' - export class DemoClass { - value: number; - constructor(value: number) { - this.value = value; - } - printValue() { - console.log(this.value); - } - } - function newInsertedFunction(): void { - console.log("This is a new function inserted before another."); - } - - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - helperFunction(); - - ''' -# --- -# name: test_replace_body[test_case0] - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - - def modify_instance_var(self): - # This body has been replaced - self.instance_var = "Replaced!" - self.reassignable_instance_var = 999 - # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_replace_body[test_case1] - ''' - export class DemoClass { - value: number; - constructor(value: number) { - this.value = value; - } - - function printValue() { - // This body has been replaced - console.warn("New value: " + this.value); - } - - } - - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - helperFunction(); - - ''' -# --- +# serializer version: 1 +# name: test_delete_symbol[test_case0] + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_delete_symbol[test_case1] + ''' + + + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } + + helperFunction(); + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case0-after] + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + new_module_var = "Inserted after typed_module_var" + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case0-before] + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + new_module_var = "Inserted after typed_module_var" + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case1-after] + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + def new_inserted_function(): + print("This is a new function inserted before another.") + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case1-before] + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def new_inserted_function(): + print("This is a new function inserted before another.") + + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case2-after] + ''' + export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + printValue() { + console.log(this.value); + } + } + + function newFunctionAfterClass(): void { + console.log("This function is after DemoClass."); + } + + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } + + helperFunction(); + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case2-before] + ''' + function newFunctionAfterClass(): void { + console.log("This function is after DemoClass."); + } + + export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + printValue() { + console.log(this.value); + } + } + + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } + + helperFunction(); + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case3-after] + ''' + export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + printValue() { + console.log(this.value); + } + } + + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } + + function newInsertedFunction(): void { + console.log("This is a new function inserted before another."); + } + + helperFunction(); + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case3-before] + ''' + export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + printValue() { + console.log(this.value); + } + } + + function newInsertedFunction(): void { + console.log("This is a new function inserted before another."); + } + + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } + + helperFunction(); + + ''' +# --- +# name: test_insert_python_class_after + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + class NewInsertedClass: + pass + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_insert_python_class_before + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + class NewInsertedClass: + pass + + + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_replace_body[test_case0] + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # This body has been replaced + self.instance_var = "Replaced!" + self.reassignable_instance_var = 999 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_replace_body[test_case1] + ''' + export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + function printValue() { + // This body has been replaced + console.warn("New value: " + this.value); + } + } + + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } + + helperFunction(); + + ''' +# --- diff --git a/test/serena/test_make_tool_process_isolation.py b/test/serena/test_make_tool_process_isolation.py new file mode 100644 index 0000000..eb9b3f0 --- /dev/null +++ b/test/serena/test_make_tool_process_isolation.py @@ -0,0 +1,162 @@ +"""Tests for make_tool consistency between regular tools and ProcessIsolatedTool.""" + +import pytest +from mcp.server.fastmcp.tools.base import Tool as MCPTool + +from serena.agent import SerenaAgent, ToolRegistry +from serena.mcp import SerenaMCPFactory +from serena.process_isolated_agent import ProcessIsolatedSerenaAgent, ProcessIsolatedTool +from test.serena.test_serena_agent import SerenaConfigForTests + +make_tool = SerenaMCPFactory.make_mcp_tool + + +@pytest.fixture +def in_memory_config(): + """Create an in-memory configuration for tests.""" + return SerenaConfigForTests() + + +@pytest.fixture +def regular_agent(in_memory_config): + """Create a regular SerenaAgent for comparison.""" + return SerenaAgent(serena_config=in_memory_config) + + +@pytest.fixture +def process_isolated_agent(in_memory_config): + """Create a ProcessIsolatedSerenaAgent for comparison.""" + agent = ProcessIsolatedSerenaAgent(serena_config=in_memory_config) + agent.start() + yield agent + agent.stop() + + +class TestMakeToolProcessIsolation: + """Test that make_tool produces identical metadata for regular and process-isolated tools.""" + + @pytest.mark.parametrize("tool_name", ToolRegistry.get_tool_names()) + def test_make_tool_metadata_consistency( + self, tool_name: str, regular_agent: SerenaAgent, process_isolated_agent: ProcessIsolatedSerenaAgent + ): + """Test that make_tool produces identical metadata for regular and process-isolated tools.""" + # Get regular tool instance + tool_class = ToolRegistry.get_tool_class_by_name(tool_name) + regular_tool = regular_agent.get_tool(tool_class) + + # Get ProcessIsolatedTool instance + isolated_tool = ProcessIsolatedTool(process_isolated_agent, tool_name) + + # Create MCP tools from both + regular_mcp_tool = make_tool(regular_tool) + isolated_mcp_tool = make_tool(isolated_tool) + + # Verify both are MCPTool instances + assert isinstance(regular_mcp_tool, MCPTool) + assert isinstance(isolated_mcp_tool, MCPTool) + + # Test name consistency + assert regular_mcp_tool.name == isolated_mcp_tool.name + assert regular_mcp_tool.name == tool_name + + # Test description consistency + assert regular_mcp_tool.description == isolated_mcp_tool.description, ( + f"Tool {tool_name}: descriptions differ\n" + f"Regular: {regular_mcp_tool.description}\n" + f"Isolated: {isolated_mcp_tool.description}" + ) + + # Test parameters schema consistency + assert regular_mcp_tool.parameters == isolated_mcp_tool.parameters, ( + f"Tool {tool_name}: parameter schemas differ\n" + f"Regular: {regular_mcp_tool.parameters}\n" + f"Isolated: {isolated_mcp_tool.parameters}" + ) + + # Test function metadata consistency (compare schemas, not class objects) + regular_schema = regular_mcp_tool.fn_metadata.arg_model.model_json_schema() + isolated_schema = isolated_mcp_tool.fn_metadata.arg_model.model_json_schema() + assert ( + regular_schema == isolated_schema + ), f"Tool {tool_name}: function metadata schemas differ\nRegular: {regular_schema}\nIsolated: {isolated_schema}" + + # Test async flag consistency + assert regular_mcp_tool.is_async == isolated_mcp_tool.is_async + + # Test context kwarg consistency + assert regular_mcp_tool.context_kwarg == isolated_mcp_tool.context_kwarg + + @pytest.mark.parametrize("tool_name", ToolRegistry.get_tool_names()[:5]) # Test first 5 tools for faster execution + def test_tool_protocol_methods_consistency( + self, tool_name: str, regular_agent: SerenaAgent, process_isolated_agent: ProcessIsolatedSerenaAgent + ): + """Test that Tool methods return identical results for regular and process-isolated tools.""" + # Get regular tool instance + tool_class = ToolRegistry.get_tool_class_by_name(tool_name) + regular_tool = regular_agent.get_tool(tool_class) + + # Get ProcessIsolatedTool instance + isolated_tool = ProcessIsolatedTool(process_isolated_agent, tool_name) + + # Test get_name() + assert regular_tool.get_name_from_cls() == isolated_tool.get_name() + assert regular_tool.get_name_from_cls() == tool_name + + # Test get_apply_docstring() + regular_docstring = regular_tool.get_apply_docstring() + isolated_docstring = isolated_tool.get_apply_docstring() + assert ( + regular_docstring == isolated_docstring + ), f"Tool {tool_name}: docstrings differ\nRegular: {regular_docstring}\nIsolated: {isolated_docstring}" + + # Test get_apply_fn_metadata() + regular_metadata = regular_tool.get_apply_fn_metadata() + isolated_metadata = isolated_tool.get_apply_fn_metadata() + + # Compare metadata properties (compare schemas, not class objects) + regular_schema = regular_metadata.arg_model.model_json_schema() + isolated_schema = isolated_metadata.arg_model.model_json_schema() + assert ( + regular_schema == isolated_schema + ), f"Tool {tool_name}: metadata schemas differ\nRegular: {regular_schema}\nIsolated: {isolated_schema}" + + def test_process_isolated_tool_uses_tool_registry(self, process_isolated_agent: ProcessIsolatedSerenaAgent): + """Test that ProcessIsolatedTool correctly uses ToolRegistry for metadata.""" + tool_name = ToolRegistry.get_tool_names()[0] # Use first available tool + isolated_tool = ProcessIsolatedTool(process_isolated_agent, tool_name) + + # Verify that the tool uses ToolRegistry + assert isolated_tool._tool_class == ToolRegistry.get_tool_class_by_name(tool_name) + + # Verify that metadata comes from the tool class + expected_docstring = isolated_tool._tool_class.get_apply_docstring_from_cls() + expected_metadata = isolated_tool._tool_class.get_apply_fn_metadata_from_cls() + + assert isolated_tool.get_apply_docstring() == expected_docstring + # Compare schemas, not class objects + isolated_schema = isolated_tool.get_apply_fn_metadata().arg_model.model_json_schema() + expected_schema = expected_metadata.arg_model.model_json_schema() + assert isolated_schema == expected_schema + + def test_tool_registry_completeness(self, regular_agent: SerenaAgent, process_isolated_agent: ProcessIsolatedSerenaAgent): + """Test that all tools are available in both agents and the registry.""" + # Get tool names from both agents + regular_active_tools = set(regular_agent.get_active_tool_names()) + regular_all_tools = set(tool.get_name_from_cls() for tool in regular_agent.get_exposed_tool_instances()) + isolated_tool_names = set(process_isolated_agent.get_exposed_tool_names()) + + # The process isolated agent should have all tools (exposed, not just active) + assert regular_all_tools == isolated_tool_names, ( + f"Tool sets differ:\n" + f"Regular exposed only: {regular_all_tools - isolated_tool_names}\n" + f"Isolated only: {isolated_tool_names - regular_all_tools}" + ) + + # Active tools should be a subset of all tools + assert regular_active_tools.issubset( + regular_all_tools + ), f"Some active tools not in exposed tools: {regular_active_tools - regular_all_tools}" + + # All tools should be in the registry + registry_tool_names = set(ToolRegistry.get_tool_names()) + assert regular_all_tools.issubset(registry_tool_names), f"Some tools not in registry: {regular_all_tools - registry_tool_names}" diff --git a/test/serena/test_mcp.py b/test/serena/test_mcp.py index f179b85..8ffb2c2 100644 --- a/test/serena/test_mcp.py +++ b/test/serena/test_mcp.py @@ -4,7 +4,9 @@ import pytest from mcp.server.fastmcp.tools.base import Tool as MCPTool from serena.agent import Tool, ToolRegistry -from serena.mcp import make_tool +from serena.mcp import SerenaMCPFactory + +make_tool = SerenaMCPFactory.make_mcp_tool class BaseMockTool(Tool): @@ -224,7 +226,7 @@ def test_make_tool_missing_apply() -> None: """, "", ), - ("", ""), + ("Description without params.", "Description without params."), ], ) def test_make_tool_descriptions(docstring, expected_description) -> None: @@ -287,7 +289,7 @@ def test_make_tool_all_tools(tool_class) -> None: # Basic validation assert isinstance(mcp_tool, MCPTool) - assert mcp_tool.name == tool_class.get_name() + assert mcp_tool.name == tool_class.get_name_from_cls() # The description should be a string (either from docstring or default) assert isinstance(mcp_tool.description, str) diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index f1208dd..4e29785 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -5,9 +5,10 @@ from dataclasses import dataclass import pytest -from multilspy.multilspy_config import Language -from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, SerenaAgent, SerenaConfigBase -from test.conftest import LanguageParamRequest, get_repo_path +from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, Project, ProjectConfig, SerenaAgent, SerenaConfigBase +from serena.process_isolated_agent import ProcessIsolatedSerenaAgent +from solidlsp.ls_config import Language +from test.conftest import get_repo_path @dataclass @@ -19,20 +20,71 @@ class SerenaConfigForTests(SerenaConfigBase): gui_log_window_enabled: bool = False web_dashboard: bool = False + def __post_init__(self): + # Initialize with empty projects list if not already set + if not hasattr(self, "projects") or self.projects is None: + self.projects = [] + @pytest.fixture def serena_config(): - return SerenaConfigForTests() + """Create an in-memory configuration for tests with test repositories pre-registered.""" + # Create test projects for all supported languages + test_projects = [] + for language in [Language.PYTHON, Language.GO, Language.JAVA, Language.RUST, Language.TYPESCRIPT, Language.PHP]: + repo_path = get_repo_path(language) + if repo_path.exists(): + project_name = f"test_repo_{language}" + project = Project( + project_root=str(repo_path), + project_config=ProjectConfig( + project_name=project_name, + language=language, + ignored_paths=[], + excluded_tools=set(), + read_only=False, + ignore_all_files_in_gitignore=True, + initial_prompt="", + encoding="utf-8", + ), + ) + test_projects.append(project) + + config = SerenaConfigForTests() + config.projects = test_projects + return config @pytest.fixture -def serena_agent(request: LanguageParamRequest, serena_config) -> SerenaAgent: +def serena_agent(request: pytest.FixtureRequest, serena_config): language = Language(request.param) - repo = get_repo_path(language) - return SerenaAgent(project=str(repo), serena_config=serena_config) + project_name = f"test_repo_{language}" + + # Check if this test should use process isolation by looking at the test parameters + isolated_process = False + if hasattr(request, "node") and hasattr(request.node, "callspec"): + # Get the isolated_process parameter value from the test + params = request.node.callspec.params + isolated_process = params.get("isolated_process", False) + + if isolated_process: + agent = ProcessIsolatedSerenaAgent(project=project_name, serena_config=serena_config) + agent.start() + + # Add cleanup to stop the process + def cleanup(): + agent.stop() + + request.addfinalizer(cleanup) + return agent + else: + return SerenaAgent(project=project_name, serena_config=serena_config) class TestSerenaAgent: + @pytest.mark.parametrize( + "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] + ) @pytest.mark.parametrize( "serena_agent,symbol_name,expected_kind,expected_file", [ @@ -45,16 +97,20 @@ class TestSerenaAgent: ], indirect=["serena_agent"], ) - def test_find_symbol(self, serena_agent: SerenaAgent, symbol_name: str, expected_kind: str, expected_file: str): + def test_find_symbol(self, serena_agent, symbol_name: str, expected_kind: str, expected_file: str, isolated_process: bool): agent = serena_agent find_symbol_tool = agent.get_tool(FindSymbolTool) - result = find_symbol_tool.apply(symbol_name) + result = find_symbol_tool.apply_ex(name_path=symbol_name) + symbols = json.loads(result) assert any( symbol_name in s["name_path"] and expected_kind.lower() in s["kind"].lower() and expected_file in s["relative_path"] for s in symbols - ), f"Expected to find {symbol_name} ({expected_kind}) in {expected_file} for {agent.get_active_project().language.name}" + ), f"Expected to find {symbol_name} ({expected_kind}) in {expected_file}" + @pytest.mark.parametrize( + "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] + ) @pytest.mark.parametrize( "serena_agent,symbol_name,def_file,ref_file", [ @@ -79,23 +135,30 @@ class TestSerenaAgent: ], indirect=["serena_agent"], ) - def test_find_symbol_references(self, serena_agent: SerenaAgent, symbol_name: str, def_file: str, ref_file: str) -> None: + def test_find_symbol_references(self, serena_agent, symbol_name: str, def_file: str, ref_file: str, isolated_process: bool) -> None: agent = serena_agent + # Find the symbol location first find_symbol_tool = agent.get_tool(FindSymbolTool) - result = find_symbol_tool.apply(symbol_name, relative_path=def_file) + result = find_symbol_tool.apply_ex(name_path=symbol_name, relative_path=def_file) + time.sleep(1) symbols = json.loads(result) # Find the definition def_symbol = symbols[0] + # Now find references find_refs_tool = agent.get_tool(FindReferencingSymbolsTool) - result = find_refs_tool.apply(name_path=def_symbol["name_path"], relative_path=def_symbol["relative_path"]) + result = find_refs_tool.apply_ex(name_path=def_symbol["name_path"], relative_path=def_symbol["relative_path"]) + refs = json.loads(result) assert any( ref["relative_path"] == ref_file for ref in refs - ), f"Expected to find reference to {symbol_name} in {ref_file} for {agent._active_project.language.name}. refs={refs}" + ), f"Expected to find reference to {symbol_name} in {ref_file}. refs={refs}" + @pytest.mark.parametrize( + "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] + ) @pytest.mark.parametrize( "serena_agent,name_path,substring_matching,expected_symbol_name,expected_kind,expected_file", [ @@ -164,16 +227,18 @@ class TestSerenaAgent: ) def test_find_symbol_name_path( self, - serena_agent: SerenaAgent, + serena_agent, name_path: str, substring_matching: bool, expected_symbol_name: str, expected_kind: str, expected_file: str, + isolated_process: bool, ): agent = serena_agent + find_symbol_tool = agent.get_tool(FindSymbolTool) - result = find_symbol_tool.apply( + result = find_symbol_tool.apply_ex( name_path=name_path, depth=0, relative_path=None, @@ -182,6 +247,7 @@ class TestSerenaAgent: exclude_kinds=None, substring_matching=substring_matching, ) + symbols = json.loads(result) assert any( expected_symbol_name == s["name_path"].split("/")[-1] @@ -190,6 +256,9 @@ class TestSerenaAgent: for s in symbols ), f"Expected to find {name_path} ({expected_kind}) in {expected_file} for {agent._active_project.language.name}. Symbols: {symbols}" + @pytest.mark.parametrize( + "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] + ) @pytest.mark.parametrize( "serena_agent,name_path", [ @@ -210,17 +279,18 @@ class TestSerenaAgent: ) def test_find_symbol_name_path_no_match( self, - serena_agent: SerenaAgent, + serena_agent, name_path: str, + isolated_process: bool, ): agent = serena_agent + find_symbol_tool = agent.get_tool(FindSymbolTool) - result = find_symbol_tool.apply( + result = find_symbol_tool.apply_ex( name_path=name_path, depth=0, substring_matching=True, ) + symbols = json.loads(result) - assert ( - not symbols - ), f"Expected to find no symbols for {name_path} for {agent._active_project.language.name}. Symbols found: {symbols}" + assert not symbols, f"Expected to find no symbols for {name_path}. Symbols found: {symbols}" diff --git a/test/serena/test_symbol.py b/test/serena/test_symbol.py index 7c21215..72fd8d4 100644 --- a/test/serena/test_symbol.py +++ b/test/serena/test_symbol.py @@ -107,7 +107,7 @@ class TestSymbolNameMatching: "bar/foo", ["mod", "bar", "foobar"], True, True, id="R: 'bar/foo' matches ['mod', 'bar', 'foobar'] as substring (suffix)" ), pytest.param("bar/foo", ["bar", "bazfoo"], True, True, id="R: 'bar/foo' matches ['bar', 'bazfoo'] as substring"), - pytest.param("bar/fo", ["bar", "foo"], True, True, id="R: 'bar/fo' matches ['bar', 'foo'] as substring"), + pytest.param("bar/fo", ["bar", "foo"], True, True, id="R: 'bar/fo' matches ['bar', 'foo'] as substring"), # codespell:ignore pytest.param("bar/foo", ["bar", "baz"], True, False, id="R: 'bar/foo' does not match ['bar', 'baz'] (last no substr)"), pytest.param( "bar/foo", ["baz", "foobar"], True, False, id="R: 'bar/foo' does not match ['baz', 'foobar'] (first part mismatch)" @@ -148,7 +148,7 @@ class TestSymbolNameMatching: # Substring matches (is_substring_match=True) pytest.param("/bar/foo", ["bar", "foobar"], True, True, id="A: '/bar/foo' matches ['bar', 'foobar'] as substring"), pytest.param("/bar/foo", ["bar", "bazfoo"], True, True, id="A: '/bar/foo' matches ['bar', 'bazfoo'] as substring"), - pytest.param("/bar/fo", ["bar", "foo"], True, True, id="A: '/bar/fo' matches ['bar', 'foo'] as substring"), + pytest.param("/bar/fo", ["bar", "foo"], True, True, id="A: '/bar/fo' matches ['bar', 'foo'] as substring"), # codespell:ignore pytest.param("/bar/foo", ["bar", "baz"], True, False, id="A: '/bar/foo' does not match ['bar', 'baz'] (last no substr)"), pytest.param( "/bar/foo", ["baz", "foobar"], True, False, id="A: '/bar/foo' does not match ['baz', 'foobar'] (first part mismatch)" diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py index c569db5..652411e 100644 --- a/test/serena/test_symbol_editing.py +++ b/test/serena/test_symbol_editing.py @@ -1,6 +1,8 @@ +import logging import os import shutil import tempfile +import time from abc import abstractmethod from collections.abc import Iterator from contextlib import contextmanager @@ -9,13 +11,15 @@ from typing import Literal import pytest -from multilspy.multilspy_config import Language from serena.symbol import CodeDiff +from solidlsp.ls_config import Language from src.serena.symbol import SymbolManager from test.conftest import create_ls, get_repo_path pytestmark = pytest.mark.snapshot +log = logging.getLogger(__name__) + class EditingTest: def __init__(self, language: Language, rel_path: str): @@ -35,14 +39,32 @@ class EditingTest: self.repo_path = temp_dir / self.original_repo_path.name language_server = None # Initialize language_server try: + print(f"Copying repo from {self.original_repo_path} to {self.repo_path}") shutil.copytree(self.original_repo_path, self.repo_path) + # prevent deadlock on Windows due to file locks caused by antivirus or some other external software + # wait for a long time here + if os.name == "nt": + time.sleep(0.1) + log.info(f"Creating language server for {self.language} {self.rel_path}") language_server = create_ls(self.language, str(self.repo_path)) + log.info(f"Starting language server for {self.language} {self.rel_path}") language_server.start() + log.info(f"Language server started for {self.language} {self.rel_path}") yield SymbolManager(lang_server=language_server) finally: if language_server is not None and language_server.is_running(): + log.info(f"Stopping language server for {self.language} {self.rel_path}") language_server.stop() - shutil.rmtree(temp_dir) + # attempt at trigger of garbage collection + language_server = None + log.info(f"Language server stopped for {self.language} {self.rel_path}") + + # prevent deadlock on Windows due to lingering file locks + if os.name == "nt": + time.sleep(0.1) + log.info(f"Removing temp directory {temp_dir}") + shutil.rmtree(temp_dir, ignore_errors=True) + log.info(f"Temp directory {temp_dir} removed") def _read_file(self, rel_path: str) -> str: """Read the content of a file in the test repository.""" @@ -112,6 +134,18 @@ def test_delete_symbol(test_case, snapshot): NEW_PYTHON_FUNCTION = """def new_inserted_function(): print("This is a new function inserted before another.")""" +NEW_PYTHON_CLASS_WITH_LEADING_NEWLINES = """ + +class NewInsertedClass: + pass +""" + +NEW_PYTHON_CLASS_WITH_TRAILING_NEWLINES = """class NewInsertedClass: + pass + + +""" + NEW_TYPESCRIPT_FUNCTION = """function newInsertedFunction(): void { console.log("This is a new function inserted before another."); }""" @@ -125,11 +159,13 @@ NEW_TYPESCRIPT_FUNCTION_AFTER = """function newFunctionAfterClass(): void { class InsertInRelToSymbolTest(EditingTest): - def __init__(self, language: Language, rel_path: str, symbol_name: str, new_content: str): + def __init__( + self, language: Language, rel_path: str, symbol_name: str, new_content: str, mode: Literal["before", "after"] | None = None + ): super().__init__(language, rel_path) self.symbol_name = symbol_name self.new_content = new_content - self.mode: Literal["before", "after"] | None = None + self.mode: Literal["before", "after"] | None = mode def set_mode(self, mode: Literal["before", "after"]): self.mode = mode @@ -137,9 +173,9 @@ class InsertInRelToSymbolTest(EditingTest): def _apply_edit(self, symbol_manager: SymbolManager) -> None: assert self.mode is not None if self.mode == "before": - symbol_manager.insert_before_symbol(self.symbol_name, self.rel_path, self.new_content) + symbol_manager.insert_before_symbol(self.symbol_name, self.rel_path, self.new_content, use_same_indentation=False) elif self.mode == "after": - symbol_manager.insert_after_symbol(self.symbol_name, self.rel_path, self.new_content) + symbol_manager.insert_after_symbol(self.symbol_name, self.rel_path, self.new_content, use_same_indentation=False) @pytest.mark.parametrize("mode", ["before", "after"]) @@ -189,18 +225,38 @@ def test_insert_in_rel_to_symbol(test_case: InsertInRelToSymbolTest, mode: Liter test_case.run_test(content_after_ground_truth=snapshot) -PYTHON_REPLACED_BODY = """ -def modify_instance_var(self): - # This body has been replaced - self.instance_var = "Replaced!" - self.reassignable_instance_var = 999 +@pytest.mark.python +def test_insert_python_class_before(snapshot): + InsertInRelToSymbolTest( + Language.PYTHON, + PYTHON_TEST_REL_FILE_PATH, + "VariableDataclass", + NEW_PYTHON_CLASS_WITH_TRAILING_NEWLINES, + mode="before", + ).run_test(snapshot) + + +@pytest.mark.python +def test_insert_python_class_after(snapshot): + InsertInRelToSymbolTest( + Language.PYTHON, + PYTHON_TEST_REL_FILE_PATH, + "VariableDataclass", + NEW_PYTHON_CLASS_WITH_LEADING_NEWLINES, + mode="after", + ).run_test(snapshot) + + +PYTHON_REPLACED_BODY = """def modify_instance_var(self): + # This body has been replaced + self.instance_var = "Replaced!" + self.reassignable_instance_var = 999 """ -TYPESCRIPT_REPLACED_BODY = """ -function printValue() { - // This body has been replaced - console.warn("New value: " + this.value); -} +TYPESCRIPT_REPLACED_BODY = """function printValue() { + // This body has been replaced + console.warn("New value: " + this.value); + } """ @@ -211,7 +267,7 @@ class ReplaceBodyTest(EditingTest): self.new_body = new_body def _apply_edit(self, symbol_manager: SymbolManager) -> None: - symbol_manager.replace_body(self.symbol_name, self.rel_path, self.new_body) + symbol_manager.replace_body(self.symbol_name, self.rel_path, self.new_body, use_same_indentation=False) @pytest.mark.parametrize( diff --git a/test/serena/test_text_utils.py b/test/serena/test_text_utils.py index 1fa6929..86ff975 100644 --- a/test/serena/test_text_utils.py +++ b/test/serena/test_text_utils.py @@ -182,6 +182,24 @@ class TestSearchText: assert any("isinstance(item, dict)" in line for line in instance_matches) assert any("isinstance(item, list)" in line for line in instance_matches) + def test_search_text_glob_with_special_chars(self): + """Glob patterns containing regex special characters should match literally.""" + content = """ + def func_square(): + print("value[42]") + + def func_curly(): + print("value{bar}") + """ + + matches_square = search_text(r"*\[42\]*", content=content, is_glob=True) + assert len(matches_square) == 1 + assert "[42]" in matches_square[0].lines[0].line_content + + matches_curly = search_text("*{bar}*", content=content, is_glob=True) + assert len(matches_curly) == 1 + assert "{bar}" in matches_curly[0].lines[0].line_content + def test_search_text_no_matches(self): """Test searching with a pattern that doesn't match anything.""" content = """ @@ -267,6 +285,87 @@ class TestSearchFiles: assert result.matched_lines[0].line_content == "This line contains a match." assert result.matched_lines[0].match_type == LineType.MATCH + @pytest.mark.parametrize( + "file_paths, pattern, paths_include_glob, paths_exclude_glob, expected_matched_files, description", + [ + # Glob patterns that were problematic with gitignore syntax + ( + ["src/serena/agent.py", "src/serena/process_isolated_agent.py", "test/agent.py"], + "match", + "src/**agent.py", + None, + ["src/serena/agent.py", "src/serena/process_isolated_agent.py"], + "Glob: src/**agent.py should match files ending with agent.py under src/", + ), + ( + ["src/serena/agent.py", "src/serena/process_isolated_agent.py", "other/agent.py"], + "match", + "**agent.py", + None, + ["src/serena/agent.py", "src/serena/process_isolated_agent.py", "other/agent.py"], + "Glob: **agent.py should match files ending with agent.py anywhere", + ), + ( + ["dir/subdir/file.py", "dir/other/file.py", "elsewhere/file.py"], + "match", + "dir/**file.py", + None, + ["dir/subdir/file.py", "dir/other/file.py"], + "Glob: dir/**file.py should match files ending with file.py under dir/", + ), + ( + ["src/a/b/c/test.py", "src/x/test.py", "other/test.py"], + "match", + "src/**/test.py", + None, + ["src/a/b/c/test.py", "src/x/test.py"], + "Glob: src/**/test.py should match test.py files under src/ at any depth", + ), + # Edge cases for ** patterns + ( + ["agent.py", "src/agent.py", "src/serena/agent.py"], + "match", + "**agent.py", + None, + ["agent.py", "src/agent.py", "src/serena/agent.py"], + "Glob: **agent.py should match at root and any depth", + ), + (["file.txt", "src/file.txt"], "match", "src/**", None, ["src/file.txt"], "Glob: src/** should match everything under src/"), + ], + ids=lambda x: x if isinstance(x, str) else "", # Use description as test ID + ) + def test_search_files_glob_patterns( + self, file_paths, pattern, paths_include_glob, paths_exclude_glob, expected_matched_files, description + ): + """ + Test glob patterns that were problematic with the previous gitignore-based implementation. + """ + results = search_files( + file_paths=file_paths, + pattern=pattern, + file_reader=mock_reader_always_match, + paths_include_glob=paths_include_glob, + paths_exclude_glob=paths_exclude_glob, + context_lines_before=0, + context_lines_after=0, + ) + + # Extract the source file paths from the results + actual_matched_files = sorted([result.source_file_path for result in results if result.source_file_path]) + + # Assert that the matched files are exactly the ones expected + assert actual_matched_files == sorted( + expected_matched_files + ), f"Pattern '{paths_include_glob}' failed: expected {sorted(expected_matched_files)}, got {actual_matched_files}" + + # Basic check on results structure if files were expected + if expected_matched_files: + assert len(results) == len(expected_matched_files) + for result in results: + assert len(result.matched_lines) == 1 # Mock reader returns one matching line + assert result.matched_lines[0].line_content == "This line contains a match." + assert result.matched_lines[0].match_type == LineType.MATCH + def test_search_files_no_pattern_match_in_content(self): """Test that no results are returned if the pattern doesn't match the file content, even if files pass filters.""" file_paths = ["a.py", "b.txt"] @@ -347,3 +446,47 @@ class TestSearchFiles: assert result.lines[1].match_type == LineType.MATCH assert result.lines[2].line_content == "Line after 1", "Incorrect 'after' context line" assert result.lines[2].match_type == LineType.AFTER_MATCH + + +class TestGlobMatch: + """Test the glob_match function directly.""" + + @pytest.mark.parametrize( + "pattern, path, expected", + [ + # Basic wildcard patterns + ("*.py", "file.py", True), + ("*.py", "file.txt", False), + ("*agent.py", "agent.py", True), + ("*agent.py", "process_isolated_agent.py", True), + ("*agent.py", "agent_test.py", False), + # Double asterisk patterns + ("**agent.py", "agent.py", True), + ("**agent.py", "src/agent.py", True), + ("**agent.py", "src/serena/agent.py", True), + ("**agent.py", "src/serena/process_isolated_agent.py", True), + ("**agent.py", "agent_test.py", False), + # Prefix with double asterisk + ("src/**agent.py", "src/agent.py", True), + ("src/**agent.py", "src/serena/agent.py", True), + ("src/**agent.py", "src/serena/process_isolated_agent.py", True), + ("src/**agent.py", "other/agent.py", False), + ("src/**agent.py", "src/agent_test.py", False), + # Directory patterns + ("src/**", "src/file.py", True), + ("src/**", "src/dir/file.py", True), + ("src/**", "other/file.py", False), + # Exact matches with double asterisk + ("src/**/test.py", "src/test.py", True), + ("src/**/test.py", "src/a/b/test.py", True), + ("src/**/test.py", "src/test_file.py", False), + # Simple patterns without asterisks + ("src/file.py", "src/file.py", True), + ("src/file.py", "src/other.py", False), + ], + ) + def test_glob_match(self, pattern, path, expected): + """Test glob_match function with various patterns.""" + from src.serena.text_utils import glob_match + + assert glob_match(pattern, path) == expected diff --git a/test/serena/util/test_file_system.py b/test/serena/util/test_file_system.py index 48d983a..3c9fd14 100644 --- a/test/serena/util/test_file_system.py +++ b/test/serena/util/test_file_system.py @@ -133,7 +133,7 @@ build/ assert "*.log" in patterns assert "build/" in patterns - assert "temp.txt" in patterns + assert "/temp.txt" in patterns def test_parse_patterns_subdirectory(self): """Test parsing gitignore patterns in subdirectory.""" @@ -227,6 +227,127 @@ data.json assert parser.should_ignore("src/subdir/data.json") assert parser.should_ignore("src/subdir/deep/data.json") + def test_root_anchored_patterns(self): + """Test anchored patterns in root .gitignore only match root-level files.""" + # Create new test structure for root anchored patterns + test_dir = self.repo_path / "test_root_anchored" + test_dir.mkdir() + (test_dir / "src").mkdir() + (test_dir / "docs").mkdir() + (test_dir / "src" / "nested").mkdir() + + # Create root .gitignore with anchored patterns + gitignore = test_dir / ".gitignore" + gitignore.write_text( + """/config.json +/temp.log +/build +*.pyc +""" + ) + + # Create test files at root level + (test_dir / "config.json").touch() + (test_dir / "temp.log").touch() + (test_dir / "build").mkdir() + (test_dir / "file.pyc").touch() + + # Create same-named files in subdirectories + (test_dir / "src" / "config.json").touch() + (test_dir / "src" / "temp.log").touch() + (test_dir / "src" / "build").mkdir() + (test_dir / "src" / "file.pyc").touch() + (test_dir / "docs" / "config.json").touch() + (test_dir / "docs" / "temp.log").touch() + (test_dir / "src" / "nested" / "config.json").touch() + (test_dir / "src" / "nested" / "temp.log").touch() + (test_dir / "src" / "nested" / "build").mkdir() + + parser = GitignoreParser(str(test_dir)) + + # Anchored patterns should only match root-level files + assert parser.should_ignore("config.json") + assert not parser.should_ignore("src/config.json") + assert not parser.should_ignore("docs/config.json") + assert not parser.should_ignore("src/nested/config.json") + + assert parser.should_ignore("temp.log") + assert not parser.should_ignore("src/temp.log") + assert not parser.should_ignore("docs/temp.log") + assert not parser.should_ignore("src/nested/temp.log") + + assert parser.should_ignore("build") + assert not parser.should_ignore("src/build") + assert not parser.should_ignore("src/nested/build") + + # Non-anchored patterns should match everywhere + assert parser.should_ignore("file.pyc") + assert parser.should_ignore("src/file.pyc") + + def test_mixed_anchored_and_non_anchored_root_patterns(self): + """Test mix of anchored and non-anchored patterns in root .gitignore.""" + test_dir = self.repo_path / "test_mixed_patterns" + test_dir.mkdir() + (test_dir / "app").mkdir() + (test_dir / "tests").mkdir() + (test_dir / "app" / "modules").mkdir() + + # Create root .gitignore with mixed patterns + gitignore = test_dir / ".gitignore" + gitignore.write_text( + """/secrets.env +/dist/ +node_modules/ +*.tmp +/app/local.config +debug.log +""" + ) + + # Create test files and directories + (test_dir / "secrets.env").touch() + (test_dir / "dist").mkdir() + (test_dir / "node_modules").mkdir() + (test_dir / "file.tmp").touch() + (test_dir / "app" / "local.config").touch() + (test_dir / "debug.log").touch() + + # Create same files in subdirectories + (test_dir / "app" / "secrets.env").touch() + (test_dir / "app" / "dist").mkdir() + (test_dir / "app" / "node_modules").mkdir() + (test_dir / "app" / "file.tmp").touch() + (test_dir / "app" / "debug.log").touch() + (test_dir / "tests" / "secrets.env").touch() + (test_dir / "tests" / "node_modules").mkdir() + (test_dir / "tests" / "debug.log").touch() + (test_dir / "app" / "modules" / "local.config").touch() + + parser = GitignoreParser(str(test_dir)) + + # Anchored patterns should only match at root + assert parser.should_ignore("secrets.env") + assert not parser.should_ignore("app/secrets.env") + assert not parser.should_ignore("tests/secrets.env") + + assert parser.should_ignore("dist") + assert not parser.should_ignore("app/dist") + + assert parser.should_ignore("app/local.config") + assert not parser.should_ignore("app/modules/local.config") + + # Non-anchored patterns should match everywhere + assert parser.should_ignore("node_modules") + assert parser.should_ignore("app/node_modules") + assert parser.should_ignore("tests/node_modules") + + assert parser.should_ignore("file.tmp") + assert parser.should_ignore("app/file.tmp") + + assert parser.should_ignore("debug.log") + assert parser.should_ignore("app/debug.log") + assert parser.should_ignore("tests/debug.log") + def test_negation_patterns(self): """Test negation patterns are parsed correctly.""" test_dir = self.repo_path / "test_negation" @@ -248,7 +369,7 @@ data.json assert "*.log" in patterns assert "!important.log" in patterns - assert "!src/keep.log" in patterns + assert "!/src/keep.log" in patterns def test_comments_and_empty_lines(self): """Test that comments and empty lines are ignored.""" diff --git a/test/multilspy/go/test_go_basic.py b/test/solidlsp/go/test_go_basic.py similarity index 81% rename from test/multilspy/go/test_go_basic.py rename to test/solidlsp/go/test_go_basic.py index 6b5e316..5632315 100644 --- a/test/multilspy/go/test_go_basic.py +++ b/test/solidlsp/go/test_go_basic.py @@ -2,22 +2,22 @@ import os import pytest -from multilspy import SyncLanguageServer -from multilspy.multilspy_config import Language -from multilspy.multilspy_utils import SymbolUtils +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from solidlsp.ls_utils import SymbolUtils @pytest.mark.go class TestGoLanguageServer: @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) - def test_find_symbol(self, language_server: SyncLanguageServer) -> None: + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "Helper"), "Helper function not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "DemoStruct"), "DemoStruct not found in symbol tree" @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) - def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> None: + def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: file_path = os.path.join("main.go") symbols = language_server.request_document_symbols(file_path) helper_symbol = None diff --git a/test/multilspy/java/test_java_basic.py b/test/solidlsp/java/test_java_basic.py similarity index 86% rename from test/multilspy/java/test_java_basic.py rename to test/solidlsp/java/test_java_basic.py index e7ff5a8..d74a9fe 100644 --- a/test/multilspy/java/test_java_basic.py +++ b/test/solidlsp/java/test_java_basic.py @@ -2,22 +2,22 @@ import os import pytest -from multilspy import SyncLanguageServer -from multilspy.multilspy_config import Language -from multilspy.multilspy_utils import SymbolUtils +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from solidlsp.ls_utils import SymbolUtils @pytest.mark.java class TestJavaLanguageServer: @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) - def test_find_symbol(self, language_server: SyncLanguageServer) -> None: + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils class not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model class not found in symbol tree" @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) - def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> None: + def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: # Use correct Maven/Java file paths file_path = os.path.join("src", "main", "java", "test_repo", "Utils.java") refs = language_server.request_references(file_path, 4, 20) @@ -43,7 +43,7 @@ class TestJavaLanguageServer: ), "Main should reference Model (tried all positions in selectionRange)" @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) - def test_overview_methods(self, language_server: SyncLanguageServer) -> None: + def test_overview_methods(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview" diff --git a/test/multilspy/php/test_php_basic.py b/test/solidlsp/php/test_php_basic.py similarity index 94% rename from test/multilspy/php/test_php_basic.py rename to test/solidlsp/php/test_php_basic.py index 942281f..391c565 100644 --- a/test/multilspy/php/test_php_basic.py +++ b/test/solidlsp/php/test_php_basic.py @@ -2,15 +2,15 @@ from pathlib import Path import pytest -from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language @pytest.mark.php class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_ls_is_running(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None: """Test that the language server starts and stops successfully.""" # The fixture already handles start and stop assert language_server.is_running() @@ -18,7 +18,7 @@ class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_find_definition_within_file(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None: # In index.php: # Line 9 (1-indexed): $greeting = greet($userName); @@ -41,7 +41,7 @@ class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_find_definition_across_files(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None: definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 12, 5) # helperFunction assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}" @@ -53,7 +53,7 @@ class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_find_definition_simple_variable(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_find_definition_simple_variable(self, language_server: SolidLanguageServer, repo_path: Path) -> None: file_path = str(repo_path / "simple_var.php") # In simple_var.php: @@ -74,7 +74,7 @@ class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_find_references_within_file(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None: index_php_path = str(repo_path / "index.php") # In index.php (0-indexed lines): @@ -108,7 +108,7 @@ class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_find_references_across_files(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None: helper_php_path = str(repo_path / "helper.php") # In index.php (0-indexed lines): # Line 13: helperFunction(); // Usage of helperFunction diff --git a/test/multilspy/python/test_python_basic.py b/test/solidlsp/python/test_python_basic.py similarity index 97% rename from test/multilspy/python/test_python_basic.py rename to test/solidlsp/python/test_python_basic.py index 5af4f95..74737c4 100644 --- a/test/multilspy/python/test_python_basic.py +++ b/test/solidlsp/python/test_python_basic.py @@ -9,9 +9,9 @@ import os import pytest -from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language from serena.text_utils import LineType +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language @pytest.mark.python @@ -19,7 +19,7 @@ class TestLanguageServerBasics: """Test basic functionality of the language server.""" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_references_user_class(self, language_server: SyncLanguageServer) -> None: + def test_request_references_user_class(self, language_server: SolidLanguageServer) -> None: """Test request_references on the User class.""" # Get references to the User class in models.py file_path = os.path.join("test_repo", "models.py") @@ -34,7 +34,7 @@ class TestLanguageServerBasics: assert len(references) > 1, "User class should be referenced in multiple files (using selectionRange if present)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_references_item_class(self, language_server: SyncLanguageServer) -> None: + def test_request_references_item_class(self, language_server: SolidLanguageServer) -> None: """Test request_references on the Item class.""" # Get references to the Item class in models.py file_path = os.path.join("test_repo", "models.py") @@ -50,7 +50,7 @@ class TestLanguageServerBasics: assert len(services_references) > 0, "At least one reference should be in services.py (using selectionRange if present)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_references_function_parameter(self, language_server: SyncLanguageServer) -> None: + def test_request_references_function_parameter(self, language_server: SolidLanguageServer) -> None: """Test request_references on a function parameter.""" # Get references to the id parameter in get_user method file_path = os.path.join("test_repo", "services.py") @@ -65,7 +65,7 @@ class TestLanguageServerBasics: assert len(references) > 0, "id parameter should be referenced within the method (using selectionRange if present)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_references_create_user_method(self, language_server: SyncLanguageServer) -> None: + def test_request_references_create_user_method(self, language_server: SolidLanguageServer) -> None: # Get references to the create_user method in UserService file_path = os.path.join("test_repo", "services.py") # Line 15 contains the create_user method definition @@ -79,7 +79,7 @@ class TestLanguageServerBasics: assert len(references) > 1, "Should get valid references for create_user (using selectionRange if present)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_retrieve_content_around_line(self, language_server: SyncLanguageServer) -> None: + def test_retrieve_content_around_line(self, language_server: SolidLanguageServer) -> None: """Test retrieve_content_around_line functionality with various scenarios.""" file_path = os.path.join("test_repo", "models.py") @@ -186,7 +186,7 @@ class TestLanguageServerBasics: assert line.match_type == LineType.AFTER_MATCH @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_search_files_for_pattern(self, language_server: SyncLanguageServer) -> None: + def test_search_files_for_pattern(self, language_server: SolidLanguageServer) -> None: """Test search_files_for_pattern with various patterns and glob filters.""" # Test 1: Search for class definitions across all files class_pattern = r"class\s+\w+\s*(?:\([^{]*\)|:)" diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/solidlsp/python/test_retrieval_with_ignored_dirs.py similarity index 66% rename from test/multilspy/python/test_retrieval_with_ignored_dirs.py rename to test/solidlsp/python/test_retrieval_with_ignored_dirs.py index 5150d53..a3d7d6a 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/solidlsp/python/test_retrieval_with_ignored_dirs.py @@ -3,26 +3,19 @@ from pathlib import Path import pytest -from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language, MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from test.conftest import get_repo_path +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.conftest import create_ls # This mark will be applied to all tests in this module pytestmark = pytest.mark.python @pytest.fixture(scope="module") -def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: +def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]: """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored.""" - config = MultilspyConfig( - code_language=Language.PYTHON, - trace_lsp_communication=False, - ignored_paths=["scripts", "custom_test"], # Configure the relative path to be ignored - ) - logger = MultilspyLogger() - repo_path = get_repo_path(Language.PYTHON) - ls = SyncLanguageServer.create(config, logger, str(repo_path)) + ignored_paths = ["scripts", "custom_test"] + ls = create_ls(ignored_paths=ignored_paths, language=Language.PYTHON) ls.start() try: yield ls @@ -31,7 +24,7 @@ def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True) -def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): +def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): """Tests that request_full_symbol_tree ignores the configured directory.""" root = ls_with_ignored_dirs.request_full_symbol_tree()[0] root_children = root["children"] @@ -40,7 +33,7 @@ def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True) -def test_find_references_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): +def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): """Tests that find_references ignores the configured directory.""" # Location of Item, which is referenced in scripts definition_file = "test_repo/models.py" @@ -56,13 +49,8 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): @pytest.mark.parametrize("repo_path", [Language.PYTHON], indirect=True) def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: """Tests that refs and symbols with glob patterns are ignored.""" - config = MultilspyConfig( - code_language=Language.PYTHON, - trace_lsp_communication=False, - ignored_paths=["*ipts", "custom_t*"], - ) - logger = MultilspyLogger() - ls = SyncLanguageServer.create(config, logger, str(repo_path)) + ignored_paths = ["*ipts", "custom_t*"] + ls = create_ls(ignored_paths=ignored_paths, repo_path=str(repo_path), language=Language.PYTHON) ls.start() # same as in the above tests root = ls.request_full_symbol_tree()[0] diff --git a/test/multilspy/python/test_symbol_retrieval.py b/test/solidlsp/python/test_symbol_retrieval.py similarity index 96% rename from test/multilspy/python/test_symbol_retrieval.py rename to test/solidlsp/python/test_symbol_retrieval.py index ba5bd61..c19bf8b 100644 --- a/test/multilspy/python/test_symbol_retrieval.py +++ b/test/solidlsp/python/test_symbol_retrieval.py @@ -10,9 +10,9 @@ import os import pytest -from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language -from multilspy.multilspy_types import SymbolKind +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind pytestmark = pytest.mark.python @@ -21,7 +21,7 @@ class TestLanguageServerSymbols: """Test the language server's symbol-related functionality.""" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_function(self, language_server: SyncLanguageServer) -> None: + def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol for a function.""" # Test for a position inside the create_user method file_path = os.path.join("test_repo", "services.py") @@ -36,7 +36,7 @@ class TestLanguageServerSymbols: assert containing_symbol["body"].strip().startswith("def create_user(self") @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_references_to_variables(self, language_server: SyncLanguageServer) -> None: + def test_references_to_variables(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a variable.""" file_path = os.path.join("test_repo", "variables.py") # Line 75 contains the field status that is later modified @@ -51,7 +51,7 @@ class TestLanguageServerSymbols: assert "second_dataclass" in ref_names @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_class(self, language_server: SyncLanguageServer) -> None: + def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol for a class.""" # Test for a position inside the UserService class but outside any method file_path = os.path.join("test_repo", "services.py") @@ -64,7 +64,7 @@ class TestLanguageServerSymbols: assert containing_symbol["kind"] == SymbolKind.Class @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_nested(self, language_server: SyncLanguageServer) -> None: + def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol with nested scopes.""" # Test for a position inside a method which is inside a class file_path = os.path.join("test_repo", "services.py") @@ -90,7 +90,7 @@ class TestLanguageServerSymbols: assert parent_symbol["kind"] == SymbolKind.Class @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_none(self, language_server: SyncLanguageServer) -> None: + def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None: """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 = os.path.join("test_repo", "services.py") @@ -101,7 +101,7 @@ class TestLanguageServerSymbols: assert containing_symbol is None or containing_symbol == {} @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_referencing_symbols_function(self, language_server: SyncLanguageServer) -> None: + def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a function.""" # Test referencing symbols for create_user function file_path = os.path.join("test_repo", "services.py") @@ -125,7 +125,7 @@ class TestLanguageServerSymbols: assert "end" in symbol["location"]["range"] @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_referencing_symbols_class(self, language_server: SyncLanguageServer) -> None: + def test_request_referencing_symbols_class(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a class.""" # Test referencing symbols for User class file_path = os.path.join("test_repo", "models.py") @@ -146,7 +146,7 @@ class TestLanguageServerSymbols: assert len(services_references) > 0, "No referencing symbols from services.py for User (selectionRange)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_referencing_symbols_parameter(self, language_server: SyncLanguageServer) -> None: + def test_request_referencing_symbols_parameter(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a function parameter.""" # Test referencing symbols for id parameter in get_user file_path = os.path.join("test_repo", "services.py") @@ -167,7 +167,7 @@ class TestLanguageServerSymbols: assert len(method_refs) > 0, "No referencing symbols within method body for get_user (selectionRange)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_referencing_symbols_none(self, language_server: SyncLanguageServer) -> None: + def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None: """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 @@ -185,7 +185,7 @@ class TestLanguageServerSymbols: # Tests for request_defining_symbol @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_variable(self, language_server: SyncLanguageServer) -> None: + def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a variable usage.""" # Test finding the definition of a symbol in the create_user method file_path = os.path.join("test_repo", "services.py") @@ -204,7 +204,7 @@ class TestLanguageServerSymbols: assert "services.py" in defining_symbol["location"]["uri"] @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_imported_class(self, language_server: SyncLanguageServer) -> None: + def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None: """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 = os.path.join("test_repo", "services.py") @@ -216,7 +216,7 @@ class TestLanguageServerSymbols: assert defining_symbol.get("name") == "User" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_method_call(self, language_server: SyncLanguageServer) -> None: + def test_request_defining_symbol_method_call(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a method call.""" # Create an example file path for a file that calls UserService.create_user examples_file_path = os.path.join("examples", "user_management.py") @@ -241,7 +241,7 @@ class TestLanguageServerSymbols: warnings.warn("Could not verify method call definition - file structure may differ from expected") @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_none(self, language_server: SyncLanguageServer) -> None: + def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a position with no symbol.""" # Test for a position with no symbol (e.g., whitespace or comment) file_path = os.path.join("test_repo", "services.py") @@ -252,7 +252,7 @@ class TestLanguageServerSymbols: assert defining_symbol is None @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_variable(self, language_server: SyncLanguageServer) -> None: + def test_request_containing_symbol_variable(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol where the symbol is a variable.""" # Test for a position inside a variable definition file_path = os.path.join("test_repo", "services.py") @@ -265,7 +265,7 @@ class TestLanguageServerSymbols: assert containing_symbol["kind"] == SymbolKind.Variable @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_nested_function(self, language_server: SyncLanguageServer) -> None: + def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a nested function or closure.""" # Use the existing nested.py file which contains nested classes and methods file_path = os.path.join("test_repo", "nested.py") @@ -316,7 +316,7 @@ class TestLanguageServerSymbols: assert defining_symbol.get("kind") == SymbolKind.Function.value @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_symbol_methods_integration(self, language_server: SyncLanguageServer) -> None: + def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None: """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 @@ -365,7 +365,7 @@ class TestLanguageServerSymbols: warnings.warn("Could not verify container hierarchy - implementation detail") @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_symbol_tree_structure(self, language_server: SyncLanguageServer) -> None: + def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None: """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file repo_structure = language_server.request_full_symbol_tree() @@ -393,7 +393,7 @@ class TestLanguageServerSymbols: assert user_management_roots == user_management_node["children"] @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_symbol_tree_structure_subdir(self, language_server: SyncLanguageServer) -> None: + def test_symbol_tree_structure_subdir(self, language_server: SolidLanguageServer) -> None: """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(within_relative_path="examples") @@ -414,7 +414,7 @@ class TestLanguageServerSymbols: assert user_management_roots == user_management_node["children"] @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_dir_overview(self, language_server: SyncLanguageServer) -> None: + def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None: """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") @@ -439,7 +439,7 @@ class TestLanguageServerSymbols: assert symbol in services_symbols @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_document_overview(self, language_server: SyncLanguageServer) -> None: + def test_request_document_overview(self, language_server: SolidLanguageServer) -> None: """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")) @@ -449,7 +449,7 @@ class TestLanguageServerSymbols: assert {"UserStats", "UserManager", "process_user_data", "main"}.issubset(symbol_names) @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_containing_symbol_of_var_is_file(self, language_server: SyncLanguageServer) -> None: + def test_containing_symbol_of_var_is_file(self, language_server: SolidLanguageServer) -> None: """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") diff --git a/test/multilspy/rust/test_rust_basic.py b/test/solidlsp/rust/test_rust_basic.py similarity index 84% rename from test/multilspy/rust/test_rust_basic.py rename to test/solidlsp/rust/test_rust_basic.py index 02e9d50..d0576de 100644 --- a/test/multilspy/rust/test_rust_basic.py +++ b/test/solidlsp/rust/test_rust_basic.py @@ -2,15 +2,15 @@ import os import pytest -from multilspy import SyncLanguageServer -from multilspy.multilspy_config import Language -from multilspy.multilspy_utils import SymbolUtils +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from solidlsp.ls_utils import SymbolUtils @pytest.mark.rust class TestRustLanguageServer: @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_find_references_raw(self, language_server: SyncLanguageServer) -> None: + def test_find_references_raw(self, language_server: SolidLanguageServer) -> None: # Directly test the request_references method for the add function file_path = os.path.join("src", "lib.rs") symbols = language_server.request_document_symbols(file_path) @@ -27,14 +27,14 @@ class TestRustLanguageServer: ), "main.rs should reference add (raw, tried all positions in selectionRange)" @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_find_symbol(self, language_server: SyncLanguageServer) -> None: + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add function not found in symbol tree" # Add more as needed based on test_repo @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> None: + def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: # Find references to 'add' defined in lib.rs, should be referenced from main.rs file_path = os.path.join("src", "lib.rs") symbols = language_server.request_document_symbols(file_path) @@ -51,7 +51,7 @@ class TestRustLanguageServer: ), "main.rs should reference add (tried all positions in selectionRange)" @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_overview_methods(self, language_server: SyncLanguageServer) -> None: + def test_overview_methods(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add missing from overview" diff --git a/test/multilspy/typescript/test_typescript_basic.py b/test/solidlsp/typescript/test_typescript_basic.py similarity index 82% rename from test/multilspy/typescript/test_typescript_basic.py rename to test/solidlsp/typescript/test_typescript_basic.py index 442b5a1..1410c5c 100644 --- a/test/multilspy/typescript/test_typescript_basic.py +++ b/test/solidlsp/typescript/test_typescript_basic.py @@ -2,22 +2,22 @@ import os import pytest -from multilspy import SyncLanguageServer -from multilspy.multilspy_config import Language -from multilspy.multilspy_utils import SymbolUtils +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from solidlsp.ls_utils import SymbolUtils @pytest.mark.typescript class TestTypescriptLanguageServer: @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) - def test_find_symbol(self, language_server: SyncLanguageServer) -> None: + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "DemoClass"), "DemoClass not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "helperFunction"), "helperFunction not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "printValue"), "printValue method not found in symbol tree" @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) - def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> None: + def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: file_path = os.path.join("index.ts") symbols = language_server.request_document_symbols(file_path) helper_symbol = None diff --git a/uv.lock b/uv.lock index 8be9884..c731811 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,10 @@ version = 1 -revision = 2 +revision = 1 requires-python = "==3.11.*" [[package]] name = "agno" -version = "1.2.15" +version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser" }, @@ -20,23 +20,23 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2d/e6/5f747db61e35500750b9f595e8836649bd0d7a22ed0c2ec41167e5dec8bc/agno-1.2.15.tar.gz", hash = "sha256:b589d1277c756e5d8ed5c5ad2052d7910e1c2ad007fa3c830a4e8332809f2099", size = 467123, upload-time = "2025-04-09T00:13:42.792Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/8b/20a6d88f0740d7a17c45016048cb4d278eb5c78a2f75c443a566159420fd/agno-1.6.2.tar.gz", hash = "sha256:f78492021e84ef3ec730d502a89963d11f42cbefd872dfe1f353975489dd9cc9", size = 635136 } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/4b/05c42f236c18d11b29af368910df398a8a67e4f5c5681ead30ebde07a548/agno-1.2.15-py3-none-any.whl", hash = "sha256:e82b57f2980bff1ce7574a16ed963485f2efa3519e9322e17fa6ba07719bd5e2", size = 616626, upload-time = "2025-04-09T00:13:40.805Z" }, + { url = "https://files.pythonhosted.org/packages/72/b9/45456c06f034553327ddcd2e8db6fd224144d0fd2e62fd1e7b07acc18353/agno-1.6.2-py3-none-any.whl", hash = "sha256:7f831893f7b70a931438390650843096da92f42961b5ce63660b35a79a94d0a7", size = 851217 }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] [[package]] name = "anthropic" -version = "0.49.0" +version = "0.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -47,9 +47,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/e3/a88c8494ce4d1a88252b9e053607e885f9b14d0a32273d47b727cbee4228/anthropic-0.49.0.tar.gz", hash = "sha256:c09e885b0f674b9119b4f296d8508907f6cff0009bc20d5cf6b35936c40b4398", size = 210016, upload-time = "2025-02-28T19:35:47.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/28/80cb9bb6e7ce77d404145b51da4257455805c17f0a6be528ff3286e3882f/anthropic-0.54.0.tar.gz", hash = "sha256:5e6f997d97ce8e70eac603c3ec2e7f23addeff953fbbb76b19430562bb6ba815", size = 312376 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/74/5d90ad14d55fbe3f9c474fdcb6e34b4bed99e3be8efac98734a5ddce88c1/anthropic-0.49.0-py3-none-any.whl", hash = "sha256:bbc17ad4e7094988d2fa86b87753ded8dce12498f4b85fe5810f208f454a8375", size = 243368, upload-time = "2025-02-28T19:35:44.963Z" }, + { url = "https://files.pythonhosted.org/packages/de/b9/6ffb48e82c5e97b03cecee872d134a6b6666c2767b2d32ed709f3a60a8fe/anthropic-0.54.0-py3-none-any.whl", hash = "sha256:c1062a0a905daeec17ca9c06c401e4b3f24cb0495841d29d752568a1d4018d56", size = 288774 }, ] [[package]] @@ -61,18 +61,18 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, ] [[package]] name = "asttokens" version = "3.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, ] [[package]] @@ -86,13 +86,13 @@ dependencies = [ { name = "pathspec" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, - { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, ] [package.optional-dependencies] @@ -101,92 +101,101 @@ jupyter = [ { name = "tokenize-rt" }, ] +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + [[package]] name = "cachetools" version = "5.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, ] [[package]] name = "certifi" -version = "2025.1.31" +version = "2025.6.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753 } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" }, + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650 }, ] [[package]] name = "charset-normalizer" -version = "3.4.1" +version = "3.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload-time = "2024-12-24T18:12:35.43Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995, upload-time = "2024-12-24T18:10:12.838Z" }, - { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471, upload-time = "2024-12-24T18:10:14.101Z" }, - { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831, upload-time = "2024-12-24T18:10:15.512Z" }, - { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335, upload-time = "2024-12-24T18:10:18.369Z" }, - { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862, upload-time = "2024-12-24T18:10:19.743Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673, upload-time = "2024-12-24T18:10:21.139Z" }, - { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211, upload-time = "2024-12-24T18:10:22.382Z" }, - { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039, upload-time = "2024-12-24T18:10:24.802Z" }, - { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939, upload-time = "2024-12-24T18:10:26.124Z" }, - { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075, upload-time = "2024-12-24T18:10:30.027Z" }, - { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340, upload-time = "2024-12-24T18:10:32.679Z" }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205, upload-time = "2024-12-24T18:10:34.724Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441, upload-time = "2024-12-24T18:10:37.574Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, ] [[package]] name = "click" -version = "8.1.8" +version = "8.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "decorator" version = "5.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, ] [[package]] name = "distro" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, ] [[package]] name = "docstring-parser" version = "0.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565, upload-time = "2024-03-15T10:39:44.419Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533, upload-time = "2024-03-15T10:39:41.527Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533 }, ] [[package]] @@ -197,47 +206,33 @@ dependencies = [ { name = "python-dotenv" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892 }, ] [[package]] name = "executing" version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, ] [[package]] -name = "fastapi" -version = "0.115.12" +name = "flask" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" }, -] - -[[package]] -name = "fastmcp" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "mcp" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "python-dotenv" }, - { name = "typer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/84/17b549133263d7ee77141970769bbc401525526bf1af043ea6842bce1a55/fastmcp-0.4.1.tar.gz", hash = "sha256:713ad3b8e4e04841c9e2f3ca022b053adb89a286ceffad0d69ae7b56f31cbe64", size = 785575, upload-time = "2024-12-09T13:33:11.101Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0b/008a340435fe8f0879e9d608f48af2737ad48440e09bd33b83b3fd03798b/fastmcp-0.4.1-py3-none-any.whl", hash = "sha256:664b42c376fb89ec90a50c9433f5a1f4d24f36696d6c41b024b427ae545f9619", size = 35282, upload-time = "2024-12-09T13:33:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305 }, ] [[package]] @@ -247,9 +242,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, ] [[package]] @@ -259,28 +254,28 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, ] [[package]] name = "google-auth" -version = "2.38.0" +version = "2.40.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866, upload-time = "2025-01-23T01:05:29.119Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770, upload-time = "2025-01-23T01:05:26.572Z" }, + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137 }, ] [[package]] name = "google-genai" -version = "1.10.0" +version = "1.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -291,48 +286,48 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/7a/224e2f70c835202042969685ee3da00a6475508d1b64f0f1e90144f96beb/google_genai-1.10.0.tar.gz", hash = "sha256:f59423e0f155dc66b7792c8a0e6724c75c72dc699d1eb7907d4d0006d4f6186f", size = 156355, upload-time = "2025-04-09T03:48:21.961Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/12/ad9f08be2ca85122ca50ac69ae70454f18a3c7d840bcc4ed43f517ab47be/google_genai-1.20.0.tar.gz", hash = "sha256:dccca78f765233844b1bd4f1f7a2237d9a76fe6038cf9aa72c0cd991e3c107b5", size = 201550 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/a0/56839a2e202d79c773edd1c1db124da8eb2a7b657267a888080b678d0369/google_genai-1.10.0-py3-none-any.whl", hash = "sha256:41b105a2fcf8a027fc45cc16694cd559b8cd1272eab7345ad58cfa2c353bf34f", size = 154705, upload-time = "2025-04-09T03:48:20.49Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b4/08f3ea414060a7e7d4436c08bb22d03dabef74cc05ef13ef8cd846156d5b/google_genai-1.20.0-py3-none-any.whl", hash = "sha256:ccd61d6ebcb14f5c778b817b8010e3955ae4f6ddfeaabf65f42f6d5e3e5a8125", size = 203039 }, ] [[package]] name = "greenlet" -version = "3.1.1" +version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752 } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload-time = "2024-09-20T17:07:22.332Z" }, - { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" }, - { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517, upload-time = "2024-09-20T17:44:24.101Z" }, - { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831, upload-time = "2024-09-20T17:08:40.577Z" }, - { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413, upload-time = "2024-09-20T17:08:31.728Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619, upload-time = "2024-09-20T17:44:14.222Z" }, - { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198, upload-time = "2024-09-20T17:09:23.903Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930, upload-time = "2024-09-20T17:25:18.656Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219 }, + { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422 }, + { url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375 }, + { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627 }, + { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502 }, + { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498 }, + { url = "https://files.pythonhosted.org/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977 }, + { url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017 }, ] [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload-time = "2022-09-25T15:40:01.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload-time = "2022-09-25T15:39:59.68Z" }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, ] [[package]] name = "httpcore" -version = "1.0.7" +version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196, upload-time = "2024-11-15T12:30:47.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551, upload-time = "2024-11-15T12:30:45.782Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, ] [[package]] @@ -345,41 +340,41 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] [[package]] name = "httpx-sse" version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] [[package]] name = "ipython" -version = "9.1.0" +version = "9.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -394,9 +389,9 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/9a/6b8984bedc990f3a4aa40ba8436dea27e23d26a64527de7c2e5e12e76841/ipython-9.1.0.tar.gz", hash = "sha256:a47e13a5e05e02f3b8e1e7a0f9db372199fe8c3763532fe7a1e0379e4e135f16", size = 4373688, upload-time = "2025-04-07T10:18:28.704Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/09/4c7e06b96fbd203e06567b60fb41b06db606b6a82db6db7b2c85bb72a15c/ipython-9.3.0.tar.gz", hash = "sha256:79eb896f9f23f50ad16c3bc205f686f6e030ad246cc309c6279a242b14afe9d8", size = 4426460 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/9d/4ff2adf55d1b6e3777b0303fdbe5b723f76e46cba4a53a32fe82260d2077/ipython-9.1.0-py3-none-any.whl", hash = "sha256:2df07257ec2f84a6b346b8d83100bcf8fa501c6e01ab75cd3799b0bb253b3d2a", size = 604053, upload-time = "2025-04-07T10:18:24.869Z" }, + { url = "https://files.pythonhosted.org/packages/3c/99/9ed3d52d00f1846679e3aa12e2326ac7044b5e7f90dc822b60115fa533ca/ipython-9.3.0-py3-none-any.whl", hash = "sha256:1a0b6dd9221a1f5dddf725b57ac0cb6fddc7b5f470576231ae9162b9b3455a04", size = 605320 }, ] [[package]] @@ -406,9 +401,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, ] [[package]] @@ -418,9 +422,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, ] [[package]] @@ -430,38 +434,38 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, ] [[package]] name = "jiter" -version = "0.9.0" +version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/c2/e4562507f52f0af7036da125bb699602ead37a2332af0788f8e0a3417f36/jiter-0.9.0.tar.gz", hash = "sha256:aadba0964deb424daa24492abc3d229c60c4a31bfee205aedbf1acc7639d7893", size = 162604, upload-time = "2025-03-10T21:37:03.278Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759 } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/44/e241a043f114299254e44d7e777ead311da400517f179665e59611ab0ee4/jiter-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6c4d99c71508912a7e556d631768dcdef43648a93660670986916b297f1c54af", size = 314654, upload-time = "2025-03-10T21:35:23.939Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/a7e5e42db9fa262baaa9489d8d14ca93f8663e7f164ed5e9acc9f467fc00/jiter-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f60fb8ce7df529812bf6c625635a19d27f30806885139e367af93f6e734ef58", size = 320909, upload-time = "2025-03-10T21:35:26.127Z" }, - { url = "https://files.pythonhosted.org/packages/60/bf/8ebdfce77bc04b81abf2ea316e9c03b4a866a7d739cf355eae4d6fd9f6fe/jiter-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51c4e1a4f8ea84d98b7b98912aa4290ac3d1eabfde8e3c34541fae30e9d1f08b", size = 341733, upload-time = "2025-03-10T21:35:27.94Z" }, - { url = "https://files.pythonhosted.org/packages/a8/4e/754ebce77cff9ab34d1d0fa0fe98f5d42590fd33622509a3ba6ec37ff466/jiter-0.9.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f4c677c424dc76684fea3e7285a7a2a7493424bea89ac441045e6a1fb1d7b3b", size = 365097, upload-time = "2025-03-10T21:35:29.605Z" }, - { url = "https://files.pythonhosted.org/packages/32/2c/6019587e6f5844c612ae18ca892f4cd7b3d8bbf49461ed29e384a0f13d98/jiter-0.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2221176dfec87f3470b21e6abca056e6b04ce9bff72315cb0b243ca9e835a4b5", size = 406603, upload-time = "2025-03-10T21:35:31.696Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/c9e6546c817ab75a1a7dab6dcc698e62e375e1017113e8e983fccbd56115/jiter-0.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c7adb66f899ffa25e3c92bfcb593391ee1947dbdd6a9a970e0d7e713237d572", size = 396625, upload-time = "2025-03-10T21:35:33.182Z" }, - { url = "https://files.pythonhosted.org/packages/be/bd/976b458add04271ebb5a255e992bd008546ea04bb4dcadc042a16279b4b4/jiter-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98d27330fdfb77913c1097a7aab07f38ff2259048949f499c9901700789ac15", size = 351832, upload-time = "2025-03-10T21:35:35.394Z" }, - { url = "https://files.pythonhosted.org/packages/07/51/fe59e307aaebec9265dbad44d9d4381d030947e47b0f23531579b9a7c2df/jiter-0.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eda3f8cc74df66892b1d06b5d41a71670c22d95a1ca2cbab73654745ce9d0419", size = 384590, upload-time = "2025-03-10T21:35:37.171Z" }, - { url = "https://files.pythonhosted.org/packages/db/55/5dcd2693794d8e6f4889389ff66ef3be557a77f8aeeca8973a97a7c00557/jiter-0.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dd5ab5ddc11418dce28343123644a100f487eaccf1de27a459ab36d6cca31043", size = 520690, upload-time = "2025-03-10T21:35:38.717Z" }, - { url = "https://files.pythonhosted.org/packages/54/d5/9f51dc90985e9eb251fbbb747ab2b13b26601f16c595a7b8baba964043bd/jiter-0.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:42f8a68a69f047b310319ef8e2f52fdb2e7976fb3313ef27df495cf77bcad965", size = 512649, upload-time = "2025-03-10T21:35:40.157Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e5/4e385945179bcf128fa10ad8dca9053d717cbe09e258110e39045c881fe5/jiter-0.9.0-cp311-cp311-win32.whl", hash = "sha256:a25519efb78a42254d59326ee417d6f5161b06f5da827d94cf521fed961b1ff2", size = 206920, upload-time = "2025-03-10T21:35:41.72Z" }, - { url = "https://files.pythonhosted.org/packages/4c/47/5e0b94c603d8e54dd1faab439b40b832c277d3b90743e7835879ab663757/jiter-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:923b54afdd697dfd00d368b7ccad008cccfeb1efb4e621f32860c75e9f25edbd", size = 210119, upload-time = "2025-03-10T21:35:43.46Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473 }, + { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971 }, + { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574 }, + { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028 }, + { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083 }, + { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821 }, + { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174 }, + { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869 }, + { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741 }, + { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527 }, + { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765 }, + { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234 }, ] [[package]] name = "joblib" version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload-time = "2025-05-23T12:04:37.097Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746 }, ] [[package]] @@ -471,27 +475,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, ] [[package]] @@ -501,14 +505,14 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, ] [[package]] name = "mcp" -version = "1.6.0" +version = "1.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -516,104 +520,106 @@ dependencies = [ { name = "httpx-sse" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "python-multipart" }, { name = "sse-starlette" }, { name = "starlette" }, - { name = "uvicorn" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031, upload-time = "2025-03-27T16:46:32.336Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f2/dc2450e566eeccf92d89a00c3e813234ad58e2ba1e31d11467a09ac4f3b9/mcp-1.9.4.tar.gz", hash = "sha256:cfb0bcd1a9535b42edaef89947b9e18a8feb49362e1cc059d6e7fc636f2cb09f", size = 333294 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077, upload-time = "2025-03-27T16:46:29.919Z" }, + { url = "https://files.pythonhosted.org/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0", size = 130232 }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] [[package]] name = "mypy" -version = "1.15.0" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, + { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139 } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338, upload-time = "2025-02-05T03:50:17.287Z" }, - { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540, upload-time = "2025-02-05T03:49:51.21Z" }, - { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051, upload-time = "2025-02-05T03:50:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751, upload-time = "2025-02-05T03:49:42.408Z" }, - { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783, upload-time = "2025-02-05T03:49:07.707Z" }, - { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618, upload-time = "2025-02-05T03:49:54.581Z" }, - { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" }, + { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498 }, + { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755 }, + { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138 }, + { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156 }, + { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426 }, + { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319 }, + { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773 }, ] [[package]] name = "mypy-extensions" -version = "1.0.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] [[package]] name = "overrides" version = "7.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, ] [[package]] name = "packaging" -version = "24.2" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] [[package]] name = "parso" version = "0.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, ] [[package]] name = "pastel" version = "0.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555, upload-time = "2020-09-16T19:21:12.43Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555 } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955 }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, ] [[package]] @@ -623,94 +629,94 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, ] [[package]] name = "platformdirs" -version = "4.3.7" +version = "4.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, ] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] [[package]] name = "poethepoet" -version = "0.33.1" +version = "0.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/1d/ec87271390cc5fafdd5996137331ad3a7ce99b715e4ee68db554d202817f/poethepoet-0.33.1.tar.gz", hash = "sha256:8775e09b64f773278b5483659ff238a708723491efadeedd1c2cbf773558cb4c", size = 62536, upload-time = "2025-03-15T20:38:56.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/b1/d4f4361b278fae10f6074675385ce3acf53c647f8e6eeba22c652f8ba985/poethepoet-0.35.0.tar.gz", hash = "sha256:b396ae862d7626e680bbd0985b423acf71634ce93a32d8b5f38340f44f5fbc3e", size = 66006 } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/ea/c476bfec360eb6831ce46df2719f76d1132b9a87da11c302081a9def5fce/poethepoet-0.33.1-py3-none-any.whl", hash = "sha256:b86d80a81b2ca4e4ce8e8f716cc6004a1a1cdead027778bc07d1c26cb3664770", size = 83512, upload-time = "2025-03-15T20:38:55.312Z" }, + { url = "https://files.pythonhosted.org/packages/38/08/abc2d7e2400dd8906e3208f9b88ac610f097d7ee0c7a1fa4a157b49a9e86/poethepoet-0.35.0-py3-none-any.whl", hash = "sha256:bed5ae1fd63f179dfa67aabb93fa253d79695c69667c927d8b24ff378799ea75", size = 87164 }, ] [[package]] name = "prompt-toolkit" -version = "3.0.50" +version = "3.0.51" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087, upload-time = "2025-01-20T15:55:35.072Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816, upload-time = "2025-01-20T15:55:29.98Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 }, ] [[package]] name = "psutil" version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, - { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, - { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, ] [[package]] name = "pure-eval" version = "0.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, ] [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, ] [[package]] @@ -720,14 +726,14 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, ] [[package]] name = "pydantic" -version = "2.11.3" +version = "2.11.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -735,133 +741,135 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513, upload-time = "2025-04-08T13:27:06.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591, upload-time = "2025-04-08T13:27:03.789Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 }, ] [[package]] name = "pydantic-core" -version = "2.33.1" +version = "2.33.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395, upload-time = "2025-04-02T09:49:41.8Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224, upload-time = "2025-04-02T09:47:04.199Z" }, - { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845, upload-time = "2025-04-02T09:47:05.686Z" }, - { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029, upload-time = "2025-04-02T09:47:07.042Z" }, - { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784, upload-time = "2025-04-02T09:47:08.63Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075, upload-time = "2025-04-02T09:47:10.267Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849, upload-time = "2025-04-02T09:47:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794, upload-time = "2025-04-02T09:47:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237, upload-time = "2025-04-02T09:47:14.355Z" }, - { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351, upload-time = "2025-04-02T09:47:15.676Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914, upload-time = "2025-04-02T09:47:17Z" }, - { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385, upload-time = "2025-04-02T09:47:18.631Z" }, - { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765, upload-time = "2025-04-02T09:47:20.34Z" }, - { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688, upload-time = "2025-04-02T09:47:22.029Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185, upload-time = "2025-04-02T09:47:23.385Z" }, - { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858, upload-time = "2025-04-02T09:49:03.419Z" }, - { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745, upload-time = "2025-04-02T09:49:05.391Z" }, - { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188, upload-time = "2025-04-02T09:49:07.352Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479, upload-time = "2025-04-02T09:49:09.304Z" }, - { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415, upload-time = "2025-04-02T09:49:11.25Z" }, - { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623, upload-time = "2025-04-02T09:49:13.292Z" }, - { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175, upload-time = "2025-04-02T09:49:15.597Z" }, - { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674, upload-time = "2025-04-02T09:49:17.61Z" }, - { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951, upload-time = "2025-04-02T09:49:19.559Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 }, ] [[package]] name = "pydantic-settings" -version = "2.8.1" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550, upload-time = "2025-02-27T10:10:32.338Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839, upload-time = "2025-02-27T10:10:30.711Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] [[package]] name = "pyright" -version = "1.1.399" +version = "1.1.402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954, upload-time = "2025-04-10T04:40:25.703Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/04/ce0c132d00e20f2d2fb3b3e7c125264ca8b909e693841210534b1ea1752f/pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683", size = 3888207 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584, upload-time = "2025-04-10T04:40:23.502Z" }, + { url = "https://files.pythonhosted.org/packages/fe/37/1a1c62d955e82adae588be8e374c7f77b165b6cb4203f7d581269959abbc/pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982", size = 5624004 }, ] [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232 } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797 }, ] [[package]] name = "python-dotenv" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] [[package]] name = "python-multipart" version = "0.0.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, ] [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -869,9 +877,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, ] [[package]] @@ -882,75 +890,75 @@ dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, ] [[package]] name = "rsa" -version = "4.9" +version = "4.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711, upload-time = "2022-07-20T10:28:36.115Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315, upload-time = "2022-07-20T10:28:34.978Z" }, + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, ] [[package]] name = "ruamel-yaml" -version = "0.18.10" +version = "0.18.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447, upload-time = "2025-01-06T14:08:51.334Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/87/6da0df742a4684263261c253f00edd5829e6aca970fff69e75028cccc547/ruamel.yaml-0.18.14.tar.gz", hash = "sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7", size = 145511 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729, upload-time = "2025-01-06T14:08:47.471Z" }, + { url = "https://files.pythonhosted.org/packages/af/6d/6fe4805235e193aad4aaf979160dd1f3c487c57d48b810c816e6e842171b/ruamel.yaml-0.18.14-py3-none-any.whl", hash = "sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2", size = 118570 }, ] [[package]] name = "ruamel-yaml-clib" version = "0.2.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315, upload-time = "2024-10-20T10:10:56.22Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224, upload-time = "2024-10-20T10:12:45.162Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480, upload-time = "2024-10-20T10:12:46.758Z" }, - { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068, upload-time = "2024-10-20T10:12:48.605Z" }, - { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012, upload-time = "2024-10-20T10:12:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352, upload-time = "2024-10-21T11:26:41.438Z" }, - { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344, upload-time = "2024-10-21T11:26:43.62Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498, upload-time = "2024-12-11T19:58:15.592Z" }, - { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205, upload-time = "2024-10-20T10:12:52.865Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185, upload-time = "2024-10-20T10:12:54.652Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224 }, + { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480 }, + { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068 }, + { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012 }, + { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352 }, + { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344 }, + { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498 }, + { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205 }, + { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185 }, ] [[package]] name = "ruff" -version = "0.11.5" +version = "0.11.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488, upload-time = "2025-04-10T17:13:29.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054 } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150, upload-time = "2025-04-10T17:12:37.886Z" }, - { url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637, upload-time = "2025-04-10T17:12:41.602Z" }, - { url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012, upload-time = "2025-04-10T17:12:44.584Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338, upload-time = "2025-04-10T17:12:47.172Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277, upload-time = "2025-04-10T17:12:50.628Z" }, - { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614, upload-time = "2025-04-10T17:12:53.783Z" }, - { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873, upload-time = "2025-04-10T17:12:56.956Z" }, - { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190, upload-time = "2025-04-10T17:13:00.194Z" }, - { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301, upload-time = "2025-04-10T17:13:03.246Z" }, - { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132, upload-time = "2025-04-10T17:13:06.209Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937, upload-time = "2025-04-10T17:13:08.855Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683, upload-time = "2025-04-10T17:13:11.378Z" }, - { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217, upload-time = "2025-04-10T17:13:14.565Z" }, - { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521, upload-time = "2025-04-10T17:13:17.8Z" }, - { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697, upload-time = "2025-04-10T17:13:20.582Z" }, - { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665, upload-time = "2025-04-10T17:13:23.349Z" }, - { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287, upload-time = "2025-04-10T17:13:26.538Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516 }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083 }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024 }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324 }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416 }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197 }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615 }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080 }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315 }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640 }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364 }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462 }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028 }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992 }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944 }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669 }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928 }, ] [[package]] @@ -960,9 +968,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/b1/9dba08e2d8ea739b58d1ac393c066169f1ca6506c86f0322d6e56e600ae2/sensai_utils-1.4.0.tar.gz", hash = "sha256:2d32bdcc91fd1428c5cae0181e98623142d2d5f7e115e23d585a842dd9dc59ba", size = 56317, upload-time = "2025-01-27T22:47:51.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/b1/9dba08e2d8ea739b58d1ac393c066169f1ca6506c86f0322d6e56e600ae2/sensai_utils-1.4.0.tar.gz", hash = "sha256:2d32bdcc91fd1428c5cae0181e98623142d2d5f7e115e23d585a842dd9dc59ba", size = 56317 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/df/49cc942b653427df0bbd3b9a2fb1e85652df3d4c1d9f6dcbfce1b6155716/sensai_utils-1.4.0-py3-none-any.whl", hash = "sha256:ed6fc57552620e43b33cf364ea0bc0fd7df39391069dd7b621b113ef55547507", size = 63214, upload-time = "2025-01-27T22:47:49.606Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/49cc942b653427df0bbd3b9a2fb1e85652df3d4c1d9f6dcbfce1b6155716/sensai_utils-1.4.0-py3-none-any.whl", hash = "sha256:ed6fc57552620e43b33cf364ea0bc0fd7df39391069dd7b621b113ef55547507", size = 63214 }, ] [[package]] @@ -972,8 +980,7 @@ source = { editable = "." } dependencies = [ { name = "docstring-parser" }, { name = "dotenv" }, - { name = "fastapi" }, - { name = "fastmcp" }, + { name = "flask" }, { name = "jinja2" }, { name = "joblib" }, { name = "mcp" }, @@ -987,6 +994,7 @@ dependencies = [ { name = "requests" }, { name = "ruamel-yaml" }, { name = "sensai-utils" }, + { name = "tqdm" }, { name = "types-pyyaml" }, ] @@ -1020,8 +1028,7 @@ requires-dist = [ { name = "black", extras = ["jupyter"], marker = "extra == 'dev'", specifier = ">=23.7.0" }, { name = "docstring-parser", specifier = ">=0.16" }, { name = "dotenv", specifier = ">=0.9.9" }, - { name = "fastapi", specifier = ">=0.115.12" }, - { name = "fastmcp", specifier = ">=0.4.1" }, + { name = "flask", specifier = ">=3.0.0" }, { name = "google-genai", marker = "extra == 'google'", specifier = ">=1.8.0" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", marker = "extra == 'dev'" }, @@ -1044,6 +1051,7 @@ requires-dist = [ { name = "sqlalchemy", marker = "extra == 'agno'", specifier = ">=2.0.40" }, { name = "syrupy", marker = "extra == 'dev'", specifier = ">=4.9.1" }, { name = "toml-sort", marker = "extra == 'dev'", specifier = ">=0.24.2" }, + { name = "tqdm", specifier = ">=4.67.1" }, { name = "types-pyyaml", specifier = ">=6.0.12.20241230" }, { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12.20241230" }, ] @@ -1053,61 +1061,60 @@ provides-extras = ["dev", "agno", "anthropic", "google"] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] [[package]] name = "smmap" version = "5.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] [[package]] name = "sqlalchemy" -version = "2.0.40" +version = "2.0.41" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299, upload-time = "2025-03-27T17:52:31.876Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/7e/55044a9ec48c3249bb38d5faae93f09579c35e862bb318ebd1ed7a1994a5/sqlalchemy-2.0.40-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6bacab7514de6146a1976bc56e1545bee247242fab030b89e5f70336fc0003e", size = 2114025, upload-time = "2025-03-27T18:49:29.456Z" }, - { url = "https://files.pythonhosted.org/packages/77/0f/dcf7bba95f847aec72f638750747b12d37914f71c8cc7c133cf326ab945c/sqlalchemy-2.0.40-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5654d1ac34e922b6c5711631f2da497d3a7bffd6f9f87ac23b35feea56098011", size = 2104419, upload-time = "2025-03-27T18:49:30.75Z" }, - { url = "https://files.pythonhosted.org/packages/75/70/c86a5c20715e4fe903dde4c2fd44fc7e7a0d5fb52c1b954d98526f65a3ea/sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35904d63412db21088739510216e9349e335f142ce4a04b69e2528020ee19ed4", size = 3222720, upload-time = "2025-03-27T18:44:29.871Z" }, - { url = "https://files.pythonhosted.org/packages/12/cf/b891a8c1d0c27ce9163361664c2128c7a57de3f35000ea5202eb3a2917b7/sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7a80ed86d6aaacb8160a1caef6680d4ddd03c944d985aecee940d168c411d1", size = 3222682, upload-time = "2025-03-27T18:55:20.097Z" }, - { url = "https://files.pythonhosted.org/packages/15/3f/7709d8c8266953d945435a96b7f425ae4172a336963756b58e996fbef7f3/sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:519624685a51525ddaa7d8ba8265a1540442a2ec71476f0e75241eb8263d6f51", size = 3159542, upload-time = "2025-03-27T18:44:31.333Z" }, - { url = "https://files.pythonhosted.org/packages/85/7e/717eaabaf0f80a0132dc2032ea8f745b7a0914451c984821a7c8737fb75a/sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ee5f9999a5b0e9689bed96e60ee53c3384f1a05c2dd8068cc2e8361b0df5b7a", size = 3179864, upload-time = "2025-03-27T18:55:21.784Z" }, - { url = "https://files.pythonhosted.org/packages/e4/cc/03eb5dfcdb575cbecd2bd82487b9848f250a4b6ecfb4707e834b4ce4ec07/sqlalchemy-2.0.40-cp311-cp311-win32.whl", hash = "sha256:c0cae71e20e3c02c52f6b9e9722bca70e4a90a466d59477822739dc31ac18b4b", size = 2084675, upload-time = "2025-03-27T18:48:55.915Z" }, - { url = "https://files.pythonhosted.org/packages/9a/48/440946bf9dc4dc231f4f31ef0d316f7135bf41d4b86aaba0c0655150d370/sqlalchemy-2.0.40-cp311-cp311-win_amd64.whl", hash = "sha256:574aea2c54d8f1dd1699449f332c7d9b71c339e04ae50163a3eb5ce4c4325ee4", size = 2110099, upload-time = "2025-03-27T18:48:57.45Z" }, - { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894, upload-time = "2025-03-27T18:40:43.796Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232 }, + { url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897 }, + { url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313 }, + { url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807 }, + { url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632 }, + { url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642 }, + { url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475 }, + { url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903 }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224 }, ] [[package]] name = "sse-starlette" -version = "2.2.1" +version = "2.3.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376, upload-time = "2024-12-25T09:09:30.616Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120, upload-time = "2024-12-25T09:09:26.761Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606 }, ] [[package]] @@ -1119,21 +1126,21 @@ dependencies = [ { name = "executing" }, { name = "pure-eval" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, ] [[package]] name = "starlette" -version = "0.46.1" +version = "0.46.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102, upload-time = "2025-03-08T10:55:34.504Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995, upload-time = "2025-03-08T10:55:32.662Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, ] [[package]] @@ -1143,18 +1150,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/f8/022d8704a3314f3e96dbd6bbd16ebe119ce30e35f41aabfa92345652fceb/syrupy-4.9.1.tar.gz", hash = "sha256:b7d0fcadad80a7d2f6c4c71917918e8ebe2483e8c703dfc8d49cdbb01081f9a4", size = 52492, upload-time = "2025-03-24T01:36:37.225Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/f8/022d8704a3314f3e96dbd6bbd16ebe119ce30e35f41aabfa92345652fceb/syrupy-4.9.1.tar.gz", hash = "sha256:b7d0fcadad80a7d2f6c4c71917918e8ebe2483e8c703dfc8d49cdbb01081f9a4", size = 52492 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/9d/aef9ec5fd5a4ee2f6a96032c4eda5888c5c7cec65cef6b28c4fc37671d88/syrupy-4.9.1-py3-none-any.whl", hash = "sha256:b94cc12ed0e5e75b448255430af642516842a2374a46936dd2650cfb6dd20eda", size = 52214, upload-time = "2025-03-24T01:36:35.278Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9d/aef9ec5fd5a4ee2f6a96032c4eda5888c5c7cec65cef6b28c4fc37671d88/syrupy-4.9.1-py3-none-any.whl", hash = "sha256:b94cc12ed0e5e75b448255430af642516842a2374a46936dd2650cfb6dd20eda", size = 52214 }, ] [[package]] name = "tokenize-rt" -version = "6.1.0" +version = "6.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/0a/5854d8ced8c1e00193d1353d13db82d7f813f99bd5dcb776ce3e2a4c0d19/tokenize_rt-6.1.0.tar.gz", hash = "sha256:e8ee836616c0877ab7c7b54776d2fefcc3bde714449a206762425ae114b53c86", size = 5506, upload-time = "2024-10-22T00:14:59.189Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/ed/8f07e893132d5051d86a553e749d5c89b2a4776eb3a579b72ed61f8559ca/tokenize_rt-6.2.0.tar.gz", hash = "sha256:8439c042b330c553fdbe1758e4a05c0ed460dbbbb24a606f11f0dee75da4cad6", size = 5476 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/ba/576aac29b10dfa49a6ce650001d1bb31f81e734660555eaf144bfe5b8995/tokenize_rt-6.1.0-py2.py3-none-any.whl", hash = "sha256:d706141cdec4aa5f358945abe36b911b8cbdc844545da99e811250c0cee9b6fc", size = 6015, upload-time = "2024-10-22T00:14:57.469Z" }, + { url = "https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl", hash = "sha256:a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44", size = 6004 }, ] [[package]] @@ -1164,51 +1171,63 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/55/b128ee446606e9f2e49582bb1e462a255317401fd7adb0b2114e50006d01/toml_sort-0.24.2.tar.gz", hash = "sha256:20cb7c5e9de9c871990f1594f028aaf8bd0f78d7ce37995a22289dc157a45b79", size = 17973, upload-time = "2024-11-19T14:03:05.092Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/55/b128ee446606e9f2e49582bb1e462a255317401fd7adb0b2114e50006d01/toml_sort-0.24.2.tar.gz", hash = "sha256:20cb7c5e9de9c871990f1594f028aaf8bd0f78d7ce37995a22289dc157a45b79", size = 17973 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/55/7a7c02de848eef670bd018b62122ce32a6e227203e5a626c96d4935958a6/toml_sort-0.24.2-py3-none-any.whl", hash = "sha256:d81d299789a1fd9dd306a4021951eab5fc0c5486599e277fcf8142c7735f3308", size = 20162, upload-time = "2024-11-19T14:03:03.338Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/7a7c02de848eef670bd018b62122ce32a6e227203e5a626c96d4935958a6/toml_sort-0.24.2-py3-none-any.whl", hash = "sha256:d81d299789a1fd9dd306a4021951eab5fc0c5486599e277fcf8142c7735f3308", size = 20162 }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] [[package]] name = "tomlkit" -version = "0.13.2" +version = "0.13.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885, upload-time = "2024-08-14T08:19:41.488Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955, upload-time = "2024-08-14T08:19:40.05Z" }, + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, ] [[package]] name = "traitlets" version = "5.14.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, ] [[package]] name = "typer" -version = "0.15.2" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1216,88 +1235,100 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711, upload-time = "2025-02-27T19:17:34.807Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061, upload-time = "2025-02-27T19:17:32.111Z" }, + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317 }, ] [[package]] name = "types-pyyaml" -version = "6.0.12.20250402" +version = "6.0.12.20250516" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282, upload-time = "2025-04-02T02:56:00.235Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/22/59e2aeb48ceeee1f7cd4537db9568df80d62bdb44a7f9e743502ea8aab9c/types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba", size = 17378 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329, upload-time = "2025-04-02T02:55:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312 }, ] [[package]] name = "typing-extensions" -version = "4.13.2" +version = "4.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, ] [[package]] name = "typing-inspection" -version = "0.4.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, ] [[package]] name = "urllib3" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, ] [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.34.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568, upload-time = "2024-12-15T13:33:30.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315, upload-time = "2024-12-15T13:33:27.467Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431 }, ] [[package]] name = "wcwidth" version = "0.2.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, ] [[package]] name = "websockets" version = "15.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423 }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082 }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330 }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878 }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883 }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252 }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521 }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958 }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918 }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, ]