From 638f452d0381d20eafdddc3461812467ce1de8f8 Mon Sep 17 00:00:00 2001 From: s5k <40916972+s5k@users.noreply.github.com> Date: Mon, 21 Apr 2025 21:51:34 +0700 Subject: [PATCH 01/61] Add PHP support with Intelephense LSP --- src/multilspy/language_server.py | 4 + .../intelephense/initialize_params.json | 36 ++++ .../intelephense/intelephense.py | 195 ++++++++++++++++++ .../intelephense/runtime_dependencies.json | 10 + src/multilspy/multilspy_config.py | 3 + 5 files changed, 248 insertions(+) create mode 100644 src/multilspy/language_servers/intelephense/initialize_params.json create mode 100644 src/multilspy/language_servers/intelephense/intelephense.py create mode 100644 src/multilspy/language_servers/intelephense/runtime_dependencies.json diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index f3ee19cb..ba636de7 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -188,6 +188,10 @@ class LanguageServer: from multilspy.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer return ClangdLanguageServer(config, logger, repository_root_path) + elif config.code_language == Language.PHP: + from multilspy.language_servers.intelephense.intelephense import Intelephense + + return Intelephense(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") diff --git a/src/multilspy/language_servers/intelephense/initialize_params.json b/src/multilspy/language_servers/intelephense/initialize_params.json new file mode 100644 index 00000000..4330560e --- /dev/null +++ b/src/multilspy/language_servers/intelephense/initialize_params.json @@ -0,0 +1,36 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "textDocument": { + "synchronization": { + "didSave": true, + "dynamicRegistration": true + }, + "completion": { + "dynamicRegistration": true, + "completionItem": { + "snippetSupport": true + } + }, + "definition": { + "dynamicRegistration": true + } + }, + "workspace": { + "workspaceFolders": true, + "didChangeConfiguration": { + "dynamicRegistration": true + } + } + }, + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/multilspy/language_servers/intelephense/intelephense.py b/src/multilspy/language_servers/intelephense/intelephense.py new file mode 100644 index 00000000..4533d590 --- /dev/null +++ b/src/multilspy/language_servers/intelephense/intelephense.py @@ -0,0 +1,195 @@ +""" +Provides PHP specific instantiation of the LanguageServer class using Intelephense. +""" + +import asyncio +import json +import shutil +import logging +import os +import subprocess +import pathlib +import stat +from contextlib import asynccontextmanager +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 InitializeParams +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_utils import PlatformUtils, PlatformId + +# Platform-specific imports +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')})() + +class Intelephense(LanguageServer): + """ + 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"] + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + """ + Setup runtime dependencies for Intelephense. + """ + platform_id = PlatformUtils.get_platform_id() + + valid_platforms = [ + PlatformId.LINUX_x64, + PlatformId.LINUX_arm64, + PlatformId.OSX, + PlatformId.OSX_x64, + PlatformId.OSX_arm64, + PlatformId.WIN_x64, + PlatformId.WIN_arm64, + ] + 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") 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 + 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 + 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 + if not os.path.exists(intelephense_ls_dir): + os.makedirs(intelephense_ls_dir, exist_ok=True) + for dependency in runtime_dependencies: + # Windows doesn't support the 'user' parameter and doesn't have pwd module + if PlatformUtils.get_platform_id().value.startswith("win"): + subprocess.run( + dependency["command"], + shell=True, + check=True, + cwd=intelephense_ls_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + else: + # On Unix-like systems, run as non-root user + user = pwd.getpwuid(os.getuid()).pw_name + subprocess.run( + dependency["command"], + shell=True, + check=True, + user=user, + cwd=intelephense_ls_dir, + stdout=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): + # 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() + 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") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + 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() + + 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["Intelephense"]: + """Start Intelephense server process""" + async def register_capability_handler(params): + return + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + async def do_nothing(params): + return + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("window/logMessage", window_log_message) + 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( + "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.server.notify.initialized({}) + self.completions_available.set() + + # Intelephense server is typically ready immediately after initialization + self.server_ready.set() + await self.server_ready.wait() + + yield self + + await self.server.shutdown() + await self.server.stop() \ No newline at end of file diff --git a/src/multilspy/language_servers/intelephense/runtime_dependencies.json b/src/multilspy/language_servers/intelephense/runtime_dependencies.json new file mode 100644 index 00000000..e3a6d7e1 --- /dev/null +++ b/src/multilspy/language_servers/intelephense/runtime_dependencies.json @@ -0,0 +1,10 @@ +{ + "_description": "Used to download the runtime dependencies for running intelephense. Obtained from https://www.npmjs.com/package/intelephense", + "runtimeDependencies": [ + { + "id": "intelephense", + "description": "Intelephense package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", + "command": "npm install --prefix ./ intelephense@1.14.4" + } + ] +} diff --git a/src/multilspy/multilspy_config.py b/src/multilspy/multilspy_config.py index bf53750a..2b4f7ca4 100644 --- a/src/multilspy/multilspy_config.py +++ b/src/multilspy/multilspy_config.py @@ -37,6 +37,7 @@ class Language(str, Enum): RUBY = "ruby" DART = "dart" CPP = "cpp" + PHP = "php" def __str__(self) -> str: return self.value @@ -65,6 +66,8 @@ class Language(str, Enum): return FilenameMatcher("*.kt", "*.kts") case self.DART: return FilenameMatcher("*.dart") + case self.PHP: + return FilenameMatcher("*.php") case _: raise ValueError(f"Unhandled language: {self}") From 9e92af474f9d154a8fb7284148f4627266a0d3dc Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 26 Apr 2025 23:24:28 +0200 Subject: [PATCH 02/61] Added cpp to project.yaml template --- myproject.template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myproject.template.yml b/myproject.template.yml index af4f6844..2722a095 100644 --- a/myproject.template.yml +++ b/myproject.template.yml @@ -2,7 +2,7 @@ # This is optional if this file is placed in the project directory under `.serena/project.yml`. project_root: /path/to/project -# language of the project (csharp, python, rust, java, typescript, javascript, go, or ruby) +# language of the project (csharp, python, rust, java, typescript, javascript, go, cpp, or ruby) # Special requirements: # * csharp: Requires the presence of a .sln file in the project folder. language: python From e491f35bbf9f0b463cb4643946d0cb4f6d15c207 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 26 Apr 2025 23:38:11 +0200 Subject: [PATCH 03/61] Readme: more setup instructions --- .gitignore | 2 +- README.md | 36 +++++++++++++++++-- ...oject.template.yml => project.template.yml | 0 3 files changed, 34 insertions(+), 4 deletions(-) rename myproject.template.yml => project.template.yml (100%) diff --git a/.gitignore b/.gitignore index b04a9643..fc862162 100644 --- a/.gitignore +++ b/.gitignore @@ -138,7 +138,7 @@ pylint.html # Serena-specific /*.yml -!myproject.demo.yml +!*.template.yml /agent-ui # dynamic LS installations /src/multilspy/language_servers/*/static diff --git a/README.md b/README.md index 15644042..d4d64945 100644 --- a/README.md +++ b/README.md @@ -150,8 +150,17 @@ Serena can be used in various ways, below you will find instructions for selecte 1. Install `uv` (instructions [here](https://docs.astral.sh/uv/getting-started/installation/)) 2. Clone the repository to `/path/to/serena`. 3. Copy `serena_config.template.yml` to `serena_config.yml` and adjust settings. -4. Copy `myproject.template.yml` to `myproject.yml` and adjust the settings specific to your project. - (Add one such file for each project you want Serena to work on.) + ```shell + cp serena_config.template.yml serena_config.yml + ``` +4. Copy `project.template.yml` to `project.yml` and adjust the settings specific to your project. + (Add one such file for each project you want Serena to work on.) We recommend that you copy + it to the `.serena` directory of your project, e.g., + ```shell + mkdir -p /myproject/.serena + cp project.template.yml /myproject/.serena/project.yml + ``` + though you can also keep it somewhere else. 5. If you want Serena to dynamically switch between projects, add the list of all project files created in the previous step to the `projects` list in `serena_config.yml`. @@ -188,7 +197,17 @@ want to use Serena. 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. +That's it! Save the config and then restart Claude Desktop. + +#### 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 --directory /path/to/serena serena-mcp-server --transport sse --port 9121 --project-file /path/to/project.yml +``` +(the `--project-file` option is optional). Then configure your client to connect to `http://localhost:9121`. 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). @@ -211,6 +230,17 @@ therefore needs to be configured with a launch command. 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 --project-file /path/to/project.yml +``` + + ### Other MCP Clients - Cline, Roo-Code, Cursor, Windsurf etc. Being an MCP Server, Serena can be included in any MCP Client. The same config as above, diff --git a/myproject.template.yml b/project.template.yml similarity index 100% rename from myproject.template.yml rename to project.template.yml From ec896329503a1664cbc6cef7d5ed8bf1a8a344b7 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 26 Apr 2025 23:50:39 +0200 Subject: [PATCH 04/61] Demo script for running tools --- CONTRIBUTING.md | 7 +++++++ scripts/demo_run_tools.py | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 scripts/demo_run_tools.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd5f2ff4..5a3d7978 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,13 @@ for you. Note: For the Windows subsystem for Linux (WSL), you may need to adjust the path for the volume. +## Running Tools Locally + +The Serena tools (and in fact all Serena code) can be executed without an LLM, and also without +any MCP specifics (though you can use the mcp inspector, if you want). + +An example script for running tools is provided in [scripts/demo_run_tools.py](scripts/demo_run_tools.py). + ## Adding a New Supported Language Serena interacts with code through language servers which are included in diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py new file mode 100644 index 00000000..2eae23c6 --- /dev/null +++ b/scripts/demo_run_tools.py @@ -0,0 +1,26 @@ +""" +This script demonstrates how to use Serena's tools locally, useful +for testing or development. Here the config is pointing to the serena repo itself, +it assumes that you already have a serena_config.yml file setup. +""" + +import json +from pprint import pprint +from pathlib import Path + +from serena.agent import * + +project_root = Path(__file__).parent.parent + +if __name__ == "__main__": + agent = SerenaAgent(project_file_path=str(project_root / ".serena" / "project.yml")) + overview_tool = agent.get_tool(GetSymbolsOverviewTool) + find_symbol_tool = agent.get_tool(FindSymbolTool) + + print("Getting an overview of the util package\n") + pprint(json.loads(overview_tool.apply("src/serena/util"))) + + print("\n\n") + + print("Finding the symbol 'SerenaAgent'\n") + pprint(json.loads(find_symbol_tool.apply("SerenaAgent"))) \ No newline at end of file From 072b5caefb1129d6ff2d3eab25670eb4ce1c0c45 Mon Sep 17 00:00:00 2001 From: Michael Panchenko <35432522+MischaPanch@users.noreply.github.com> Date: Sun, 27 Apr 2025 11:00:33 +0200 Subject: [PATCH 05/61] Minor in README.md --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d4d64945..a81eaba7 100644 --- a/README.md +++ b/README.md @@ -153,15 +153,14 @@ Serena can be used in various ways, below you will find instructions for selecte ```shell cp serena_config.template.yml serena_config.yml ``` -4. Copy `project.template.yml` to `project.yml` and adjust the settings specific to your project. - (Add one such file for each project you want Serena to work on.) We recommend that you copy +4. Copy `project.template.yml` to `project.yml` and adjust the settings specific to your project + (add one such file for each project you want Serena to work on). We recommend that you copy it to the `.serena` directory of your project, e.g., - ```shell - mkdir -p /myproject/.serena - cp project.template.yml /myproject/.serena/project.yml - ``` - though you can also keep it somewhere else. -5. If you want Serena to dynamically switch between projects, add the list of all project files + ```shell + mkdir -p /myproject/.serena + cp project.template.yml /myproject/.serena/project.yml + ``` +6. If you want Serena to dynamically switch between projects, add the list of all project files created in the previous step to the `projects` list in `serena_config.yml`. > ⚠️ **Note:** Serena is under active development. We are continuously adding features, improving stability and the UX. From 433d7f4d5943c3c8606e9a3041fc9103ff2795d1 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 27 Apr 2025 13:00:23 +0200 Subject: [PATCH 06/61] Refactoring in tests: prepare for testing with multiple languages --- scripts/demo_run_tools.py | 10 +- .../test_repo/test_repo => }/__init__.py | 0 test/conftest.py | 71 +++++++-- test/multilspy/{ => python}/test_basic.py | 9 ++ .../test_retrieval_with_ignored_dirs.py | 7 +- .../{ => python}/test_symbol_retrieval.py | 24 +++ .../{ => repos/python}/test_repo/.gitignore | 0 .../python}/test_repo/custom_test/__init__.py | 0 .../custom_test/advanced_features.py | 0 .../python}/test_repo/examples/__init__.py | 0 .../test_repo/examples/user_management.py | 0 .../python}/test_repo/scripts/__init__.py | 0 .../python}/test_repo/scripts/run_app.py | 0 .../python/test_repo/test_repo/__init__.py | 0 .../test_repo/test_repo/complex_types.py | 0 .../python}/test_repo/test_repo/models.py | 0 .../test_repo/test_repo/name_collisions.py | 0 .../python}/test_repo/test_repo/nested.py | 0 .../test_repo/test_repo/nested_base.py | 0 .../python}/test_repo/test_repo/overloaded.py | 0 .../python}/test_repo/test_repo/services.py | 0 .../python}/test_repo/test_repo/utils.py | 0 .../python}/test_repo/test_repo/variables.py | 0 .../ignored_module.py | 141 ------------------ 24 files changed, 103 insertions(+), 159 deletions(-) rename test/{resources/test_repo/test_repo => }/__init__.py (100%) rename test/multilspy/{ => python}/test_basic.py (95%) rename test/multilspy/{ => python}/test_retrieval_with_ignored_dirs.py (86%) rename test/multilspy/{ => python}/test_symbol_retrieval.py (92%) rename test/resources/{ => repos/python}/test_repo/.gitignore (100%) rename test/resources/{ => repos/python}/test_repo/custom_test/__init__.py (100%) rename test/resources/{ => repos/python}/test_repo/custom_test/advanced_features.py (100%) rename test/resources/{ => repos/python}/test_repo/examples/__init__.py (100%) rename test/resources/{ => repos/python}/test_repo/examples/user_management.py (100%) rename test/resources/{ => repos/python}/test_repo/scripts/__init__.py (100%) rename test/resources/{ => repos/python}/test_repo/scripts/run_app.py (100%) create mode 100644 test/resources/repos/python/test_repo/test_repo/__init__.py rename test/resources/{ => repos/python}/test_repo/test_repo/complex_types.py (100%) rename test/resources/{ => repos/python}/test_repo/test_repo/models.py (100%) rename test/resources/{ => repos/python}/test_repo/test_repo/name_collisions.py (100%) rename test/resources/{ => repos/python}/test_repo/test_repo/nested.py (100%) rename test/resources/{ => repos/python}/test_repo/test_repo/nested_base.py (100%) rename test/resources/{ => repos/python}/test_repo/test_repo/overloaded.py (100%) rename test/resources/{ => repos/python}/test_repo/test_repo/services.py (100%) rename test/resources/{ => repos/python}/test_repo/test_repo/utils.py (100%) rename test/resources/{ => repos/python}/test_repo/test_repo/variables.py (100%) delete mode 100644 test/resources/test_repo/ignore_this_dir_with_postfix/ignored_module.py diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index 2eae23c6..0ebcc0c3 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -5,8 +5,8 @@ it assumes that you already have a serena_config.yml file setup. """ import json -from pprint import pprint from pathlib import Path +from pprint import pprint from serena.agent import * @@ -16,11 +16,11 @@ if __name__ == "__main__": agent = SerenaAgent(project_file_path=str(project_root / ".serena" / "project.yml")) overview_tool = agent.get_tool(GetSymbolsOverviewTool) find_symbol_tool = agent.get_tool(FindSymbolTool) - + print("Getting an overview of the util package\n") pprint(json.loads(overview_tool.apply("src/serena/util"))) - + print("\n\n") - + print("Finding the symbol 'SerenaAgent'\n") - pprint(json.loads(find_symbol_tool.apply("SerenaAgent"))) \ No newline at end of file + pprint(json.loads(find_symbol_tool.apply("SerenaAgent"))) diff --git a/test/resources/test_repo/test_repo/__init__.py b/test/__init__.py similarity index 100% rename from test/resources/test_repo/test_repo/__init__.py rename to test/__init__.py diff --git a/test/conftest.py b/test/conftest.py index 59cb70e0..0c6321da 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -14,25 +14,72 @@ def resources_dir() -> Path: return current_dir / "resources" -@pytest.fixture(scope="session") -def repo_path() -> Path: - return Path(__file__).parent / "resources" / "test_repo" +class LanguageParamRequest: + param: Language -@pytest.fixture(scope="session") -def language_server(repo_path: Path): - """Create a SyncLanguageServer instance configured to use the test repository.""" - config = MultilspyConfig(code_language=Language.PYTHON) +def get_repo_path(language: Language) -> Path: + return Path(__file__).parent / "resources" / "repos" / language / "test_repo" + + +def create_default_ls(language: Language) -> SyncLanguageServer: + config = MultilspyConfig(code_language=language) + repo_path = str(get_repo_path(language)) logger = MultilspyLogger() + return SyncLanguageServer.create(config, logger, repo_path) - # Create a language server instance - server = SyncLanguageServer.create(config, logger, str(repo_path)) - # Start the server +@pytest.fixture(scope="session") +def repo_path(request: LanguageParamRequest) -> Path: + """Get the repository path for a specific language. + + This fixture requires a language parameter via pytest.mark.parametrize: + + Example: + ``` + @pytest.mark.parametrize("repo_path", [Language.PYTHON], indirect=True) + def test_python_repo(repo_path): + assert (repo_path / "src").exists() + ``` + + """ + if not hasattr(request, "param"): + raise ValueError("Language parameter must be provided via pytest.mark.parametrize") + + language = request.param + return get_repo_path(language) + + +@pytest.fixture(scope="session") +def language_server(request: LanguageParamRequest): + """Create a SyncLanguageServer instance configured for the specified language. + + This fixture requires a language parameter via pytest.mark.parametrize: + + Example: + ``` + @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + def test_python_server(language_server): + # Use the Python language server + pass + ``` + + You can also test multiple languages in a single test: + ``` + @pytest.mark.parametrize("language_server", [Language.PYTHON, Language.TYPESCRIPT], indirect=True) + def test_multiple_languages(language_server): + # This test will run once for each language + pass + ``` + + """ + if not hasattr(request, "param"): + raise ValueError("Language parameter must be provided via pytest.mark.parametrize") + + language = request.param + server = create_default_ls(language) server.start() - try: yield server finally: - # Ensure server is shut down server.stop() diff --git a/test/multilspy/test_basic.py b/test/multilspy/python/test_basic.py similarity index 95% rename from test/multilspy/test_basic.py rename to test/multilspy/python/test_basic.py index a22c0fb1..391906f2 100644 --- a/test/multilspy/test_basic.py +++ b/test/multilspy/python/test_basic.py @@ -7,13 +7,17 @@ like request_references using the test repository. import os +import pytest + from multilspy.language_server import SyncLanguageServer +from multilspy.multilspy_config import Language from serena.text_utils import LineType 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): """Test request_references on the User class.""" # Get references to the User class in models.py @@ -27,6 +31,7 @@ class TestLanguageServerBasics: # At least two references should be found (one for the class definition itself) assert len(references) > 1 + @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) def test_request_references_item_class(self, language_server: SyncLanguageServer): """Test request_references on the Item class.""" # Get references to the Item class in models.py @@ -41,6 +46,7 @@ class TestLanguageServerBasics: services_references = [ref for ref in references if "services.py" in ref["uri"]] assert len(services_references) > 0 + @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) def test_request_references_function_parameter(self, language_server: SyncLanguageServer): """Test request_references on a function parameter.""" # Get references to the id parameter in get_user method @@ -51,6 +57,7 @@ class TestLanguageServerBasics: # id parameter should be referenced within the method assert len(references) > 0 + @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) def test_request_references_create_user_method(self, language_server: SyncLanguageServer): # Get references to the create_user method in UserService file_path = os.path.join("test_repo", "services.py") @@ -60,6 +67,7 @@ class TestLanguageServerBasics: # Verify that we get valid references assert len(references) > 1 + @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) def test_retrieve_content_around_line(self, language_server: SyncLanguageServer): """Test retrieve_content_around_line functionality with various scenarios.""" file_path = os.path.join("test_repo", "models.py") @@ -166,6 +174,7 @@ class TestLanguageServerBasics: else: 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): """Test search_files_for_pattern with various patterns and glob filters.""" # Test 1: Search for class definitions across all files diff --git a/test/multilspy/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py similarity index 86% rename from test/multilspy/test_retrieval_with_ignored_dirs.py rename to test/multilspy/python/test_retrieval_with_ignored_dirs.py index 09e4f538..7615830f 100644 --- a/test/multilspy/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -6,10 +6,11 @@ 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 @pytest.fixture(scope="module") -def ls_with_ignored_dirs(repo_path: Path) -> Generator[SyncLanguageServer, None, None]: +def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored.""" config = MultilspyConfig( code_language=Language.PYTHON, @@ -17,6 +18,7 @@ def ls_with_ignored_dirs(repo_path: Path) -> Generator[SyncLanguageServer, None, 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)) ls.start() try: @@ -25,6 +27,7 @@ def ls_with_ignored_dirs(repo_path: Path) -> Generator[SyncLanguageServer, None, ls.stop() +@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True) def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): """Tests that request_full_symbol_tree ignores the configured directory.""" root = ls_with_ignored_dirs.request_full_symbol_tree()[0] @@ -33,6 +36,7 @@ def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): assert children_names == {"test_repo", "examples"} +@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True) def test_find_references_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): """Tests that find_references ignores the configured directory.""" # Location of Item, which is referenced in scripts @@ -46,6 +50,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): assert not any("scripts" in ref["relativePath"] for ref in references) +@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( diff --git a/test/multilspy/test_symbol_retrieval.py b/test/multilspy/python/test_symbol_retrieval.py similarity index 92% rename from test/multilspy/test_symbol_retrieval.py rename to test/multilspy/python/test_symbol_retrieval.py index 14fe1fc7..cc4b35a2 100644 --- a/test/multilspy/test_symbol_retrieval.py +++ b/test/multilspy/python/test_symbol_retrieval.py @@ -8,13 +8,17 @@ These tests focus on the following methods: import os +import pytest + from multilspy.language_server import SyncLanguageServer +from multilspy.multilspy_config import Language from multilspy.multilspy_types import SymbolKind 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): """Test request_containing_symbol for a function.""" # Test for a position inside the create_user method @@ -29,6 +33,7 @@ class TestLanguageServerSymbols: if "body" in containing_symbol: 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): """Test request_referencing_symbols for a variable.""" file_path = os.path.join("test_repo", "variables.py") @@ -43,6 +48,7 @@ class TestLanguageServerSymbols: assert "dataclass_instance" in ref_names 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): """Test request_containing_symbol for a class.""" # Test for a position inside the UserService class but outside any method @@ -55,6 +61,7 @@ class TestLanguageServerSymbols: assert containing_symbol["name"] == "UserService" 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): """Test request_containing_symbol with nested scopes.""" # Test for a position inside a method which is inside a class @@ -80,6 +87,7 @@ class TestLanguageServerSymbols: assert parent_symbol["name"] == "UserService" 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): """Test request_containing_symbol for a position with no containing symbol.""" # Test for a position outside any function/class (e.g., in imports) @@ -90,6 +98,7 @@ class TestLanguageServerSymbols: # Should return None or an empty dictionary 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): """Test request_referencing_symbols for a function.""" # Test referencing symbols for create_user function @@ -108,6 +117,7 @@ class TestLanguageServerSymbols: assert "start" in symbol["location"]["range"] 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): """Test request_referencing_symbols for a class.""" # Test referencing symbols for User class @@ -127,6 +137,7 @@ class TestLanguageServerSymbols: assert len(services_references) > 0 + @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) def test_request_referencing_symbols_parameter(self, language_server: SyncLanguageServer): """Test request_referencing_symbols for a function parameter.""" # Test referencing symbols for id parameter in get_user @@ -146,6 +157,7 @@ class TestLanguageServerSymbols: assert len(method_refs) > 0 + @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) def test_request_referencing_symbols_none(self, language_server: SyncLanguageServer): """Test request_referencing_symbols for a position with no symbol.""" # For positions with no symbol, the method might throw an error or return None/empty list @@ -163,6 +175,7 @@ class TestLanguageServerSymbols: pass # Tests for request_defining_symbol + @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) def test_request_defining_symbol_variable(self, language_server: SyncLanguageServer): """Test request_defining_symbol for a variable usage.""" # Test finding the definition of a symbol in the create_user method @@ -181,6 +194,7 @@ class TestLanguageServerSymbols: if "location" in defining_symbol and "uri" in defining_symbol["location"]: 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): """Test request_defining_symbol for an imported class.""" # Test finding the definition of the 'User' class used in the UserService.create_user method @@ -192,6 +206,7 @@ class TestLanguageServerSymbols: assert defining_symbol is not None 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): """Test request_defining_symbol for a method call.""" # Create an example file path for a file that calls UserService.create_user @@ -216,6 +231,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): """Test request_defining_symbol for a position with no symbol.""" # Test for a position with no symbol (e.g., whitespace or comment) @@ -226,6 +242,7 @@ class TestLanguageServerSymbols: # Should return None for positions with no symbol assert defining_symbol is None + @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) def test_request_containing_symbol_variable(self, language_server: SyncLanguageServer): """Test request_containing_symbol where the symbol is a variable.""" # Test for a position inside a variable definition @@ -238,6 +255,7 @@ class TestLanguageServerSymbols: assert containing_symbol["name"] == "user_var_str" 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): """Test request_defining_symbol for a nested function or closure.""" # Use the existing nested.py file which contains nested classes and methods @@ -288,6 +306,7 @@ class TestLanguageServerSymbols: assert defining_symbol.get("name") == "func_within_func" 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): """Test the integration between different symbol-related methods.""" # This test demonstrates using the various symbol methods together @@ -336,6 +355,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): """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file @@ -363,6 +383,7 @@ class TestLanguageServerSymbols: _, user_management_roots = language_server.request_document_symbols(os.path.join("examples", "user_management.py")) 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): """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file @@ -383,6 +404,7 @@ class TestLanguageServerSymbols: _, user_management_roots = language_server.request_document_symbols(os.path.join("examples", "user_management.py")) 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): """Test that request_dir_overview returns correct symbol information for files in a directory.""" # Get overview of the examples directory @@ -407,6 +429,7 @@ class TestLanguageServerSymbols: for symbol in expected_symbols: assert symbol in services_symbols + @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) def test_request_document_overview(self, language_server: SyncLanguageServer): """Test that request_document_overview returns correct symbol information for a file.""" # Get overview of the user_management.py file @@ -416,6 +439,7 @@ class TestLanguageServerSymbols: symbol_names = {s_info[0] for s_info in overview} 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): """Test that the containing symbol of a variable is the file itself.""" # Get the containing symbol of a variable in a file diff --git a/test/resources/test_repo/.gitignore b/test/resources/repos/python/test_repo/.gitignore similarity index 100% rename from test/resources/test_repo/.gitignore rename to test/resources/repos/python/test_repo/.gitignore diff --git a/test/resources/test_repo/custom_test/__init__.py b/test/resources/repos/python/test_repo/custom_test/__init__.py similarity index 100% rename from test/resources/test_repo/custom_test/__init__.py rename to test/resources/repos/python/test_repo/custom_test/__init__.py diff --git a/test/resources/test_repo/custom_test/advanced_features.py b/test/resources/repos/python/test_repo/custom_test/advanced_features.py similarity index 100% rename from test/resources/test_repo/custom_test/advanced_features.py rename to test/resources/repos/python/test_repo/custom_test/advanced_features.py diff --git a/test/resources/test_repo/examples/__init__.py b/test/resources/repos/python/test_repo/examples/__init__.py similarity index 100% rename from test/resources/test_repo/examples/__init__.py rename to test/resources/repos/python/test_repo/examples/__init__.py diff --git a/test/resources/test_repo/examples/user_management.py b/test/resources/repos/python/test_repo/examples/user_management.py similarity index 100% rename from test/resources/test_repo/examples/user_management.py rename to test/resources/repos/python/test_repo/examples/user_management.py diff --git a/test/resources/test_repo/scripts/__init__.py b/test/resources/repos/python/test_repo/scripts/__init__.py similarity index 100% rename from test/resources/test_repo/scripts/__init__.py rename to test/resources/repos/python/test_repo/scripts/__init__.py diff --git a/test/resources/test_repo/scripts/run_app.py b/test/resources/repos/python/test_repo/scripts/run_app.py similarity index 100% rename from test/resources/test_repo/scripts/run_app.py rename to test/resources/repos/python/test_repo/scripts/run_app.py diff --git a/test/resources/repos/python/test_repo/test_repo/__init__.py b/test/resources/repos/python/test_repo/test_repo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/resources/test_repo/test_repo/complex_types.py b/test/resources/repos/python/test_repo/test_repo/complex_types.py similarity index 100% rename from test/resources/test_repo/test_repo/complex_types.py rename to test/resources/repos/python/test_repo/test_repo/complex_types.py diff --git a/test/resources/test_repo/test_repo/models.py b/test/resources/repos/python/test_repo/test_repo/models.py similarity index 100% rename from test/resources/test_repo/test_repo/models.py rename to test/resources/repos/python/test_repo/test_repo/models.py diff --git a/test/resources/test_repo/test_repo/name_collisions.py b/test/resources/repos/python/test_repo/test_repo/name_collisions.py similarity index 100% rename from test/resources/test_repo/test_repo/name_collisions.py rename to test/resources/repos/python/test_repo/test_repo/name_collisions.py diff --git a/test/resources/test_repo/test_repo/nested.py b/test/resources/repos/python/test_repo/test_repo/nested.py similarity index 100% rename from test/resources/test_repo/test_repo/nested.py rename to test/resources/repos/python/test_repo/test_repo/nested.py diff --git a/test/resources/test_repo/test_repo/nested_base.py b/test/resources/repos/python/test_repo/test_repo/nested_base.py similarity index 100% rename from test/resources/test_repo/test_repo/nested_base.py rename to test/resources/repos/python/test_repo/test_repo/nested_base.py diff --git a/test/resources/test_repo/test_repo/overloaded.py b/test/resources/repos/python/test_repo/test_repo/overloaded.py similarity index 100% rename from test/resources/test_repo/test_repo/overloaded.py rename to test/resources/repos/python/test_repo/test_repo/overloaded.py diff --git a/test/resources/test_repo/test_repo/services.py b/test/resources/repos/python/test_repo/test_repo/services.py similarity index 100% rename from test/resources/test_repo/test_repo/services.py rename to test/resources/repos/python/test_repo/test_repo/services.py diff --git a/test/resources/test_repo/test_repo/utils.py b/test/resources/repos/python/test_repo/test_repo/utils.py similarity index 100% rename from test/resources/test_repo/test_repo/utils.py rename to test/resources/repos/python/test_repo/test_repo/utils.py diff --git a/test/resources/test_repo/test_repo/variables.py b/test/resources/repos/python/test_repo/test_repo/variables.py similarity index 100% rename from test/resources/test_repo/test_repo/variables.py rename to test/resources/repos/python/test_repo/test_repo/variables.py diff --git a/test/resources/test_repo/ignore_this_dir_with_postfix/ignored_module.py b/test/resources/test_repo/ignore_this_dir_with_postfix/ignored_module.py deleted file mode 100644 index 1e6da936..00000000 --- a/test/resources/test_repo/ignore_this_dir_with_postfix/ignored_module.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -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() From fadfcc4f26ddbdfca3c08c6e88a53dfc9ab9bd07 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 27 Apr 2025 13:43:23 +0200 Subject: [PATCH 07/61] Tests for java, minor extension for multilspy Written by windsurf ang GPT4.1 --- src/multilspy/language_server.py | 52 ++++++++++-------- src/multilspy/multilspy_utils.py | 9 +++- test/multilspy/java/test_java_basic.py | 54 +++++++++++++++++++ .../{test_basic.py => test_python_basic.py} | 0 test/resources/repos/java/test_repo/pom.xml | 14 +++++ .../src/main/java/test_repo/Main.java | 13 +++++ .../src/main/java/test_repo/Model.java | 13 +++++ .../src/main/java/test_repo/Utils.java | 7 +++ 8 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 test/multilspy/java/test_java_basic.py rename test/multilspy/python/{test_basic.py => test_python_basic.py} (100%) create mode 100644 test/resources/repos/java/test_repo/pom.xml create mode 100644 test/resources/repos/java/test_repo/src/main/java/test_repo/Main.java create mode 100644 test/resources/repos/java/test_repo/src/main/java/test_repo/Model.java create mode 100644 test/resources/repos/java/test_repo/src/main/java/test_repo/Utils.java diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 0fad4728..abcc5c1f 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -592,33 +592,30 @@ class LanguageServer: :param line: The line number of the symbol :param column: The column number of the symbol - :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") - - - with self.open_file(relative_file_path): - # sending request to the language server and waiting for response + try: response = await self.server.send.references( { - "context": {"includeDeclaration": False}, - "textDocument": { - "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() - }, + "textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))}, "position": {"line": line, "character": column}, + "context": {"includeDeclaration": False}, } ) + except Exception as e: + # Catch LSP internal error (-32603) and raise a more informative exception + 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 {relative_file_path}:{line}:{column}. " + "This often occurs when requesting references for a symbol not referenced in the expected way. " + "Please check your test setup and ensure the symbol is referenced in your code." + ) from e + raise + if response is None: + return [] - ret: List[multilspy_types.Location] = [] - assert isinstance(response, list), f"Unexpected response from Language Server: {response}" + ret = [] for item in response: - assert isinstance(item, dict) assert LSPConstants.URI in item assert LSPConstants.RANGE in item @@ -1720,11 +1717,22 @@ class SyncLanguageServer: :return List[multilspy_types.Location]: A list of locations where the symbol is referenced """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_references(file_path, line, column), self.loop - ).result(timeout=self.timeout) + 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. " + "Please check your test setup and ensure the symbol is referenced in your code." + ) 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]: diff --git a/src/multilspy/multilspy_utils.py b/src/multilspy/multilspy_utils.py index 7c46d37d..c87f6e65 100644 --- a/src/multilspy/multilspy_utils.py +++ b/src/multilspy/multilspy_utils.py @@ -90,7 +90,14 @@ class PathUtils: parsed = urlparse(uri) host = "{0}{0}{mnt}{0}".format(os.path.sep, mnt=parsed.netloc) return os.path.normpath(os.path.join(host, url2pathname(unquote(parsed.path)))) - + + @staticmethod + def path_to_uri(path: str) -> str: + """ + Converts a file path to a file URI (file:///...). + """ + return str(Path(path).absolute().as_uri()) + @staticmethod def is_glob_pattern(pattern: str) -> bool: """Check if a pattern contains glob-specific characters.""" diff --git a/test/multilspy/java/test_java_basic.py b/test/multilspy/java/test_java_basic.py new file mode 100644 index 00000000..0cf6f26d --- /dev/null +++ b/test/multilspy/java/test_java_basic.py @@ -0,0 +1,54 @@ +import os + +import pytest + +from multilspy.multilspy_config import Language + + +def find_symbol_recursive(symbols, name): + for symbol in symbols: + if symbol.get("name") == name: + return True + if symbol.get("children"): + if find_symbol_recursive(symbol["children"], name): + return True + return False + + +class TestJavaLanguageServer: + @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) + def test_find_symbol(self, language_server): + symbols = language_server.request_full_symbol_tree() + assert find_symbol_recursive(symbols, "Main"), "Main class not found in symbol tree" + assert find_symbol_recursive(symbols, "Utils"), "Utils class not found in symbol tree" + assert find_symbol_recursive(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): + # 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) + print(f"References for Utils.printHello: {refs}") + assert any("Main.java" in ref.get("relativePath", "") for ref in refs), "Main should reference Utils.printHello" + + # Dynamically determine the correct line/column for the 'Model' class name + file_path = os.path.join("src", "main", "java", "test_repo", "Model.java") + symbols = language_server.request_document_symbols(file_path) + model_symbol = None + for sym in symbols[0]: + if sym.get("name") == "Model" and sym.get("kind") == 5: # 5 = Class + model_symbol = sym + break + assert model_symbol is not None, "Could not find 'Model' class symbol in Model.java" + rng = model_symbol["range"]["start"] + print(f"Model symbol range: {model_symbol['range']}") + refs = language_server.request_references(file_path, rng["line"], rng["character"]) + print(f"References for Model class: {refs}") + assert any("Main.java" in ref.get("relativePath", "") for ref in refs), "Main should reference Model" + + @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) + def test_overview_methods(self, language_server): + symbols = language_server.request_full_symbol_tree() + assert find_symbol_recursive(symbols, "Main"), "Main missing from overview" + assert find_symbol_recursive(symbols, "Utils"), "Utils missing from overview" + assert find_symbol_recursive(symbols, "Model"), "Model missing from overview" diff --git a/test/multilspy/python/test_basic.py b/test/multilspy/python/test_python_basic.py similarity index 100% rename from test/multilspy/python/test_basic.py rename to test/multilspy/python/test_python_basic.py diff --git a/test/resources/repos/java/test_repo/pom.xml b/test/resources/repos/java/test_repo/pom.xml new file mode 100644 index 00000000..7df64e11 --- /dev/null +++ b/test/resources/repos/java/test_repo/pom.xml @@ -0,0 +1,14 @@ + + 4.0.0 + org.example + test_repo + 1.0-SNAPSHOT + jar + Java Test Repo + + 17 + 17 + + diff --git a/test/resources/repos/java/test_repo/src/main/java/test_repo/Main.java b/test/resources/repos/java/test_repo/src/main/java/test_repo/Main.java new file mode 100644 index 00000000..ed79b9fe --- /dev/null +++ b/test/resources/repos/java/test_repo/src/main/java/test_repo/Main.java @@ -0,0 +1,13 @@ +package test_repo; + +public class Main { + public static void main(String[] args) { + Utils.printHello(); + Model model = new Model("Cascade"); + System.out.println(model.getName()); + acceptModel(model); + } + public static void acceptModel(Model m) { + // Do nothing, just for LSP reference + } +} diff --git a/test/resources/repos/java/test_repo/src/main/java/test_repo/Model.java b/test/resources/repos/java/test_repo/src/main/java/test_repo/Model.java new file mode 100644 index 00000000..a2c6459b --- /dev/null +++ b/test/resources/repos/java/test_repo/src/main/java/test_repo/Model.java @@ -0,0 +1,13 @@ +package test_repo; + +public class Model { + private String name; + + public Model(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/test/resources/repos/java/test_repo/src/main/java/test_repo/Utils.java b/test/resources/repos/java/test_repo/src/main/java/test_repo/Utils.java new file mode 100644 index 00000000..107f4c80 --- /dev/null +++ b/test/resources/repos/java/test_repo/src/main/java/test_repo/Utils.java @@ -0,0 +1,7 @@ +package test_repo; + +public class Utils { + public static void printHello() { + System.out.println("Hello from Utils!"); + } +} From 90620b3912a3edcc1187dd59bda08cf9888771f6 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 27 Apr 2025 14:40:41 +0200 Subject: [PATCH 08/61] Minor cleanup after overeager AI --- src/multilspy/language_server.py | 49 +++++++++++++++++++------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index abcc5c1f..3845ba84 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -592,30 +592,42 @@ class LanguageServer: :param line: The line number of the symbol :param column: The column number of the symbol + :return: A list of locations where the symbol is referenced (excluding ignored directories) """ - try: - response = await 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}, - "context": {"includeDeclaration": False}, - } + + if not self.server_started: + self.logger.log( + "request_references called before Language Server started", + logging.ERROR, ) - except Exception as e: - # Catch LSP internal error (-32603) and raise a more informative exception - 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 {relative_file_path}:{line}:{column}. " - "This often occurs when requesting references for a symbol not referenced in the expected way. " - "Please check your test setup and ensure the symbol is referenced in your code." - ) from e - raise + raise MultilspyException("Language Server not started") + + + with self.open_file(relative_file_path): + try: + response = await 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}, + "context": {"includeDeclaration": False}, + } + ) + except Exception as e: + # Catch LSP internal error (-32603) and raise a more informative exception + 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 {relative_file_path}:{line}:{column}. " + "This often occurs when requesting references for a symbol not referenced in the expected way. " + ) from e + raise if response is None: return [] - ret = [] + ret: List[multilspy_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}" assert LSPConstants.URI in item assert LSPConstants.RANGE in item @@ -1727,7 +1739,6 @@ class SyncLanguageServer: 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. " - "Please check your test setup and ensure the symbol is referenced in your code." ) from e raise return result From e6f0ddacef9b42e754ecd8730d447fc09148987b Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 27 Apr 2025 15:34:29 +0200 Subject: [PATCH 09/61] Tests for LS on rust, written by windsurf with GPT4.1 --- test/multilspy/rust/test_rust_basic.py | 87 +++++++++++++++++++ .../resources/repos/rust/test_repo/Cargo.lock | 7 ++ .../resources/repos/rust/test_repo/Cargo.toml | 6 ++ .../resources/repos/rust/test_repo/src/lib.rs | 9 ++ .../repos/rust/test_repo/src/main.rs | 8 ++ 5 files changed, 117 insertions(+) create mode 100644 test/multilspy/rust/test_rust_basic.py create mode 100644 test/resources/repos/rust/test_repo/Cargo.lock create mode 100644 test/resources/repos/rust/test_repo/Cargo.toml create mode 100644 test/resources/repos/rust/test_repo/src/lib.rs create mode 100644 test/resources/repos/rust/test_repo/src/main.rs diff --git a/test/multilspy/rust/test_rust_basic.py b/test/multilspy/rust/test_rust_basic.py new file mode 100644 index 00000000..23282271 --- /dev/null +++ b/test/multilspy/rust/test_rust_basic.py @@ -0,0 +1,87 @@ +import os +import pytest +from multilspy.multilspy_config import Language + +def find_symbol_recursive(symbols, name): + for symbol in symbols: + if symbol.get("name") == name: + return True + if symbol.get("children"): + if find_symbol_recursive(symbol["children"], name): + return True + return False + +class TestRustLanguageServer: + @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) + def test_find_references_raw(self, language_server): + # 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) + add_symbol = None + for sym in symbols[0]: + if sym.get("name") == "add": + add_symbol = sym + break + assert add_symbol is not None, "Could not find 'add' function symbol in lib.rs" + sel_start = add_symbol["selectionRange"]["start"] + sel_end = add_symbol["selectionRange"]["end"] + found = False + if sel_start["line"] == sel_end["line"]: + for col in range(sel_start["character"], sel_end["character"]+1): + refs = language_server.request_references(file_path, sel_start["line"], col) + # f"Raw request_references for add in lib.rs at col {col}: {refs}") + if any("main.rs" in ref.get("relativePath", "") for ref in refs): + found = True + else: + refs_start = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + refs_end = language_server.request_references(file_path, sel_end["line"], sel_end["character"]) + # f"Raw request_references for add in lib.rs at selectionRange start: {refs_start}") + # f"Raw request_references for add in lib.rs at selectionRange end: {refs_end}") + if any("main.rs" in ref.get("relativePath", "") for ref in refs_start + refs_end): + found = True + assert found, "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): + symbols = language_server.request_full_symbol_tree() + assert find_symbol_recursive(symbols, "main"), "main function not found in symbol tree" + assert find_symbol_recursive(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): + # 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) + add_symbol = None + for sym in symbols[0]: + if sym.get("name") == "add": + add_symbol = sym + break + assert add_symbol is not None, "Could not find 'add' function symbol in lib.rs" + sel_start = add_symbol["selectionRange"]["start"] + sel_end = add_symbol["selectionRange"]["end"] + # f"add symbol selectionRange: start={sel_start}, end={sel_end}") + found = False + # Try all character positions in the selectionRange (same line, from start to end character) + if sel_start["line"] == sel_end["line"]: + for col in range(sel_start["character"], sel_end["character"]+1): + refs = language_server.request_references(file_path, sel_start["line"], col) + # f"References for add in lib.rs at col {col} (selectionRange): {refs}") + if any("main.rs" in ref.get("relativePath", "") for ref in refs): + found = True + else: + # If the function name spans multiple lines, try start and end + refs_start = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + refs_end = language_server.request_references(file_path, sel_end["line"], sel_end["character"]) + # f"References for add in lib.rs at selectionRange start: {refs_start}") + # f"References for add in lib.rs at selectionRange end: {refs_end}") + if any("main.rs" in ref.get("relativePath", "") for ref in refs_start + refs_end): + found = True + assert found, "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): + symbols = language_server.request_full_symbol_tree() + assert find_symbol_recursive(symbols, "main"), "main missing from overview" + assert find_symbol_recursive(symbols, "add"), "add missing from overview" diff --git a/test/resources/repos/rust/test_repo/Cargo.lock b/test/resources/repos/rust/test_repo/Cargo.lock new file mode 100644 index 00000000..ebc459f1 --- /dev/null +++ b/test/resources/repos/rust/test_repo/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "rsandbox" +version = "0.1.0" diff --git a/test/resources/repos/rust/test_repo/Cargo.toml b/test/resources/repos/rust/test_repo/Cargo.toml new file mode 100644 index 00000000..43ffbf6e --- /dev/null +++ b/test/resources/repos/rust/test_repo/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "rsandbox" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/test/resources/repos/rust/test_repo/src/lib.rs b/test/resources/repos/rust/test_repo/src/lib.rs new file mode 100644 index 00000000..8ad43ead --- /dev/null +++ b/test/resources/repos/rust/test_repo/src/lib.rs @@ -0,0 +1,9 @@ +// This function returns the sum of 2 + 2 +pub fn add() -> i32 { + let res = 2 + 2; + res +} +pub fn multiply() -> i32 { + 2 * 3 +} + diff --git a/test/resources/repos/rust/test_repo/src/main.rs b/test/resources/repos/rust/test_repo/src/main.rs new file mode 100644 index 00000000..7385c9d9 --- /dev/null +++ b/test/resources/repos/rust/test_repo/src/main.rs @@ -0,0 +1,8 @@ +use rsandbox::add; + +fn main() { + println!("Hello, World!"); + println!("Good morning!"); + println!("add result: {}", add()); + println!("inserted line"); +} From 6a46a3768d7f31923d56dc218dabf92cd728ef7b Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 27 Apr 2025 15:42:55 +0200 Subject: [PATCH 10/61] Use selectionRange for referencing symbols in tests --- test/multilspy/java/test_java_basic.py | 25 ++++-- test/multilspy/python/test_python_basic.py | 76 ++++++++++++------ .../multilspy/python/test_symbol_retrieval.py | 78 ++++++++++++------- test/multilspy/rust/test_rust_basic.py | 8 +- 4 files changed, 126 insertions(+), 61 deletions(-) diff --git a/test/multilspy/java/test_java_basic.py b/test/multilspy/java/test_java_basic.py index 0cf6f26d..d66753f1 100644 --- a/test/multilspy/java/test_java_basic.py +++ b/test/multilspy/java/test_java_basic.py @@ -28,7 +28,6 @@ class TestJavaLanguageServer: # 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) - print(f"References for Utils.printHello: {refs}") assert any("Main.java" in ref.get("relativePath", "") for ref in refs), "Main should reference Utils.printHello" # Dynamically determine the correct line/column for the 'Model' class name @@ -40,11 +39,25 @@ class TestJavaLanguageServer: model_symbol = sym break assert model_symbol is not None, "Could not find 'Model' class symbol in Model.java" - rng = model_symbol["range"]["start"] - print(f"Model symbol range: {model_symbol['range']}") - refs = language_server.request_references(file_path, rng["line"], rng["character"]) - print(f"References for Model class: {refs}") - assert any("Main.java" in ref.get("relativePath", "") for ref in refs), "Main should reference Model" + # Use selectionRange if present, otherwise fall back to range + if "selectionRange" in model_symbol: + sel_start = model_symbol["selectionRange"]["start"] + sel_end = model_symbol["selectionRange"]["end"] + else: + sel_start = model_symbol["range"]["start"] + sel_end = model_symbol["range"]["end"] + found = False + if sel_start["line"] == sel_end["line"]: + for col in range(sel_start["character"], sel_end["character"] + 1): + refs = language_server.request_references(file_path, sel_start["line"], col) + if any("Main.java" in ref.get("relativePath", "") for ref in refs): + found = True + else: + refs_start = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + refs_end = language_server.request_references(file_path, sel_end["line"], sel_end["character"]) + if any("Main.java" in ref.get("relativePath", "") for ref in refs_start + refs_end): + found = True + assert found, "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): diff --git a/test/multilspy/python/test_python_basic.py b/test/multilspy/python/test_python_basic.py index 391906f2..36e1778f 100644 --- a/test/multilspy/python/test_python_basic.py +++ b/test/multilspy/python/test_python_basic.py @@ -23,13 +23,19 @@ class TestLanguageServerBasics: # Get references to the User class in models.py file_path = os.path.join("test_repo", "models.py") # Line 31 contains the User class definition - references = language_server.request_references(file_path, 31, 6) - - # User class should be referenced in multiple files - assert len(references) > 0 - - # At least two references should be found (one for the class definition itself) - assert len(references) > 1 + # Use selectionRange only + symbols = language_server.request_document_symbols(file_path) + user_symbol = next((s for s in symbols[0] if s.get("name") == "User"), None) + if not user_symbol or "selectionRange" not in user_symbol: + raise AssertionError("User symbol or its selectionRange not found") + sel_start = user_symbol["selectionRange"]["start"] + sel_end = user_symbol["selectionRange"]["end"] + found = False + for col in range(sel_start["character"], sel_end["character"] + 1): + references = language_server.request_references(file_path, sel_start["line"], col) + if len(references) > 1: + found = True + assert found, "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): @@ -37,14 +43,20 @@ class TestLanguageServerBasics: # Get references to the Item class in models.py file_path = os.path.join("test_repo", "models.py") # Line 56 contains the Item class definition - references = language_server.request_references(file_path, 56, 6) - - # Item class should be referenced in multiple places - assert len(references) > 0 - - # At least one reference should be in services.py (ItemService class) - services_references = [ref for ref in references if "services.py" in ref["uri"]] - assert len(services_references) > 0 + # Use selectionRange only + symbols = language_server.request_document_symbols(file_path) + item_symbol = next((s for s in symbols[0] if s.get("name") == "Item"), None) + if not item_symbol or "selectionRange" not in item_symbol: + raise AssertionError("Item symbol or its selectionRange not found") + sel_start = item_symbol["selectionRange"]["start"] + sel_end = item_symbol["selectionRange"]["end"] + found = False + for col in range(sel_start["character"], sel_end["character"] + 1): + references = language_server.request_references(file_path, sel_start["line"], col) + services_references = [ref for ref in references if "services.py" in ref["uri"]] + if len(services_references) > 0: + found = True + assert found, "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): @@ -52,20 +64,38 @@ class TestLanguageServerBasics: # Get references to the id parameter in get_user method file_path = os.path.join("test_repo", "services.py") # Line 24 contains the get_user method with id parameter - references = language_server.request_references(file_path, 24, 16) - - # id parameter should be referenced within the method - assert len(references) > 0 + # Use selectionRange only + symbols = language_server.request_document_symbols(file_path) + get_user_symbol = next((s for s in symbols[0] if s.get("name") == "get_user"), None) + if not get_user_symbol or "selectionRange" not in get_user_symbol: + raise AssertionError("get_user symbol or its selectionRange not found") + sel_start = get_user_symbol["selectionRange"]["start"] + sel_end = get_user_symbol["selectionRange"]["end"] + found = False + for col in range(sel_start["character"], sel_end["character"] + 1): + references = language_server.request_references(file_path, sel_start["line"], col) + if len(references) > 0: + found = True + assert found, "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): # 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 - references = language_server.request_references(file_path, 15, 9) - - # Verify that we get valid references - assert len(references) > 1 + # Use selectionRange only + symbols = language_server.request_document_symbols(file_path) + create_user_symbol = next((s for s in symbols[0] if s.get("name") == "create_user"), None) + if not create_user_symbol or "selectionRange" not in create_user_symbol: + raise AssertionError("create_user symbol or its selectionRange not found") + sel_start = create_user_symbol["selectionRange"]["start"] + sel_end = create_user_symbol["selectionRange"]["end"] + found = False + for col in range(sel_start["character"], sel_end["character"] + 1): + references = language_server.request_references(file_path, sel_start["line"], col) + if len(references) > 1: + found = True + assert found, "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): diff --git a/test/multilspy/python/test_symbol_retrieval.py b/test/multilspy/python/test_symbol_retrieval.py index cc4b35a2..293beb0c 100644 --- a/test/multilspy/python/test_symbol_retrieval.py +++ b/test/multilspy/python/test_symbol_retrieval.py @@ -104,10 +104,18 @@ class TestLanguageServerSymbols: # Test referencing symbols for create_user function file_path = os.path.join("test_repo", "services.py") # Line 15 contains the create_user function definition - ref_symbols = language_server.request_referencing_symbols(file_path, 15, 9) - - # Verify we get referencing symbols - assert len(ref_symbols) > 0 + symbols = language_server.request_document_symbols(file_path) + create_user_symbol = next((s for s in symbols[0] if s.get("name") == "create_user"), None) + if not create_user_symbol or "selectionRange" not in create_user_symbol: + raise AssertionError("create_user symbol or its selectionRange not found") + sel_start = create_user_symbol["selectionRange"]["start"] + sel_end = create_user_symbol["selectionRange"]["end"] + found = False + for col in range(sel_start["character"], sel_end["character"] + 1): + ref_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], col) + if len(ref_symbols) > 0: + found = True + assert found, "No referencing symbols found for create_user (selectionRange)" # Verify the structure of referencing symbols for symbol in ref_symbols: @@ -123,19 +131,23 @@ class TestLanguageServerSymbols: # Test referencing symbols for User class file_path = os.path.join("test_repo", "models.py") # Line 31 contains the User class definition - ref_symbols = language_server.request_referencing_symbols(file_path, 31, 6) - - # Verify we get referencing symbols - assert len(ref_symbols) > 0 - - # At least one reference should be from services.py - services_references = [ - symbol - for symbol in ref_symbols - if "location" in symbol and "uri" in symbol["location"] and "services.py" in symbol["location"]["uri"] - ] - - assert len(services_references) > 0 + symbols = language_server.request_document_symbols(file_path) + user_symbol = next((s for s in symbols[0] if s.get("name") == "User"), None) + if not user_symbol or "selectionRange" not in user_symbol: + raise AssertionError("User symbol or its selectionRange not found") + sel_start = user_symbol["selectionRange"]["start"] + sel_end = user_symbol["selectionRange"]["end"] + found = False + for col in range(sel_start["character"], sel_end["character"] + 1): + ref_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], col) + services_references = [ + symbol + for symbol in ref_symbols + if "location" in symbol and "uri" in symbol["location"] and "services.py" in symbol["location"]["uri"] + ] + if len(services_references) > 0: + found = True + assert found, "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): @@ -143,19 +155,25 @@ class TestLanguageServerSymbols: # Test referencing symbols for id parameter in get_user file_path = os.path.join("test_repo", "services.py") # Line 24 contains the get_user method with id parameter - ref_symbols = language_server.request_referencing_symbols(file_path, 24, 16) - - # Verify we get referencing symbols - assert len(ref_symbols) > 0 - - # Verify the symbols include references within the method body - method_refs = [ - symbol - for symbol in ref_symbols - if "location" in symbol and "range" in symbol["location"] and symbol["location"]["range"]["start"]["line"] > 24 - ] - - assert len(method_refs) > 0 + symbols = language_server.request_document_symbols(file_path) + get_user_symbol = next((s for s in symbols[0] if s.get("name") == "get_user"), None) + if not get_user_symbol or "selectionRange" not in get_user_symbol: + raise AssertionError("get_user symbol or its selectionRange not found") + sel_start = get_user_symbol["selectionRange"]["start"] + sel_end = get_user_symbol["selectionRange"]["end"] + found = False + for col in range(sel_start["character"], sel_end["character"] + 1): + ref_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], col) + method_refs = [ + symbol + for symbol in ref_symbols + if "location" in symbol + and "range" in symbol["location"] + and symbol["location"]["range"]["start"]["line"] > sel_start["line"] + ] + if len(method_refs) > 0: + found = True + assert found, "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): diff --git a/test/multilspy/rust/test_rust_basic.py b/test/multilspy/rust/test_rust_basic.py index 23282271..8b1d1a57 100644 --- a/test/multilspy/rust/test_rust_basic.py +++ b/test/multilspy/rust/test_rust_basic.py @@ -1,7 +1,10 @@ import os + import pytest + from multilspy.multilspy_config import Language + def find_symbol_recursive(symbols, name): for symbol in symbols: if symbol.get("name") == name: @@ -11,6 +14,7 @@ def find_symbol_recursive(symbols, name): return True return False + class TestRustLanguageServer: @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) def test_find_references_raw(self, language_server): @@ -27,7 +31,7 @@ class TestRustLanguageServer: sel_end = add_symbol["selectionRange"]["end"] found = False if sel_start["line"] == sel_end["line"]: - for col in range(sel_start["character"], sel_end["character"]+1): + for col in range(sel_start["character"], sel_end["character"] + 1): refs = language_server.request_references(file_path, sel_start["line"], col) # f"Raw request_references for add in lib.rs at col {col}: {refs}") if any("main.rs" in ref.get("relativePath", "") for ref in refs): @@ -65,7 +69,7 @@ class TestRustLanguageServer: found = False # Try all character positions in the selectionRange (same line, from start to end character) if sel_start["line"] == sel_end["line"]: - for col in range(sel_start["character"], sel_end["character"]+1): + for col in range(sel_start["character"], sel_end["character"] + 1): refs = language_server.request_references(file_path, sel_start["line"], col) # f"References for add in lib.rs at col {col} (selectionRange): {refs}") if any("main.rs" in ref.get("relativePath", "") for ref in refs): From 14f58c9c2e723ffd5b8ef87e80ad9d2220f8d003 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 27 Apr 2025 16:04:27 +0200 Subject: [PATCH 11/61] Tests: removed for-loops over col for finding references --- test/multilspy/python/test_python_basic.py | 38 ++++---------- .../multilspy/python/test_symbol_retrieval.py | 51 +++++++------------ 2 files changed, 27 insertions(+), 62 deletions(-) diff --git a/test/multilspy/python/test_python_basic.py b/test/multilspy/python/test_python_basic.py index 36e1778f..ddf50369 100644 --- a/test/multilspy/python/test_python_basic.py +++ b/test/multilspy/python/test_python_basic.py @@ -29,13 +29,8 @@ class TestLanguageServerBasics: if not user_symbol or "selectionRange" not in user_symbol: raise AssertionError("User symbol or its selectionRange not found") sel_start = user_symbol["selectionRange"]["start"] - sel_end = user_symbol["selectionRange"]["end"] - found = False - for col in range(sel_start["character"], sel_end["character"] + 1): - references = language_server.request_references(file_path, sel_start["line"], col) - if len(references) > 1: - found = True - assert found, "User class should be referenced in multiple files (using selectionRange if present)" + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + 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): @@ -49,14 +44,9 @@ class TestLanguageServerBasics: if not item_symbol or "selectionRange" not in item_symbol: raise AssertionError("Item symbol or its selectionRange not found") sel_start = item_symbol["selectionRange"]["start"] - sel_end = item_symbol["selectionRange"]["end"] - found = False - for col in range(sel_start["character"], sel_end["character"] + 1): - references = language_server.request_references(file_path, sel_start["line"], col) - services_references = [ref for ref in references if "services.py" in ref["uri"]] - if len(services_references) > 0: - found = True - assert found, "At least one reference should be in services.py (using selectionRange if present)" + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + services_references = [ref for ref in references if "services.py" in ref["uri"]] + 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): @@ -70,13 +60,8 @@ class TestLanguageServerBasics: if not get_user_symbol or "selectionRange" not in get_user_symbol: raise AssertionError("get_user symbol or its selectionRange not found") sel_start = get_user_symbol["selectionRange"]["start"] - sel_end = get_user_symbol["selectionRange"]["end"] - found = False - for col in range(sel_start["character"], sel_end["character"] + 1): - references = language_server.request_references(file_path, sel_start["line"], col) - if len(references) > 0: - found = True - assert found, "id parameter should be referenced within the method (using selectionRange if present)" + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + 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): @@ -89,13 +74,8 @@ class TestLanguageServerBasics: if not create_user_symbol or "selectionRange" not in create_user_symbol: raise AssertionError("create_user symbol or its selectionRange not found") sel_start = create_user_symbol["selectionRange"]["start"] - sel_end = create_user_symbol["selectionRange"]["end"] - found = False - for col in range(sel_start["character"], sel_end["character"] + 1): - references = language_server.request_references(file_path, sel_start["line"], col) - if len(references) > 1: - found = True - assert found, "Should get valid references for create_user (using selectionRange if present)" + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + 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): diff --git a/test/multilspy/python/test_symbol_retrieval.py b/test/multilspy/python/test_symbol_retrieval.py index 293beb0c..700f7e6a 100644 --- a/test/multilspy/python/test_symbol_retrieval.py +++ b/test/multilspy/python/test_symbol_retrieval.py @@ -109,13 +109,8 @@ class TestLanguageServerSymbols: if not create_user_symbol or "selectionRange" not in create_user_symbol: raise AssertionError("create_user symbol or its selectionRange not found") sel_start = create_user_symbol["selectionRange"]["start"] - sel_end = create_user_symbol["selectionRange"]["end"] - found = False - for col in range(sel_start["character"], sel_end["character"] + 1): - ref_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], col) - if len(ref_symbols) > 0: - found = True - assert found, "No referencing symbols found for create_user (selectionRange)" + ref_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"]) + assert len(ref_symbols) > 0, "No referencing symbols found for create_user (selectionRange)" # Verify the structure of referencing symbols for symbol in ref_symbols: @@ -136,18 +131,13 @@ class TestLanguageServerSymbols: if not user_symbol or "selectionRange" not in user_symbol: raise AssertionError("User symbol or its selectionRange not found") sel_start = user_symbol["selectionRange"]["start"] - sel_end = user_symbol["selectionRange"]["end"] - found = False - for col in range(sel_start["character"], sel_end["character"] + 1): - ref_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], col) - services_references = [ - symbol - for symbol in ref_symbols - if "location" in symbol and "uri" in symbol["location"] and "services.py" in symbol["location"]["uri"] - ] - if len(services_references) > 0: - found = True - assert found, "No referencing symbols from services.py for User (selectionRange)" + ref_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"]) + services_references = [ + symbol + for symbol in ref_symbols + if "location" in symbol and "uri" in symbol["location"] and "services.py" in symbol["location"]["uri"] + ] + 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): @@ -160,20 +150,15 @@ class TestLanguageServerSymbols: if not get_user_symbol or "selectionRange" not in get_user_symbol: raise AssertionError("get_user symbol or its selectionRange not found") sel_start = get_user_symbol["selectionRange"]["start"] - sel_end = get_user_symbol["selectionRange"]["end"] - found = False - for col in range(sel_start["character"], sel_end["character"] + 1): - ref_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], col) - method_refs = [ - symbol - for symbol in ref_symbols - if "location" in symbol - and "range" in symbol["location"] - and symbol["location"]["range"]["start"]["line"] > sel_start["line"] - ] - if len(method_refs) > 0: - found = True - assert found, "No referencing symbols within method body for get_user (selectionRange)" + ref_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"]) + method_refs = [ + symbol + for symbol in ref_symbols + if "location" in symbol + and "range" in symbol["location"] + and symbol["location"]["range"]["start"]["line"] > sel_start["line"] + ] + 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): From 0f95cc81ad93c00e8010c81bf403c3c99d428874 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 28 Apr 2025 14:58:20 +0200 Subject: [PATCH 12/61] Multilspy: minor, better error messages in gopls --- src/multilspy/language_servers/gopls/gopls.py | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/multilspy/language_servers/gopls/gopls.py b/src/multilspy/language_servers/gopls/gopls.py index 6dbe88cd..f3ca2d03 100644 --- a/src/multilspy/language_servers/gopls/gopls.py +++ b/src/multilspy/language_servers/gopls/gopls.py @@ -5,13 +5,15 @@ import os import pathlib import subprocess from contextlib import asynccontextmanager -from typing import AsyncIterator +from typing import AsyncIterator, List from overrides import override +from multilspy import multilspy_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 ProcessLaunchInfo +from multilspy.lsp_protocol_handler.server import Error, ProcessLaunchInfo from multilspy.lsp_protocol_handler.lsp_types import InitializeParams from multilspy.multilspy_config import MultilspyConfig @@ -57,28 +59,21 @@ class Gopls(LanguageServer): Check if required Go runtime dependencies are available. Raises RuntimeError with helpful message if dependencies are missing. """ - missing_deps = [] - - # Check for Go installation go_version = cls._get_go_version() if not go_version: - missing_deps.append(("Go", "https://golang.org/doc/install")) + raise RuntimeError("Go is not installed. Please install Go from https://golang.org/doc/install and make sure it is added to your PATH.") - # Check for gopls gopls_version = cls._get_gopls_version() if not gopls_version: - missing_deps.append(("gopls", "https://pkg.go.dev/golang.org/x/tools/gopls#section-readme")) - - if missing_deps: - error_msg = "Missing required dependencies:\n" - for dep, install_url in missing_deps: - error_msg += f"- {dep}: Please install from {install_url}\n" - raise RuntimeError(error_msg) + raise RuntimeError( + "Found a Go version but gopls is not installed.\n" + "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): - # Check runtime dependencies before initializing self.setup_runtime_dependency() super().__init__( From 0a74937d9091cb3074e692bd8bc431148c8b783b Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 28 Apr 2025 15:10:48 +0200 Subject: [PATCH 13/61] Multilspy: gopls - added a horrible hack to compensate for the wrong selectionRange --- src/multilspy/language_servers/gopls/gopls.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/multilspy/language_servers/gopls/gopls.py b/src/multilspy/language_servers/gopls/gopls.py index f3ca2d03..73970cfd 100644 --- a/src/multilspy/language_servers/gopls/gopls.py +++ b/src/multilspy/language_servers/gopls/gopls.py @@ -154,3 +154,18 @@ class Gopls(LanguageServer): await self.server.shutdown() await self.server.stop() + + @override + async def request_references( + self, relative_file_path: str, line: int, column: int + ) -> List[multilspy_types.Location]: + NUM_COLS_TO_TRY = 10 + """selectionRange in Gopls always return the wrong column, + the one at the beginnning of the declaration instead of the symbol. We do a dirty hack here.""" + actual_column = column + for actual_column in range(column, column+NUM_COLS_TO_TRY): + try: + return await super().request_references(relative_file_path, line, actual_column) + except (Error, RuntimeError, MultilspyException) as e: + self.logger.log(f"Cannot find symbol at {line=}, column={actual_column}, trying to bump column by 1", logging.INFO) + raise RuntimeError(f"Failed to find references for symbol at {line=}, {column=}") From 42fdb01d9aae9d1791dcdd894473101f53aa98a6 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 28 Apr 2025 15:12:46 +0200 Subject: [PATCH 14/61] Multilspy: import, comment --- src/multilspy/language_server.py | 2 +- src/multilspy/language_servers/gopls/gopls.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 3845ba84..d797d132 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -32,6 +32,7 @@ from .lsp_protocol_handler import lsp_types as LSPTypes from .lsp_protocol_handler.lsp_constants import LSPConstants from .lsp_protocol_handler.lsp_types import SymbolKind from .lsp_protocol_handler.server import ( + Error, LanguageServerHandler, ProcessLaunchInfo, ) @@ -614,7 +615,6 @@ class LanguageServer: ) except Exception as e: # Catch LSP internal error (-32603) and raise a more informative exception - 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 {relative_file_path}:{line}:{column}. " diff --git a/src/multilspy/language_servers/gopls/gopls.py b/src/multilspy/language_servers/gopls/gopls.py index 73970cfd..f17ffcf8 100644 --- a/src/multilspy/language_servers/gopls/gopls.py +++ b/src/multilspy/language_servers/gopls/gopls.py @@ -161,7 +161,9 @@ class Gopls(LanguageServer): ) -> List[multilspy_types.Location]: NUM_COLS_TO_TRY = 10 """selectionRange in Gopls always return the wrong column, - the one at the beginnning of the declaration instead of the symbol. We do a dirty hack here.""" + the one at the beginnning of the declaration instead of the symbol. We do a dirty hack here. + See https://github.com/golang/go/issues/73521 + """ actual_column = column for actual_column in range(column, column+NUM_COLS_TO_TRY): try: From e48031ea73f759a2df1b07b07fa690ebbc012241 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 28 Apr 2025 15:34:29 +0200 Subject: [PATCH 15/61] Tests for TS and Go, general improvements in tests --- .vscode/settings.json | 4 +- src/multilspy/lsp_protocol_handler/server.py | 3 +- test/conftest.py | 14 +++- test/multilspy/go/test_go_basic.py | 32 +++++++++ test/multilspy/java/test_java_basic.py | 48 ++++--------- test/multilspy/python/test_python_basic.py | 12 ++-- .../multilspy/python/test_symbol_retrieval.py | 46 ++++++------ test/multilspy/rust/test_rust_basic.py | 71 +++++-------------- .../typescript/test_typescript_basic.py | 32 +++++++++ test/resources/repos/go/test_repo/main.go | 20 ++++++ .../repos/typescript/test_repo/index.ts | 16 +++++ 11 files changed, 178 insertions(+), 120 deletions(-) create mode 100644 test/multilspy/go/test_go_basic.py create mode 100644 test/multilspy/typescript/test_typescript_basic.py create mode 100644 test/resources/repos/go/test_repo/main.go create mode 100644 test/resources/repos/typescript/test_repo/index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f32d63a..258e3b5c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,7 @@ "pixi", "sensai", "vibing" - ] + ], + "basedpyright.disableLanguageServices": true, + "python.languageServer": "None" } \ No newline at end of file diff --git a/src/multilspy/lsp_protocol_handler/server.py b/src/multilspy/lsp_protocol_handler/server.py index b9dd0de8..81a9dc4a 100644 --- a/src/multilspy/lsp_protocol_handler/server.py +++ b/src/multilspy/lsp_protocol_handler/server.py @@ -38,6 +38,7 @@ from typing import Any, Dict, List, Optional, Union from .lsp_requests import LspNotification, LspRequest from .lsp_types import ErrorCodes +from ..multilspy_exceptions import MultilspyException StringDict = Dict[str, Any] PayloadLike = Union[List[StringDict], StringDict, None] @@ -481,7 +482,7 @@ class LanguageServerHandler: await self._send_payload(make_request(method, request_id, params)) await request.cv.wait() if isinstance(request.error, Error): - raise request.error + raise MultilspyException(f"Could not process request {method} with params:\n{params}.\n Language server error: {request.error}") from request.error return request.result def _send_payload_sync(self, payload: StringDict) -> None: diff --git a/test/conftest.py b/test/conftest.py index 0c6321da..afac89fc 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,6 +5,7 @@ import pytest from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_types import UnifiedSymbolInformation @pytest.fixture(scope="session") @@ -59,7 +60,7 @@ def language_server(request: LanguageParamRequest): Example: ``` @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_python_server(language_server): + def test_python_server(language_server: SyncLanguageServer) -> None: # Use the Python language server pass ``` @@ -67,7 +68,7 @@ def language_server(request: LanguageParamRequest): You can also test multiple languages in a single test: ``` @pytest.mark.parametrize("language_server", [Language.PYTHON, Language.TYPESCRIPT], indirect=True) - def test_multiple_languages(language_server): + def test_multiple_languages(language_server: SyncLanguageServer) -> None: # This test will run once for each language pass ``` @@ -83,3 +84,12 @@ def language_server(request: LanguageParamRequest): yield server finally: server.stop() + + +def symbol_tree_contains_name(roots: list[UnifiedSymbolInformation], name: str) -> bool: + for symbol in roots: + if symbol["name"] == name: + return True + if symbol_tree_contains_name(symbol["children"], name): + return True + return False diff --git a/test/multilspy/go/test_go_basic.py b/test/multilspy/go/test_go_basic.py new file mode 100644 index 00000000..551d92af --- /dev/null +++ b/test/multilspy/go/test_go_basic.py @@ -0,0 +1,32 @@ +import os + +import pytest +from conftest import symbol_tree_contains_name + +from multilspy import SyncLanguageServer +from multilspy.multilspy_config import Language + + +class TestGoLanguageServer: + @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) + def test_find_symbol(self, language_server: SyncLanguageServer) -> None: + symbols = language_server.request_full_symbol_tree() + assert symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree" + assert symbol_tree_contains_name(symbols, "Helper"), "Helper function not found in symbol tree" + assert 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: + file_path = os.path.join("main.go") + symbols = language_server.request_document_symbols(file_path) + helper_symbol = None + for sym in symbols[0]: + if sym.get("name") == "Helper": + helper_symbol = sym + break + assert helper_symbol is not None, "Could not find 'Helper' function symbol in main.go" + sel_start = helper_symbol["selectionRange"]["start"] + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert any( + "main.go" in ref.get("relativePath", "") for ref in refs + ), "main.go should reference Helper (tried all positions in selectionRange)" diff --git a/test/multilspy/java/test_java_basic.py b/test/multilspy/java/test_java_basic.py index d66753f1..9b9cf92b 100644 --- a/test/multilspy/java/test_java_basic.py +++ b/test/multilspy/java/test_java_basic.py @@ -1,30 +1,22 @@ import os import pytest +from conftest import symbol_tree_contains_name +from multilspy import SyncLanguageServer from multilspy.multilspy_config import Language -def find_symbol_recursive(symbols, name): - for symbol in symbols: - if symbol.get("name") == name: - return True - if symbol.get("children"): - if find_symbol_recursive(symbol["children"], name): - return True - return False - - class TestJavaLanguageServer: @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) - def test_find_symbol(self, language_server): + def test_find_symbol(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert find_symbol_recursive(symbols, "Main"), "Main class not found in symbol tree" - assert find_symbol_recursive(symbols, "Utils"), "Utils class not found in symbol tree" - assert find_symbol_recursive(symbols, "Model"), "Model class not found in symbol tree" + assert symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree" + assert symbol_tree_contains_name(symbols, "Utils"), "Utils class not found in symbol tree" + assert 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): + def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> 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) @@ -42,26 +34,16 @@ class TestJavaLanguageServer: # Use selectionRange if present, otherwise fall back to range if "selectionRange" in model_symbol: sel_start = model_symbol["selectionRange"]["start"] - sel_end = model_symbol["selectionRange"]["end"] else: sel_start = model_symbol["range"]["start"] - sel_end = model_symbol["range"]["end"] - found = False - if sel_start["line"] == sel_end["line"]: - for col in range(sel_start["character"], sel_end["character"] + 1): - refs = language_server.request_references(file_path, sel_start["line"], col) - if any("Main.java" in ref.get("relativePath", "") for ref in refs): - found = True - else: - refs_start = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) - refs_end = language_server.request_references(file_path, sel_end["line"], sel_end["character"]) - if any("Main.java" in ref.get("relativePath", "") for ref in refs_start + refs_end): - found = True - assert found, "Main should reference Model (tried all positions in selectionRange)" + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert any( + "Main.java" in ref.get("relativePath", "") for ref in refs + ), "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): + def test_overview_methods(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert find_symbol_recursive(symbols, "Main"), "Main missing from overview" - assert find_symbol_recursive(symbols, "Utils"), "Utils missing from overview" - assert find_symbol_recursive(symbols, "Model"), "Model missing from overview" + assert symbol_tree_contains_name(symbols, "Main"), "Main missing from overview" + assert symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview" + assert symbol_tree_contains_name(symbols, "Model"), "Model missing from overview" diff --git a/test/multilspy/python/test_python_basic.py b/test/multilspy/python/test_python_basic.py index ddf50369..635b19fe 100644 --- a/test/multilspy/python/test_python_basic.py +++ b/test/multilspy/python/test_python_basic.py @@ -18,7 +18,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): + def test_request_references_user_class(self, language_server: SyncLanguageServer) -> 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") @@ -33,7 +33,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): + def test_request_references_item_class(self, language_server: SyncLanguageServer) -> 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") @@ -49,7 +49,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): + def test_request_references_function_parameter(self, language_server: SyncLanguageServer) -> 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") @@ -64,7 +64,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): + def test_request_references_create_user_method(self, language_server: SyncLanguageServer) -> 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 @@ -78,7 +78,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): + def test_retrieve_content_around_line(self, language_server: SyncLanguageServer) -> None: """Test retrieve_content_around_line functionality with various scenarios.""" file_path = os.path.join("test_repo", "models.py") @@ -185,7 +185,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): + def test_search_files_for_pattern(self, language_server: SyncLanguageServer) -> 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_symbol_retrieval.py b/test/multilspy/python/test_symbol_retrieval.py index 700f7e6a..6f7b22cd 100644 --- a/test/multilspy/python/test_symbol_retrieval.py +++ b/test/multilspy/python/test_symbol_retrieval.py @@ -19,7 +19,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): + def test_request_containing_symbol_function(self, language_server: SyncLanguageServer) -> 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") @@ -34,7 +34,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): + def test_references_to_variables(self, language_server: SyncLanguageServer) -> 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 @@ -49,7 +49,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): + def test_request_containing_symbol_class(self, language_server: SyncLanguageServer) -> 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") @@ -62,7 +62,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): + def test_request_containing_symbol_nested(self, language_server: SyncLanguageServer) -> 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") @@ -88,7 +88,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): + def test_request_containing_symbol_none(self, language_server: SyncLanguageServer) -> 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") @@ -99,7 +99,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): + def test_request_referencing_symbols_function(self, language_server: SyncLanguageServer) -> None: """Test request_referencing_symbols for a function.""" # Test referencing symbols for create_user function file_path = os.path.join("test_repo", "services.py") @@ -121,7 +121,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): + def test_request_referencing_symbols_class(self, language_server: SyncLanguageServer) -> None: """Test request_referencing_symbols for a class.""" # Test referencing symbols for User class file_path = os.path.join("test_repo", "models.py") @@ -140,7 +140,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): + def test_request_referencing_symbols_parameter(self, language_server: SyncLanguageServer) -> 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") @@ -154,14 +154,12 @@ class TestLanguageServerSymbols: method_refs = [ symbol for symbol in ref_symbols - if "location" in symbol - and "range" in symbol["location"] - and symbol["location"]["range"]["start"]["line"] > sel_start["line"] + if "location" in symbol and "range" in symbol["location"] and symbol["location"]["range"]["start"]["line"] > sel_start["line"] ] 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): + def test_request_referencing_symbols_none(self, language_server: SyncLanguageServer) -> 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 @@ -179,7 +177,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): + def test_request_defining_symbol_variable(self, language_server: SyncLanguageServer) -> 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") @@ -198,7 +196,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): + def test_request_defining_symbol_imported_class(self, language_server: SyncLanguageServer) -> 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") @@ -210,7 +208,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): + def test_request_defining_symbol_method_call(self, language_server: SyncLanguageServer) -> 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") @@ -235,7 +233,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): + def test_request_defining_symbol_none(self, language_server: SyncLanguageServer) -> 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") @@ -246,7 +244,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): + def test_request_containing_symbol_variable(self, language_server: SyncLanguageServer) -> 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") @@ -259,7 +257,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): + def test_request_defining_symbol_nested_function(self, language_server: SyncLanguageServer) -> 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") @@ -310,7 +308,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): + def test_symbol_methods_integration(self, language_server: SyncLanguageServer) -> 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 @@ -359,7 +357,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): + def test_symbol_tree_structure(self, language_server: SyncLanguageServer) -> 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() @@ -387,7 +385,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): + def test_symbol_tree_structure_subdir(self, language_server: SyncLanguageServer) -> 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") @@ -408,7 +406,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): + def test_request_dir_overview(self, language_server: SyncLanguageServer) -> 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") @@ -433,7 +431,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): + def test_request_document_overview(self, language_server: SyncLanguageServer) -> 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")) @@ -443,7 +441,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): + def test_containing_symbol_of_var_is_file(self, language_server: SyncLanguageServer) -> 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/multilspy/rust/test_rust_basic.py index 8b1d1a57..17a92f28 100644 --- a/test/multilspy/rust/test_rust_basic.py +++ b/test/multilspy/rust/test_rust_basic.py @@ -1,23 +1,15 @@ import os import pytest +from conftest import symbol_tree_contains_name +from multilspy import SyncLanguageServer from multilspy.multilspy_config import Language -def find_symbol_recursive(symbols, name): - for symbol in symbols: - if symbol.get("name") == name: - return True - if symbol.get("children"): - if find_symbol_recursive(symbol["children"], name): - return True - return False - - class TestRustLanguageServer: @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_find_references_raw(self, language_server): + def test_find_references_raw(self, language_server: SyncLanguageServer) -> 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) @@ -28,32 +20,20 @@ class TestRustLanguageServer: break assert add_symbol is not None, "Could not find 'add' function symbol in lib.rs" sel_start = add_symbol["selectionRange"]["start"] - sel_end = add_symbol["selectionRange"]["end"] - found = False - if sel_start["line"] == sel_end["line"]: - for col in range(sel_start["character"], sel_end["character"] + 1): - refs = language_server.request_references(file_path, sel_start["line"], col) - # f"Raw request_references for add in lib.rs at col {col}: {refs}") - if any("main.rs" in ref.get("relativePath", "") for ref in refs): - found = True - else: - refs_start = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) - refs_end = language_server.request_references(file_path, sel_end["line"], sel_end["character"]) - # f"Raw request_references for add in lib.rs at selectionRange start: {refs_start}") - # f"Raw request_references for add in lib.rs at selectionRange end: {refs_end}") - if any("main.rs" in ref.get("relativePath", "") for ref in refs_start + refs_end): - found = True - assert found, "main.rs should reference add (raw, tried all positions in selectionRange)" + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert any( + "main.rs" in ref.get("relativePath", "") for ref in refs + ), "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): + def test_find_symbol(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert find_symbol_recursive(symbols, "main"), "main function not found in symbol tree" - assert find_symbol_recursive(symbols, "add"), "add function not found in symbol tree" + assert symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree" + assert 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): + def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> 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) @@ -64,28 +44,13 @@ class TestRustLanguageServer: break assert add_symbol is not None, "Could not find 'add' function symbol in lib.rs" sel_start = add_symbol["selectionRange"]["start"] - sel_end = add_symbol["selectionRange"]["end"] - # f"add symbol selectionRange: start={sel_start}, end={sel_end}") - found = False - # Try all character positions in the selectionRange (same line, from start to end character) - if sel_start["line"] == sel_end["line"]: - for col in range(sel_start["character"], sel_end["character"] + 1): - refs = language_server.request_references(file_path, sel_start["line"], col) - # f"References for add in lib.rs at col {col} (selectionRange): {refs}") - if any("main.rs" in ref.get("relativePath", "") for ref in refs): - found = True - else: - # If the function name spans multiple lines, try start and end - refs_start = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) - refs_end = language_server.request_references(file_path, sel_end["line"], sel_end["character"]) - # f"References for add in lib.rs at selectionRange start: {refs_start}") - # f"References for add in lib.rs at selectionRange end: {refs_end}") - if any("main.rs" in ref.get("relativePath", "") for ref in refs_start + refs_end): - found = True - assert found, "main.rs should reference add (tried all positions in selectionRange)" + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert any( + "main.rs" in ref.get("relativePath", "") for ref in refs + ), "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): + def test_overview_methods(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert find_symbol_recursive(symbols, "main"), "main missing from overview" - assert find_symbol_recursive(symbols, "add"), "add missing from overview" + assert symbol_tree_contains_name(symbols, "main"), "main missing from overview" + assert symbol_tree_contains_name(symbols, "add"), "add missing from overview" diff --git a/test/multilspy/typescript/test_typescript_basic.py b/test/multilspy/typescript/test_typescript_basic.py new file mode 100644 index 00000000..5c4bd77b --- /dev/null +++ b/test/multilspy/typescript/test_typescript_basic.py @@ -0,0 +1,32 @@ +import os + +import pytest +from conftest import symbol_tree_contains_name + +from multilspy import SyncLanguageServer +from multilspy.multilspy_config import Language + + +class TestTypescriptLanguageServer: + @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) + def test_find_symbol(self, language_server: SyncLanguageServer) -> None: + symbols = language_server.request_full_symbol_tree() + assert symbol_tree_contains_name(symbols, "DemoClass"), "DemoClass not found in symbol tree" + assert symbol_tree_contains_name(symbols, "helperFunction"), "helperFunction not found in symbol tree" + assert 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: + file_path = os.path.join("index.ts") + symbols = language_server.request_document_symbols(file_path) + helper_symbol = None + for sym in symbols[0]: + if sym.get("name") == "helperFunction": + helper_symbol = sym + break + assert helper_symbol is not None, "Could not find 'helperFunction' symbol in index.ts" + sel_start = helper_symbol["selectionRange"]["start"] + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert any( + "index.ts" in ref.get("relativePath", "") for ref in refs + ), "index.ts should reference helperFunction (tried all positions in selectionRange)" diff --git a/test/resources/repos/go/test_repo/main.go b/test/resources/repos/go/test_repo/main.go new file mode 100644 index 00000000..b38c31ff --- /dev/null +++ b/test/resources/repos/go/test_repo/main.go @@ -0,0 +1,20 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Hello, Go!") + Helper() +} + +func Helper() { + fmt.Println("Helper function called") +} + +type DemoStruct struct { + Field int +} + +func UsingHelper() { + Helper() +} diff --git a/test/resources/repos/typescript/test_repo/index.ts b/test/resources/repos/typescript/test_repo/index.ts new file mode 100644 index 00000000..7b8bd1da --- /dev/null +++ b/test/resources/repos/typescript/test_repo/index.ts @@ -0,0 +1,16 @@ +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(); From add17381b89e79ff4190fbfe36a7d4bf3c138596 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 28 Apr 2025 15:44:16 +0200 Subject: [PATCH 16/61] Multislpy: added minor util (SymbolUtils) --- src/multilspy/multilspy_utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/multilspy/multilspy_utils.py b/src/multilspy/multilspy_utils.py index c87f6e65..75f023a8 100644 --- a/src/multilspy/multilspy_utils.py +++ b/src/multilspy/multilspy_utils.py @@ -17,6 +17,7 @@ from enum import Enum from multilspy.multilspy_exceptions import MultilspyException from pathlib import PurePath, Path from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_types import UnifiedSymbolInformation class TextUtils: @@ -322,3 +323,13 @@ class PlatformUtils: except (FileNotFoundError, subprocess.CalledProcessError): raise MultilspyException("dotnet or mono not found on the system") + +class SymbolUtils: + @staticmethod + def symbol_tree_contains_name(roots: list[UnifiedSymbolInformation], name: str) -> bool: + for symbol in roots: + if symbol["name"] == name: + return True + if SymbolUtils.symbol_tree_contains_name(symbol["children"], name): + return True + return False From 7293478fa5bfbd7b68505242a71be2d43e087900 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 28 Apr 2025 15:44:37 +0200 Subject: [PATCH 17/61] Tests: don't import util from conftest --- test/conftest.py | 10 ---------- test/multilspy/go/test_go_basic.py | 8 ++++---- test/multilspy/java/test_java_basic.py | 14 +++++++------- test/multilspy/rust/test_rust_basic.py | 10 +++++----- test/multilspy/typescript/test_typescript_basic.py | 8 ++++---- 5 files changed, 20 insertions(+), 30 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index afac89fc..4e403dfe 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,7 +5,6 @@ import pytest from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_types import UnifiedSymbolInformation @pytest.fixture(scope="session") @@ -84,12 +83,3 @@ def language_server(request: LanguageParamRequest): yield server finally: server.stop() - - -def symbol_tree_contains_name(roots: list[UnifiedSymbolInformation], name: str) -> bool: - for symbol in roots: - if symbol["name"] == name: - return True - if symbol_tree_contains_name(symbol["children"], name): - return True - return False diff --git a/test/multilspy/go/test_go_basic.py b/test/multilspy/go/test_go_basic.py index 551d92af..a87e9fe1 100644 --- a/test/multilspy/go/test_go_basic.py +++ b/test/multilspy/go/test_go_basic.py @@ -1,19 +1,19 @@ import os import pytest -from conftest import symbol_tree_contains_name from multilspy import SyncLanguageServer from multilspy.multilspy_config import Language +from multilspy.multilspy_utils import SymbolUtils class TestGoLanguageServer: @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) def test_find_symbol(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree" - assert symbol_tree_contains_name(symbols, "Helper"), "Helper function not found in symbol tree" - assert symbol_tree_contains_name(symbols, "DemoStruct"), "DemoStruct not found in 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: diff --git a/test/multilspy/java/test_java_basic.py b/test/multilspy/java/test_java_basic.py index 9b9cf92b..8dcf4efd 100644 --- a/test/multilspy/java/test_java_basic.py +++ b/test/multilspy/java/test_java_basic.py @@ -1,19 +1,19 @@ import os import pytest -from conftest import symbol_tree_contains_name from multilspy import SyncLanguageServer from multilspy.multilspy_config import Language +from multilspy.multilspy_utils import SymbolUtils class TestJavaLanguageServer: @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) def test_find_symbol(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree" - assert symbol_tree_contains_name(symbols, "Utils"), "Utils class not found in symbol tree" - assert symbol_tree_contains_name(symbols, "Model"), "Model class not found in 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: @@ -44,6 +44,6 @@ class TestJavaLanguageServer: @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) def test_overview_methods(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert symbol_tree_contains_name(symbols, "Main"), "Main missing from overview" - assert symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview" - assert symbol_tree_contains_name(symbols, "Model"), "Model missing from overview" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main missing from overview" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model missing from overview" diff --git a/test/multilspy/rust/test_rust_basic.py b/test/multilspy/rust/test_rust_basic.py index 17a92f28..aeaf85a4 100644 --- a/test/multilspy/rust/test_rust_basic.py +++ b/test/multilspy/rust/test_rust_basic.py @@ -1,10 +1,10 @@ import os import pytest -from conftest import symbol_tree_contains_name from multilspy import SyncLanguageServer from multilspy.multilspy_config import Language +from multilspy.multilspy_utils import SymbolUtils class TestRustLanguageServer: @@ -28,8 +28,8 @@ class TestRustLanguageServer: @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) def test_find_symbol(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree" - assert symbol_tree_contains_name(symbols, "add"), "add function not found in 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) @@ -52,5 +52,5 @@ class TestRustLanguageServer: @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) def test_overview_methods(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert symbol_tree_contains_name(symbols, "main"), "main missing from overview" - assert symbol_tree_contains_name(symbols, "add"), "add missing from overview" + 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/multilspy/typescript/test_typescript_basic.py index 5c4bd77b..0b8243d9 100644 --- a/test/multilspy/typescript/test_typescript_basic.py +++ b/test/multilspy/typescript/test_typescript_basic.py @@ -1,19 +1,19 @@ import os import pytest -from conftest import symbol_tree_contains_name from multilspy import SyncLanguageServer from multilspy.multilspy_config import Language +from multilspy.multilspy_utils import SymbolUtils class TestTypescriptLanguageServer: @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) def test_find_symbol(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert symbol_tree_contains_name(symbols, "DemoClass"), "DemoClass not found in symbol tree" - assert symbol_tree_contains_name(symbols, "helperFunction"), "helperFunction not found in symbol tree" - assert symbol_tree_contains_name(symbols, "printValue"), "printValue method not found in 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: From 3de5da604705ae6c3f8131c0aad13ce64f81e2bc Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 28 Apr 2025 15:50:12 +0200 Subject: [PATCH 18/61] GH: install gopls before tests --- .github/workflows/pytest.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 1b036c3f..98fd1649 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -16,8 +16,14 @@ jobs: with: python-version: "${{ matrix.python-version }}" - # Include the appropriate package manager steps based on the selection - + # Add Go bin directory to PATH for this workflow + # GITHUB_PATH is a special file that GitHub Actions uses to modify PATH + # Writing to this file adds the directory to the PATH for subsequent steps + - name: Install gopls + run: | + go install golang.org/x/tools/gopls@latest + echo "$HOME/go/bin" >> $GITHUB_PATH + - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create virtual environment From 10bbd62aa27dd0d842368cb03b63c56779a6fc97 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 28 Apr 2025 14:48:20 +0200 Subject: [PATCH 19/61] Convert SerenaConfig and ProjectConfig to dataclasses, facilitating testing --- scripts/demo_run_tools.py | 2 +- src/serena/agent.py | 118 ++++++++++++++++++++++++++++---------- 2 files changed, 89 insertions(+), 31 deletions(-) diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index 0ebcc0c3..4d639b48 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -13,7 +13,7 @@ from serena.agent import * project_root = Path(__file__).parent.parent if __name__ == "__main__": - agent = SerenaAgent(project_file_path=str(project_root / ".serena" / "project.yml")) + agent = SerenaAgent(project_config=str(project_root / ".serena" / "project.yml")) overview_tool = agent.get_tool(GetSymbolsOverviewTool) find_symbol_tool = agent.get_tool(FindSymbolTool) diff --git a/src/serena/agent.py b/src/serena/agent.py index fe8028af..554eb5b8 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -11,6 +11,7 @@ import traceback from abc import ABC from collections import defaultdict from collections.abc import Callable, Generator, Iterable +from dataclasses import field from logging import Logger from pathlib import Path from typing import TYPE_CHECKING, Any, Self, TypeVar, Union, cast @@ -28,7 +29,6 @@ from serena import serena_root_path, serena_version from serena.llm.prompt_factory import PromptFactory from serena.symbol import SymbolLocation, SymbolManager from serena.text_utils import search_files -from serena.util.class_decorators import singleton from serena.util.file_system import scan_directory from serena.util.inspection import iter_subclasses from serena.util.shell import execute_shell_command @@ -67,22 +67,40 @@ class SerenaConfigError(Exception): pass +import logging +from dataclasses import dataclass + + +@dataclass class ProjectConfig(ToStringMixin): + project_name: str + language: Language + project_root: str + ignored_paths: list[str] = field(default_factory=list) + excluded_tools: set[str] = field(default_factory=set) + read_only: bool = False + ignore_all_files_in_gitignore: bool = True + SERENA_MANAGED_DIR = ".serena" SERENA_DEFAULT_PROJECT_FILE = "project.yml" - def __init__(self, config_dict: dict[str, Any], project_name: str, project_root: Path | None = None): - self.project_name: str = project_name + @classmethod + def from_config_dict(cls, config_dict: dict[str, Any], project_name: str, project_root: Path | None = None) -> Self: + """ + Create a ProjectConfig instance from a configuration dictionary + """ try: - self.language: Language = Language(config_dict["language"].lower()) + language = Language(config_dict["language"].lower()) except ValueError as e: raise ValueError(f"Invalid language: {config_dict['language']}.\nValid languages are: {[l.value for l in Language]}") from e + if project_root is None: project_root = Path(config_dict["project_root"]) - self.project_root: str = str(project_root.resolve()) - self.ignored_paths: list[str] = config_dict.get("ignored_paths", []) - self.excluded_tools: set[str] = set(config_dict.get("excluded_tools", [])) - self.read_only: bool = config_dict.get("read_only", False) + project_root_str = str(project_root.resolve()) + + ignored_paths = config_dict.get("ignored_paths", []) + excluded_tools = set(config_dict.get("excluded_tools", [])) + read_only = config_dict.get("read_only", False) if "ignore_all_files_in_gitignore" not in config_dict: raise SerenaConfigError( @@ -90,7 +108,7 @@ class ProjectConfig(ToStringMixin): "Please update your `.yml` configuration file for this project. " "It is recommended to set this to `True`." ) - self.ignore_all_files_in_gitignore = config_dict["ignore_all_files_in_gitignore"] + ignore_all_files_in_gitignore = config_dict["ignore_all_files_in_gitignore"] # Raise errors for deprecated keys if "ignored_dirs" in config_dict: @@ -99,9 +117,24 @@ class ProjectConfig(ToStringMixin): "Note that you can also set `ignore_all_files_in_gitignore` to `True`, which will be enough for most cases." ) + return cls( + project_name=project_name, + language=language, + project_root=project_root_str, + ignored_paths=ignored_paths, + excluded_tools=excluded_tools, + read_only=read_only, + ignore_all_files_in_gitignore=ignore_all_files_in_gitignore, + ) + @classmethod def from_yml(cls, yml_path: Path) -> Self: + """ + Create a ProjectConfig instance from a YAML file + """ log.info(f"Loading project configuration from {yml_path}") + if not os.path.exists(yml_path): + raise FileNotFoundError(f"Project file not found: {yml_path}") try: with open(yml_path, encoding="utf-8") as f: config_dict = yaml.safe_load(f) @@ -111,7 +144,7 @@ class ProjectConfig(ToStringMixin): else: project_root = None project_name = yml_path.stem - return cls(config_dict, project_name=project_name, project_root=project_root) + return cls.from_config_dict(config_dict, project_name=project_name, project_root=project_root) except Exception as e: raise ValueError(f"Error loading project configuration from {yml_path}: {e}") from e @@ -119,18 +152,28 @@ class ProjectConfig(ToStringMixin): return os.path.join(self.project_root, self.SERENA_MANAGED_DIR) -@singleton class SerenaConfig: """ Handles user-defined Serena configuration based on the configuration file """ + projects: dict[str, "ProjectConfig"] = field(default_factory=dict) + project_names: list[str] = field(default_factory=list) + gui_log_window_enabled: bool = False + gui_log_window_level: int = logging.INFO + enable_project_activation: bool = True + CONFIG_FILE = "serena_config.yml" - def __init__(self) -> None: - config_file = os.path.join(serena_root_path(), self.CONFIG_FILE) + @classmethod + def from_config_file(cls) -> "SerenaConfig": + """ + Static constructor to create SerenaConfig from the configuration file + """ + config_file = os.path.join(serena_root_path(), cls.CONFIG_FILE) if not os.path.exists(config_file): raise FileNotFoundError(f"Serena configuration file not found: {config_file}") + with open(config_file, encoding="utf-8") as f: try: log.info(f"Loading Serena configuration from {config_file}") @@ -138,10 +181,13 @@ class SerenaConfig: except Exception as e: raise ValueError(f"Error loading Serena configuration from {config_file}: {e}") from e + # Create instance + instance = cls() + # read projects - self.projects: dict[str, ProjectConfig] = {} if "projects" not in config_yaml: raise SerenaConfigError("`projects` key not found in Serena configuration. Please update your `serena_config.yml` file.") + projects = {} for project_config_path in config_yaml["projects"]: project_config_path = Path(project_config_path) if not project_config_path.is_absolute(): @@ -152,14 +198,17 @@ class SerenaConfig: raise FileNotFoundError(f"Project file not found: {project_config_path}") log.info(f"Loading project configuration from {project_config_path}") project_config = ProjectConfig.from_yml(project_config_path) - self.projects[project_config.project_name] = project_config - self.project_names = list(self.projects.keys()) + projects[project_config.project_name] = project_config - self.gui_log_window_enabled = config_yaml.get("gui_log_window", False) - self.gui_log_window_level = config_yaml.get("gui_log_level", logging.INFO) - self.enable_project_activation = config_yaml.get("enable_project_activation", True) + instance.projects = projects + instance.project_names = list(projects.keys()) + instance.gui_log_window_enabled = config_yaml.get("gui_log_window", False) + instance.gui_log_window_level = config_yaml.get("gui_log_level", logging.INFO) + instance.enable_project_activation = config_yaml.get("enable_project_activation", True) - def get_project_configuration(self, project_name: str) -> ProjectConfig: + return instance + + def get_project_configuration(self, project_name: str) -> "ProjectConfig": if project_name not in self.projects: raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}") return self.projects[project_name] @@ -182,15 +231,22 @@ class LinesRead: class SerenaAgent: - def __init__(self, project_file_path: str | None = None, project_activation_callback: Callable[[], None] | None = None): + def __init__( + self, + project_config: ProjectConfig | str | None = None, + project_activation_callback: Callable[[], None] | None = None, + serena_config: SerenaConfig | None = None, + ) -> None: """ - :param project_file_path: the configuration file (.yml) of the project to load immediately; + :param project_config: the project to load immediately; may be a path to the project configuration file + or a configuration instance; if None, do not load any project (must use project selection tool to activate a project). If a project is provided, the corresponding language server will be started. :param project_activation_callback: a callback function to be called when a project is activated. + :param serena_config: the Serena configuration or None to read the configuration from the default location. """ # obtain serena configuration - self.serena_config = SerenaConfig() + self.serena_config = serena_config | SerenaConfig.from_config_file() # open GUI log window if enabled self._gui_log_handler: Union["GuiLogViewerHandler", None] = None # noqa @@ -240,12 +296,14 @@ class SerenaAgent: self._gui_log_handler.log_viewer.set_tool_names(tool_names) # activate a project configuration (if provided or if there is only a single project available) - project_config: ProjectConfig | None = None - if project_file_path is not None: - if not os.path.exists(project_file_path): - raise FileNotFoundError(f"Project file not found: {project_file_path}") - log.info(f"Loading project configuration from {project_file_path}") - project_config = ProjectConfig.from_yml(Path(project_file_path)) + project_config_to_load: ProjectConfig | None = None + if project_config is not None: + if isinstance(project_config, str): + project_config_to_load = ProjectConfig.from_yml(Path(project_config)) + elif isinstance(project_config, ProjectConfig): + project_config_to_load = project_config + else: + raise ValueError(f"Invalid project configuration: {project_config}, (type {type(project_config)})") else: match len(self.serena_config.projects): case 0: @@ -253,7 +311,7 @@ class SerenaAgent: case 1: project_config = self.serena_config.get_project_configuration(self.serena_config.project_names[0]) if project_config is not None: - self.activate_project(project_config) + self.activate_project(project_config_to_load) else: if not self.serena_config.enable_project_activation: raise ValueError("Tool-based project activation is disabled in the configuration but no project file was provided.") From 6bc03191a714c00577a771153da3156a13014ba8 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 29 Apr 2025 14:15:04 +0200 Subject: [PATCH 20/61] Multilspy: sleep in TS language server before requesting references --- src/multilspy/language_server.py | 26 ++++++++--------- src/multilspy/language_servers/gopls/gopls.py | 12 ++++---- .../typescript_language_server.py | 28 +++++++++++++------ 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index d797d132..27dceb1f 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -14,19 +14,16 @@ import os import pathlib import pickle import re -from site import abs_paths import threading from collections import defaultdict from contextlib import asynccontextmanager, contextmanager from copy import copy -from typing import Dict, List, Optional, Tuple, Union -from fnmatch import fnmatch from pathlib import Path, PurePath from typing import AsyncIterator, Dict, Iterator, List, Optional, Tuple, Union, cast import pathspec -from serena.text_utils import LineType, MatchedConsecutiveLines, TextLine, search_files, search_text +from serena.text_utils import LineType, MatchedConsecutiveLines, TextLine, search_files from . import multilspy_types from .lsp_protocol_handler import lsp_types as LSPTypes from .lsp_protocol_handler.lsp_constants import LSPConstants @@ -38,9 +35,6 @@ from .lsp_protocol_handler.server import ( ) from .multilspy_config import Language, MultilspyConfig from .multilspy_exceptions import MultilspyException -from .multilspy_utils import PathUtils, FileUtils, TextUtils -from pathlib import PurePath -from typing import AsyncIterator, Iterator, List, Dict, Optional, Union, Tuple from .multilspy_logger import MultilspyLogger from .multilspy_utils import FileUtils, PathUtils, TextUtils from .type_helpers import ensure_all_methods_implemented @@ -581,6 +575,16 @@ class LanguageServer: return ret + # 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): + return await 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}, + "context": {"includeDeclaration": False}, + } + ) + async def request_references( self, relative_file_path: str, line: int, column: int ) -> List[multilspy_types.Location]: @@ -606,13 +610,7 @@ class LanguageServer: with self.open_file(relative_file_path): try: - response = await 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}, - "context": {"includeDeclaration": False}, - } - ) + response = await 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: diff --git a/src/multilspy/language_servers/gopls/gopls.py b/src/multilspy/language_servers/gopls/gopls.py index f17ffcf8..6269db82 100644 --- a/src/multilspy/language_servers/gopls/gopls.py +++ b/src/multilspy/language_servers/gopls/gopls.py @@ -156,18 +156,16 @@ class Gopls(LanguageServer): await self.server.stop() @override - async def request_references( - self, relative_file_path: str, line: int, column: int - ) -> List[multilspy_types.Location]: + async def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[multilspy_types.Location]: NUM_COLS_TO_TRY = 10 - """selectionRange in Gopls always return the wrong column, - the one at the beginnning of the declaration instead of the symbol. We do a dirty hack here. + """selectionRange in Gopls always contains the wrong column, + the one at the beginning of the declaration instead of the symbol. We do a dirty hack here (loop over some columns). See https://github.com/golang/go/issues/73521 """ actual_column = column for actual_column in range(column, column+NUM_COLS_TO_TRY): try: - return await super().request_references(relative_file_path, line, actual_column) + return await super()._send_references_request(relative_file_path, line, actual_column) except (Error, RuntimeError, MultilspyException) as e: self.logger.log(f"Cannot find symbol at {line=}, column={actual_column}, trying to bump column by 1", logging.INFO) - raise RuntimeError(f"Failed to find references for symbol at {line=}, {column=}") + raise MultilspyException(f"Failed to find references for symbol in\n{relative_file_path=} at {line=}, {column=}") diff --git a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py index 14426c6f..6657ebba 100644 --- a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py @@ -4,22 +4,23 @@ Provides TypeScript specific instantiation of the LanguageServer class. Contains import asyncio import json -import shutil import logging import os -import subprocess import pathlib +import shutil +import subprocess +from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from typing import AsyncIterator, Tuple +from time import sleep 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.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import PlatformUtils, PlatformId +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_utils import PlatformId, PlatformUtils # Platform-specific imports if os.name != 'nt': # Unix-like systems @@ -82,7 +83,7 @@ class TypeScriptLanguageServer(LanguageServer): ] assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy javascript/typescript at the moment" - 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"] @@ -132,7 +133,7 @@ class TypeScriptLanguageServer(LanguageServer): """ Returns the initialize params for the TypeScript 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"] @@ -222,3 +223,14 @@ class TypeScriptLanguageServer(LanguageServer): 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): + # 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) From 4cd4ac79264161fce5b25e7d2358c5bad3c4dd3c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 29 Apr 2025 14:16:33 +0200 Subject: [PATCH 21/61] Minor relaxations in config interfaces --- src/serena/agent.py | 48 ++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 554eb5b8..ec1e2d2b 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -11,7 +11,7 @@ import traceback from abc import ABC from collections import defaultdict from collections.abc import Callable, Generator, Iterable -from dataclasses import field +from dataclasses import dataclass, field from logging import Logger from pathlib import Path from typing import TYPE_CHECKING, Any, Self, TypeVar, Union, cast @@ -67,10 +67,6 @@ class SerenaConfigError(Exception): pass -import logging -from dataclasses import dataclass - - @dataclass class ProjectConfig(ToStringMixin): project_name: str @@ -84,6 +80,10 @@ class ProjectConfig(ToStringMixin): SERENA_MANAGED_DIR = ".serena" SERENA_DEFAULT_PROJECT_FILE = "project.yml" + @classmethod + def rel_path_to_project_yml(cls): + return os.path.join(cls.SERENA_MANAGED_DIR, cls.SERENA_DEFAULT_PROJECT_FILE) + @classmethod def from_config_dict(cls, config_dict: dict[str, Any], project_name: str, project_root: Path | None = None) -> Self: """ @@ -128,12 +128,12 @@ class ProjectConfig(ToStringMixin): ) @classmethod - def from_yml(cls, yml_path: Path) -> Self: + def _from_yml(cls, yml_path: Path) -> Self: """ Create a ProjectConfig instance from a YAML file """ log.info(f"Loading project configuration from {yml_path}") - if not os.path.exists(yml_path): + if not yml_path.exists(): raise FileNotFoundError(f"Project file not found: {yml_path}") try: with open(yml_path, encoding="utf-8") as f: @@ -148,16 +148,30 @@ class ProjectConfig(ToStringMixin): except Exception as e: raise ValueError(f"Error loading project configuration from {yml_path}: {e}") from e + @classmethod + def from_path(cls, path: Path | str) -> Self: + """ + Create a ProjectConfig instance from a path. Can either be a path to a yaml file or a directory + containing `.serena/project.yml`. + """ + path = Path(path) + if not path.exists(): + raise FileNotFoundError(f"Path not found: {path}") + if path.is_dir(): + path = path / cls.SERENA_MANAGED_DIR / cls.SERENA_DEFAULT_PROJECT_FILE + return cls._from_yml(path) + def get_serena_managed_dir(self) -> str: return os.path.join(self.project_root, self.SERENA_MANAGED_DIR) +@dataclass class SerenaConfig: """ Handles user-defined Serena configuration based on the configuration file """ - projects: dict[str, "ProjectConfig"] = field(default_factory=dict) + projects: dict[str, ProjectConfig] = field(default_factory=dict) project_names: list[str] = field(default_factory=list) gui_log_window_enabled: bool = False gui_log_window_level: int = logging.INFO @@ -197,7 +211,7 @@ class SerenaConfig: if not project_config_path.is_file(): raise FileNotFoundError(f"Project file not found: {project_config_path}") log.info(f"Loading project configuration from {project_config_path}") - project_config = ProjectConfig.from_yml(project_config_path) + project_config = ProjectConfig.from_path(project_config_path) projects[project_config.project_name] = project_config instance.projects = projects @@ -233,7 +247,7 @@ class LinesRead: class SerenaAgent: def __init__( self, - project_config: ProjectConfig | str | None = None, + project_config: ProjectConfig | str | Path | None = None, project_activation_callback: Callable[[], None] | None = None, serena_config: SerenaConfig | None = None, ) -> None: @@ -246,7 +260,7 @@ class SerenaAgent: :param serena_config: the Serena configuration or None to read the configuration from the default location. """ # obtain serena configuration - self.serena_config = serena_config | SerenaConfig.from_config_file() + self.serena_config = serena_config or SerenaConfig.from_config_file() # open GUI log window if enabled self._gui_log_handler: Union["GuiLogViewerHandler", None] = None # noqa @@ -298,19 +312,17 @@ class SerenaAgent: # activate a project configuration (if provided or if there is only a single project available) project_config_to_load: ProjectConfig | None = None if project_config is not None: - if isinstance(project_config, str): - project_config_to_load = ProjectConfig.from_yml(Path(project_config)) - elif isinstance(project_config, ProjectConfig): - project_config_to_load = project_config + if isinstance(project_config, (str, Path)): + project_config_to_load = ProjectConfig.from_path(project_config) else: - raise ValueError(f"Invalid project configuration: {project_config}, (type {type(project_config)})") + project_config_to_load = project_config else: match len(self.serena_config.projects): case 0: raise RuntimeError(f"No projects found in {SerenaConfig.CONFIG_FILE} and no project file specified.") case 1: - project_config = self.serena_config.get_project_configuration(self.serena_config.project_names[0]) - if project_config is not None: + project_config_to_load = self.serena_config.get_project_configuration(self.serena_config.project_names[0]) + if project_config_to_load is not None: self.activate_project(project_config_to_load) else: if not self.serena_config.enable_project_activation: From adad5008c7fb67bde92592642dfd3ef215b36326 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 29 Apr 2025 14:16:49 +0200 Subject: [PATCH 22/61] Integration tests of SerenaAgen --- .../src/main/java/test_repo/ModelUser.java | 8 ++ .../repos/typescript/test_repo/tsconfig.json | 11 +++ .../repos/typescript/test_repo/use_helper.ts | 8 ++ test/serena/test_serena_agent.py | 75 +++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 test/resources/repos/java/test_repo/src/main/java/test_repo/ModelUser.java create mode 100644 test/resources/repos/typescript/test_repo/tsconfig.json create mode 100644 test/resources/repos/typescript/test_repo/use_helper.ts create mode 100644 test/serena/test_serena_agent.py diff --git a/test/resources/repos/java/test_repo/src/main/java/test_repo/ModelUser.java b/test/resources/repos/java/test_repo/src/main/java/test_repo/ModelUser.java new file mode 100644 index 00000000..9cffa9fd --- /dev/null +++ b/test/resources/repos/java/test_repo/src/main/java/test_repo/ModelUser.java @@ -0,0 +1,8 @@ +package test_repo; + +public class ModelUser { + public static void main(String[] args) { + Model model = new Model("Cascade"); + System.out.println(model.getName()); + } +} diff --git a/test/resources/repos/typescript/test_repo/tsconfig.json b/test/resources/repos/typescript/test_repo/tsconfig.json new file mode 100644 index 00000000..ba0514f5 --- /dev/null +++ b/test/resources/repos/typescript/test_repo/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2017", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"] +} diff --git a/test/resources/repos/typescript/test_repo/use_helper.ts b/test/resources/repos/typescript/test_repo/use_helper.ts new file mode 100644 index 00000000..e9147d27 --- /dev/null +++ b/test/resources/repos/typescript/test_repo/use_helper.ts @@ -0,0 +1,8 @@ +import {helperFunction} from "./index"; + + +export function useHelper() { + helperFunction(); +} + +useHelper(); diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py new file mode 100644 index 00000000..bf64d5a0 --- /dev/null +++ b/test/serena/test_serena_agent.py @@ -0,0 +1,75 @@ +import json +import os +import time + +import pytest + +from multilspy.multilspy_config import Language +from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, ProjectConfig, SerenaAgent, SerenaConfig +from test.conftest import LanguageParamRequest, get_repo_path + + +@pytest.fixture +def serena_config(): + return SerenaConfig() + + +@pytest.fixture +def serena_agent(request: LanguageParamRequest) -> SerenaAgent: + language = Language(request.param) + repo = get_repo_path(language) + project_config = ProjectConfig(project_name=os.path.basename(str(repo)), language=language, project_root=str(repo)) + return SerenaAgent(project_config=project_config) + + +class TestSerenaAgent: + @pytest.mark.parametrize( + "serena_agent,symbol_name,expected_kind,expected_file", + [ + (Language.PYTHON, "User", "Class", "models.py"), + (Language.GO, "Helper", "Function", "main.go"), + (Language.JAVA, "Model", "Class", "Model.java"), + (Language.RUST, "add", "Function", "lib.rs"), + (Language.TYPESCRIPT, "DemoClass", "Class", "index.ts"), + ], + indirect=["serena_agent"], + ) + def test_find_symbol(self, serena_agent, symbol_name, expected_kind, expected_file): + agent = serena_agent + find_symbol_tool = agent.get_tool(FindSymbolTool) + result = find_symbol_tool.apply(symbol_name) + symbols = json.loads(result) + assert any( + symbol_name in s["name"] and expected_kind.lower() in s["kind"].lower() and expected_file in s["location"]["relative_path"] + for s in symbols + ), f"Expected to find {symbol_name} ({expected_kind}) in {expected_file} for {agent.project_config.language.name}" + + @pytest.mark.parametrize( + "serena_agent,symbol_name,def_file,ref_file", + [ + (Language.PYTHON, "User", "test_repo/models.py", "test_repo/services.py"), + (Language.GO, "Helper", "main.go", "main.go"), + (Language.JAVA, "Model", "src/main/java/test_repo/Model.java", "src/main/java/test_repo/Main.java"), + (Language.RUST, "add", "src/lib.rs", "src/main.rs"), + (Language.TYPESCRIPT, "helperFunction", "index.ts", "use_helper.ts"), + ], + indirect=["serena_agent"], + ) + def test_find_symbol_references(self, serena_agent: SerenaAgent, symbol_name: str, def_file: str, ref_file: str) -> None: + agent = serena_agent + # Find the symbol location first + find_symbol_tool = agent.get_tool(FindSymbolTool) + result = find_symbol_tool.apply(symbol_name, within_relative_path=def_file) + time.sleep(1) + symbols = json.loads(result) + # Find the definition location + def_symbol = symbols[0] + loc = def_symbol["location"] + # sel_start = def_symbol["location"]["selectionRange"]["start"] + # Now find references + find_refs_tool = agent.get_tool(FindReferencingSymbolsTool) + result = find_refs_tool.apply(def_file, loc["line"], loc["column"]) + refs = json.loads(result) + assert any( + ref["location"]["relative_path"] == ref_file for ref in refs + ), f"Expected to find reference to {symbol_name} in {ref_file} for {agent.project_config.language.name}. refs={refs}" From e5e8fc2aa658f3000c0c7c4837ad0df221eef9b4 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 29 Apr 2025 14:18:52 +0200 Subject: [PATCH 23/61] Comment --- src/multilspy/language_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 27dceb1f..b05e3c77 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -575,7 +575,7 @@ class LanguageServer: return ret - # For some reason, the LS may need longer to process this, so we just retry + # 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): return await self.server.send.references( { From 801bef4998b0e89890b12c21edc1f3385ffc7a14 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 29 Apr 2025 14:20:48 +0200 Subject: [PATCH 24/61] Typo in test --- test/serena/test_serena_agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index bf64d5a0..4d16bbec 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -15,11 +15,11 @@ def serena_config(): @pytest.fixture -def serena_agent(request: LanguageParamRequest) -> SerenaAgent: +def serena_agent(request: LanguageParamRequest, serena_config) -> SerenaAgent: language = Language(request.param) repo = get_repo_path(language) project_config = ProjectConfig(project_name=os.path.basename(str(repo)), language=language, project_root=str(repo)) - return SerenaAgent(project_config=project_config) + return SerenaAgent(project_config=project_config, serena_config=serena_config) class TestSerenaAgent: From d396b07990fa2114a5ba63ea0f96fe0dc315ab66 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 29 Apr 2025 14:22:12 +0200 Subject: [PATCH 25/61] GH action: don't run outside of PR --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 98fd1649..350288e1 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,6 +1,6 @@ name: Tests on Ubuntu -on: [push, pull_request] +on: [pull_request] jobs: cpu: From db60f80b01509ea7603662eccc98ac041b6d01bf Mon Sep 17 00:00:00 2001 From: ShyRaptor <18635772+shyraptor@users.noreply.github.com> Date: Thu, 1 May 2025 16:13:06 +0200 Subject: [PATCH 26/61] specified utf-8 encoding in symbol.py --- src/serena/symbol.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index b9e75b25..f49e33f6 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -297,10 +297,11 @@ class SymbolManager: yield root_path = self.lang_server.language_server.repository_root_path abs_path = os.path.join(root_path, relative_path) - with open(abs_path, "w") as f: + with open(abs_path, "w", encoding="utf-8") as f: f.write(file_buffer.contents) self.agent.mark_file_modified(relative_path) + @contextmanager def _edited_symbol_location(self, location: SymbolLocation) -> Iterator[Symbol]: symbol = self.find_by_location(location) From 3e9384f5dbf4cc2f963baf979b12e2e4f987bc87 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 1 May 2025 22:46:08 +0200 Subject: [PATCH 27/61] Switched to MIT license --- LICENSE | 352 +++----------------------------------------------------- 1 file changed, 17 insertions(+), 335 deletions(-) diff --git a/LICENSE b/LICENSE index d159169d..d27bba64 100644 --- a/LICENSE +++ b/LICENSE @@ -1,339 +1,21 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 +MIT License - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. +Copyright (c) 2025 Oraios AI - Preamble +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 licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. +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. From 515870b55b9d02612d8306cff850f606326a7394 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 1 May 2025 22:57:55 +0200 Subject: [PATCH 28/61] Tests: use markers --- src/serena/symbol.py | 1 - test/multilspy/java/test_java_basic.py | 1 + test/multilspy/python/test_python_basic.py | 1 + .../python/test_retrieval_with_ignored_dirs.py | 3 +++ test/multilspy/python/test_symbol_retrieval.py | 2 ++ test/multilspy/rust/test_rust_basic.py | 1 + test/multilspy/typescript/test_typescript_basic.py | 1 + test/serena/test_serena_agent.py | 12 ++++++------ 8 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index f49e33f6..0fcc553f 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -301,7 +301,6 @@ class SymbolManager: f.write(file_buffer.contents) self.agent.mark_file_modified(relative_path) - @contextmanager def _edited_symbol_location(self, location: SymbolLocation) -> Iterator[Symbol]: symbol = self.find_by_location(location) diff --git a/test/multilspy/java/test_java_basic.py b/test/multilspy/java/test_java_basic.py index 8dcf4efd..e7ff5a8c 100644 --- a/test/multilspy/java/test_java_basic.py +++ b/test/multilspy/java/test_java_basic.py @@ -7,6 +7,7 @@ from multilspy.multilspy_config import Language from multilspy.multilspy_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: diff --git a/test/multilspy/python/test_python_basic.py b/test/multilspy/python/test_python_basic.py index 635b19fe..5af4f953 100644 --- a/test/multilspy/python/test_python_basic.py +++ b/test/multilspy/python/test_python_basic.py @@ -14,6 +14,7 @@ from multilspy.multilspy_config import Language from serena.text_utils import LineType +@pytest.mark.python class TestLanguageServerBasics: """Test basic functionality of the language server.""" diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index 7615830f..5150d53a 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -8,6 +8,9 @@ from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger from test.conftest import get_repo_path +# 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]: diff --git a/test/multilspy/python/test_symbol_retrieval.py b/test/multilspy/python/test_symbol_retrieval.py index 6f7b22cd..61a83cea 100644 --- a/test/multilspy/python/test_symbol_retrieval.py +++ b/test/multilspy/python/test_symbol_retrieval.py @@ -14,6 +14,8 @@ from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language from multilspy.multilspy_types import SymbolKind +pytestmark = pytest.mark.python + class TestLanguageServerSymbols: """Test the language server's symbol-related functionality.""" diff --git a/test/multilspy/rust/test_rust_basic.py b/test/multilspy/rust/test_rust_basic.py index aeaf85a4..02e9d502 100644 --- a/test/multilspy/rust/test_rust_basic.py +++ b/test/multilspy/rust/test_rust_basic.py @@ -7,6 +7,7 @@ from multilspy.multilspy_config import Language from multilspy.multilspy_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: diff --git a/test/multilspy/typescript/test_typescript_basic.py b/test/multilspy/typescript/test_typescript_basic.py index 0b8243d9..442b5a1e 100644 --- a/test/multilspy/typescript/test_typescript_basic.py +++ b/test/multilspy/typescript/test_typescript_basic.py @@ -7,6 +7,7 @@ from multilspy.multilspy_config import Language from multilspy.multilspy_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: diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 4d16bbec..7c3d76eb 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -26,15 +26,15 @@ class TestSerenaAgent: @pytest.mark.parametrize( "serena_agent,symbol_name,expected_kind,expected_file", [ - (Language.PYTHON, "User", "Class", "models.py"), - (Language.GO, "Helper", "Function", "main.go"), - (Language.JAVA, "Model", "Class", "Model.java"), - (Language.RUST, "add", "Function", "lib.rs"), - (Language.TYPESCRIPT, "DemoClass", "Class", "index.ts"), + pytest.param(Language.PYTHON, "User", "Class", "models.py", marks=pytest.mark.python), + pytest.param(Language.GO, "Helper", "Function", "main.go", marks=pytest.mark.go), + pytest.param(Language.JAVA, "Model", "Class", "Model.java", marks=pytest.mark.java), + pytest.param(Language.RUST, "add", "Function", "lib.rs", marks=pytest.mark.rust), + pytest.param(Language.TYPESCRIPT, "DemoClass", "Class", "index.ts", marks=pytest.mark.typescript), ], indirect=["serena_agent"], ) - def test_find_symbol(self, serena_agent, symbol_name, expected_kind, expected_file): + def test_find_symbol(self, serena_agent: SerenaAgent, symbol_name: str, expected_kind: str, expected_file: str): agent = serena_agent find_symbol_tool = agent.get_tool(FindSymbolTool) result = find_symbol_tool.apply(symbol_name) From 81f786a2b413cc3f5e4d02bd1f1f10c4719c2751 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 1 May 2025 23:10:48 +0200 Subject: [PATCH 29/61] Pyproject, minor --- pyproject.toml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6f4e41a1..e7f1cdbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ serena-mcp-server = "serena.mcp:start_mcp_server" serena-list-tools = "serena.agent:print_tool_overview" [project.license] -text = "GPL-2.0" +text = "MIT" [project.optional-dependencies] dev = [ @@ -107,7 +107,7 @@ exclude = "^build/|^docs/" PYDEVD_DISABLE_FILE_VALIDATION = "1" [tool.poe.tasks] -test = "pytest test --cov=serena --cov-report=xml --cov-report=term-missing --durations=0 -v --color=yes" +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" @@ -272,3 +272,12 @@ max-complexity = 20 "scripts/**" = [ "D103" ] + +[tool.pytest.ini_options] +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", +] \ No newline at end of file From 2d5437e7f7a2cab919bffdb54656cb1aa4615301 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 3 May 2025 15:30:41 +0200 Subject: [PATCH 30/61] Fix scripts/mcp_server.py --- scripts/mcp_server.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/mcp_server.py b/scripts/mcp_server.py index 299dd287..433d9b4e 100644 --- a/scripts/mcp_server.py +++ b/scripts/mcp_server.py @@ -1,6 +1,4 @@ -from serena.mcp import create_mcp_server - -mcp = create_mcp_server() +from serena.mcp import start_mcp_server if __name__ == "__main__": - mcp.run() + start_mcp_server() From 81b719241035fa8d87dd94ca6a313c394cfd653c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 12:49:16 +0200 Subject: [PATCH 31/61] Added tests for PhP language server --- pyproject.toml | 1 + test/multilspy/php/test_php_basic.py | 155 ++++++++++++++++++ test/resources/repos/php/test_repo/helper.php | 7 + test/resources/repos/php/test_repo/index.php | 16 ++ .../repos/php/test_repo/simple_var.php | 4 + 5 files changed, 183 insertions(+) create mode 100644 test/multilspy/php/test_php_basic.py create mode 100644 test/resources/repos/php/test_repo/helper.php create mode 100644 test/resources/repos/php/test_repo/index.php create mode 100644 test/resources/repos/php/test_repo/simple_var.php diff --git a/pyproject.toml b/pyproject.toml index e7f1cdbd..ea20a504 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -280,4 +280,5 @@ markers = [ "java: language server running for Java", "rust: language server running for Rust", "typescript: language server running for TypeScript", + "php: language server running for PHP", ] \ No newline at end of file diff --git a/test/multilspy/php/test_php_basic.py b/test/multilspy/php/test_php_basic.py new file mode 100644 index 00000000..896302e6 --- /dev/null +++ b/test/multilspy/php/test_php_basic.py @@ -0,0 +1,155 @@ +import time # Import time module +from pathlib import Path + +import pytest + +from multilspy.language_server import SyncLanguageServer +from multilspy.multilspy_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_initialize_and_shutdown(self, language_server: SyncLanguageServer, 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() + + + @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: + + # In index.php: + # Line 9 (1-indexed): $greeting = greet($userName); + # Line 11 (1-indexed): echo $greeting; + # We want to find the definition of $greeting (defined on line 9) + # from its usage in echo $greeting; on line 11. + # LSP is 0-indexed: definition on line 8, usage on line 10. + # $greeting in echo $greeting; is at char 5 on line 11 (0-indexed: line 10, char 5) + # e c h o $ g r e e t i n g + # ^ char 5 + definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 10, 6) # cursor on 'g' in $greeting + + assert definition_location_list is not None + assert len(definition_location_list) == 1 + definition_location = definition_location_list[0] + assert definition_location["uri"].endswith("index.php") + # Definition of $greeting is on line 10 (1-indexed) / line 9 (0-indexed), char 0 + assert definition_location["range"]["start"]["line"] == 9 + assert definition_location["range"]["start"]["character"] == 0 + + + @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: + definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 12, 5) # helperFunction + + assert definition_location_list is not None + assert len(definition_location_list) == 1 + definition_location = definition_location_list[0] + assert definition_location["uri"].endswith("helper.php") + assert definition_location["range"]["start"]["line"] == 2 + assert definition_location["range"]["start"]["character"] == 0 + + + @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: + file_path = str(repo_path / "simple_var.php") + + # In simple_var.php: + # Line 2 (1-indexed): $localVar = "test"; + # Line 3 (1-indexed): echo $localVar; + # LSP is 0-indexed: definition on line 1, usage on line 2 + # Find definition of $localVar (char 5 on line 3 / 0-indexed: line 2, char 5) + # $localVar in echo $localVar; (e c h o $ l o c a l V a r) + # ^ char 5 + definition_location_list = language_server.request_definition(file_path, 2, 6) # cursor on 'l' in $localVar + + assert definition_location_list is not None + assert len(definition_location_list) == 1 + definition_location = definition_location_list[0] + assert definition_location["uri"].endswith("simple_var.php") + assert definition_location["range"]["start"]["line"] == 1 # Definition of $localVar (0-indexed) + assert definition_location["range"]["start"]["character"] == 0 # $localVar (0-indexed) + + + @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: + index_php_path = str(repo_path / "index.php") + + # In index.php (0-indexed lines): + # Line 9: $greeting = greet($userName); // Definition of $greeting + # Line 11: echo $greeting; // Usage of $greeting + # Find references for $greeting from its usage in "echo $greeting;" (line 11, char 6 for 'g') + references = language_server.request_references(index_php_path, 11, 6) + + assert references is not None + # Intelephense, when asked for references from usage, seems to only return the usage itself. + assert len(references) == 1, "Expected to find 1 reference for $greeting (the usage itself)" + + expected_locations = [{"uri_suffix": "index.php", "line": 11, "character": 5}] # Usage: echo $greeting (points to $) + + # Convert actual references to a comparable format and sort + actual_locations = sorted( + [ + { + "uri_suffix": loc["uri"].split("/")[-1], + "line": loc["range"]["start"]["line"], + "character": loc["range"]["start"]["character"], + } + for loc in references + ], + key=lambda x: (x["uri_suffix"], x["line"], x["character"]), + ) + + expected_locations = sorted(expected_locations, key=lambda x: (x["uri_suffix"], x["line"], x["character"])) + + assert actual_locations == expected_locations + + + @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: + index_php_path = str(repo_path / "index.php") + # In index.php (0-indexed lines): + # Line 13: helperFunction(); // Usage of helperFunction + # Find references for helperFunction from its usage (line 13, char 0 for 'h') + references = language_server.request_references(index_php_path, 13, 0) + + assert references is not None + # Intelephense might return 1 (usage) or 2 (usage + definition) references. + # Let's check for at least the usage in index.php + # Definition is in helper.php, line 2, char 0 (based on previous findings) + # Usage is in index.php, line 13, char 0 + + actual_locations_comparable = [] + for loc in references: + actual_locations_comparable.append( + { + "uri_suffix": loc["uri"].split("/")[-1], + "line": loc["range"]["start"]["line"], + "character": loc["range"]["start"]["character"], + } + ) + + usage_in_index_php = {"uri_suffix": "index.php", "line": 13, "character": 0} + definition_in_helper_php = {"uri_suffix": "helper.php", "line": 2, "character": 0} + + assert usage_in_index_php in actual_locations_comparable, "Usage of helperFunction in index.php not found" + + # Depending on Intelephense's behavior, it might also include the definition. + # For now, we are flexible: it must find the usage. If it finds more, that's also okay. + # If we want to be strict about finding both or only one, this part would need adjustment. + if len(references) == 2: + assert ( + definition_in_helper_php in actual_locations_comparable + ), "Definition of helperFunction in helper.php expected but not found when 2 references returned" + elif len(references) == 1: + # If only one reference, ensure it's the usage we definitely expect + assert actual_locations_comparable[0] == usage_in_index_php + else: + assert False, f"Expected 1 or 2 references, but got {len(references)}" diff --git a/test/resources/repos/php/test_repo/helper.php b/test/resources/repos/php/test_repo/helper.php new file mode 100644 index 00000000..782d67d9 --- /dev/null +++ b/test/resources/repos/php/test_repo/helper.php @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/test/resources/repos/php/test_repo/index.php b/test/resources/repos/php/test_repo/index.php new file mode 100644 index 00000000..70d0d340 --- /dev/null +++ b/test/resources/repos/php/test_repo/index.php @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/test/resources/repos/php/test_repo/simple_var.php b/test/resources/repos/php/test_repo/simple_var.php new file mode 100644 index 00000000..4ffe86e2 --- /dev/null +++ b/test/resources/repos/php/test_repo/simple_var.php @@ -0,0 +1,4 @@ + \ No newline at end of file From 2b92c486227109388a88433e771a16f9642f8570 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 12:52:24 +0200 Subject: [PATCH 32/61] Minor, fixed accidental commit to settings.json --- .vscode/settings.json | 2 -- src/serena/agent.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 258e3b5c..56a7855f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,4 @@ "sensai", "vibing" ], - "basedpyright.disableLanguageServices": true, - "python.languageServer": "None" } \ No newline at end of file diff --git a/src/serena/agent.py b/src/serena/agent.py index ec1e2d2b..0c8d5d10 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -81,7 +81,7 @@ class ProjectConfig(ToStringMixin): SERENA_DEFAULT_PROJECT_FILE = "project.yml" @classmethod - def rel_path_to_project_yml(cls): + def rel_path_to_project_yml(cls) -> str: return os.path.join(cls.SERENA_MANAGED_DIR, cls.SERENA_DEFAULT_PROJECT_FILE) @classmethod From 8f7a80f759bbadd5afbcd8a554320a4a0f67d0b9 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 12:53:07 +0200 Subject: [PATCH 33/61] Readme, changelog --- CHANGELOG.md | 1 + README.md | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1114bcb7..a62c46b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Changes prior to the next official version change will appear here. * Language Servers: * Add further file extensions considered by the language servers for Python (.pyi), JavaScript (.jsx) and TypeScript (.tsx, .jsx) * Updated multilspy, adding support for Kotlin, Dart and C/C++ and several improvements. + * Added support for PhP # 2025-04-07 diff --git a/README.md b/README.md index a81eaba7..69958126 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,16 @@ With Serena, we provide * Python * Java (_Note_: startup is slow, initial startup especially so) * TypeScript + * PhP + * Go (need to install go and gopls first) + * Rust + * C/C++ * indirect support (may require some code changes/manual installation) for: * Ruby (untested) - * Go (untested) * C# (untested) * Rust (untested) * Kotlin (untested) * Dart (untested) - * C/C++ (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. From 4901f72629175e25b3ba1f4b78c239888d3f39ac Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 12:57:26 +0200 Subject: [PATCH 34/61] Made paths in test os independent --- test/multilspy/php/test_php_basic.py | 6 ------ test/serena/test_serena_agent.py | 11 ++++++++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/test/multilspy/php/test_php_basic.py b/test/multilspy/php/test_php_basic.py index 896302e6..6f7aba0a 100644 --- a/test/multilspy/php/test_php_basic.py +++ b/test/multilspy/php/test_php_basic.py @@ -1,4 +1,3 @@ -import time # Import time module from pathlib import Path import pytest @@ -16,7 +15,6 @@ class TestPhpLanguageServer: # The fixture already handles start and stop assert language_server.is_running() - @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: @@ -40,7 +38,6 @@ class TestPhpLanguageServer: assert definition_location["range"]["start"]["line"] == 9 assert definition_location["range"]["start"]["character"] == 0 - @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: @@ -53,7 +50,6 @@ class TestPhpLanguageServer: assert definition_location["range"]["start"]["line"] == 2 assert definition_location["range"]["start"]["character"] == 0 - @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: @@ -75,7 +71,6 @@ class TestPhpLanguageServer: assert definition_location["range"]["start"]["line"] == 1 # Definition of $localVar (0-indexed) assert definition_location["range"]["start"]["character"] == 0 # $localVar (0-indexed) - @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: @@ -110,7 +105,6 @@ class TestPhpLanguageServer: assert actual_locations == expected_locations - @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: diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 7c3d76eb..03d660d1 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -47,10 +47,15 @@ class TestSerenaAgent: @pytest.mark.parametrize( "serena_agent,symbol_name,def_file,ref_file", [ - (Language.PYTHON, "User", "test_repo/models.py", "test_repo/services.py"), + (Language.PYTHON, "User", os.path.join("test_repo", "models.py"), os.path.join("test_repo", "services.py")), (Language.GO, "Helper", "main.go", "main.go"), - (Language.JAVA, "Model", "src/main/java/test_repo/Model.java", "src/main/java/test_repo/Main.java"), - (Language.RUST, "add", "src/lib.rs", "src/main.rs"), + ( + Language.JAVA, + "Model", + os.path.join("src", "main", "java", "test_repo", "Model.java"), + os.path.join("src", "main", "java", "test_repo", "Main.java"), + ), + (Language.RUST, "add", os.path.join("src", "lib.rs"), os.path.join("src", "main.rs")), (Language.TYPESCRIPT, "helperFunction", "index.ts", "use_helper.ts"), ], indirect=["serena_agent"], From e9beef6bc75dd210f6c7ecc78c76734b46740bdb Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 13:44:20 +0200 Subject: [PATCH 35/61] GH: run tests on all OS --- .github/workflows/pytest.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 350288e1..e8bdd975 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,13 +1,15 @@ -name: Tests on Ubuntu +name: Tests on CI on: [pull_request] jobs: cpu: - runs-on: ubuntu-latest + name: Tests on ${{ matrix.os }} + runs-on: ${{ matrix.os }} if: "!contains(github.event.head_commit.message, 'ci skip')" strategy: matrix: + os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.11"] steps: - uses: actions/checkout@v3 @@ -20,17 +22,26 @@ jobs: # GITHUB_PATH is a special file that GitHub Actions uses to modify PATH # Writing to this file adds the directory to the PATH for subsequent steps - name: Install gopls + shell: bash run: | go install golang.org/x/tools/gopls@latest - echo "$HOME/go/bin" >> $GITHUB_PATH - + if [ "$RUNNER_OS" = "Windows" ]; then + echo "$HOME/go/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + else + echo "$HOME/go/bin" >> $GITHUB_PATH + fi + - name: Install uv + shell: bash run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create virtual environment + shell: bash run: uv venv - name: Install dependencies + shell: bash run: uv pip install -e ".[dev]" - name: Test with pytest + shell: bash run: uv run poe test - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 From 0f13643b6b099d6cb8e3b0ac138351151b7a0b76 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 13:49:52 +0200 Subject: [PATCH 36/61] Removed unused build things (leftover from initial commit) --- .github/workflows/pytest.yml | 9 +- pyproject.toml | 24 -- uv.lock | 668 ++++++++++++++++------------------- 3 files changed, 315 insertions(+), 386 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index e8bdd975..16075117 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -43,11 +43,4 @@ jobs: - name: Test with pytest shell: bash run: uv run poe test - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV }} - file: ./coverage.xml - flags: unittests - name: codecov-umbrella - fail_ci_if_error: false + diff --git a/pyproject.toml b/pyproject.toml index ea20a504..3c711e82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,6 @@ dev = [ "mypy>=1.4.1", "poethepoet>=0.20.0", "pytest>=8.0.2", - "pytest-cov", "ruff>=0.0.285", "toml-sort>=0.24.2", "types-pyyaml>=6.0.12.20241230", @@ -116,27 +115,10 @@ lint = [ "_black_check", "_ruff_check", ] -clean-nbs = "python docs/nbstripout.py" format = [ "_ruff_format", "_black_format" ] -_autogen_rst = "python docs/autogen_rst.py" -_sphinx_build = "sphinx-build -W -b html docs docs/_build" -_jb_generate_toc = "python docs/create_toc.py" -_jb_generate_config = "jupyter-book config sphinx docs/" -doc-clean = "rm -rf docs/_build" -doc-generate-files = [ - "_autogen_rst", - "_jb_generate_toc", - "_jb_generate_config" -] -doc-spellcheck = "sphinx-build -W -b spelling docs docs/_build" -doc-build = [ - "doc-generate-files", - "doc-spellcheck", - "_sphinx_build" -] _mypy = "mypy src/serena" type-check = [ "_mypy", @@ -263,12 +245,6 @@ max-complexity = 20 "tests/**" = [ "D103" ] -"docs/**" = [ - "D103" -] -"examples/**" = [ - "D103" -] "scripts/**" = [ "D103" ] diff --git a/uv.lock b/uv.lock index d8185ded..eef6518f 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 1 +revision = 2 requires-python = "==3.11.*" [[package]] @@ -20,18 +20,18 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/4b/05c42f236c18d11b29af368910df398a8a67e4f5c5681ead30ebde07a548/agno-1.2.15-py3-none-any.whl", hash = "sha256:e82b57f2980bff1ce7574a16ed963485f2efa3519e9322e17fa6ba07719bd5e2", size = 616626 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, + { 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" }, ] [[package]] @@ -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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/74/5d90ad14d55fbe3f9c474fdcb6e34b4bed99e3be8efac98734a5ddce88c1/anthropic-0.49.0-py3-none-any.whl", hash = "sha256:bbc17ad4e7094988d2fa86b87753ded8dce12498f4b85fe5810f208f454a8375", size = 243368 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, + { 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" }, ] [[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 } +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" } 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 }, - { 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 }, + { 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" }, ] [package.optional-dependencies] @@ -105,40 +105,40 @@ jupyter = [ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, + { 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" }, ] [[package]] name = "certifi" version = "2025.1.31" 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, + { 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" }, ] [[package]] name = "charset-normalizer" version = "3.4.1" 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 } +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" } 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, + { 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" }, ] [[package]] @@ -148,70 +148,45 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, + { 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" }, ] [[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 } +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" } wheels = [ - { 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 = "coverage" -version = "7.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493 }, - { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921 }, - { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556 }, - { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245 }, - { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032 }, - { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679 }, - { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852 }, - { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389 }, - { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997 }, - { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911 }, - { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443 }, - { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533 }, + { 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" }, ] [[package]] @@ -222,16 +197,16 @@ 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 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, + { 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" }, ] [[package]] @@ -243,9 +218,9 @@ dependencies = [ { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, + { 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]] @@ -260,9 +235,9 @@ dependencies = [ { 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 } +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 }, + { 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" }, ] [[package]] @@ -272,9 +247,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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, + { 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" }, ] [[package]] @@ -284,9 +259,9 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, + { 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" }, ] [[package]] @@ -298,9 +273,9 @@ dependencies = [ { 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 } +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" } 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 }, + { 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" }, ] [[package]] @@ -316,35 +291,35 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/a0/56839a2e202d79c773edd1c1db124da8eb2a7b657267a888080b678d0369/google_genai-1.10.0-py3-none-any.whl", hash = "sha256:41b105a2fcf8a027fc45cc16694cd559b8cd1272eab7345ad58cfa2c353bf34f", size = 154705 }, + { 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" }, ] [[package]] name = "greenlet" version = "3.1.1" 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 } +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" } 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, + { 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" }, ] [[package]] name = "h11" version = "0.14.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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, + { 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" }, ] [[package]] @@ -355,9 +330,9 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, + { 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" }, ] [[package]] @@ -370,36 +345,36 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, + { 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" }, ] [[package]] @@ -419,9 +394,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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/9d/4ff2adf55d1b6e3777b0303fdbe5b723f76e46cba4a53a32fe82260d2077/ipython-9.1.0-py3-none-any.whl", hash = "sha256:2df07257ec2f84a6b346b8d83100bcf8fa501c6e01ab75cd3799b0bb253b3d2a", size = 604053 }, + { 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" }, ] [[package]] @@ -431,9 +406,9 @@ 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 } +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" } 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 }, + { 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" }, ] [[package]] @@ -443,9 +418,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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, + { 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" }, ] [[package]] @@ -455,29 +430,29 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { 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" }, ] [[package]] name = "jiter" version = "0.9.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 } +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" } 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/a6/e5/4e385945179bcf128fa10ad8dca9053d717cbe09e258110e39045c881fe5/jiter-0.9.0-cp311-cp311-win32.whl", hash = "sha256:a25519efb78a42254d59326ee417d6f5161b06f5da827d94cf521fed961b1ff2", size = 206920 }, - { url = "https://files.pythonhosted.org/packages/4c/47/5e0b94c603d8e54dd1faab439b40b832c277d3b90743e7835879ab663757/jiter-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:923b54afdd697dfd00d368b7ccad008cccfeb1efb4e621f32860c75e9f25edbd", size = 210119 }, + { 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" }, ] [[package]] @@ -487,27 +462,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 } +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" } 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 }, + { 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" }, ] [[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 } +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" } 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 }, - { 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 }, + { 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" }, ] [[package]] @@ -517,9 +492,9 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, + { 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" }, ] [[package]] @@ -536,18 +511,18 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { 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" }, ] [[package]] @@ -558,78 +533,78 @@ dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +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" } 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 }, - { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, + { 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" }, ] [[package]] name = "mypy-extensions" version = "1.0.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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, + { 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" }, ] [[package]] name = "packaging" version = "24.2" 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, + { 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" }, ] [[package]] @@ -639,27 +614,27 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, + { 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" }, ] [[package]] name = "platformdirs" version = "4.3.7" 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, + { 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" }, ] [[package]] name = "pluggy" version = "1.5.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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, + { 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" }, ] [[package]] @@ -670,9 +645,9 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/ea/c476bfec360eb6831ce46df2719f76d1132b9a87da11c302081a9def5fce/poethepoet-0.33.1-py3-none-any.whl", hash = "sha256:b86d80a81b2ca4e4ce8e8f716cc6004a1a1cdead027778bc07d1c26cb3664770", size = 83512 }, + { 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" }, ] [[package]] @@ -682,51 +657,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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 }, + { 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" }, ] [[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 } +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" } 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 }, - { 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 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, + { 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" }, ] [[package]] @@ -736,9 +711,9 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, + { 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" }, ] [[package]] @@ -751,9 +726,9 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, + { 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" }, ] [[package]] @@ -763,31 +738,31 @@ 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 } +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" } 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 }, - { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 }, - { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, + { 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" }, ] [[package]] @@ -798,18 +773,18 @@ dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { 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" }, ] [[package]] @@ -820,9 +795,9 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584 }, + { 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" }, ] [[package]] @@ -835,57 +810,44 @@ dependencies = [ { name = "packaging" }, { name = "pluggy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, -] - -[[package]] -name = "pytest-cov" -version = "6.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, + { 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" }, ] [[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 } +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" } 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 }, - { 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 }, + { 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" }, ] [[package]] @@ -898,9 +860,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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, + { 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" }, ] [[package]] @@ -911,9 +873,9 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, + { 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" }, ] [[package]] @@ -923,34 +885,34 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, + { 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" }, ] [[package]] name = "ruff" version = "0.11.5" 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697 }, - { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665 }, - { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287 }, + { 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" }, ] [[package]] @@ -960,9 +922,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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/df/49cc942b653427df0bbd3b9a2fb1e85652df3d4c1d9f6dcbfce1b6155716/sensai_utils-1.4.0-py3-none-any.whl", hash = "sha256:ed6fc57552620e43b33cf364ea0bc0fd7df39391069dd7b621b113ef55547507", size = 63214 }, + { 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" }, ] [[package]] @@ -1003,7 +965,6 @@ dev = [ { name = "mypy" }, { name = "poethepoet" }, { name = "pytest" }, - { name = "pytest-cov" }, { name = "ruff" }, { name = "toml-sort" }, { name = "types-pyyaml" }, @@ -1034,7 +995,6 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.10.6" }, { name = "pyright", specifier = ">=1.1.396,<2" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.2" }, - { name = "pytest-cov", marker = "extra == 'dev'" }, { name = "python-dotenv", specifier = ">=1.0.0,<2" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "requests", specifier = ">=2.32.3,<3" }, @@ -1051,27 +1011,27 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { 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" }, ] [[package]] @@ -1082,17 +1042,17 @@ 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 } +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" } 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/e4/cc/03eb5dfcdb575cbecd2bd82487b9848f250a4b6ecfb4707e834b4ce4ec07/sqlalchemy-2.0.40-cp311-cp311-win32.whl", hash = "sha256:c0cae71e20e3c02c52f6b9e9722bca70e4a90a466d59477822739dc31ac18b4b", size = 2084675 }, - { url = "https://files.pythonhosted.org/packages/9a/48/440946bf9dc4dc231f4f31ef0d316f7135bf41d4b86aaba0c0655150d370/sqlalchemy-2.0.40-cp311-cp311-win_amd64.whl", hash = "sha256:574aea2c54d8f1dd1699449f332c7d9b71c339e04ae50163a3eb5ce4c4325ee4", size = 2110099 }, - { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894 }, + { 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" }, ] [[package]] @@ -1103,9 +1063,9 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, + { 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" }, ] [[package]] @@ -1117,9 +1077,9 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, + { 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" }, ] [[package]] @@ -1129,18 +1089,18 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, + { 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" }, ] [[package]] name = "tokenize-rt" version = "6.1.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 } +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" } 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 }, + { 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" }, ] [[package]] @@ -1150,46 +1110,46 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/55/7a7c02de848eef670bd018b62122ce32a6e227203e5a626c96d4935958a6/toml_sort-0.24.2-py3-none-any.whl", hash = "sha256:d81d299789a1fd9dd306a4021951eab5fc0c5486599e277fcf8142c7735f3308", size = 20162 }, + { 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" }, ] [[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 } +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" } 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 }, - { 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 }, + { 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" }, ] [[package]] name = "tomlkit" version = "0.13.2" 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, + { 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" }, ] [[package]] @@ -1202,27 +1162,27 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, + { 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" }, ] [[package]] name = "types-pyyaml" version = "6.0.12.20250402" 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 } +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" } 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 }, + { 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" }, ] [[package]] name = "typing-extensions" version = "4.13.2" 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, + { 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" }, ] [[package]] @@ -1232,18 +1192,18 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, + { 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" }, ] [[package]] @@ -1254,36 +1214,36 @@ 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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, + { 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" }, ] [[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 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, + { 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" }, ] [[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 } +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" } 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 }, - { 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 }, + { 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" }, ] From 4e6ef6badd22b8fb2e19b00aee949665a0261afe Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 13:54:10 +0200 Subject: [PATCH 37/61] GH: add setup go action --- .github/workflows/pytest.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 16075117..01b381b4 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -17,7 +17,9 @@ jobs: uses: actions/setup-python@v4 with: python-version: "${{ matrix.python-version }}" - + - uses: actions/setup-go@v5 + with: + go-version: '>=1.17.0' # Add Go bin directory to PATH for this workflow # GITHUB_PATH is a special file that GitHub Actions uses to modify PATH # Writing to this file adds the directory to the PATH for subsequent steps From 5c7c2d0690c29545d5c5a96e647533af757c2673 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 14:00:19 +0200 Subject: [PATCH 38/61] GH action: gopls install --- .github/workflows/pytest.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 01b381b4..1ca6d168 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -27,12 +27,7 @@ jobs: shell: bash run: | go install golang.org/x/tools/gopls@latest - if [ "$RUNNER_OS" = "Windows" ]; then - echo "$HOME/go/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - else - echo "$HOME/go/bin" >> $GITHUB_PATH - fi - + echo "$HOME/go/bin" >> $GITHUB_PATH - name: Install uv shell: bash run: curl -LsSf https://astral.sh/uv/install.sh | sh From 132a139070698ff18335b34e4e077c92b20c97d2 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 14:01:11 +0200 Subject: [PATCH 39/61] GH: test if simplified gopls install now works --- .github/workflows/pytest.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 1ca6d168..9ab4e7f9 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -25,9 +25,7 @@ jobs: # Writing to this file adds the directory to the PATH for subsequent steps - name: Install gopls shell: bash - run: | - go install golang.org/x/tools/gopls@latest - echo "$HOME/go/bin" >> $GITHUB_PATH + run: go install golang.org/x/tools/gopls@latest - name: Install uv shell: bash run: curl -LsSf https://astral.sh/uv/install.sh | sh From bef724f2da23131ea79c02e21b771e804d9bc751 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 14:05:38 +0200 Subject: [PATCH 40/61] GH: don't fail fast --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 9ab4e7f9..42e2373a 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -8,6 +8,7 @@ jobs: runs-on: ${{ matrix.os }} if: "!contains(github.event.head_commit.message, 'ci skip')" strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.11"] @@ -38,4 +39,3 @@ jobs: - name: Test with pytest shell: bash run: uv run poe test - From a9ef430629bd6d1b7bbf395674539650edb62536 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 15:41:49 +0200 Subject: [PATCH 41/61] Don't run java tests on macos for now --- .github/workflows/pytest.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 42e2373a..0f0ced83 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -38,4 +38,10 @@ jobs: run: uv pip install -e ".[dev]" - name: Test with pytest shell: bash - run: uv run poe test + # Currently java Language Server is not working on macos + run: | + if [ "${{ matrix.os }}" = "macos-latest" ]; then + uv run pytest -m "not java" + else + uv run poe test + fi From dcf48e1aa8ad0c508641236ac6f4dc6d35b28b79 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 15:49:22 +0200 Subject: [PATCH 42/61] GH: cache packages --- .github/workflows/pytest.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 0f0ced83..54fc2c5e 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -30,9 +30,18 @@ jobs: - name: Install uv shell: bash run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Cache uv virtualenv + id: cache-uv + uses: actions/cache@v3 + with: + path: .venv + key: uv-venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }} - name: Create virtual environment shell: bash - run: uv venv + run: | + if [ ! -d ".venv" ]; then + uv venv + fi - name: Install dependencies shell: bash run: uv pip install -e ".[dev]" From 7975675a981a313cd751a6141eee6693180ca92b Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 16:11:02 +0200 Subject: [PATCH 43/61] More explicit asserts (debugging in CI) --- test/multilspy/php/test_php_basic.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/multilspy/php/test_php_basic.py b/test/multilspy/php/test_php_basic.py index 6f7aba0a..08cf73a2 100644 --- a/test/multilspy/php/test_php_basic.py +++ b/test/multilspy/php/test_php_basic.py @@ -10,10 +10,11 @@ from multilspy.multilspy_config import Language class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_initialize_and_shutdown(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_ls_is_running(self, language_server: SyncLanguageServer, 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() + assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve() @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) @@ -30,7 +31,7 @@ class TestPhpLanguageServer: # ^ char 5 definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 10, 6) # cursor on 'g' in $greeting - assert definition_location_list is not None + assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}" assert len(definition_location_list) == 1 definition_location = definition_location_list[0] assert definition_location["uri"].endswith("index.php") @@ -43,7 +44,7 @@ class TestPhpLanguageServer: def test_find_definition_across_files(self, language_server: SyncLanguageServer, repo_path: Path) -> None: definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 12, 5) # helperFunction - assert definition_location_list is not None + assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}" assert len(definition_location_list) == 1 definition_location = definition_location_list[0] assert definition_location["uri"].endswith("helper.php") @@ -64,7 +65,7 @@ class TestPhpLanguageServer: # ^ char 5 definition_location_list = language_server.request_definition(file_path, 2, 6) # cursor on 'l' in $localVar - assert definition_location_list is not None + assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}" assert len(definition_location_list) == 1 definition_location = definition_location_list[0] assert definition_location["uri"].endswith("simple_var.php") @@ -82,7 +83,7 @@ class TestPhpLanguageServer: # Find references for $greeting from its usage in "echo $greeting;" (line 11, char 6 for 'g') references = language_server.request_references(index_php_path, 11, 6) - assert references is not None + assert references # Intelephense, when asked for references from usage, seems to only return the usage itself. assert len(references) == 1, "Expected to find 1 reference for $greeting (the usage itself)" @@ -114,7 +115,7 @@ class TestPhpLanguageServer: # Find references for helperFunction from its usage (line 13, char 0 for 'h') references = language_server.request_references(index_php_path, 13, 0) - assert references is not None + assert references, f"Expected non-empty references for helperFunction but got {references=}" # Intelephense might return 1 (usage) or 2 (usage + definition) references. # Let's check for at least the usage in index.php # Definition is in helper.php, line 2, char 0 (based on previous findings) From 66a94d727457781389adc40a9f48e87c82bbd0a9 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 16:18:59 +0200 Subject: [PATCH 44/61] PHP: try waiting before requesting references Same fix was used for typescript. PHP tests don't fail on my system but do fail in CI --- .../intelephense/intelephense.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/multilspy/language_servers/intelephense/intelephense.py b/src/multilspy/language_servers/intelephense/intelephense.py index 4533d590..7d976c2d 100644 --- a/src/multilspy/language_servers/intelephense/intelephense.py +++ b/src/multilspy/language_servers/intelephense/intelephense.py @@ -9,8 +9,8 @@ import logging import os import subprocess import pathlib -import stat from contextlib import asynccontextmanager +from time import sleep from typing import AsyncIterator from overrides import override @@ -22,16 +22,6 @@ from multilspy.lsp_protocol_handler.lsp_types import InitializeParams from multilspy.multilspy_config import MultilspyConfig from multilspy.multilspy_utils import PlatformUtils, PlatformId -# Platform-specific imports -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')})() - class Intelephense(LanguageServer): """ Provides PHP specific instantiation of the LanguageServer class using Intelephense. @@ -91,6 +81,7 @@ class Intelephense(LanguageServer): ) else: # On Unix-like systems, run as non-root user + import pwd user = pwd.getpwuid(os.getuid()).pw_name subprocess.run( dependency["command"], @@ -192,4 +183,15 @@ class Intelephense(LanguageServer): yield self await self.server.shutdown() - await self.server.stop() \ No newline at end of file + 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): + # 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) \ No newline at end of file From c44749868dde221f125c7b19d3a96da60572bb8a Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 16:32:41 +0200 Subject: [PATCH 45/61] PHP: also sleep for definition requests --- src/multilspy/language_server.py | 30 ++++++++++--------- .../intelephense/intelephense.py | 14 ++++++--- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 35a3d591..98ab6dbc 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -27,7 +27,7 @@ from serena.text_utils import LineType, MatchedConsecutiveLines, TextLine, searc 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 SymbolKind +from .lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind from .lsp_protocol_handler.server import ( Error, LanguageServerHandler, @@ -495,6 +495,9 @@ 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]: @@ -518,19 +521,18 @@ class LanguageServer: with self.open_file(relative_file_path): # sending request to the language server and waiting for response - response = await self.server.send.definition( - { - 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, - }, - } - ) + 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, + }, + }) + response = await self._send_definition_request(definition_params) ret: List[multilspy_types.Location] = [] if isinstance(response, list): diff --git a/src/multilspy/language_servers/intelephense/intelephense.py b/src/multilspy/language_servers/intelephense/intelephense.py index 7d976c2d..1e7da5a2 100644 --- a/src/multilspy/language_servers/intelephense/intelephense.py +++ b/src/multilspy/language_servers/intelephense/intelephense.py @@ -18,7 +18,7 @@ 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.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams from multilspy.multilspy_config import MultilspyConfig from multilspy.multilspy_utils import PlatformUtils, PlatformId @@ -191,7 +191,13 @@ class Intelephense(LanguageServer): # 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. + # 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) \ No newline at end of file + return await super()._send_references_request(relative_file_path, line, column) + + @override + async 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) From 30b241637787b660cd6850af4c03693534b822b6 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 16:42:28 +0200 Subject: [PATCH 46/61] GH: cancel action on new push --- .github/workflows/pytest.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 54fc2c5e..549b036b 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -2,6 +2,10 @@ name: Tests on CI on: [pull_request] +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: cpu: name: Tests on ${{ matrix.os }} From c596bce092d9f9c93fc816f24bebed451d43069f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 16:56:19 +0200 Subject: [PATCH 47/61] Bump version --- CHANGELOG.md | 4 ++-- src/serena/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a62c46b5..d40d6d57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Latest +# 2025-05-19 Changes prior to the next official version change will appear here. @@ -17,7 +17,7 @@ Changes prior to the next official version change will appear here. * Language Servers: * Add further file extensions considered by the language servers for Python (.pyi), JavaScript (.jsx) and TypeScript (.tsx, .jsx) * Updated multilspy, adding support for Kotlin, Dart and C/C++ and several improvements. - * Added support for PhP + * Added support for PHP # 2025-04-07 diff --git a/src/serena/__init__.py b/src/serena/__init__.py index bcb2bdbe..d37e1f6c 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2025-04-07" +__version__ = "2025-05-19" def serena_root_path() -> str: From e8d493f4d635cee6127fc6d1c9a7c3a48f9ca385 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 17:18:51 +0200 Subject: [PATCH 48/61] Use utf8 in all file read operations --- scripts/gen_prompt_factory.py | 4 ++-- src/multilspy/language_server.py | 2 +- .../language_servers/eclipse_jdtls/eclipse_jdtls.py | 4 ++-- src/multilspy/language_servers/gopls/gopls.py | 2 +- src/multilspy/language_servers/intelephense/intelephense.py | 4 ++-- .../language_servers/jedi_language_server/jedi_server.py | 2 +- .../kotlin_language_server/kotlin_language_server.py | 4 ++-- src/multilspy/language_servers/omnisharp/omnisharp.py | 6 +++--- .../language_servers/rust_analyzer/rust_analyzer.py | 4 ++-- src/multilspy/language_servers/solargraph/solargraph.py | 4 ++-- src/serena/llm/multilang_prompt.py | 2 +- test/resources/repos/python/test_repo/scripts/run_app.py | 2 +- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/scripts/gen_prompt_factory.py b/scripts/gen_prompt_factory.py index a1f477bf..bc7a2e59 100644 --- a/scripts/gen_prompt_factory.py +++ b/scripts/gen_prompt_factory.py @@ -31,13 +31,13 @@ def main(): ) # write prompt factory with added methods - with open("code_templates/prompt_factory_template.py") as f: + with open("code_templates/prompt_factory_template.py", encoding="utf-8") as f: code = f.read() methods_str = "".join(methods) code = code.replace("# methods", methods_str) prompt_factory_module = f"{package}/llm/prompt_factory.py" - with open(prompt_factory_module, "w") as f: + with open(prompt_factory_module, "w", encoding="utf-8") as f: f.write(code) log.info(f"Prompt factory generated successfully in {prompt_factory_module}") diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 98ab6dbc..b556b943 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -123,7 +123,7 @@ class LanguageServer: 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) as f: + with open(gitignore_path, "r", encoding="utf-8") as f: gitignore_file_content = f.read() config.gitignore_file_content = gitignore_file_content diff --git a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py index 1f952283..6753bf12 100644 --- a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ b/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -165,7 +165,7 @@ class EclipseJDTLS(LanguageServer): """ platformId = PlatformUtils.get_platform_id() - with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), "r") as f: + with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), "r", encoding="utf-8") as f: runtimeDependencies = json.load(f) del runtimeDependencies["_description"] @@ -264,7 +264,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") as f: + with open(str(PurePath(os.path.dirname(__file__), "initialize_params.json")), "r", encoding="utf-8") as f: d: InitializeParams = json.load(f) del d["_description"] diff --git a/src/multilspy/language_servers/gopls/gopls.py b/src/multilspy/language_servers/gopls/gopls.py index 6269db82..ac474451 100644 --- a/src/multilspy/language_servers/gopls/gopls.py +++ b/src/multilspy/language_servers/gopls/gopls.py @@ -90,7 +90,7 @@ class Gopls(LanguageServer): """ Returns the initialize params for the TypeScript 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"), "r", encoding="utf-8") as f: d = json.load(f) del d["_description"] diff --git a/src/multilspy/language_servers/intelephense/intelephense.py b/src/multilspy/language_servers/intelephense/intelephense.py index 1e7da5a2..33382cfc 100644 --- a/src/multilspy/language_servers/intelephense/intelephense.py +++ b/src/multilspy/language_servers/intelephense/intelephense.py @@ -52,7 +52,7 @@ 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") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -116,7 +116,7 @@ class Intelephense(LanguageServer): """ Returns the initialize params for the TypeScript 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"), "r", encoding="utf-8") as f: d = json.load(f) del d["_description"] diff --git a/src/multilspy/language_servers/jedi_language_server/jedi_server.py b/src/multilspy/language_servers/jedi_language_server/jedi_server.py index 9dec2c7d..90d4376e 100644 --- a/src/multilspy/language_servers/jedi_language_server/jedi_server.py +++ b/src/multilspy/language_servers/jedi_language_server/jedi_server.py @@ -43,7 +43,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") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: d = json.load(f) del d["_description"] diff --git a/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py b/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py index 76bc1e86..5edc0e76 100644 --- a/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py +++ b/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py @@ -67,7 +67,7 @@ class KotlinLanguageServer(LanguageServer): 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") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -134,7 +134,7 @@ class KotlinLanguageServer(LanguageServer): """ Returns the initialize params for the Kotlin Language Server. """ - with open(str(pathlib.PurePath(os.path.dirname(__file__), "initialize_params.json")), "r") as f: + with open(str(pathlib.PurePath(os.path.dirname(__file__), "initialize_params.json")), "r", encoding="utf-8") as f: d: InitializeParams = json.load(f) del d["_description"] diff --git a/src/multilspy/language_servers/omnisharp/omnisharp.py b/src/multilspy/language_servers/omnisharp/omnisharp.py index 27ad76a5..08295a91 100644 --- a/src/multilspy/language_servers/omnisharp/omnisharp.py +++ b/src/multilspy/language_servers/omnisharp/omnisharp.py @@ -118,7 +118,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") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -145,7 +145,7 @@ class OmniSharp(LanguageServer): 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") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -384,7 +384,7 @@ class OmniSharp(LanguageServer): ) 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") as f: + 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) }) diff --git a/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py b/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py index f36a9d18..6a2dcee5 100644 --- a/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py +++ b/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py @@ -51,7 +51,7 @@ class RustAnalyzer(LanguageServer): """ 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"), "r", encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -88,7 +88,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") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: d = json.load(f) del d["_description"] diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index 5017748c..057c5b8f 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -56,7 +56,7 @@ class Solargraph(LanguageServer): if platform_id in [PlatformId.WIN_x64, PlatformId.WIN_arm64, PlatformId.WIN_x86]: which_cmd = "where" - 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"), "r", encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -100,7 +100,7 @@ class Solargraph(LanguageServer): """ Returns the initialize params for the Solargraph 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"), "r", encoding="utf-8") as f: d = json.load(f) del d["_description"] diff --git a/src/serena/llm/multilang_prompt.py b/src/serena/llm/multilang_prompt.py index 4092e735..1c8f8f5e 100644 --- a/src/serena/llm/multilang_prompt.py +++ b/src/serena/llm/multilang_prompt.py @@ -180,7 +180,7 @@ class MultiLangPromptTemplateCollection: for fn in os.listdir(prompts_dir): path = os.path.join(prompts_dir, fn) if fn.endswith(".txt"): - with open(path) as file: + with open(path, encoding="utf-8") as file: prompt_template_string = file.read() self._add_prompt_template(fn[:-4], prompt_template_string) elif fn.endswith(".yml"): diff --git a/test/resources/repos/python/test_repo/scripts/run_app.py b/test/resources/repos/python/test_repo/scripts/run_app.py index 40c76c6d..96254510 100644 --- a/test/resources/repos/python/test_repo/scripts/run_app.py +++ b/test/resources/repos/python/test_repo/scripts/run_app.py @@ -44,7 +44,7 @@ def load_config(config_path: str) -> dict[str, Any]: return {} try: - with open(config_path) as f: + with open(config_path, encoding="utf-8") as f: return json.load(f) except json.JSONDecodeError: logger.error(f"Invalid JSON in configuration file: {config_path}") From ba51084fd9dc8b6257204b8a8e0efa249323c7ad Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 20 May 2025 01:40:13 +0200 Subject: [PATCH 49/61] Bugfix in LS.request_containing_symbol - column condition was incorrect (too strict) --- src/multilspy/language_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index b556b943..f46800ad 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -1457,11 +1457,11 @@ class LanguageServer: column_condition = True if strict: line_condition = end["line"] >= line > start["line"] - if column is not None: + if column is not None and line == start["line"]: column_condition = column > start["character"] else: line_condition = end["line"] >= line >= start["line"] - if column is not None: + if column is not None and line == start["line"]: column_condition = column >= start["character"] return line_condition and column_condition From c848e8a7cadbb2a7156e6df891315a4dfe8c380a Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 20 May 2025 01:41:15 +0200 Subject: [PATCH 50/61] Extended tests --- test/multilspy/php/test_php_basic.py | 21 +++----------------- test/resources/repos/php/test_repo/index.php | 4 ++++ test/serena/test_serena_agent.py | 19 +++++++++++++----- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/test/multilspy/php/test_php_basic.py b/test/multilspy/php/test_php_basic.py index 08cf73a2..942281fe 100644 --- a/test/multilspy/php/test_php_basic.py +++ b/test/multilspy/php/test_php_basic.py @@ -109,11 +109,11 @@ 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: - index_php_path = str(repo_path / "index.php") + helper_php_path = str(repo_path / "helper.php") # In index.php (0-indexed lines): # Line 13: helperFunction(); // Usage of helperFunction - # Find references for helperFunction from its usage (line 13, char 0 for 'h') - references = language_server.request_references(index_php_path, 13, 0) + # Find references for helperFunction from its definition + references = language_server.request_references(helper_php_path, 2, len("function ")) assert references, f"Expected non-empty references for helperFunction but got {references=}" # Intelephense might return 1 (usage) or 2 (usage + definition) references. @@ -132,19 +132,4 @@ class TestPhpLanguageServer: ) usage_in_index_php = {"uri_suffix": "index.php", "line": 13, "character": 0} - definition_in_helper_php = {"uri_suffix": "helper.php", "line": 2, "character": 0} - assert usage_in_index_php in actual_locations_comparable, "Usage of helperFunction in index.php not found" - - # Depending on Intelephense's behavior, it might also include the definition. - # For now, we are flexible: it must find the usage. If it finds more, that's also okay. - # If we want to be strict about finding both or only one, this part would need adjustment. - if len(references) == 2: - assert ( - definition_in_helper_php in actual_locations_comparable - ), "Definition of helperFunction in helper.php expected but not found when 2 references returned" - elif len(references) == 1: - # If only one reference, ensure it's the usage we definitely expect - assert actual_locations_comparable[0] == usage_in_index_php - else: - assert False, f"Expected 1 or 2 references, but got {len(references)}" diff --git a/test/resources/repos/php/test_repo/index.php b/test/resources/repos/php/test_repo/index.php index 70d0d340..f8c4cda8 100644 --- a/test/resources/repos/php/test_repo/index.php +++ b/test/resources/repos/php/test_repo/index.php @@ -13,4 +13,8 @@ echo $greeting; helperFunction(); +function useHelperFunction() { + helperFunction(); +} + ?> \ No newline at end of file diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 03d660d1..143bdfe1 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -31,6 +31,7 @@ class TestSerenaAgent: pytest.param(Language.JAVA, "Model", "Class", "Model.java", marks=pytest.mark.java), pytest.param(Language.RUST, "add", "Function", "lib.rs", marks=pytest.mark.rust), pytest.param(Language.TYPESCRIPT, "DemoClass", "Class", "index.ts", marks=pytest.mark.typescript), + pytest.param(Language.PHP, "helperFunction", "Function", "helper.php", marks=pytest.mark.php), ], indirect=["serena_agent"], ) @@ -47,16 +48,24 @@ class TestSerenaAgent: @pytest.mark.parametrize( "serena_agent,symbol_name,def_file,ref_file", [ - (Language.PYTHON, "User", os.path.join("test_repo", "models.py"), os.path.join("test_repo", "services.py")), - (Language.GO, "Helper", "main.go", "main.go"), - ( + pytest.param( + Language.PYTHON, + "User", + os.path.join("test_repo", "models.py"), + os.path.join("test_repo", "services.py"), + marks=pytest.mark.python, + ), + pytest.param(Language.GO, "Helper", "main.go", "main.go", marks=pytest.mark.go), + pytest.param( Language.JAVA, "Model", os.path.join("src", "main", "java", "test_repo", "Model.java"), os.path.join("src", "main", "java", "test_repo", "Main.java"), + marks=pytest.mark.java, ), - (Language.RUST, "add", os.path.join("src", "lib.rs"), os.path.join("src", "main.rs")), - (Language.TYPESCRIPT, "helperFunction", "index.ts", "use_helper.ts"), + pytest.param(Language.RUST, "add", os.path.join("src", "lib.rs"), os.path.join("src", "main.rs"), marks=pytest.mark.rust), + pytest.param(Language.TYPESCRIPT, "helperFunction", "index.ts", "use_helper.ts", marks=pytest.mark.typescript), + pytest.param(Language.PHP, "helperFunction", "helper.php", "index.php", marks=pytest.mark.php), ], indirect=["serena_agent"], ) From 34ad871b212ec8e0bc66cbb75dd046d1fe22b3ea Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 20 May 2025 01:41:36 +0200 Subject: [PATCH 51/61] GH: run on push to main --- .github/workflows/pytest.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 549b036b..1a3b040a 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,6 +1,10 @@ name: Tests on CI -on: [pull_request] +on: + pull_request: + push: + branches: + - main concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} From 6c7b2e9078a261c31ca00a81b59b430f784bef1d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 20 May 2025 11:28:31 +0200 Subject: [PATCH 52/61] Only use utf-8 encoding for now --- src/multilspy/multilspy_utils.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/multilspy/multilspy_utils.py b/src/multilspy/multilspy_utils.py index 75f023a8..9dcfd8a9 100644 --- a/src/multilspy/multilspy_utils.py +++ b/src/multilspy/multilspy_utils.py @@ -128,22 +128,12 @@ class FileUtils: 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.") - encodings = ["utf-8-sig", "utf-16", "utf-8", "latin-1"] try: - for encoding in encodings: - try: - with open(file_path, "r", encoding=encoding) as inp_file: - return inp_file.read() - except UnicodeError: - continue - # Try system default encoding as a last resort - with open(file_path, "r") as inp_file: + with open(file_path, "r", encoding="utf-8") as inp_file: return inp_file.read() except Exception as exc: - logger.log(f"File read '{file_path}' failed: {exc}", logging.ERROR) + logger.log(f"File read '{file_path}' failed to read with encoding 'utf-8': {exc}", logging.ERROR) raise MultilspyException("File read failed.") from None - logger.log(f"File read '{file_path}' failed: Unsupported encoding.", logging.ERROR) - raise MultilspyException(f"File read '{file_path}' failed: Unsupported encoding.") from None @staticmethod def download_file(logger: MultilspyLogger, url: str, target_path: str) -> None: From b20a4942c81d3cfb2dcd4e24a6d29e4aad252b25 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 20 May 2025 13:11:01 +0200 Subject: [PATCH 53/61] Added parents to symbols, various fixes in symbol-tree code --- src/multilspy/language_server.py | 144 ++++++++++++++++++------------- src/multilspy/multilspy_types.py | 10 ++- 2 files changed, 92 insertions(+), 62 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index f46800ad..dadcba07 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -801,13 +801,16 @@ class LanguageServer: async 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. + to find symbols in the given file. Wait for the zresponse 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. - All symbols will have a location and a children attribute. + All symbols will have a location, a children, and a parent attribute, + where the parent attribute is None for root symbols. + Note that this is slightly different from the call to request_full_symbol_tree, + where the parent attribute will be the file symbol which in turn may have a package symbol as parent. + If you need a symbol tree that contains file symbols as well, you should use `request_full_symbol_tree` instead. """ self.logger.log(f"Requesting document symbols for {relative_file_path} for the first time", logging.DEBUG) # TODO: it's kinda dumb to not use the cache if include_body is False after include_body was True once @@ -860,24 +863,32 @@ class LanguageServer: item["selectionRange"] = item["range"] else: item["selectionRange"] = item["location"]["range"] - item[LSPConstants.CHILDREN] = item.get(LSPConstants.CHILDREN, []) + children = item.get(LSPConstants.CHILDREN, []) + for child in children: + child["parent"] = item + item[LSPConstants.CHILDREN] = children flat_all_symbol_list: List[multilspy_types.UnifiedSymbolInformation] = [] assert isinstance(response, list), f"Unexpected response from Language Server: {response}" root_nodes: List[multilspy_types.UnifiedSymbolInformation] = [] - for item in response: - if "range" not in item and "location" not in item: - if item["kind"] in [SymbolKind.File, SymbolKind.Module]: + 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]: ... - turn_item_into_symbol_with_children(item) - item = cast(multilspy_types.UnifiedSymbolInformation, item) - root_nodes.append(item) - assert isinstance(item, dict) - assert LSPConstants.NAME in item - assert LSPConstants.KIND in item + # mutation is more convenient than creating a new dict, + # 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["parent"] = None + + root_nodes.append(root_symbol) + assert isinstance(root_symbol, dict) + assert LSPConstants.NAME in root_symbol + assert LSPConstants.KIND in root_symbol - if LSPConstants.CHILDREN in item: + 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]: @@ -891,9 +902,9 @@ class LanguageServer: l.extend(visit_tree_nodes_and_build_tree_repr(child)) return l - flat_all_symbol_list.extend(visit_tree_nodes_and_build_tree_repr(item)) + flat_all_symbol_list.extend(visit_tree_nodes_and_build_tree_repr(root_symbol)) else: - flat_all_symbol_list.append(multilspy_types.UnifiedSymbolInformation(**item)) + flat_all_symbol_list.append(multilspy_types.UnifiedSymbolInformation(**root_symbol)) result = flat_all_symbol_list, root_nodes self.logger.log(f"Caching document symbols for {relative_file_path}", logging.DEBUG) @@ -903,17 +914,19 @@ class LanguageServer: async 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. + 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 Module (3) will be created. For directories, a symbol of kind Package (4) will be created. + For each file, a symbol of kind File (2) 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. + All symbols except the root packages will have a parent attribute. 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 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. :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. @@ -932,17 +945,17 @@ class LanguageServer: return root_nodes # Helper function to recursively process directories - async def process_directory(dir_path: str) -> List[multilspy_types.UnifiedSymbolInformation]: - abs_dir_path = self.repository_root_path if dir_path == "." else os.path.join(self.repository_root_path, dir_path) + async def process_directory(rel_dir_path: str) -> List[multilspy_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) if self.is_ignored_path(str(Path(abs_dir_path).relative_to(self.repository_root_path))): - self.logger.log(f"Skipping directory: {dir_path}\n(because it should be ignored)", logging.DEBUG) + self.logger.log(f"Skipping directory: {rel_dir_path}\n(because it should be ignored)", logging.DEBUG) return [] result = [] try: - items = os.listdir(abs_dir_path) + contained_dir_or_file_names = os.listdir(abs_dir_path) except OSError: return [] @@ -960,20 +973,45 @@ class LanguageServer: ) result.append(package_symbol) - for item in items: - item_path = os.path.join(abs_dir_path, item) - abs_item_path = os.path.join(self.repository_root_path, item_path) - rel_item_path = str(Path(abs_item_path).resolve().relative_to(self.repository_root_path)) - if self.is_ignored_path(rel_item_path): - self.logger.log(f"Skipping item: {rel_item_path}\n(because it should be ignored)", logging.DEBUG) + for contained_dir_or_file_name in contained_dir_or_file_names: + contained_dir_or_file_abs_path = os.path.join(abs_dir_path, contained_dir_or_file_name) + contained_dir_or_file_rel_path = str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)) + if self.is_ignored_path(contained_dir_or_file_rel_path): + self.logger.log(f"Skipping item: {contained_dir_or_file_rel_path}\n(because it should be ignored)", logging.DEBUG) continue - if os.path.isdir(abs_item_path): - child_symbols = await process_directory(item_path) + if os.path.isdir(contained_dir_or_file_abs_path): + child_symbols = await 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) + + # 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 + name=os.path.splitext(contained_dir_or_file_name)[0], + kind=multilspy_types.SymbolKind.File, + range=fileRange, + selectionRange=fileRange, + location=multilspy_types.Location( + uri=str(pathlib.Path(contained_dir_or_file_abs_path).as_uri()), + range=fileRange, + absolutePath=str(contained_dir_or_file_abs_path), + relativePath=str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)), + ), + children=file_root_nodes, + parent=package_symbol, + ) + for child in file_root_nodes: + child["parent"] = file_symbol - elif os.path.isfile(abs_item_path): - _, root_nodes = await self.request_document_symbols(item_path, include_body=include_body) + # Link file symbol with package + 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]): @@ -989,33 +1027,13 @@ class LanguageServer: if "children" in node: fix_relative_path(node["children"]) - fix_relative_path(root_nodes) - - # Create file symbol - file_rel_path = str(Path(abs_item_path).resolve().relative_to(self.repository_root_path)) - with self.open_file(file_rel_path) as file_data: - fileRange = self._get_range_from_file_content(file_data.contents) - file_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore - name=os.path.splitext(item)[0], - kind=multilspy_types.SymbolKind.File, - range=fileRange, - selectionRange=fileRange, - location=multilspy_types.Location( - uri=str(pathlib.Path(abs_item_path).as_uri()), - range=fileRange, - absolutePath=str(abs_item_path), - relativePath=str(Path(abs_item_path).resolve().relative_to(self.repository_root_path)), - ), - children=root_nodes - ) - - package_symbol["children"].append(file_symbol) + fix_relative_path(file_root_nodes) return result # Start from the root or the specified directory - start_path = within_relative_path or "." - return await process_directory(start_path) + start_rel_path = within_relative_path or "." + return await process_directory(start_rel_path) @staticmethod def _get_range_from_file_content(file_content: str) -> multilspy_types.Range: @@ -1496,12 +1514,16 @@ class LanguageServer: async 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. + Finds the container of the given symbol if there is one. If the parent attribute is present, the parent is returned + without further searching. :param symbol: The symbol to find the container of. :param include_body: whether to include the body of the symbol in the result. + :return: The container of the given symbol or None if no container is found. """ - assert "location" in symbol + 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( symbol["location"]["relativePath"], symbol["location"]["range"]["start"]["line"], @@ -1556,7 +1578,7 @@ class LanguageServer: @property def _cache_path(self) -> Path: - return Path(self.repository_root_path) / ".serena" / "cache" / "document_symbols_cache.pkl" + return Path(self.repository_root_path) / ".serena" / "cache" / "document_symbols_cache_v20-05-25.pkl" def save_cache(self): if self._cache_has_changed: @@ -1594,7 +1616,7 @@ class LanguageServer: :param query: The query string to filter symbols by - :return Union[List[multilspy_types.UnifiedSymbolInformation], None]: A list of matching symbols + :return: A list of matching symbols """ response = await self.server.send.workspace_symbol({"query": query}) if response is None: diff --git a/src/multilspy/multilspy_types.py b/src/multilspy/multilspy_types.py index 4a0f97e0..02fa7acf 100644 --- a/src/multilspy/multilspy_types.py +++ b/src/multilspy/multilspy_types.py @@ -199,7 +199,10 @@ class UnifiedSymbolInformation(TypedDict): """ The name of the symbol containing this symbol. This information is for user interface purposes (e.g. to render a qualifier in the user interface if necessary). It can't be used to re-infer a hierarchy for the document - symbols. """ + symbols. + + Note: within serena, the parent attribute was added and should be used instead. Most LS don't provide contianerName. + """ detail: NotRequired[str] """ More detail for this symbol, e.g the signature of a function. """ @@ -220,6 +223,11 @@ class UnifiedSymbolInformation(TypedDict): 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 From a7f1af7174d689eeff26bf474ec69970f701f90c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 20 May 2025 15:40:45 +0200 Subject: [PATCH 54/61] Allow finding symbols by qualname. Also allow finding parents in Symbol Fairly extensive testing included --- src/serena/symbol.py | 215 +++++++++++++++++++++++++++---- test/__init__.py | 1 + test/serena/__init__.py | 1 + test/serena/test_serena_agent.py | 74 +++++++++++ test/serena/test_symbol.py | 133 +++++++++++++++++++ 5 files changed, 397 insertions(+), 27 deletions(-) create mode 100644 test/serena/__init__.py create mode 100644 test/serena/test_symbol.py diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 0fcc553f..25c50e19 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -50,18 +50,90 @@ class SymbolLocation: class Symbol(ToStringMixin): - def __init__(self, s: UnifiedSymbolInformation) -> None: - self.s = s + _QAULNAME_SEPARATOR = "/" + + @staticmethod + def match_against_qualname( + name_pattern: str, + qual_name_parts: list[str], + substring_matching: bool, + ) -> bool: + """ + Checks if a given name/pattern matches a symbol's qualified name parts. + + :param name_pattern: The name or pattern to match. Can be a simple name + (e.g., "my_func") or a qualified name pattern + (e.g., "MyClass/my_method", "MyClass/"). + :param qual_name_parts: A list of strings representing the parts of the + a qualified name. + :param substring_matching: If True, allows substring matching for the relevant part(s). + - For simple names, the whole `name_pattern` is checked as a substring + of the symbol's simple name (`qual_name_parts[-1]`). + - For qualified name patterns, only the *last* part of the + `name_to_match` pattern is checked as a substring. + Other parts must match exactly. + :return: True if the name matches, False otherwise. + """ + assert name_pattern, "name_to_match must not be empty" + assert qual_name_parts, "symbol_qual_name_parts must not be empty" + qname_separator = Symbol._QAULNAME_SEPARATOR + is_qualified_pattern = qname_separator in name_pattern + + if not is_qualified_pattern: + # Simple name matching + symbol_simple_name = qual_name_parts[-1] + if substring_matching: + return name_pattern in symbol_simple_name + else: + return name_pattern == symbol_simple_name + # Qualified name pattern matching + # A pattern like "Lib/" or "Lib/Class/" is treated as a prefix. + # We strip the trailing separator to get the core parts to match. + # For example, "Lib/" becomes "Lib", "Lib/Class/" becomes "Lib/Class". + # A pattern of just "/" becomes an empty string. + cleaned_name_pattern = name_pattern.rstrip(qname_separator) + + # Splitting the cleaned pattern gives the segments to match. + # - "Lib" -> ["Lib"] + # - "Lib/Class" -> ["Lib", "Class"] + # - "" (from "/") -> [""] (split of empty string results in list with one empty string) + pattern_segments = cleaned_name_pattern.split(qname_separator) + + # The number of segments in the pattern must match the number of parts in the symbol's qualified name. + # Example: pattern "Lib/Class" (2 segments) should match a symbol like `MyClass` in `MyLib` (2 qual_name_parts: ["MyLib", "MyClass"]). + # It should not match `MyMethod` in `MyClass` in `MyLib` (3 qual_name_parts). + # Also, pattern "Lib/" (effectively "Lib", 1 segment) should match `MyLib` (1 qual_name_part: ["MyLib"]). + if len(pattern_segments) != len(qual_name_parts): + return False + + # Match all segments of the pattern except the last one. These must be exact matches. + # If pattern_segments has only one segment (e.g., "Lib", or from "Lib/", or from "/"), this loop is skipped. + for i in range(len(pattern_segments) - 1): + if pattern_segments[i] != qual_name_parts[i]: + return False + + # Match the last segment of the pattern against the last part of the symbol's qualified name. + last_pattern_segment = pattern_segments[-1] + last_qual_name_segment = qual_name_parts[-1] + + # The `substring_matching` flag applies to this last segment comparison. + if substring_matching: + return last_pattern_segment in last_qual_name_segment + else: + return last_pattern_segment == last_qual_name_segment + + def __init__(self, symbol_root_from_ls: UnifiedSymbolInformation) -> None: + self.symbol_root = symbol_root_from_ls def _tostring_includes(self) -> list[str]: return [] def _tostring_additional_entries(self) -> dict[str, Any]: - return dict(name=self.name, kind=self.kind, num_children=len(self.s["children"])) + return dict(name=self.name, kind=self.kind, num_children=len(self.symbol_root["children"])) @property def name(self) -> str: - return self.s["name"] + return self.symbol_root["name"] @property def kind(self) -> str: @@ -69,11 +141,14 @@ class Symbol(ToStringMixin): @property def symbol_kind(self) -> SymbolKind: - return self.s["kind"] + return self.symbol_root["kind"] @property def relative_path(self) -> str | None: - return self.s["location"]["relativePath"] + location = self.symbol_root.get("location") + if location: + return location.get("relativePath") + return None @property def location(self) -> SymbolLocation: @@ -83,36 +158,84 @@ class Symbol(ToStringMixin): return SymbolLocation(relative_path=self.relative_path, line=self.line, column=self.column) @property - def body_start_position(self) -> Position: - return self.s["location"]["range"]["start"] + def body_start_position(self) -> Position | None: + location = self.symbol_root.get("location") + if location: + range_info = location.get("range") + if range_info: + start_pos = range_info.get("start") + if start_pos: + return start_pos + return None @property - def body_end_position(self) -> Position: - return self.s["location"]["range"]["end"] + def body_end_position(self) -> Position | None: + location = self.symbol_root.get("location") + if location: + range_info = location.get("range") + if range_info: + end_pos = range_info.get("end") + if end_pos: + return end_pos + return None @property def line(self) -> int | None: - if "selectionRange" in self.s: - return self.s["selectionRange"]["start"]["line"] + if "selectionRange" in self.symbol_root: + return self.symbol_root["selectionRange"]["start"]["line"] else: # line is expected to be undefined for some types of symbols (e.g. SymbolKind.File) return None @property def column(self) -> int | None: - if "selectionRange" in self.s: - return self.s["selectionRange"]["start"]["character"] + if "selectionRange" in self.symbol_root: + return self.symbol_root["selectionRange"]["start"]["character"] else: # precise location is expected to be undefined for some types of symbols (e.g. SymbolKind.File) return None @property def body(self) -> str | None: - return self.s.get("body") + return self.symbol_root.get("body") - def iter_children(self) -> Iterator["Symbol"]: - for c in self.s["children"]: - yield Symbol(c) + def get_qualified_name(self) -> str: + """ + Get the qualified name of the symbol (e.g. "class/method/inner_function"). + """ + return self._QAULNAME_SEPARATOR.join(self.get_qualified_name_parts()) + + def get_qualified_name_parts(self) -> list[str]: + """ + Get the parts of the qualified name of the symbol (e.g. ["class", "method", "inner_function"]). + """ + ancestors_within_file = list(self.iter_ancestors(up_to_symbol_kind=SymbolKind.File)) + ancestors_within_file.reverse() + return [a.name for a in ancestors_within_file] + [self.name] + + def iter_children(self) -> Iterator[Self]: + for c in self.symbol_root["children"]: + yield self.__class__(c) + + def iter_ancestors(self, up_to_symbol_kind: SymbolKind | None = None) -> Iterator[Self]: + """ + Iterate over all ancestors of the symbol, starting with the parent and going up to the root or + the given symbol kind. + + :param up_to_symbol_kind: if provided, iteration will stop *before* the first ancestor of the given kind. + A typical use case is to pass `SymbolKind.File` or `SymbolKind.Package`. + """ + parent = self.get_parent() + if parent is not None: + if up_to_symbol_kind is None or parent.symbol_kind != up_to_symbol_kind: + yield parent + yield from parent.iter_ancestors(up_to_symbol_kind=up_to_symbol_kind) + + def get_parent(self) -> Self | None: + parent_root = self.symbol_root.get("parent") + if parent_root is None: + return None + return self.__class__(parent_root) def find( self, @@ -120,17 +243,45 @@ class Symbol(ToStringMixin): substring_matching: bool = False, include_kinds: Sequence[SymbolKind] | None = None, exclude_kinds: Sequence[SymbolKind] | None = None, - ) -> list["Symbol"]: + ) -> list[Self]: + """ + Find all symbols within the symbol's subtree that match the given name. + The name matching behavior depends on whether a qualified name or a simple name is provided. + It is assumed that the provided name is a qualified name if it contains the `/` character. + If substring matching is allowed, only the last element of the qualified name will be checked against + the symbol name using substring matching. + + + Examples: + - Providing "foo" will find all symbols named "foo" regardless where they are contained in the symbol tree. + - Providing "bar/foo" will only find symbols named "foo" that are direct children of a symbol called "bar". + - Providing "foo/" will only find symbols named "foo" that are top-level symbols (have no parent). + - Allowing substring matching with "bar" will find symbols with names containing "foo" anywhere in the symbol tree. + - Allowing substring matching with "foo/" will find only top-level symbols with names containing "foo". + - Allowing substring matching with "bar/foo" will find only symbols with names containing "foo" that are direct children of a symbol named "bar". + + :param name: the name of the symbol to find. Can use a qualified name (e.g. "class/method/inner_function") + to restrict the search. + :param substring_matching: whether to use substring matching for the symbol name. + If a qualified name is provided, the last element of the qualified name will be checked against + the symbol name using substring matching. + :param include_kinds: an optional sequence of ints representing the LSP symbol kind. + If provided, only symbols of the given kinds will be included in the result. + :param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result. + + """ result = [] def should_include(s: "Symbol") -> bool: - if not ((substring_matching and name in s.name) or name == s.name): - return False if include_kinds is not None and s.symbol_kind not in include_kinds: return False if exclude_kinds is not None and s.symbol_kind in exclude_kinds: return False - return True + return Symbol.match_against_qualname( + name_pattern=name, + qual_name_parts=s.get_qualified_name_parts(), + substring_matching=substring_matching, + ) def traverse(s: "Symbol") -> None: if should_include(s): @@ -322,10 +473,13 @@ class SymbolManager: body += "\n" with self._edited_symbol_location(location) as symbol: assert location.relative_path is not None - self.lang_server.delete_text_between_positions(location.relative_path, symbol.body_start_position, symbol.body_end_position) - self.lang_server.insert_text_at_position( - location.relative_path, symbol.body_start_position["line"], symbol.body_start_position["character"], body - ) + start_pos = symbol.body_start_position + end_pos = symbol.body_end_position + if start_pos is None or end_pos is None: + raise ValueError(f"Symbol at {location} does not have a defined body range.") + # At this point, start_pos and end_pos are guaranteed to be Position objects + 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_pos["line"], start_pos["character"], body) def insert_after(self, location: SymbolLocation, body: str) -> None: """ @@ -339,6 +493,9 @@ class SymbolManager: body += "\n" with self._edited_symbol_location(location) as symbol: pos = symbol.body_end_position + if pos is None: + raise ValueError(f"Symbol at {location} does not have a defined end position.") + # At this point, pos is guaranteed to be a Position object assert location.relative_path is not None self.lang_server.insert_text_at_position(location.relative_path, pos["line"], pos["character"], body) @@ -353,7 +510,11 @@ class SymbolManager: if not body.endswith("\n"): body += "\n" with self._edited_symbol_location(location) as symbol: - pos = copy(symbol.body_start_position) + original_start_pos = symbol.body_start_position + if original_start_pos is None: + raise ValueError(f"Symbol at {location} does not have a defined start position.") + # At this point, original_start_pos is guaranteed to be a Position object, so copying is safe. + pos = copy(original_start_pos) assert location.relative_path is not None self.lang_server.insert_text_at_position(location.relative_path, pos["line"], pos["character"], body) diff --git a/test/__init__.py b/test/__init__.py index e69de29b..8b137891 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -0,0 +1 @@ + diff --git a/test/serena/__init__.py b/test/serena/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test/serena/__init__.py @@ -0,0 +1 @@ + diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 143bdfe1..9fb3783e 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -87,3 +87,77 @@ class TestSerenaAgent: assert any( ref["location"]["relative_path"] == ref_file for ref in refs ), f"Expected to find reference to {symbol_name} in {ref_file} for {agent.project_config.language.name}. refs={refs}" + + @pytest.mark.parametrize( + "serena_agent,qualified_name,substring_matching,expected_symbol_name,expected_kind,expected_file", + [ + pytest.param( + Language.PYTHON, + "OuterClass/NestedClass", + False, + "NestedClass", + "Class", + os.path.join("test_repo", "nested.py"), + id="exact_qualname_class", + marks=pytest.mark.python, + ), + pytest.param( + Language.PYTHON, + "OuterClass/NestedClass/find_me", + False, + "find_me", + "Method", + os.path.join("test_repo", "nested.py"), + id="exact_qualname_method", + marks=pytest.mark.python, + ), + pytest.param( + Language.PYTHON, + "OuterClass/NestedCl", # Substring for NestedClass + True, + "NestedClass", + "Class", + os.path.join("test_repo", "nested.py"), + id="substring_qualname_class", + marks=pytest.mark.python, + ), + pytest.param( + Language.PYTHON, + "OuterClass/NestedClass/find_m", # Substring for find_me + True, + "find_me", + "Method", + os.path.join("test_repo", "nested.py"), + id="substring_qualname_method", + marks=pytest.mark.python, + ), + ], + indirect=["serena_agent"], + ) + def test_find_symbol_qualified_name( + self, + serena_agent: SerenaAgent, + qualified_name: str, + substring_matching: bool, + expected_symbol_name: str, + expected_kind: str, + expected_file: str, + ): + agent = serena_agent + find_symbol_tool = agent.get_tool(FindSymbolTool) + result = find_symbol_tool.apply( + name=qualified_name, + depth=0, + within_relative_path=None, + include_body=False, + include_kinds=None, + exclude_kinds=None, + substring_matching=substring_matching, + ) + symbols = json.loads(result) + assert any( + expected_symbol_name == s["name"] + and expected_kind.lower() in s["kind"].lower() + and expected_file in s["location"]["relative_path"] + for s in symbols + ), f"Expected to find {qualified_name} ({expected_kind}) in {expected_file} for {agent.project_config.language.name}. Symbols: {symbols}" diff --git a/test/serena/test_symbol.py b/test/serena/test_symbol.py new file mode 100644 index 00000000..fdd48f7e --- /dev/null +++ b/test/serena/test_symbol.py @@ -0,0 +1,133 @@ +import pytest + +from src.serena.symbol import Symbol + + +class TestSymbolNameMatching: + def _create_assertion_error_message( + self, + name_pattern: str, + qual_name_parts: list[str], + is_substring_match: bool, + expected_result: bool, + actual_result: bool, + ) -> str: + """Helper to create a detailed error message for assertions.""" + qnp_repr = "/".join(qual_name_parts) + + return ( + f"Pattern '{name_pattern}' (substring: {is_substring_match}) vs " + f"Qualname parts {qual_name_parts} (as '{qnp_repr}'). " + f"Expected: {expected_result}, Got: {actual_result}" + ) + + @pytest.mark.parametrize( + "name_pattern, qual_name_parts, is_substring_match, expected", + [ + # Exact matches (is_substring_match=False) + pytest.param("foo", ["foo"], False, True, id="'foo' matches 'foo' exactly"), + pytest.param("foo", ["bar", "foo"], False, True, id="'foo' matches 'bar,foo' exactly"), + pytest.param("foo", ["foobar"], False, False, id="'foo' does not match 'foobar' exactly"), + pytest.param("foo", ["bar", "foobar"], False, False, id="'foo' does not match 'bar,foobar' exactly"), + pytest.param("foo", ["path", "to", "foo"], False, True, id="'foo' matches 'path,to,foo' exactly"), + # Substring matches (is_substring_match=True) + pytest.param("foo", ["foobar"], True, True, id="'foo' matches 'foobar' as substring"), + pytest.param("foo", ["bar", "foobar"], True, True, id="'foo' matches 'bar,foobar' as substring"), + pytest.param("foo", ["barfoo"], True, True, id="'foo' matches 'barfoo' as substring"), + pytest.param("foo", ["baz"], True, False, id="'foo' does not match 'baz' as substring"), + pytest.param("foo", ["bar", "baz"], True, False, id="'foo' does not match 'bar,baz' as substring"), + pytest.param("foo", ["my_foobar_func"], True, True, id="'foo' matches 'my_foobar_func' as substring"), + pytest.param("foo", ["ClassA", "my_foobar_method"], True, True, id="'foo' matches 'ClassA,my_foobar_method' as substring"), + pytest.param("foo", ["my_bar_func"], True, False, id="'foo' does not match 'my_bar_func' as substring"), + ], + ) + def test_match_simple_name(self, name_pattern, qual_name_parts, is_substring_match, expected): + """Tests matching for simple names (no '/' in pattern).""" + result = Symbol.match_against_qualname(name_pattern, qual_name_parts, is_substring_match) + error_msg = self._create_assertion_error_message(name_pattern, qual_name_parts, is_substring_match, expected, result) + assert result == expected, error_msg + + @pytest.mark.parametrize( + "name_pattern, qual_name_parts, is_substring_match, expected", + [ + # Exact matches (is_substring_match=False) + pytest.param("bar/foo", ["bar", "foo"], False, True, id="'bar/foo' matches 'bar,foo' exactly"), + pytest.param( + "bar/foo", ["bar", "foo", "baz"], False, False, id="'bar/foo' does not match 'bar,foo,baz' exactly (len mismatch)" + ), + pytest.param("bar/foo", ["bar"], False, False, id="'bar/foo' does not match 'bar' exactly (len mismatch)"), + pytest.param("bar/foo", ["baz", "foo"], False, False, id="'bar/foo' does not match 'baz,foo' exactly (first mismatch)"), + pytest.param("bar/foo", ["bar", "baz"], False, False, id="'bar/foo' does not match 'bar,baz' exactly (last mismatch)"), + # from docstring examples + pytest.param("bar/foo", ["foo"], False, False, id="'bar/foo' does not match 'foo' exactly"), + pytest.param("bar/foo", ["other", "foo"], False, False, id="'bar/foo' does not match 'other,foo' exactly"), + pytest.param("bar/foo", ["bar", "otherfoo"], False, False, id="'bar/foo' does not match 'bar,otherfoo' exactly"), + # Substring matches (is_substring_match=True) + pytest.param("bar/foo", ["bar", "foobar"], True, True, id="'bar/foo' matches 'bar,foobar' as substring"), + pytest.param("bar/foo", ["bar", "bazfoo"], True, True, id="'bar/foo' matches 'bar,bazfoo' as substring"), + pytest.param("bar/fo", ["bar", "foo"], True, True, id="'bar/fo' matches 'bar,foo' as substring"), + pytest.param("bar/foo", ["bar", "baz"], True, False, id="'bar/foo' does not match 'bar,baz' as substring (last no substr)"), + pytest.param( + "bar/foo", ["baz", "foobar"], True, False, id="'bar/foo' does not match 'baz,foobar' as substring (first mismatch)" + ), + # from docstring examples + pytest.param("bar/foo", ["bar", "my_foobar_method"], True, True, id="'bar/foo' matches 'bar,my_foobar_method' as substring"), + pytest.param( + "bar/foo", ["bar", "another_method"], True, False, id="'bar/foo' does not match 'bar,another_method' as substring" + ), + pytest.param( + "bar/foo", ["other", "my_foobar_method"], True, False, id="'bar/foo' does not match 'other,my_foobar_method' as substring" + ), + pytest.param("bar/f", ["bar", "foo"], True, True, id="'bar/f' matches 'bar,foo' as substring"), + ], + ) + def test_match_name_pattern_ends_without_slash(self, name_pattern, qual_name_parts, is_substring_match, expected): + """Tests matching for qualified names (e.g. 'module/class/func').""" + result = Symbol.match_against_qualname(name_pattern, qual_name_parts, is_substring_match) + error_msg = self._create_assertion_error_message(name_pattern, qual_name_parts, is_substring_match, expected, result) + assert result == expected, error_msg + + @pytest.mark.parametrize( + "name_pattern, qual_name_parts, is_substring_match, expected", + [ + # Exact matches (is_substring_match=False) + pytest.param("foo/", ["foo"], False, True, id="'foo/' matches 'foo' exactly"), + pytest.param("bar/foo/", ["bar", "foo"], False, True, id="'bar/foo/' matches 'bar,foo' exactly"), + pytest.param("foo/", ["foobar"], False, False, id="'foo/' does not match 'foobar' exactly"), + pytest.param("foo/", ["bar", "foo"], False, False, id="'foo/' does not match 'bar,foo' exactly (not toplevel)"), + # Substring matches (is_substring_match=True) + pytest.param("foo/", ["foobar"], True, True, id="'foo/' matches 'foobar' as substring"), + pytest.param( + "bar/foo/", ["bar", "the_foobar_method"], True, True, id="'bar/foo/' matches 'bar,the_foobar_method' as substring" + ), + pytest.param( + "bar/foo/", + ["bar", "another_method"], + True, + False, + id="'bar/foo/' does not match 'bar,another_method' as substring (last no substr)", + ), + pytest.param( + "bar/foo/", + ["baz", "the_foobar_method"], + True, + False, + id="'bar/foo/' does not match 'baz,the_foobar_method' as substring (first mismatch)", + ), + # from docstring examples + pytest.param("foo/", ["my_foobar_func"], True, True, id="'foo/' matches 'my_foobar_func' as substring"), + pytest.param( + "foo/", + ["ClassA", "my_foobar_method"], + True, + False, + id="'foo/' does not match 'ClassA,my_foobar_method' as substring (not toplevel)", + ), + pytest.param("foo/", ["foo"], True, True, id="'foo/' matches 'foo' as substring (exact is substr)"), + ], + ) + def test_match_name_pattern_ending_in_slash(self, name_pattern, qual_name_parts, is_substring_match, expected): + """Tests matching for patterns ending with '/' (prefix/namespace style).""" + result = Symbol.match_against_qualname(name_pattern, qual_name_parts, is_substring_match) + error_msg = self._create_assertion_error_message(name_pattern, qual_name_parts, is_substring_match, expected, result) + assert result == expected, error_msg From e931c8cb581764015fc0057a18b7de71b8af0938 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 20 May 2025 15:46:43 +0200 Subject: [PATCH 55/61] Typo --- src/multilspy/language_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index dadcba07..2c50fbbf 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -801,7 +801,7 @@ class LanguageServer: async 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 zresponse and return the result. + 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. From b94d97425f2fd04e99041d862c5673c4cd38b10d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 20 May 2025 15:52:29 +0200 Subject: [PATCH 56/61] Typo --- src/multilspy/multilspy_types.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/multilspy/multilspy_types.py b/src/multilspy/multilspy_types.py index 02fa7acf..dad01167 100644 --- a/src/multilspy/multilspy_types.py +++ b/src/multilspy/multilspy_types.py @@ -201,7 +201,8 @@ class UnifiedSymbolInformation(TypedDict): if necessary). It can't be used to re-infer a hierarchy for the document symbols. - Note: within serena, the parent attribute was added and should be used instead. Most LS don't provide contianerName. + Note: within Serena, the parent attribute was added and should be used instead. + Most LS don't provide containerName. """ detail: NotRequired[str] From c4b70580cfd3aa6ba81bb8d6d401fc36193fe402 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 20 May 2025 15:55:59 +0200 Subject: [PATCH 57/61] Remove overly verbose comments --- src/serena/symbol.py | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 25c50e19..1ccbb769 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -87,36 +87,18 @@ class Symbol(ToStringMixin): else: return name_pattern == symbol_simple_name # Qualified name pattern matching - # A pattern like "Lib/" or "Lib/Class/" is treated as a prefix. - # We strip the trailing separator to get the core parts to match. - # For example, "Lib/" becomes "Lib", "Lib/Class/" becomes "Lib/Class". - # A pattern of just "/" becomes an empty string. - cleaned_name_pattern = name_pattern.rstrip(qname_separator) + name_parts = name_pattern.rstrip(qname_separator).split(qname_separator) - # Splitting the cleaned pattern gives the segments to match. - # - "Lib" -> ["Lib"] - # - "Lib/Class" -> ["Lib", "Class"] - # - "" (from "/") -> [""] (split of empty string results in list with one empty string) - pattern_segments = cleaned_name_pattern.split(qname_separator) - - # The number of segments in the pattern must match the number of parts in the symbol's qualified name. - # Example: pattern "Lib/Class" (2 segments) should match a symbol like `MyClass` in `MyLib` (2 qual_name_parts: ["MyLib", "MyClass"]). - # It should not match `MyMethod` in `MyClass` in `MyLib` (3 qual_name_parts). - # Also, pattern "Lib/" (effectively "Lib", 1 segment) should match `MyLib` (1 qual_name_part: ["MyLib"]). - if len(pattern_segments) != len(qual_name_parts): + if len(name_parts) != len(qual_name_parts): return False - # Match all segments of the pattern except the last one. These must be exact matches. - # If pattern_segments has only one segment (e.g., "Lib", or from "Lib/", or from "/"), this loop is skipped. - for i in range(len(pattern_segments) - 1): - if pattern_segments[i] != qual_name_parts[i]: - return False + # Segments before the last one must be exact matches + if name_parts[:-1] != qual_name_parts[:-1]: + return False # Match the last segment of the pattern against the last part of the symbol's qualified name. - last_pattern_segment = pattern_segments[-1] + last_pattern_segment = name_parts[-1] last_qual_name_segment = qual_name_parts[-1] - - # The `substring_matching` flag applies to this last segment comparison. if substring_matching: return last_pattern_segment in last_qual_name_segment else: From bf39636a5a48c9ac2cd036343fd6dd413e147349 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 20 May 2025 16:43:45 +0200 Subject: [PATCH 58/61] Docstring --- src/serena/agent.py | 16 +++++++++++++++- src/serena/symbol.py | 29 +++++++---------------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 0c8d5d10..a60b09da 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -794,8 +794,22 @@ class FindSymbolTool(Tool): or to retrieve further information using other tools. If you already anticipate that you will need to reference children of the symbol (like methods or fields contained in a class), you can specify a depth > 0. + + The name matching behavior depends on whether a qualified name or a simple name is provided. + It is assumed that the provided name is a qualified name if it contains the `/` character. + If substring matching is allowed, only the last element of the qualified name will be checked against + the symbol name using substring matching. - :param name: the name of the symbols to find + Examples: + - Providing "foo" will find all symbols named "foo" regardless where they are contained in the symbol tree. + - Providing "bar/foo" will only find symbols named "foo" that are direct children of a symbol called "bar". + - Providing "foo/" will only find symbols named "foo" that are top-level symbols (have no parent). + - Allowing substring matching with "bar" will find symbols with names containing "foo" anywhere in the symbol tree. + - Allowing substring matching with "foo/" will find only top-level symbols with names containing "foo". + - Allowing substring matching with "bar/foo" will find only symbols with names containing "foo" that are direct children of a symbol named "bar". + + :param name: the name of the symbols to find. A "qualified" name that includes the symbol's parents + separated by `/` (e.g. "class/method/inner_function") can be used to restrict the search. :param depth: specifies the depth up to which descendants of the symbol are to be retrieved (e.g. depth 1 will retrieve methods and attributes for the case where the symbol refers to a class). Provide a non-zero depth if you intend to subsequently query symbols that are contained in the diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 1ccbb769..7a6e227b 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -233,7 +233,6 @@ class Symbol(ToStringMixin): If substring matching is allowed, only the last element of the qualified name will be checked against the symbol name using substring matching. - Examples: - Providing "foo" will find all symbols named "foo" regardless where they are contained in the symbol tree. - Providing "bar/foo" will only find symbols named "foo" that are direct children of a symbol called "bar". @@ -242,8 +241,8 @@ class Symbol(ToStringMixin): - Allowing substring matching with "foo/" will find only top-level symbols with names containing "foo". - Allowing substring matching with "bar/foo" will find only symbols with names containing "foo" that are direct children of a symbol named "bar". - :param name: the name of the symbol to find. Can use a qualified name (e.g. "class/method/inner_function") - to restrict the search. + :param name: the name of the symbols to find. A "qualified" name that includes the symbol's parents + separated by `/` (e.g. "class/method/inner_function") can be used to restrict the search. :param substring_matching: whether to use substring matching for the symbol name. If a qualified name is provided, the last element of the qualified name will be checked against the symbol name using substring matching. @@ -290,7 +289,7 @@ class Symbol(ToStringMixin): and pass the children without passing the parent body to the LM. :return: a dictionary representation of the symbol """ - result: dict[str, Any] = {"name": self.name} + result: dict[str, Any] = {"name": self.name, "qualname": self.get_qualified_name()} if kind: result["kind"] = self.kind @@ -334,30 +333,16 @@ class SymbolManager: def find_by_name( self, name: str, - within_relative_path: str | None = None, include_body: bool = False, include_kinds: Sequence[SymbolKind] | None = None, exclude_kinds: Sequence[SymbolKind] | None = None, substring_matching: bool = False, + within_relative_path: str | None = None, ) -> list[Symbol]: """ - Find all symbols that match the given name. - - :param name: the name of the symbol to find - :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 all symbols in the result. - Note: you can filter out the bodies of the children if you set include_children_body=False - in the to_dict method. - :param include_kinds: an optional sequence of ints representing the LSP symbol kind. - If provided, only symbols of the given kinds will be included in the result. - :param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result. - Takes precedence over include_kinds. - :param substring_matching: whether to use substring matching for the symbol name. - If True, the symbol name will be matched if it contains the given name as a substring. - :return: a list of symbols that match the given name + Find all symbols that match the given name. See docstring of `Symbol.find` for more details. + The only parameter not mentioned there is `within_relative_path`, which can be used to restrict the search + 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) From 39854c8baa96f786227a26d8ca213b4768e3486c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 20 May 2025 17:21:21 +0200 Subject: [PATCH 59/61] Fix minor typing issues, fix initialization of gopls, remove hack --- src/multilspy/language_server.py | 3 ++- src/multilspy/language_servers/gopls/gopls.py | 16 +--------------- .../gopls/initialize_params.json | 10 ++++++++++ src/serena/agent.py | 3 ++- test/multilspy/go/test_go_basic.py | 1 + 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 2c50fbbf..6ca98b9a 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -38,6 +38,7 @@ 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 @@ -582,7 +583,7 @@ 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): + 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( { "textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))}, diff --git a/src/multilspy/language_servers/gopls/gopls.py b/src/multilspy/language_servers/gopls/gopls.py index ac474451..d17901a0 100644 --- a/src/multilspy/language_servers/gopls/gopls.py +++ b/src/multilspy/language_servers/gopls/gopls.py @@ -10,6 +10,7 @@ from typing import AsyncIterator, List 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 @@ -154,18 +155,3 @@ class Gopls(LanguageServer): await self.server.shutdown() await self.server.stop() - - @override - async def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[multilspy_types.Location]: - NUM_COLS_TO_TRY = 10 - """selectionRange in Gopls always contains the wrong column, - the one at the beginning of the declaration instead of the symbol. We do a dirty hack here (loop over some columns). - See https://github.com/golang/go/issues/73521 - """ - actual_column = column - for actual_column in range(column, column+NUM_COLS_TO_TRY): - try: - return await super()._send_references_request(relative_file_path, line, actual_column) - except (Error, RuntimeError, MultilspyException) as e: - self.logger.log(f"Cannot find symbol at {line=}, column={actual_column}, trying to bump column by 1", logging.INFO) - raise MultilspyException(f"Failed to find references for symbol in\n{relative_file_path=} at {line=}, {column=}") diff --git a/src/multilspy/language_servers/gopls/initialize_params.json b/src/multilspy/language_servers/gopls/initialize_params.json index 4330560e..40592e1a 100644 --- a/src/multilspy/language_servers/gopls/initialize_params.json +++ b/src/multilspy/language_servers/gopls/initialize_params.json @@ -18,6 +18,16 @@ }, "definition": { "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "hierarchicalDocumentSymbolSupport": true, + "symbolKind": { + "valueSet": [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 + ] + } } }, "workspace": { diff --git a/src/serena/agent.py b/src/serena/agent.py index a60b09da..a8a6fcd0 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -794,7 +794,7 @@ class FindSymbolTool(Tool): or to retrieve further information using other tools. If you already anticipate that you will need to reference children of the symbol (like methods or fields contained in a class), you can specify a depth > 0. - + The name matching behavior depends on whether a qualified name or a simple name is provided. It is assumed that the provided name is a qualified name if it contains the `/` character. If substring matching is allowed, only the last element of the qualified name will be checked against @@ -836,6 +836,7 @@ class FindSymbolTool(Tool): required for the task. Instead, if the output is too long, you should make a stricter query. :return: a list of symbols (with symbol locations) that match the given name in JSON format + """ include_kinds = cast(list[SymbolKind] | None, include_kinds) exclude_kinds = cast(list[SymbolKind] | None, exclude_kinds) diff --git a/test/multilspy/go/test_go_basic.py b/test/multilspy/go/test_go_basic.py index a87e9fe1..6b5e316b 100644 --- a/test/multilspy/go/test_go_basic.py +++ b/test/multilspy/go/test_go_basic.py @@ -7,6 +7,7 @@ from multilspy.multilspy_config import Language from multilspy.multilspy_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: From 0cf0ff3d48bf3d97a7f7d97400cb0d88c58fcebc Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 21 May 2025 12:35:26 +0200 Subject: [PATCH 60/61] Improvements and renamings of symbol name_path matching --- src/serena/agent.py | 105 ++-- src/serena/symbol.py | 1004 +++++++++++++++--------------- test/serena/test_serena_agent.py | 65 +- test/serena/test_symbol.py | 197 +++--- 4 files changed, 718 insertions(+), 653 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index a8a6fcd0..7d0a6927 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -10,11 +10,11 @@ import sys import traceback from abc import ABC from collections import defaultdict -from collections.abc import Callable, Generator, Iterable +from collections.abc import Callable, Generator, Iterable, Sequence from dataclasses import dataclass, field from logging import Logger from pathlib import Path -from typing import TYPE_CHECKING, Any, Self, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Self, TypeVar, Union import yaml from sensai.util import logging @@ -778,7 +778,7 @@ class FindSymbolTool(Tool): def apply( self, - name: str, + name_path: str, depth: int = 0, within_relative_path: str | None = None, include_body: bool = False, @@ -788,63 +788,56 @@ class FindSymbolTool(Tool): max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH, ) -> str: """ - Retrieves information on all symbols/code entities, i.e. classes, methods, attributes, variables, etc. - with the given name. - The returned symbol location information can subsequently be used to edit the returned symbols - or to retrieve further information using other tools. - If you already anticipate that you will need to reference children of the symbol (like methods or fields contained in a class), - you can specify a depth > 0. + Retrieves information on all symbols/code entities (classes, methods, etc.) based on the given `name_path`, + which represents a pattern for the symbol's path within the symbol tree of a single file. + The returned symbol location can be used for edits or further queries. + Specify `depth > 0` to retrieve children (e.g., methods of a class). - The name matching behavior depends on whether a qualified name or a simple name is provided. - It is assumed that the provided name is a qualified name if it contains the `/` character. - If substring matching is allowed, only the last element of the qualified name will be checked against - the symbol name using substring matching. + The matching behavior is determined by the structure of `name_path`, which can + either be a simple name (e.g. "method") or a name path like "class/method" (relative name path) + or "/class/method" (absolute name path). Note that the name path is not a path in the file system + but rather a path in the symbol tree **within a single file**. Thus, file or directory names should never + be included in the `name_path`. For restricting the search to a single file or directory, + the `within_relative_path` parameter should be used instead. The retrieved symbols' `name_path` attribute + will always be composed of symbol names, never file or directory names. - Examples: - - Providing "foo" will find all symbols named "foo" regardless where they are contained in the symbol tree. - - Providing "bar/foo" will only find symbols named "foo" that are direct children of a symbol called "bar". - - Providing "foo/" will only find symbols named "foo" that are top-level symbols (have no parent). - - Allowing substring matching with "bar" will find symbols with names containing "foo" anywhere in the symbol tree. - - Allowing substring matching with "foo/" will find only top-level symbols with names containing "foo". - - Allowing substring matching with "bar/foo" will find only symbols with names containing "foo" that are direct children of a symbol named "bar". + Key aspects of the name path matching behavior: + - Trailing slashes in `name_path` play no role and are ignored. + - The name of the retrieved symbols will match (either exactly or as a substring) + the last segment of `name_path`, while other segments will restrict the search to symbols that + have a desired sequence of ancestors. + - If there is no starting or intermediate slash in `name_path`, there is no + restriction on the ancestor symbols. For example, passing `method` will match + against symbols with name paths like `method`, `class/method`, `class/nested_class/method`, etc. + - If `name_path` contains a `/` but doesn't start with a `/`, the matching is restricted to symbols + with the same ancestors as the last segment of `name_path`. For example, passing `class/method` will match against + `class/method` as well as `nested_class/class/method` but not `method`. + - If `name_path` starts with a `/`, it will be treated as an absolute name path pattern, meaning + that the first segment of it must match the first segment of the symbol's name path. + For example, passing `/class` will match only against top-level symbols like `class` but not against `nested_class/class`. + Passing `/class/method` will match against `class/method` but not `nested_class/class/method` or `method`. - :param name: the name of the symbols to find. A "qualified" name that includes the symbol's parents - separated by `/` (e.g. "class/method/inner_function") can be used to restrict the search. - :param depth: specifies the depth up to which descendants of the symbol are to be retrieved - (e.g. depth 1 will retrieve methods and attributes for the case where the symbol refers to a class). - Provide a non-zero depth if you intend to subsequently query symbols that are contained in the - retrieved symbol. - :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 all symbols in the result. You should only use this - if you actually need the body of the symbol for the task at hand (for example, for a deep analysis - of the functionality or for an editing task). - :param include_kinds: an optional list of ints representing the LSP symbol kind. - If provided, only symbols of the given kinds will be included in the result. - Valid kinds: - 1=file, 2=module, 3=namespace, 4=package, 5=class, 6=method, 7=property, 8=field, 9=constructor, 10=enum, + + :param name_path: The name path pattern to search for, see above for details. + :param depth: Depth to retrieve descendants (e.g., 1 for class methods/attributes). + :param within_relative_path: Optional. Restrict search to this file or directory. If None, searches entire codebase. + :param include_body: If True, include the symbol's source code. Use judiciously. + :param include_kinds: Optional. List of LSP symbol kind integers to include. (e.g., 5 for Class, 12 for Function). + Valid kinds: 1=file, 2=module, 3=namespace, 4=package, 5=class, 6=method, 7=property, 8=field, 9=constructor, 10=enum, 11=interface, 12=function, 13=variable, 14=constant, 15=string, 16=number, 17=boolean, 18=array, 19=object, 20=key, 21=null, 22=enum member, 23=struct, 24=event, 25=operator, 26=type parameter - :param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result. - Takes precedence over include_kinds. - :param substring_matching: whether to use substring matching for the symbol name. - If True, the symbol name will be matched if it contains the given name as a substring. - :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. - :return: a list of symbols (with symbol locations) that match the given name in JSON format - + :param exclude_kinds: Optional. List of LSP symbol kind integers to exclude. Takes precedence over `include_kinds`. + :param substring_matching: If True, use substring matching for the last segment of `name`. + :param max_answer_chars: Max characters for the JSON result. If exceeded, no content is returned. + :return: JSON string: a list of symbols (with locations) matching the name. """ - include_kinds = cast(list[SymbolKind] | None, include_kinds) - exclude_kinds = cast(list[SymbolKind] | None, exclude_kinds) + parsed_include_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in include_kinds] if include_kinds else None + parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None symbols = self.symbol_manager.find_by_name( - name, + name_path, include_body=include_body, - include_kinds=include_kinds, - exclude_kinds=exclude_kinds, + include_kinds=parsed_include_kinds, + exclude_kinds=parsed_exclude_kinds, substring_matching=substring_matching, within_relative_path=within_relative_path, ) @@ -895,13 +888,13 @@ class FindReferencingSymbolsTool(Tool): make a stricter query. :return: a list of JSON objects with the symbols referencing the requested symbol """ - include_kinds = cast(list[SymbolKind] | None, include_kinds) - exclude_kinds = cast(list[SymbolKind] | None, exclude_kinds) + parsed_include_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in include_kinds] if include_kinds else None + parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None symbols = self.symbol_manager.find_referencing_symbols( SymbolLocation(relative_path, line, column), include_body=include_body, - include_kinds=include_kinds, - exclude_kinds=exclude_kinds, + include_kinds=parsed_include_kinds, + exclude_kinds=parsed_exclude_kinds, ) symbol_dicts = [s.to_dict(kind=True, location=True, depth=0, include_body=include_body) for s in symbols] result = json.dumps(symbol_dicts) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 7a6e227b..171e209a 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -1,508 +1,496 @@ -import logging -import os -from collections.abc import Iterator, Sequence -from contextlib import contextmanager -from copy import copy -from dataclasses import asdict, dataclass -from typing import TYPE_CHECKING, Any, Self - -from sensai.util.string import ToStringMixin - -from multilspy import SyncLanguageServer -from multilspy.multilspy_types import Position, SymbolKind, UnifiedSymbolInformation - -if TYPE_CHECKING: - from .agent import SerenaAgent - -log = logging.getLogger(__name__) - - -@dataclass -class SymbolLocation: - """ - Represents the (start) location of a symbol identifier - """ - - relative_path: str | None - """ - the relative path of the file containing the symbol; if None, the symbol is defined outside of the project's scope - """ - line: int | None - """ - the line number in which the symbol identifier is defined (if the symbol is a function, class, etc.); - may be None for some types of symbols (e.g. SymbolKind.File) - """ - column: int | None - """ - the column number in which the symbol identifier is defined (if the symbol is a function, class, etc.); - may be None for some types of symbols (e.g. SymbolKind.File) - """ - - def __post_init__(self) -> None: - if self.relative_path is not None: - self.relative_path = self.relative_path.replace("/", os.path.sep) - - def to_dict(self) -> dict[str, Any]: - return asdict(self) - - def has_position_in_file(self) -> bool: - return self.relative_path is not None and self.line is not None and self.column is not None - - -class Symbol(ToStringMixin): - _QAULNAME_SEPARATOR = "/" - - @staticmethod - def match_against_qualname( - name_pattern: str, - qual_name_parts: list[str], - substring_matching: bool, - ) -> bool: - """ - Checks if a given name/pattern matches a symbol's qualified name parts. - - :param name_pattern: The name or pattern to match. Can be a simple name - (e.g., "my_func") or a qualified name pattern - (e.g., "MyClass/my_method", "MyClass/"). - :param qual_name_parts: A list of strings representing the parts of the - a qualified name. - :param substring_matching: If True, allows substring matching for the relevant part(s). - - For simple names, the whole `name_pattern` is checked as a substring - of the symbol's simple name (`qual_name_parts[-1]`). - - For qualified name patterns, only the *last* part of the - `name_to_match` pattern is checked as a substring. - Other parts must match exactly. - :return: True if the name matches, False otherwise. - """ - assert name_pattern, "name_to_match must not be empty" - assert qual_name_parts, "symbol_qual_name_parts must not be empty" - qname_separator = Symbol._QAULNAME_SEPARATOR - is_qualified_pattern = qname_separator in name_pattern - - if not is_qualified_pattern: - # Simple name matching - symbol_simple_name = qual_name_parts[-1] - if substring_matching: - return name_pattern in symbol_simple_name - else: - return name_pattern == symbol_simple_name - # Qualified name pattern matching - name_parts = name_pattern.rstrip(qname_separator).split(qname_separator) - - if len(name_parts) != len(qual_name_parts): - return False - - # Segments before the last one must be exact matches - if name_parts[:-1] != qual_name_parts[:-1]: - return False - - # Match the last segment of the pattern against the last part of the symbol's qualified name. - last_pattern_segment = name_parts[-1] - last_qual_name_segment = qual_name_parts[-1] - if substring_matching: - return last_pattern_segment in last_qual_name_segment - else: - return last_pattern_segment == last_qual_name_segment - - def __init__(self, symbol_root_from_ls: UnifiedSymbolInformation) -> None: - self.symbol_root = symbol_root_from_ls - - def _tostring_includes(self) -> list[str]: - return [] - - def _tostring_additional_entries(self) -> dict[str, Any]: - return dict(name=self.name, kind=self.kind, num_children=len(self.symbol_root["children"])) - - @property - def name(self) -> str: - return self.symbol_root["name"] - - @property - def kind(self) -> str: - return SymbolKind(self.symbol_kind).name - - @property - def symbol_kind(self) -> SymbolKind: - return self.symbol_root["kind"] - - @property - def relative_path(self) -> str | None: - location = self.symbol_root.get("location") - if location: - return location.get("relativePath") - return None - - @property - def location(self) -> SymbolLocation: - """ - :return: the start location of the actual symbol identifier - """ - return SymbolLocation(relative_path=self.relative_path, line=self.line, column=self.column) - - @property - def body_start_position(self) -> Position | None: - location = self.symbol_root.get("location") - if location: - range_info = location.get("range") - if range_info: - start_pos = range_info.get("start") - if start_pos: - return start_pos - return None - - @property - def body_end_position(self) -> Position | None: - location = self.symbol_root.get("location") - if location: - range_info = location.get("range") - if range_info: - end_pos = range_info.get("end") - if end_pos: - return end_pos - return None - - @property - def line(self) -> int | None: - if "selectionRange" in self.symbol_root: - return self.symbol_root["selectionRange"]["start"]["line"] - else: - # line is expected to be undefined for some types of symbols (e.g. SymbolKind.File) - return None - - @property - def column(self) -> int | None: - if "selectionRange" in self.symbol_root: - return self.symbol_root["selectionRange"]["start"]["character"] - else: - # precise location is expected to be undefined for some types of symbols (e.g. SymbolKind.File) - return None - - @property - def body(self) -> str | None: - return self.symbol_root.get("body") - - def get_qualified_name(self) -> str: - """ - Get the qualified name of the symbol (e.g. "class/method/inner_function"). - """ - return self._QAULNAME_SEPARATOR.join(self.get_qualified_name_parts()) - - def get_qualified_name_parts(self) -> list[str]: - """ - Get the parts of the qualified name of the symbol (e.g. ["class", "method", "inner_function"]). - """ - ancestors_within_file = list(self.iter_ancestors(up_to_symbol_kind=SymbolKind.File)) - ancestors_within_file.reverse() - return [a.name for a in ancestors_within_file] + [self.name] - - def iter_children(self) -> Iterator[Self]: - for c in self.symbol_root["children"]: - yield self.__class__(c) - - def iter_ancestors(self, up_to_symbol_kind: SymbolKind | None = None) -> Iterator[Self]: - """ - Iterate over all ancestors of the symbol, starting with the parent and going up to the root or - the given symbol kind. - - :param up_to_symbol_kind: if provided, iteration will stop *before* the first ancestor of the given kind. - A typical use case is to pass `SymbolKind.File` or `SymbolKind.Package`. - """ - parent = self.get_parent() - if parent is not None: - if up_to_symbol_kind is None or parent.symbol_kind != up_to_symbol_kind: - yield parent - yield from parent.iter_ancestors(up_to_symbol_kind=up_to_symbol_kind) - - def get_parent(self) -> Self | None: - parent_root = self.symbol_root.get("parent") - if parent_root is None: - return None - return self.__class__(parent_root) - - def find( - self, - name: str, - substring_matching: bool = False, - include_kinds: Sequence[SymbolKind] | None = None, - exclude_kinds: Sequence[SymbolKind] | None = None, - ) -> list[Self]: - """ - Find all symbols within the symbol's subtree that match the given name. - The name matching behavior depends on whether a qualified name or a simple name is provided. - It is assumed that the provided name is a qualified name if it contains the `/` character. - If substring matching is allowed, only the last element of the qualified name will be checked against - the symbol name using substring matching. - - Examples: - - Providing "foo" will find all symbols named "foo" regardless where they are contained in the symbol tree. - - Providing "bar/foo" will only find symbols named "foo" that are direct children of a symbol called "bar". - - Providing "foo/" will only find symbols named "foo" that are top-level symbols (have no parent). - - Allowing substring matching with "bar" will find symbols with names containing "foo" anywhere in the symbol tree. - - Allowing substring matching with "foo/" will find only top-level symbols with names containing "foo". - - Allowing substring matching with "bar/foo" will find only symbols with names containing "foo" that are direct children of a symbol named "bar". - - :param name: the name of the symbols to find. A "qualified" name that includes the symbol's parents - separated by `/` (e.g. "class/method/inner_function") can be used to restrict the search. - :param substring_matching: whether to use substring matching for the symbol name. - If a qualified name is provided, the last element of the qualified name will be checked against - the symbol name using substring matching. - :param include_kinds: an optional sequence of ints representing the LSP symbol kind. - If provided, only symbols of the given kinds will be included in the result. - :param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result. - - """ - result = [] - - def should_include(s: "Symbol") -> bool: - if include_kinds is not None and s.symbol_kind not in include_kinds: - return False - if exclude_kinds is not None and s.symbol_kind in exclude_kinds: - return False - return Symbol.match_against_qualname( - name_pattern=name, - qual_name_parts=s.get_qualified_name_parts(), - substring_matching=substring_matching, - ) - - def traverse(s: "Symbol") -> None: - if should_include(s): - result.append(s) - for c in s.iter_children(): - traverse(c) - - traverse(self) - return result - - def to_dict( - self, kind: bool = False, location: bool = False, depth: int = 0, include_body: bool = False, include_children_body: bool = False - ) -> dict[str, Any]: - """ - Convert the symbol to a dictionary. - - :param kind: whether to include the kind of the symbol - :param location: whether to include the location of the symbol - :param depth: the depth of the symbol - :param include_body: whether to include the body of the top-level symbol. - :param include_children_body: whether to also include the body of the children. - Note that the body of the children is part of the body of the parent symbol, - so there is usually no need to set this to True unless you want process the output - and pass the children without passing the parent body to the LM. - :return: a dictionary representation of the symbol - """ - result: dict[str, Any] = {"name": self.name, "qualname": self.get_qualified_name()} - - if kind: - result["kind"] = self.kind - - if location: - result["location"] = self.location.to_dict() - - if include_body: - if self.body is None: - log.warning("Requested body for symbol, but it is not present. The symbol might have been loaded with include_body=False.") - result["body"] = self.body - - def add_children(s: Self) -> list[dict[str, Any]]: - children = [] - for c in s.iter_children(): - children.append( - c.to_dict( - kind=kind, - location=location, - depth=depth - 1, - include_body=include_children_body, - include_children_body=include_children_body, - ) - ) - return children - - if depth > 0: - result["children"] = add_children(self) - - return result - - -class SymbolManager: - def __init__(self, lang_server: SyncLanguageServer, agent: "SerenaAgent") -> None: - self.lang_server = lang_server - self.agent = agent - - def _to_symbols(self, items: list[UnifiedSymbolInformation]) -> list[Symbol]: - return [Symbol(s) for s in items] - - def find_by_name( - self, - name: str, - include_body: bool = False, - include_kinds: Sequence[SymbolKind] | None = None, - exclude_kinds: Sequence[SymbolKind] | None = None, - substring_matching: bool = False, - within_relative_path: str | None = None, - ) -> list[Symbol]: - """ - Find all symbols that match the given name. See docstring of `Symbol.find` for more details. - The only parameter not mentioned there is `within_relative_path`, which can be used to restrict the search - 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) - for root in symbol_roots: - symbols.extend( - Symbol(root).find(name, include_kinds=include_kinds, exclude_kinds=exclude_kinds, substring_matching=substring_matching) - ) - 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) - 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) - for symbol_dict in symbol_dicts: - symbol = Symbol(symbol_dict) - if symbol.location == location: - return symbol - return None - - def find_referencing_symbols( - self, - symbol_location: SymbolLocation, - include_body: bool = False, - include_kinds: Sequence[SymbolKind] | None = None, - exclude_kinds: Sequence[SymbolKind] | None = None, - ) -> list[Symbol]: - """ - Find all symbols that reference the symbol at the given location. - - :param symbol_location: the location of the symbol for which to find references - :param include_body: whether to include the body of all symbols in the result. - Note: you can filter out the bodies of the children if you set include_children_body=False - in the to_dict method. - :param include_kinds: an optional sequence of ints representing the LSP symbol kind. - If provided, only symbols of the given kinds will be included in the result. - :param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result. - Takes precedence over include_kinds. - :return: a list of symbols that reference the given symbol - """ - if not symbol_location.has_position_in_file(): - raise ValueError("Symbol location does not contain a valid position in a file") - assert symbol_location.relative_path is not None - assert symbol_location.line is not None - assert symbol_location.column is not None - symbol_dicts = self.lang_server.request_referencing_symbols( - relative_file_path=symbol_location.relative_path, - line=symbol_location.line, - column=symbol_location.column, - include_imports=False, - include_self=False, - include_body=include_body, - ) - - if include_kinds is not None: - symbol_dicts = [s for s in symbol_dicts if s["kind"] in include_kinds] - - if exclude_kinds is not None: - symbol_dicts = [s for s in symbol_dicts if s["kind"] not in exclude_kinds] - - return self._to_symbols(symbol_dicts) - - @contextmanager - def _edited_file(self, relative_path: str) -> Iterator[None]: - with self.lang_server.open_file(relative_path) as file_buffer: - yield - 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) - self.agent.mark_file_modified(relative_path) - - @contextmanager - def _edited_symbol_location(self, location: SymbolLocation) -> Iterator[Symbol]: - symbol = self.find_by_location(location) - if symbol is None: - raise ValueError("Symbol not found/has no defined location within a file") - assert location.relative_path is not None - with self._edited_file(location.relative_path): - yield symbol - - def replace_body(self, location: SymbolLocation, body: str) -> None: - """ - Replace the body of the symbol at the given location with the given body - - :param location: the location of the symbol to replace - :param body: the new body - """ - # make sure body always ends with at least one newline - if not body.endswith("\n"): - body += "\n" - with self._edited_symbol_location(location) as symbol: - assert location.relative_path is not None - start_pos = symbol.body_start_position - end_pos = symbol.body_end_position - if start_pos is None or end_pos is None: - raise ValueError(f"Symbol at {location} does not have a defined body range.") - # At this point, start_pos and end_pos are guaranteed to be Position objects - 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_pos["line"], start_pos["character"], body) - - def insert_after(self, location: SymbolLocation, body: str) -> None: - """ - Appends content after the given symbol - - :param location: the location of the symbol after which to add new lines - :param body: the body of the entity to append - """ - # make sure body always ends with at least one newline - if not body.endswith("\n"): - body += "\n" - with self._edited_symbol_location(location) as symbol: - pos = symbol.body_end_position - if pos is None: - raise ValueError(f"Symbol at {location} does not have a defined end position.") - # At this point, pos is guaranteed to be a Position object - assert location.relative_path is not None - self.lang_server.insert_text_at_position(location.relative_path, pos["line"], pos["character"], body) - - def insert_before(self, location: SymbolLocation, body: str) -> None: - """ - Inserts content before the given symbol - - :param location: the location of the symbol before which to add new lines - :param body: the body of the entity to insert - """ - # make sure body always ends with at least one newline - if not body.endswith("\n"): - body += "\n" - with self._edited_symbol_location(location) as symbol: - original_start_pos = symbol.body_start_position - if original_start_pos is None: - raise ValueError(f"Symbol at {location} does not have a defined start position.") - # At this point, original_start_pos is guaranteed to be a Position object, so copying is safe. - pos = copy(original_start_pos) - assert location.relative_path is not None - self.lang_server.insert_text_at_position(location.relative_path, pos["line"], pos["character"], body) - - def insert_at_line(self, relative_path: str, line: int, content: str) -> None: - """ - Inserts content at the given line in the given file. - - :param line: the 0-based index of the line to insert content at - :param content: the content to insert - """ - with self._edited_file(relative_path): - 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: - """ - Deletes lines in the given file. - - :param start_line: the 0-based index of the first line to delete (inclusive) - :param end_line: the 0-based index of the last line to delete (inclusive) - """ - with self._edited_file(relative_path): - start_pos = Position(line=start_line, character=0) - end_pos = Position(line=end_line + 1, character=0) - self.lang_server.delete_text_between_positions(relative_path, start_pos, end_pos) +import logging +import os +from collections.abc import Iterator, Sequence +from contextlib import contextmanager +from copy import copy +from dataclasses import asdict, dataclass +from typing import TYPE_CHECKING, Any, Self + +from sensai.util.string import ToStringMixin + +from multilspy import SyncLanguageServer +from multilspy.multilspy_types import Position, SymbolKind, UnifiedSymbolInformation + +if TYPE_CHECKING: + from .agent import SerenaAgent + +log = logging.getLogger(__name__) + + +@dataclass +class SymbolLocation: + """ + Represents the (start) location of a symbol identifier + """ + + relative_path: str | None + """ + the relative path of the file containing the symbol; if None, the symbol is defined outside of the project's scope + """ + line: int | None + """ + the line number in which the symbol identifier is defined (if the symbol is a function, class, etc.); + may be None for some types of symbols (e.g. SymbolKind.File) + """ + column: int | None + """ + the column number in which the symbol identifier is defined (if the symbol is a function, class, etc.); + may be None for some types of symbols (e.g. SymbolKind.File) + """ + + def __post_init__(self) -> None: + if self.relative_path is not None: + self.relative_path = self.relative_path.replace("/", os.path.sep) + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + def has_position_in_file(self) -> bool: + return self.relative_path is not None and self.line is not None and self.column is not None + + +class Symbol(ToStringMixin): + _NAME_PATH_SEP = "/" + + @staticmethod + def match_name_path( + name_path: str, + symbol_name_path_parts: list[str], + substring_matching: bool, + ) -> bool: + """ + Checks if a given `name_path` matches a symbol's qualified name parts. + See docstring of `Symbol.find` for more details. + """ + assert name_path, "name_path must not be empty" + assert symbol_name_path_parts, "symbol_name_path_parts must not be empty" + name_path_sep = Symbol._NAME_PATH_SEP + + is_absolute_pattern = name_path.startswith(name_path_sep) + pattern_parts = name_path.lstrip(name_path_sep).rstrip(name_path_sep).split(name_path_sep) + + # filtering based on ancestors + if len(pattern_parts) > len(symbol_name_path_parts): + # can't possibly match if pattern has more parts than symbol + return False + if is_absolute_pattern and len(pattern_parts) != len(symbol_name_path_parts): + # for absolute patterns, the number of parts must match exactly + return False + if symbol_name_path_parts[-len(pattern_parts) : -1] != pattern_parts[:-1]: + # ancestors must match + return False + + # matching the last part of the symbol name + name_to_match = pattern_parts[-1] + symbol_name = symbol_name_path_parts[-1] + if substring_matching: + return name_to_match in symbol_name + else: + return name_to_match == symbol_name + + def __init__(self, symbol_root_from_ls: UnifiedSymbolInformation) -> None: + self.symbol_root = symbol_root_from_ls + + def _tostring_includes(self) -> list[str]: + return [] + + def _tostring_additional_entries(self) -> dict[str, Any]: + return dict(name=self.name, kind=self.kind, num_children=len(self.symbol_root["children"])) + + @property + def name(self) -> str: + return self.symbol_root["name"] + + @property + def kind(self) -> str: + return SymbolKind(self.symbol_kind).name + + @property + def symbol_kind(self) -> SymbolKind: + return self.symbol_root["kind"] + + @property + def relative_path(self) -> str | None: + location = self.symbol_root.get("location") + if location: + return location.get("relativePath") + return None + + @property + def location(self) -> SymbolLocation: + """ + :return: the start location of the actual symbol identifier + """ + return SymbolLocation(relative_path=self.relative_path, line=self.line, column=self.column) + + @property + def body_start_position(self) -> Position | None: + location = self.symbol_root.get("location") + if location: + range_info = location.get("range") + if range_info: + start_pos = range_info.get("start") + if start_pos: + return start_pos + return None + + @property + def body_end_position(self) -> Position | None: + location = self.symbol_root.get("location") + if location: + range_info = location.get("range") + if range_info: + end_pos = range_info.get("end") + if end_pos: + return end_pos + return None + + @property + def line(self) -> int | None: + if "selectionRange" in self.symbol_root: + return self.symbol_root["selectionRange"]["start"]["line"] + else: + # line is expected to be undefined for some types of symbols (e.g. SymbolKind.File) + return None + + @property + def column(self) -> int | None: + if "selectionRange" in self.symbol_root: + return self.symbol_root["selectionRange"]["start"]["character"] + else: + # precise location is expected to be undefined for some types of symbols (e.g. SymbolKind.File) + return None + + @property + def body(self) -> str | None: + return self.symbol_root.get("body") + + def get_name_path(self) -> str: + """ + Get the name path of the symbol (e.g. "class/method/inner_function"). + """ + return self._NAME_PATH_SEP.join(self.get_name_path_parts()) + + def get_name_path_parts(self) -> list[str]: + """ + Get the parts of the name path of the symbol (e.g. ["class", "method", "inner_function"]). + """ + ancestors_within_file = list(self.iter_ancestors(up_to_symbol_kind=SymbolKind.File)) + ancestors_within_file.reverse() + return [a.name for a in ancestors_within_file] + [self.name] + + def iter_children(self) -> Iterator[Self]: + for c in self.symbol_root["children"]: + yield self.__class__(c) + + def iter_ancestors(self, up_to_symbol_kind: SymbolKind | None = None) -> Iterator[Self]: + """ + Iterate over all ancestors of the symbol, starting with the parent and going up to the root or + the given symbol kind. + + :param up_to_symbol_kind: if provided, iteration will stop *before* the first ancestor of the given kind. + A typical use case is to pass `SymbolKind.File` or `SymbolKind.Package`. + """ + parent = self.get_parent() + if parent is not None: + if up_to_symbol_kind is None or parent.symbol_kind != up_to_symbol_kind: + yield parent + yield from parent.iter_ancestors(up_to_symbol_kind=up_to_symbol_kind) + + def get_parent(self) -> Self | None: + parent_root = self.symbol_root.get("parent") + if parent_root is None: + return None + return self.__class__(parent_root) + + def find( + self, + name_path: str, + substring_matching: bool = False, + include_kinds: Sequence[SymbolKind] | None = None, + exclude_kinds: Sequence[SymbolKind] | None = None, + ) -> list[Self]: + """ + Find all symbols within the symbol's subtree that match the given `name_path`. + The matching behavior is determined by the structure of `name_path`, which can + either be a simple name (e.g. "method") or a name path like "class/method" (relative name path) + or "/class/method" (absolute name path). + + Key aspects of the name path matching behavior: + - Trailing slashes in `name_path` play no role and are ignored. + - The name of the retrieved symbols will match (either exactly or as a substring) + the last segment of `name_path`, while other segments will restrict the search to symbols that + have a desired sequence of ancestors. + - If there is no starting or intermediate slash in `name_path`, there is no + restriction on the ancestor symbols. For example, passing `method` will match + against symbols with name paths like `method`, `class/method`, `class/nested_class/method`, etc. + - If `name_path` contains a `/` but doesn't start with a `/`, the matching is restricted to symbols + with the same ancestors as the last segment of `name_path`. For example, passing `class/method` will match against + `class/method` as well as `nested_class/class/method` but not `method`. + - If `name_path` starts with a `/`, it will be treated as an absolute name path pattern, meaning + that the first segment of it must match the first segment of the symbol's name path. + For example, passing `/class` will match only against top-level symbols like `class` but not against `nested_class/class`. + Passing `/class/method` will match against `class/method` but not `nested_class/class/method` or `method`. + + :param name_path: the name path to match against + :param substring_matching: whether to use substring matching (as opposed to exact matching) + of the last segment of `name_path` against the symbol name. + :param include_kinds: an optional sequence of ints representing the LSP symbol kind. + If provided, only symbols of the given kinds will be included in the result. + :param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result. + """ + result = [] + + def should_include(s: "Symbol") -> bool: + if include_kinds is not None and s.symbol_kind not in include_kinds: + return False + if exclude_kinds is not None and s.symbol_kind in exclude_kinds: + return False + return Symbol.match_name_path( + name_path=name_path, + symbol_name_path_parts=s.get_name_path_parts(), + substring_matching=substring_matching, + ) + + def traverse(s: "Symbol") -> None: + if should_include(s): + result.append(s) + for c in s.iter_children(): + traverse(c) + + traverse(self) + return result + + def to_dict( + self, kind: bool = False, location: bool = False, depth: int = 0, include_body: bool = False, include_children_body: bool = False + ) -> dict[str, Any]: + """ + Convert the symbol to a dictionary. + + :param kind: whether to include the kind of the symbol + :param location: whether to include the location of the symbol + :param depth: the depth of the symbol + :param include_body: whether to include the body of the top-level symbol. + :param include_children_body: whether to also include the body of the children. + Note that the body of the children is part of the body of the parent symbol, + so there is usually no need to set this to True unless you want process the output + and pass the children without passing the parent body to the LM. + :return: a dictionary representation of the symbol + """ + result: dict[str, Any] = {"name": self.name, "name_path": self.get_name_path()} + + if kind: + result["kind"] = self.kind + + if location: + result["location"] = self.location.to_dict() + + if include_body: + if self.body is None: + log.warning("Requested body for symbol, but it is not present. The symbol might have been loaded with include_body=False.") + result["body"] = self.body + + def add_children(s: Self) -> list[dict[str, Any]]: + children = [] + for c in s.iter_children(): + children.append( + c.to_dict( + kind=kind, + location=location, + depth=depth - 1, + include_body=include_children_body, + include_children_body=include_children_body, + ) + ) + return children + + if depth > 0: + result["children"] = add_children(self) + + return result + + +class SymbolManager: + def __init__(self, lang_server: SyncLanguageServer, agent: "SerenaAgent") -> None: + self.lang_server = lang_server + self.agent = agent + + def _to_symbols(self, items: list[UnifiedSymbolInformation]) -> list[Symbol]: + return [Symbol(s) for s in items] + + def find_by_name( + self, + name: str, + include_body: bool = False, + include_kinds: Sequence[SymbolKind] | None = None, + exclude_kinds: Sequence[SymbolKind] | None = None, + substring_matching: bool = False, + within_relative_path: str | None = None, + ) -> list[Symbol]: + """ + Find all symbols that match the given name. See docstring of `Symbol.find` for more details. + The only parameter not mentioned there is `within_relative_path`, which can be used to restrict the search + 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) + for root in symbol_roots: + symbols.extend( + Symbol(root).find(name, include_kinds=include_kinds, exclude_kinds=exclude_kinds, substring_matching=substring_matching) + ) + 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) + 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) + for symbol_dict in symbol_dicts: + symbol = Symbol(symbol_dict) + if symbol.location == location: + return symbol + return None + + def find_referencing_symbols( + self, + symbol_location: SymbolLocation, + include_body: bool = False, + include_kinds: Sequence[SymbolKind] | None = None, + exclude_kinds: Sequence[SymbolKind] | None = None, + ) -> list[Symbol]: + """ + Find all symbols that reference the symbol at the given location. + + :param symbol_location: the location of the symbol for which to find references + :param include_body: whether to include the body of all symbols in the result. + Note: you can filter out the bodies of the children if you set include_children_body=False + in the to_dict method. + :param include_kinds: an optional sequence of ints representing the LSP symbol kind. + If provided, only symbols of the given kinds will be included in the result. + :param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result. + Takes precedence over include_kinds. + :return: a list of symbols that reference the given symbol + """ + if not symbol_location.has_position_in_file(): + raise ValueError("Symbol location does not contain a valid position in a file") + assert symbol_location.relative_path is not None + assert symbol_location.line is not None + assert symbol_location.column is not None + symbol_dicts = self.lang_server.request_referencing_symbols( + relative_file_path=symbol_location.relative_path, + line=symbol_location.line, + column=symbol_location.column, + include_imports=False, + include_self=False, + include_body=include_body, + ) + + if include_kinds is not None: + symbol_dicts = [s for s in symbol_dicts if s["kind"] in include_kinds] + + if exclude_kinds is not None: + symbol_dicts = [s for s in symbol_dicts if s["kind"] not in exclude_kinds] + + return self._to_symbols(symbol_dicts) + + @contextmanager + def _edited_file(self, relative_path: str) -> Iterator[None]: + with self.lang_server.open_file(relative_path) as file_buffer: + yield + 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) + self.agent.mark_file_modified(relative_path) + + @contextmanager + def _edited_symbol_location(self, location: SymbolLocation) -> Iterator[Symbol]: + symbol = self.find_by_location(location) + if symbol is None: + raise ValueError("Symbol not found/has no defined location within a file") + assert location.relative_path is not None + with self._edited_file(location.relative_path): + yield symbol + + def replace_body(self, location: SymbolLocation, body: str) -> None: + """ + Replace the body of the symbol at the given location with the given body + + :param location: the location of the symbol to replace + :param body: the new body + """ + # make sure body always ends with at least one newline + if not body.endswith("\n"): + body += "\n" + with self._edited_symbol_location(location) as symbol: + assert location.relative_path is not None + start_pos = symbol.body_start_position + end_pos = symbol.body_end_position + if start_pos is None or end_pos is None: + raise ValueError(f"Symbol at {location} does not have a defined body range.") + # At this point, start_pos and end_pos are guaranteed to be Position objects + 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_pos["line"], start_pos["character"], body) + + def insert_after(self, location: SymbolLocation, body: str) -> None: + """ + Appends content after the given symbol + + :param location: the location of the symbol after which to add new lines + :param body: the body of the entity to append + """ + # make sure body always ends with at least one newline + if not body.endswith("\n"): + body += "\n" + with self._edited_symbol_location(location) as symbol: + pos = symbol.body_end_position + if pos is None: + raise ValueError(f"Symbol at {location} does not have a defined end position.") + # At this point, pos is guaranteed to be a Position object + assert location.relative_path is not None + self.lang_server.insert_text_at_position(location.relative_path, pos["line"], pos["character"], body) + + def insert_before(self, location: SymbolLocation, body: str) -> None: + """ + Inserts content before the given symbol + + :param location: the location of the symbol before which to add new lines + :param body: the body of the entity to insert + """ + # make sure body always ends with at least one newline + if not body.endswith("\n"): + body += "\n" + with self._edited_symbol_location(location) as symbol: + original_start_pos = symbol.body_start_position + if original_start_pos is None: + raise ValueError(f"Symbol at {location} does not have a defined start position.") + # At this point, original_start_pos is guaranteed to be a Position object, so copying is safe. + pos = copy(original_start_pos) + assert location.relative_path is not None + self.lang_server.insert_text_at_position(location.relative_path, pos["line"], pos["character"], body) + + def insert_at_line(self, relative_path: str, line: int, content: str) -> None: + """ + Inserts content at the given line in the given file. + + :param line: the 0-based index of the line to insert content at + :param content: the content to insert + """ + with self._edited_file(relative_path): + 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: + """ + Deletes lines in the given file. + + :param start_line: the 0-based index of the first line to delete (inclusive) + :param end_line: the 0-based index of the last line to delete (inclusive) + """ + with self._edited_file(relative_path): + start_pos = Position(line=start_line, character=0) + end_pos = Position(line=end_line + 1, character=0) + self.lang_server.delete_text_between_positions(relative_path, start_pos, end_pos) diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 9fb3783e..2b0616a2 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -89,7 +89,7 @@ class TestSerenaAgent: ), f"Expected to find reference to {symbol_name} in {ref_file} for {agent.project_config.language.name}. refs={refs}" @pytest.mark.parametrize( - "serena_agent,qualified_name,substring_matching,expected_symbol_name,expected_kind,expected_file", + "serena_agent,name_path,substring_matching,expected_symbol_name,expected_kind,expected_file", [ pytest.param( Language.PYTHON, @@ -131,13 +131,33 @@ class TestSerenaAgent: id="substring_qualname_method", marks=pytest.mark.python, ), + pytest.param( + Language.PYTHON, + "/OuterClass", # Absolute path + False, + "OuterClass", + "Class", + os.path.join("test_repo", "nested.py"), + id="absolute_qualname_class", + marks=pytest.mark.python, + ), + pytest.param( + Language.PYTHON, + "/OuterClass/NestedClass/find_m", # Absolute path with substring + True, + "find_me", + "Method", + os.path.join("test_repo", "nested.py"), + id="absolute_substring_qualname_method", + marks=pytest.mark.python, + ), ], indirect=["serena_agent"], ) - def test_find_symbol_qualified_name( + def test_find_symbol_name_path( self, serena_agent: SerenaAgent, - qualified_name: str, + name_path: str, substring_matching: bool, expected_symbol_name: str, expected_kind: str, @@ -146,7 +166,7 @@ class TestSerenaAgent: agent = serena_agent find_symbol_tool = agent.get_tool(FindSymbolTool) result = find_symbol_tool.apply( - name=qualified_name, + name_path=name_path, depth=0, within_relative_path=None, include_body=False, @@ -160,4 +180,39 @@ class TestSerenaAgent: and expected_kind.lower() in s["kind"].lower() and expected_file in s["location"]["relative_path"] for s in symbols - ), f"Expected to find {qualified_name} ({expected_kind}) in {expected_file} for {agent.project_config.language.name}. Symbols: {symbols}" + ), f"Expected to find {name_path} ({expected_kind}) in {expected_file} for {agent.project_config.language.name}. Symbols: {symbols}" + + @pytest.mark.parametrize( + "serena_agent,name_path", + [ + pytest.param( + Language.PYTHON, + "/NestedClass", # Absolute path, NestedClass is not top-level + id="absolute_path_non_top_level_no_match", + marks=pytest.mark.python, + ), + pytest.param( + Language.PYTHON, + "/NoSuchParent/NestedClass", # Absolute path with non-existent parent + id="absolute_path_non_existent_parent_no_match", + marks=pytest.mark.python, + ), + ], + indirect=["serena_agent"], + ) + def test_find_symbol_name_path_no_match( + self, + serena_agent: SerenaAgent, + name_path: str, + ): + agent = serena_agent + find_symbol_tool = agent.get_tool(FindSymbolTool) + result = find_symbol_tool.apply( + 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.project_config.language.name}. Symbols found: {symbols}" diff --git a/test/serena/test_symbol.py b/test/serena/test_symbol.py index fdd48f7e..7c21215f 100644 --- a/test/serena/test_symbol.py +++ b/test/serena/test_symbol.py @@ -6,128 +6,157 @@ from src.serena.symbol import Symbol class TestSymbolNameMatching: def _create_assertion_error_message( self, - name_pattern: str, - qual_name_parts: list[str], + name_path_pattern: str, + symbol_name_path_parts: list[str], is_substring_match: bool, expected_result: bool, actual_result: bool, ) -> str: """Helper to create a detailed error message for assertions.""" - qnp_repr = "/".join(qual_name_parts) + qnp_repr = "/".join(symbol_name_path_parts) return ( - f"Pattern '{name_pattern}' (substring: {is_substring_match}) vs " - f"Qualname parts {qual_name_parts} (as '{qnp_repr}'). " + f"Pattern '{name_path_pattern}' (substring: {is_substring_match}) vs " + f"Qualname parts {symbol_name_path_parts} (as '{qnp_repr}'). " f"Expected: {expected_result}, Got: {actual_result}" ) @pytest.mark.parametrize( - "name_pattern, qual_name_parts, is_substring_match, expected", + "name_path_pattern, symbol_name_path_parts, is_substring_match, expected", [ - # Exact matches (is_substring_match=False) - pytest.param("foo", ["foo"], False, True, id="'foo' matches 'foo' exactly"), - pytest.param("foo", ["bar", "foo"], False, True, id="'foo' matches 'bar,foo' exactly"), - pytest.param("foo", ["foobar"], False, False, id="'foo' does not match 'foobar' exactly"), - pytest.param("foo", ["bar", "foobar"], False, False, id="'foo' does not match 'bar,foobar' exactly"), - pytest.param("foo", ["path", "to", "foo"], False, True, id="'foo' matches 'path,to,foo' exactly"), - # Substring matches (is_substring_match=True) - pytest.param("foo", ["foobar"], True, True, id="'foo' matches 'foobar' as substring"), - pytest.param("foo", ["bar", "foobar"], True, True, id="'foo' matches 'bar,foobar' as substring"), - pytest.param("foo", ["barfoo"], True, True, id="'foo' matches 'barfoo' as substring"), - pytest.param("foo", ["baz"], True, False, id="'foo' does not match 'baz' as substring"), - pytest.param("foo", ["bar", "baz"], True, False, id="'foo' does not match 'bar,baz' as substring"), - pytest.param("foo", ["my_foobar_func"], True, True, id="'foo' matches 'my_foobar_func' as substring"), - pytest.param("foo", ["ClassA", "my_foobar_method"], True, True, id="'foo' matches 'ClassA,my_foobar_method' as substring"), - pytest.param("foo", ["my_bar_func"], True, False, id="'foo' does not match 'my_bar_func' as substring"), + # Exact matches, anywhere in the name (is_substring_match=False) + pytest.param("foo", ["foo"], False, True, id="'foo' matches 'foo' exactly (simple)"), + pytest.param("foo/", ["foo"], False, True, id="'foo/' matches 'foo' exactly (simple)"), + pytest.param("foo", ["bar", "foo"], False, True, id="'foo' matches ['bar', 'foo'] exactly (simple, last element)"), + pytest.param("foo", ["foobar"], False, False, id="'foo' does not match 'foobar' exactly (simple)"), + pytest.param( + "foo", ["bar", "foobar"], False, False, id="'foo' does not match ['bar', 'foobar'] exactly (simple, last element)" + ), + pytest.param( + "foo", ["path", "to", "foo"], False, True, id="'foo' matches ['path', 'to', 'foo'] exactly (simple, last element)" + ), + # Exact matches, absolute patterns (is_substring_match=False) + pytest.param("/foo", ["foo"], False, True, id="'/foo' matches ['foo'] exactly (absolute simple)"), + pytest.param("/foo", ["foo", "bar"], False, False, id="'/foo' does not match ['foo', 'bar'] (absolute simple, len mismatch)"), + pytest.param("/foo", ["bar"], False, False, id="'/foo' does not match ['bar'] (absolute simple, name mismatch)"), + pytest.param( + "/foo", ["bar", "foo"], False, False, id="'/foo' does not match ['bar', 'foo'] (absolute simple, position mismatch)" + ), + # Substring matches, anywhere in the name (is_substring_match=True) + pytest.param("foo", ["foobar"], True, True, id="'foo' matches 'foobar' as substring (simple)"), + pytest.param("foo", ["bar", "foobar"], True, True, id="'foo' matches ['bar', 'foobar'] as substring (simple, last element)"), + pytest.param( + "foo", ["barfoo"], True, True, id="'foo' matches 'barfoo' as substring (simple)" + ), # This was potentially ambiguous before + pytest.param("foo", ["baz"], True, False, id="'foo' does not match 'baz' as substring (simple)"), + pytest.param("foo", ["bar", "baz"], True, False, id="'foo' does not match ['bar', 'baz'] as substring (simple, last element)"), + pytest.param("foo", ["my_foobar_func"], True, True, id="'foo' matches 'my_foobar_func' as substring (simple)"), + pytest.param( + "foo", + ["ClassA", "my_foobar_method"], + True, + True, + id="'foo' matches ['ClassA', 'my_foobar_method'] as substring (simple, last element)", + ), + pytest.param("foo", ["my_bar_func"], True, False, id="'foo' does not match 'my_bar_func' as substring (simple)"), + # Substring matches, absolute patterns (is_substring_match=True) + pytest.param("/foo", ["foobar"], True, True, id="'/foo' matches ['foobar'] as substring (absolute simple)"), + pytest.param("/foo/", ["foobar"], True, True, id="'/foo/' matches ['foobar'] as substring (absolute simple, last element)"), + pytest.param("/foo", ["barfoobaz"], True, True, id="'/foo' matches ['barfoobaz'] as substring (absolute simple)"), + pytest.param( + "/foo", ["foo", "bar"], True, False, id="'/foo' does not match ['foo', 'bar'] as substring (absolute simple, len mismatch)" + ), + pytest.param("/foo", ["bar"], True, False, id="'/foo' does not match ['bar'] (absolute simple, no substr)"), + pytest.param( + "/foo", ["bar", "foo"], True, False, id="'/foo' does not match ['bar', 'foo'] (absolute simple, position mismatch)" + ), + pytest.param( + "/foo/", ["bar", "foo"], True, False, id="'/foo/' does not match ['bar', 'foo'] (absolute simple, position mismatch)" + ), ], ) - def test_match_simple_name(self, name_pattern, qual_name_parts, is_substring_match, expected): + def test_match_simple_name(self, name_path_pattern, symbol_name_path_parts, is_substring_match, expected): """Tests matching for simple names (no '/' in pattern).""" - result = Symbol.match_against_qualname(name_pattern, qual_name_parts, is_substring_match) - error_msg = self._create_assertion_error_message(name_pattern, qual_name_parts, is_substring_match, expected, result) + result = Symbol.match_name_path(name_path_pattern, symbol_name_path_parts, is_substring_match) + error_msg = self._create_assertion_error_message(name_path_pattern, symbol_name_path_parts, is_substring_match, expected, result) assert result == expected, error_msg @pytest.mark.parametrize( - "name_pattern, qual_name_parts, is_substring_match, expected", + "name_path_pattern, symbol_name_path_parts, is_substring_match, expected", [ - # Exact matches (is_substring_match=False) - pytest.param("bar/foo", ["bar", "foo"], False, True, id="'bar/foo' matches 'bar,foo' exactly"), + # --- Relative patterns (suffix matching) --- + # Exact matches, relative patterns (is_substring_match=False) + pytest.param("bar/foo", ["bar", "foo"], False, True, id="R: 'bar/foo' matches ['bar', 'foo'] exactly"), + pytest.param("bar/foo", ["mod", "bar", "foo"], False, True, id="R: 'bar/foo' matches ['mod', 'bar', 'foo'] exactly (suffix)"), pytest.param( - "bar/foo", ["bar", "foo", "baz"], False, False, id="'bar/foo' does not match 'bar,foo,baz' exactly (len mismatch)" + "bar/foo", ["bar", "foo", "baz"], False, False, id="R: 'bar/foo' does not match ['bar', 'foo', 'baz'] (pattern shorter)" ), - pytest.param("bar/foo", ["bar"], False, False, id="'bar/foo' does not match 'bar' exactly (len mismatch)"), - pytest.param("bar/foo", ["baz", "foo"], False, False, id="'bar/foo' does not match 'baz,foo' exactly (first mismatch)"), - pytest.param("bar/foo", ["bar", "baz"], False, False, id="'bar/foo' does not match 'bar,baz' exactly (last mismatch)"), - # from docstring examples - pytest.param("bar/foo", ["foo"], False, False, id="'bar/foo' does not match 'foo' exactly"), - pytest.param("bar/foo", ["other", "foo"], False, False, id="'bar/foo' does not match 'other,foo' exactly"), - pytest.param("bar/foo", ["bar", "otherfoo"], False, False, id="'bar/foo' does not match 'bar,otherfoo' exactly"), - # Substring matches (is_substring_match=True) - pytest.param("bar/foo", ["bar", "foobar"], True, True, id="'bar/foo' matches 'bar,foobar' as substring"), - pytest.param("bar/foo", ["bar", "bazfoo"], True, True, id="'bar/foo' matches 'bar,bazfoo' as substring"), - pytest.param("bar/fo", ["bar", "foo"], True, True, id="'bar/fo' matches 'bar,foo' as substring"), - pytest.param("bar/foo", ["bar", "baz"], True, False, id="'bar/foo' does not match 'bar,baz' as substring (last no substr)"), + pytest.param("bar/foo", ["bar"], False, False, id="R: 'bar/foo' does not match ['bar'] (pattern longer)"), + pytest.param("bar/foo", ["baz", "foo"], False, False, id="R: 'bar/foo' does not match ['baz', 'foo'] (first part mismatch)"), + pytest.param("bar/foo", ["bar", "baz"], False, False, id="R: 'bar/foo' does not match ['bar', 'baz'] (last part mismatch)"), + pytest.param("bar/foo", ["foo"], False, False, id="R: 'bar/foo' does not match ['foo'] (pattern longer)"), pytest.param( - "bar/foo", ["baz", "foobar"], True, False, id="'bar/foo' does not match 'baz,foobar' as substring (first mismatch)" - ), - # from docstring examples - pytest.param("bar/foo", ["bar", "my_foobar_method"], True, True, id="'bar/foo' matches 'bar,my_foobar_method' as substring"), - pytest.param( - "bar/foo", ["bar", "another_method"], True, False, id="'bar/foo' does not match 'bar,another_method' as substring" + "bar/foo", ["other", "foo"], False, False, id="R: 'bar/foo' does not match ['other', 'foo'] (first part mismatch)" ), pytest.param( - "bar/foo", ["other", "my_foobar_method"], True, False, id="'bar/foo' does not match 'other,my_foobar_method' as substring" + "bar/foo", ["bar", "otherfoo"], False, False, id="R: 'bar/foo' does not match ['bar', 'otherfoo'] (last part mismatch)" ), - pytest.param("bar/f", ["bar", "foo"], True, True, id="'bar/f' matches 'bar,foo' as substring"), - ], - ) - def test_match_name_pattern_ends_without_slash(self, name_pattern, qual_name_parts, is_substring_match, expected): - """Tests matching for qualified names (e.g. 'module/class/func').""" - result = Symbol.match_against_qualname(name_pattern, qual_name_parts, is_substring_match) - error_msg = self._create_assertion_error_message(name_pattern, qual_name_parts, is_substring_match, expected, result) - assert result == expected, error_msg - - @pytest.mark.parametrize( - "name_pattern, qual_name_parts, is_substring_match, expected", - [ - # Exact matches (is_substring_match=False) - pytest.param("foo/", ["foo"], False, True, id="'foo/' matches 'foo' exactly"), - pytest.param("bar/foo/", ["bar", "foo"], False, True, id="'bar/foo/' matches 'bar,foo' exactly"), - pytest.param("foo/", ["foobar"], False, False, id="'foo/' does not match 'foobar' exactly"), - pytest.param("foo/", ["bar", "foo"], False, False, id="'foo/' does not match 'bar,foo' exactly (not toplevel)"), - # Substring matches (is_substring_match=True) - pytest.param("foo/", ["foobar"], True, True, id="'foo/' matches 'foobar' as substring"), + # Substring matches, relative patterns (is_substring_match=True) + pytest.param("bar/foo", ["bar", "foobar"], True, True, id="R: 'bar/foo' matches ['bar', 'foobar'] as substring"), pytest.param( - "bar/foo/", ["bar", "the_foobar_method"], True, True, id="'bar/foo/' matches 'bar,the_foobar_method' as substring" + "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/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)" ), pytest.param( - "bar/foo/", + "bar/foo", ["bar", "my_foobar_method"], True, True, id="R: 'bar/foo' matches ['bar', 'my_foobar_method'] as substring" + ), + pytest.param( + "bar/foo", + ["mod", "bar", "my_foobar_method"], + True, + True, + id="R: 'bar/foo' matches ['mod', 'bar', 'my_foobar_method'] as substring (suffix)", + ), + pytest.param( + "bar/foo", ["bar", "another_method"], True, False, - id="'bar/foo/' does not match 'bar,another_method' as substring (last no substr)", + id="R: 'bar/foo' does not match ['bar', 'another_method'] (last no substr)", ), pytest.param( - "bar/foo/", - ["baz", "the_foobar_method"], + "bar/foo", + ["other", "my_foobar_method"], True, False, - id="'bar/foo/' does not match 'baz,the_foobar_method' as substring (first mismatch)", + id="R: 'bar/foo' does not match ['other', 'my_foobar_method'] (first part mismatch)", ), - # from docstring examples - pytest.param("foo/", ["my_foobar_func"], True, True, id="'foo/' matches 'my_foobar_func' as substring"), + pytest.param("bar/f", ["bar", "foo"], True, True, id="R: 'bar/f' matches ['bar', 'foo'] as substring"), + # Exact matches, absolute patterns (is_substring_match=False) + pytest.param("/bar/foo", ["bar", "foo"], False, True, id="A: '/bar/foo' matches ['bar', 'foo'] exactly"), pytest.param( - "foo/", - ["ClassA", "my_foobar_method"], - True, - False, - id="'foo/' does not match 'ClassA,my_foobar_method' as substring (not toplevel)", + "/bar/foo", ["bar", "foo", "baz"], False, False, id="A: '/bar/foo' does not match ['bar', 'foo', 'baz'] (pattern shorter)" + ), + pytest.param("/bar/foo", ["bar"], False, False, id="A: '/bar/foo' does not match ['bar'] (pattern longer)"), + pytest.param("/bar/foo", ["baz", "foo"], False, False, id="A: '/bar/foo' does not match ['baz', 'foo'] (first part mismatch)"), + pytest.param("/bar/foo", ["bar", "baz"], False, False, id="A: '/bar/foo' does not match ['bar', 'baz'] (last part mismatch)"), + # 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/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)" ), - pytest.param("foo/", ["foo"], True, True, id="'foo/' matches 'foo' as substring (exact is substr)"), ], ) - def test_match_name_pattern_ending_in_slash(self, name_pattern, qual_name_parts, is_substring_match, expected): - """Tests matching for patterns ending with '/' (prefix/namespace style).""" - result = Symbol.match_against_qualname(name_pattern, qual_name_parts, is_substring_match) - error_msg = self._create_assertion_error_message(name_pattern, qual_name_parts, is_substring_match, expected, result) + def test_match_name_path_pattern_path_len_2(self, name_path_pattern, symbol_name_path_parts, is_substring_match, expected): + """Tests matching for qualified names (e.g. 'module/class/func').""" + result = Symbol.match_name_path(name_path_pattern, symbol_name_path_parts, is_substring_match) + error_msg = self._create_assertion_error_message(name_path_pattern, symbol_name_path_parts, is_substring_match, expected, result) assert result == expected, error_msg From 8ed8fce62088e8c423487466cee6be4e2fc8740c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 21 May 2025 19:17:58 +0200 Subject: [PATCH 61/61] Bumped version [ci skip] --- CHANGELOG.md | 16 ++++++++++++++-- README.md | 5 ++--- src/serena/__init__.py | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d40d6d57..1529a84b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,19 @@ -# 2025-05-19 - +# Latest Changes prior to the next official version change will appear here. +# 2025-05-21 + +**Signficant improvement in symbol finding!** + +* Serena core: + * `FindSymbolTool` now can look for symbols by specifying paths to them, not just the symbol name +* Language Servers: + * Fixed `gopls` initialization + * Symbols retrieved through the symbol tree or through overview methods now are linked to their parents + + +# 2025-05-19 + * Serena core: * Bugfix in `FindSymbolTool` (a bug fixed in LS) * Fix in `ListDirTool`: Do not ignore files with extensions not understood by the language server, only skip ignored directories diff --git a/README.md b/README.md index 69958126..56d319dc 100644 --- a/README.md +++ b/README.md @@ -48,16 +48,15 @@ Language servers provide support for a wide range of programming languages. With Serena, we provide * direct, out-of-the-box support for: * Python - * Java (_Note_: startup is slow, initial startup especially so) - * TypeScript + * TypeScript/Javascript * PhP * Go (need to install go and gopls first) * Rust * C/C++ + * Java (_Note_: startup is slow, initial startup especially so. There seem to be issues with java on macos) * indirect support (may require some code changes/manual installation) for: * Ruby (untested) * C# (untested) - * Rust (untested) * Kotlin (untested) * Dart (untested) diff --git a/src/serena/__init__.py b/src/serena/__init__.py index d37e1f6c..3e1a38d7 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2025-05-19" +__version__ = "2025-05-21" def serena_root_path() -> str: