diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index e757f030..1f20f585 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -566,7 +566,7 @@ jobs: df -h - name: Test with pytest shell: bash - run: uv run poe test + run: uv run poe test -q --tb=short - name: Type-checking with mypy shell: bash run: uv run poe type-check diff --git a/docs/02-usage/060_dashboard.md b/docs/02-usage/060_dashboard.md index 9bb6406e..3e4e3a5a 100644 --- a/docs/02-usage/060_dashboard.md +++ b/docs/02-usage/060_dashboard.md @@ -2,38 +2,80 @@ Serena comes with built-in tools for monitoring and managing the current session: -* the **web-based dashboard** (enabled by default) +* the **Serena Dashboard** (enabled by default) The dashboard provides detailed information on your Serena session, the current configuration and provides access to logs. Some settings (e.g. the current set of active programming languages) can also be directly modified through the dashboard. The dashboard is supported on all platforms. - - By default, it will be accessible at `http://localhost:24282/dashboard/index.html`, - but a higher port may be used if the default port is unavailable/multiple instances are running. - **We recommend always enabling the dashboard**. If you don't want the browser to open automatically, - you can disable it while still keeping the dashboard running in the background (see below). - -* the **GUI tool** (disabled by default) +* the **GUI Log Viewer** (disabled by default) - The GUI tool is a native application window which displays logs. - It furthermore allows you to shut down the agent and to access the dashboard's URL (if it is running). + The GUI tool is a legacy native application which displays Serena's live logs. This is mainly supported on Windows, but it may also work on Linux; macOS is unsupported. Both can be configured in Serena's [configuration](050_configuration) file (`serena_config.yml`). -If enabled, they will automatically be opened as soon as the Serena agent/MCP server is started. -For the dashboard, this can be disabled if desired (see below). -## Disabling Automatic Browser Opening +## The Serena Dashboard -If you prefer not to have the dashboard open automatically (e.g., to avoid focus stealing), you can disable it -by setting `web_dashboard_open_on_launch: False` in your `serena_config.yml` or by passing `--open-web-dashboard False` -to `start-mcp-server` CLI command. +The dashboard is a web application, which is opened via a native application wrapper on Windows and macOS, +where it will appear in the system tray. **Click on the tray icon** to open the application window. -When automatic opening is disabled, you can still access the dashboard by: +On Linux, native app support is difficult to provide, so the dashboard will be opened in your default web browser instead. + +By default, the dashboard can be accessed at `http://localhost:24282/dashboard/index.html`, +but a higher port may be used if the default port is unavailable/multiple instances are running. + +### Features + +The dashboard provides ... + * a detailed overview of + * the current Serena status and configuration (e.g. active tools, active programming languages, enabled modes and contexts, etc.) + * ongoing and past tool calls (and statistics on tool usage) + * access to Serena's live logs + * the ability to modify settings (e.g. the set of active programming languages) on the fly + * the ability to edit + * configuration files (global `serena_config.yml` and project-specific `project.yml`) + * memories of the current project + * the ability to shut down the Serena MCP server. + +### Configuring the Dashboard + +You can configure settings for the dashboard in the [global configuration file](global-config). + +In particular, you can configure + * whether the dashboard is enabled at all. + We highly recommend keeping it enabled, as it provides valuable insights into Serena's operation and allows you to adjust configuration conveniently. + * whether the dashboard window is opened automatically when Serena is started (see below). + * network settings (for cases where you might want to access the Dashboard non-locally). + +#### Dashboard Opening Behaviour + +When Serena is started, the Dashboard window is opened by default in order to make users aware of its existence. + +If you prefer not to have this happen (e.g., to avoid focus stealing), you can disable prevent it from opening +by setting `web_dashboard_open_on_launch: False` in the [global configuration file](global-config) or by passing `--open-web-dashboard False` +to the `start-mcp-server` CLI command. + +On Windows and macOS, you can conveniently open the dashboard at any time by clicking on the tray icon, so automatic +opening is not a requirement to be able to access the dashboard on these platforms. + +On Linux, access to the dashboard is more cumbersome without native app support. +Yet you may still access it by * asking the LLM to "open the Serena dashboard", which will open the dashboard in your default browser (the tool `open_dashboard` is enabled for this purpose, provided that the dashboard is active, not opened by default and the GUI tool, which can provide the URL, is not enabled) -* navigating directly to the URL (see above) +* navigating directly to the URL (see above). + +## The GUI Log Viewer + +The Serena GUI Log Viewer is a legacy application which provides access to Serena's live logs. + +Via its menu, it furthermore allows you to + * shut down the agent + * access the dashboard's URL (if it is running). + +The tool is mainly supported on Windows, but it may work on some Linux systems as well (depending on your desktop environment). + +To enable the tool, set `gui_log_window` to `true` in Serena's [global configuration file](global-config). \ No newline at end of file diff --git a/flake.nix b/flake.nix index e8d9ccfa..e172a186 100644 --- a/flake.nix +++ b/flake.nix @@ -57,6 +57,17 @@ final.setuptools ]; }); + proxy-tools = prev.proxy-tools.overrideAttrs (old: { + nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ + final.setuptools + ]; + }); + pywebview = prev.pywebview.overrideAttrs (old: { + nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ + final.setuptools + final.setuptools-scm + ]; + }); }; python = pkgs.python311; diff --git a/pyproject.toml b/pyproject.toml index 683bd9ad..3d693eb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,8 @@ dependencies = [ "tiktoken==0.12.0", "anthropic==0.59.0", "beautifulsoup4==4.14.2", + "pywebview", + "pystray==0.19.5", # Transitive deps pinned for security (dependabot alerts). # Exact pins because uvx installs from git, ignoring the lock file. "urllib3==2.6.3", @@ -45,8 +47,12 @@ dependencies = [ "filelock==3.25.2", "cryptography==46.0.6", "regex==2026.2.28", + "pythonnet==3.1.0-rc0 ; sys_platform == 'win32'", ] +[tool.uv.sources] +pywebview = { git = "https://github.com/r0x0r/pywebview", rev = "345596c1f4fd59fd26c7aa02d4176c5362bcbabe" } + [[tool.uv.index]] name = "testpypi" url = "https://test.pypi.org/simple/" @@ -88,7 +94,7 @@ dev = [ "nbconvert==7.17.0", "wheel==0.46.3", "pyasn1==0.6.3", - "PyJWT==2.10.1", + "PyJWT==2.12.0", ] agno = ["agno==2.5.10", "sqlalchemy==2.0.41"] # agno bumped for session state overwrite CVE google = ["google-genai==1.27.0"] diff --git a/resources/serena-mac-dock-icon.cdr b/resources/serena-mac-dock-icon.cdr new file mode 100644 index 00000000..3f3f164c Binary files /dev/null and b/resources/serena-mac-dock-icon.cdr differ diff --git a/resources/serena-mac-tray-icon.cdr b/resources/serena-mac-tray-icon.cdr new file mode 100644 index 00000000..98a26a13 Binary files /dev/null and b/resources/serena-mac-tray-icon.cdr differ diff --git a/src/serena/agent.py b/src/serena/agent.py index 7775ca73..91431192 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -2,6 +2,7 @@ The Serena Model Context Protocol (MCP) Server """ +import multiprocessing import os import platform import subprocess @@ -11,6 +12,7 @@ from contextlib import contextmanager from logging import Logger from typing import TYPE_CHECKING, Optional, TypeVar +import webview from sensai.util import logging from sensai.util.logging import LogTime from sensai.util.string import dict_string @@ -28,7 +30,7 @@ from serena.config.serena_config import ( SerenaPaths, ToolInclusionDefinition, ) -from serena.dashboard import SerenaDashboardAPI +from serena.dashboard import SerenaDashboardAPI, SerenaDashboardViewer from serena.ls_manager import LanguageServerManager from serena.project import MemoriesManager, Project from serena.prompt_factory import SerenaPromptFactory @@ -375,8 +377,7 @@ class SerenaAgent: dashboard_url = f"http://{dashboard_host}:{port}/dashboard/index.html" self._dashboard_url = dashboard_url log.info("Serena web dashboard started at %s", dashboard_url) - if self.serena_config.web_dashboard_open_on_launch: - self.open_dashboard() + self._start_dashboard_viewer(minimized=not self.serena_config.web_dashboard_open_on_launch) # inform the GUI window (if any) if self._gui_log_viewer is not None: self._gui_log_viewer.set_dashboard_url(dashboard_url) @@ -519,11 +520,46 @@ class SerenaAgent: """ return self._dashboard_url + @staticmethod + def _start_dashboard_viewer_process_function(url: str, minimized: bool) -> None: + """ + Main function of the subprocess for starting the dashboard viewer + """ + try: + SerenaDashboardViewer(url, start_minimized=minimized).run() + except webview.errors.WebViewException as e: + log.warning(f"Could not open Serena Dashboard viewer. Cause:\n{e}") + # Fall back to opening the browser window if the window was supposed to be shown directly + if not minimized: + SerenaAgent._open_dashboard_in_browser(url) + + def _start_dashboard_viewer(self, minimized: bool) -> None: + """ + Starts the dashboard viewer (in a separate process) or, if the current platform does not support it, + opens the dashboard in the default web browser. + + :param minimized: whether the dashboard viewer should be started minimized (if supported on the current platform). + If the viewer is not supported on the current platform, then this controls whether to open the browser window. + """ + if not system_has_usable_display(): + log.info("Not starting the Serena dashboard viewer because no usable display was detected.") + return + + url = self.get_dashboard_url() + assert url is not None + if SerenaDashboardViewer.is_current_platform_supported(): + process = multiprocessing.Process(target=self._start_dashboard_viewer_process_function, args=(url, minimized), daemon=True) + process.start() + else: + log.info("Not starting Serena dashboard viewer because the current platform does not support it; using browser-based fallback") + if not minimized: + self._open_dashboard_in_browser(url) + def open_dashboard(self) -> bool: """ - Opens the Serena web dashboard in the default web browser. + Opens the Serena dashboard (for on-demand usage as triggered by the user, e.g. via a tool) - :return: a message indicating success or failure + :return: True if the dashboard was opened, False if it could not be opened """ if self._dashboard_url is None: raise Exception("Dashboard is not running.") @@ -532,15 +568,19 @@ class SerenaAgent: log.warning("Not opening the Serena web dashboard because no usable display was detected.") return False + self._open_dashboard_in_browser(self._dashboard_url) + return True + + @staticmethod + def _open_dashboard_in_browser(url: str) -> None: # Use a subprocess to avoid any output from webbrowser.open being written to stdout subprocess.Popen( - [sys.executable, "-c", f"import webbrowser; webbrowser.open({self._dashboard_url!r})"], + [sys.executable, "-c", f"import webbrowser; webbrowser.open({url!r})"], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, # Detach from parent process ) - return True def get_exposed_tool_instances(self) -> list["Tool"]: """ diff --git a/src/serena/cli.py b/src/serena/cli.py index ab84994d..7ad3fb2d 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -406,6 +406,22 @@ class TopLevelCommands(AutoRegisteringGroup): run_kwargs["port"] = port server.run(**run_kwargs) + @staticmethod + @click.command( + "dashboard-viewer", + help="Open the Serena dashboard viewer for a given URL.", + context_settings={"max_content_width": _MAX_CONTENT_WIDTH}, + ) + @click.argument("url", type=str) + @click.option("--width", type=int, default=1400, show_default=True, help="Window width.") + @click.option("--height", type=int, default=900, show_default=True, help="Window height.") + @click.option("--minimized", is_flag=True, default=False, help="Whether to start minimized/in tray.") + def dashboard_viewer(url: str, width: int, height: int, minimized: bool) -> None: + from serena.dashboard import SerenaDashboardViewer + + viewer = SerenaDashboardViewer(url, start_minimized=minimized, width=width, height=height) + viewer.run() + class ModeCommands(AutoRegisteringGroup): """Group for 'mode' subcommands.""" diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 844d7ebf..9a8f3f50 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -1,11 +1,14 @@ import os import socket +import sys import threading from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING, Any, Self +import webview from flask import Flask, Response, redirect, request, send_from_directory +from PIL import Image from pydantic import BaseModel from sensai.util import logging @@ -676,3 +679,199 @@ class SerenaDashboardAPI: thread = threading.Thread(target=lambda: self.run(host=host, port=port), daemon=True) thread.start() return thread, port + + +class SerenaDashboardViewer: + """ + Minimal pywebview wrapper with optional system tray. + """ + + def __init__( + self, + url: str, + *, + start_minimized: bool = False, + width: int = 1400, + height: int = 900, + ): + self.url = url + # Use system tray to allow hiding to tray and intercepting close to hide instead of quit + self.tray = True + self.title = "Serena Dashboard" + self.width = width + self.height = height + self.start_minimized = start_minimized + + self.window: webview.Window + self._tray_icon: Any + self._quitting = False + self._app_icon_path: str | None = None + + @staticmethod + def is_current_platform_supported() -> bool: + """ + :return: whether the current platform supports the dashboard viewer + """ + # The dashboard viewer (with system tray) is technically supported only on Windows and macOS. + # Linux support is problematic; see https://github.com/oraios/serena/pull/1117#issuecomment-4128753943 + supported_platforms = [ + "win32", + # NOTE: Disabling macOS support for now, because the tray behaviour is suboptimal (too many icons when + # subagents are spawned, etc.) + # "darwin" + ] + return sys.platform in supported_platforms + + def run(self) -> None: + # set app id (avoid app being lumped together with other Python-based apps in Windows taskbar) + if sys.platform == "win32": + import ctypes + + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("oraios.serena") + + dashboard_path = Path(SERENA_DASHBOARD_DIR) + # .ico is Windows-only; macOS expects a PNG for the window/dock icon. + icon_filename = "serena.ico" if sys.platform == "win32" else "serena-icon-1024-mac.png" + icon_path = str(dashboard_path / icon_filename) + self._app_icon_path = icon_path + + # Create hidden to avoid flash; show/restore/minimize in start callback. + window = webview.create_window( + self.title, + self.url, + width=self.width, + height=self.height, + hidden=self.start_minimized, + ) + assert window is not None + self.window = window + + if self.tray: + self.window.events.closing += self._on_closing + self._start_tray() + + def _start_callback() -> None: + if self.start_minimized: + self._hide_window() + else: + self._show_window() + + webview.start(_start_callback, icon=icon_path) + + def _show_window(self) -> None: + if not self.window: + return + + if sys.platform == "darwin": + from PyObjCTools.AppHelper import callAfter + + callAfter(self._show_window_on_macos) + else: + self.window.show() + self.window.restore() + + def _hide_window(self) -> None: + if not self.window: + return + + if sys.platform == "darwin": + from PyObjCTools.AppHelper import callAfter + + callAfter(self._hide_window_on_macos) + else: + self.window.hide() + + def _show_window_on_macos(self) -> None: + from AppKit import ( + NSApplication, + NSApplicationActivationPolicyRegular, + ) + from PyObjCTools.AppHelper import callLater + + ns_app = NSApplication.sharedApplication() + ns_app.setActivationPolicy_(NSApplicationActivationPolicyRegular) + self._set_macos_app_icon(ns_app) + ns_app.unhide_(None) + ns_app.activateIgnoringOtherApps_(True) + # Give the status item menu a beat to close before restoring the window. + callLater(0.1, self._restore_window_on_macos) + + def _restore_window_on_macos(self) -> None: + self.window.show() + self.window.restore() + + def _hide_window_on_macos(self) -> None: + from AppKit import ( + NSApplication, + NSApplicationActivationPolicyAccessory, + ) + + self.window.hide() + NSApplication.sharedApplication().setActivationPolicy_(NSApplicationActivationPolicyAccessory) + + def _set_macos_app_icon(self, ns_app: Any) -> None: + if not self._app_icon_path: + return + + from AppKit import NSImage + + ns_image = NSImage.alloc().initByReferencingFile_(self._app_icon_path) + if ns_image is not None: + ns_app.setApplicationIconImage_(ns_image) + + def _on_closing(self) -> bool: + """Intercept window close: hide window instead of quitting (macOS standard behavior).""" + if self._quitting: + return True + self._hide_window() + return False # prevent the window from actually closing + + def _start_tray(self) -> None: + # import pystray locally, because the import fails when there is no display! + import pystray + from pystray import MenuItem as Item + from pystray._base import Icon as TrayIcon + + dashboard_path = Path(SERENA_DASHBOARD_DIR) + + # macOS menu bar icons are displayed at 16pt; 32px covers Retina (@2x). + # Windows/Linux tray icons are larger, so 48px is the better fit there. + icon_filename = "serena-icon-tray-mac.png" if sys.platform == "darwin" else "serena-icon-48.png" + icon_img = Image.open(dashboard_path / icon_filename) + + def show(_icon: TrayIcon, _item: Item) -> None: + self._show_window() + + def hide(_icon: TrayIcon, _item: Item) -> None: + self._hide_window() + + def quit_app(_icon: TrayIcon, _item: Item) -> None: + self._quitting = True + try: + _icon.stop() + finally: + if self.window: + self.window.destroy() + + menu = pystray.Menu( + Item("Open", show, default=True), + Item("Hide", hide), + Item("Quit", quit_app), + ) + + kwargs: dict[str, Any] = {} + if sys.platform == "darwin": + # Passing darwin_nsapplication integrates pystray with the NSApplication + # run loop that webview.start() is about to enter. sharedApplication() + # is idempotent; pywebview will reuse the same singleton. + from AppKit import NSApplication + + kwargs["darwin_nsapplication"] = NSApplication.sharedApplication() + + self._tray_icon = pystray.Icon("dashboard_viewer", icon_img, self.title, menu, **kwargs) + + # On Windows/Linux, run_detached spawns pystray's own internal thread and + # returns immediately. On macOS it hooks into the NSApplication run loop + # that webview.start() is about to enter (run_detached is always called + # before webview.start() on macOS — see run()). + self._tray_icon.run_detached() diff --git a/src/serena/resources/dashboard/serena-icon-1024-mac.png b/src/serena/resources/dashboard/serena-icon-1024-mac.png new file mode 100644 index 00000000..2ad41dae Binary files /dev/null and b/src/serena/resources/dashboard/serena-icon-1024-mac.png differ diff --git a/src/serena/resources/dashboard/serena-icon-48.png b/src/serena/resources/dashboard/serena-icon-48.png index acb6e08c..6aadd7b6 100644 Binary files a/src/serena/resources/dashboard/serena-icon-48.png and b/src/serena/resources/dashboard/serena-icon-48.png differ diff --git a/src/serena/resources/dashboard/serena-icon-64.png b/src/serena/resources/dashboard/serena-icon-64.png new file mode 100644 index 00000000..28dc7d04 Binary files /dev/null and b/src/serena/resources/dashboard/serena-icon-64.png differ diff --git a/src/serena/resources/dashboard/serena-icon-tray-mac.png b/src/serena/resources/dashboard/serena-icon-tray-mac.png new file mode 100644 index 00000000..8e807057 Binary files /dev/null and b/src/serena/resources/dashboard/serena-icon-tray-mac.png differ diff --git a/src/serena/resources/dashboard/serena.ico b/src/serena/resources/dashboard/serena.ico new file mode 100644 index 00000000..e6ec48a1 Binary files /dev/null and b/src/serena/resources/dashboard/serena.ico differ diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index 557f43e6..a823c743 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -29,28 +29,26 @@ line_ending: native # we have to live with this limitation for now. gui_log_window: False -# whether to start the Serena web dashboard, which provides access to Serena's configuration -# and state as well as the current session logs. -# The web dashboard is supported on all platforms. -# We strongly recommend to always enable this option, since the dashboard provides important information -# about the current state of Serena. -# If you don't want the browser window to pop up automatically, set `web_dashboard_open_on_launch` to False -# (see option below). +# whether to start the Serena Dashboard, which provides detailed information on your Serena session, +# the current configuration and furthermore allows some settings to be conveniently modified on the fly. +# We strongly recommend to always enable this option! +# If you want to prevent the Dashboard window from being opened on launch, +# set `web_dashboard_open_on_launch` to false (see below). # Further information: https://oraios.github.io/serena/02-usage/060_dashboard.html web_dashboard: True -# the address the web dashboard will listen on (bind address). -web_dashboard_listen_address: 127.0.0.1 - -# whether to open a browser window with the web dashboard when Serena starts (provided that web_dashboard -# is enabled). See also the web_dashboard option. -# If set to false, you can still open the dashboard manually by +# whether to open the Dashboard window/browser tab when Serena starts (provided that web_dashboard is enabled). +# If set to false, you can still open the dashboard manually by clicking on the Serena icon in your system +# tray on Windows and macOS. On Linux, there is no system tray support, so you can only open the dashboard by # a) telling the LLM to "open the dashboard" (provided that the open_dashboard tool is enabled) or by # b) manually navigating to http://localhost:24282/dashboard/ in your web browser (actual port # may be higher if you have multiple instances running; try ports 24283, 24284, etc.) # See also: https://oraios.github.io/serena/02-usage/060_dashboard.html web_dashboard_open_on_launch: True +# the address the web dashboard will listen on (bind address). +web_dashboard_listen_address: 127.0.0.1 + # address where JetBrains plugin servers are running (only relevant when using the JetBrains language backend) jetbrains_plugin_server_address: 127.0.0.1 diff --git a/src/solidlsp/language_servers/al_language_server.py b/src/solidlsp/language_servers/al_language_server.py index 43cf1ab3..ed3cf9c7 100644 --- a/src/solidlsp/language_servers/al_language_server.py +++ b/src/solidlsp/language_servers/al_language_server.py @@ -1013,9 +1013,18 @@ class ALLanguageServer(SolidLanguageServer): original_name = symbol["name"] normalized_name = self._extract_al_display_name(original_name) - # Store original name if it was normalized (for hover injection) + if symbol.get("kind") in (SymbolKind.Function, SymbolKind.Method) and "(" in normalized_name: + normalized_name = normalized_name.split("(", 1)[0].strip() + + if symbol.get("kind") == SymbolKind.Method and normalized_name.lower().startswith("action "): + normalized_name = normalized_name.split(None, 1)[-1].strip() + + if symbol.get("kind") == SymbolKind.Field and ":" in normalized_name: + normalized_name = normalized_name.split(":", 1)[0].strip() + + # Store original name if it was normalized for an AL object declaration # Only store if we have valid position data to avoid false matches at (0, 0) - if original_name != normalized_name: + if original_name != normalized_name and self._AL_OBJECT_NAME_PATTERN.match(original_name): sel_range = symbol.get("selectionRange") if sel_range: start = sel_range.get("start") # type: ignore @@ -1026,6 +1035,11 @@ class ALLanguageServer(SolidLanguageServer): return normalized_name + @override + def _document_symbols_cache_fingerprint(self) -> int: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + @override def request_hover( self, relative_file_path: str, line: int, column: int, file_buffer: LSPFileBuffer | None = None diff --git a/src/solidlsp/language_servers/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server.py index 406c06e0..11f78106 100644 --- a/src/solidlsp/language_servers/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server.py @@ -1,9 +1,12 @@ import logging import os import pathlib +from collections.abc import Hashable from typing import cast -from solidlsp.ls import SolidLanguageServer +from overrides import override + +from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings @@ -34,6 +37,15 @@ class DartLanguageServer(SolidLanguageServer): config, repository_root_path, ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), "dart", solidlsp_settings ) + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + return symbol["name"].rsplit(".", 1)[-1] + @classmethod def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str: dart_settings = solidlsp_settings.get_ls_specific_settings(Language.DART) diff --git a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py index d200c899..688da5e8 100644 --- a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py +++ b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py @@ -4,11 +4,12 @@ import pathlib import stat import subprocess import threading +from collections.abc import Hashable from typing import Any, cast from overrides import override -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -215,6 +216,15 @@ class ElixirTools(SolidLanguageServer): # Set generous timeout for Expert which can be slow to initialize and respond self.set_request_timeout(180.0) + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + return symbol["name"].removeprefix("defp ").removeprefix("def ").split("(", 1)[0].strip() + @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ diff --git a/src/solidlsp/language_servers/gopls.py b/src/solidlsp/language_servers/gopls.py index 40768842..c855b836 100644 --- a/src/solidlsp/language_servers/gopls.py +++ b/src/solidlsp/language_servers/gopls.py @@ -1,12 +1,15 @@ +import hashlib +import json import logging import os import pathlib import subprocess +from collections.abc import Hashable from typing import Any, cast from overrides import override -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo @@ -165,15 +168,13 @@ class Gopls(SolidLanguageServer): _CACHE_CONTEXT_ENV_KEYS = ("GOFLAGS", "GOOS", "GOARCH", "CGO_ENABLED") @override - def _document_symbols_cache_fingerprint(self) -> str | None: + def _document_symbols_cache_fingerprint(self) -> Hashable: """ Compute a deterministic fingerprint of the Go build context. The fingerprint includes gopls_settings and selected env vars that affect symbol discovery. """ - import hashlib - import json - + normalize_symbol_name_version = 1 gopls_settings_raw = self._custom_settings.settings.get("gopls_settings") gopls_settings: dict | None @@ -190,11 +191,14 @@ class Gopls(SolidLanguageServer): if value: env_subset[key] = value - # Return None only when BOTH settings and env subset are effectively empty. + # Version processed symbols even when the build context itself is empty. if gopls_settings is None and not env_subset: - return None + return normalize_symbol_name_version - fingerprint_data: dict[str, object] = {"env": env_subset} + fingerprint_data: dict[str, object] = { + "env": env_subset, + "normalize_symbol_name_version": normalize_symbol_name_version, + } if gopls_settings is not None: fingerprint_data["gopls_settings"] = gopls_settings @@ -202,6 +206,10 @@ class Gopls(SolidLanguageServer): return hashlib.sha256(canonical_json.encode("utf-8")).hexdigest()[:16] + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + return symbol["name"].rsplit(".", 1)[-1] + def _start_server(self) -> None: """Start gopls server process""" diff --git a/src/solidlsp/language_servers/lua_ls.py b/src/solidlsp/language_servers/lua_ls.py index f8a37b3f..ef5e8674 100644 --- a/src/solidlsp/language_servers/lua_ls.py +++ b/src/solidlsp/language_servers/lua_ls.py @@ -11,12 +11,14 @@ import os import pathlib import platform import shutil +from collections.abc import Hashable from pathlib import Path from overrides import override -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import FileUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo @@ -169,6 +171,26 @@ class LuaLanguageServer(SolidLanguageServer): ) self.request_id = 0 + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + original_name = symbol["name"] + + if symbol.get("kind") not in (SymbolKind.Function, SymbolKind.Method): + return original_name + + if "." in original_name: + return original_name.rsplit(".", 1)[-1] + + if ":" in original_name: + return original_name.rsplit(":", 1)[-1] + + return original_name + @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ diff --git a/src/solidlsp/language_servers/powershell_language_server.py b/src/solidlsp/language_servers/powershell_language_server.py index 828babd9..72f9ac27 100644 --- a/src/solidlsp/language_servers/powershell_language_server.py +++ b/src/solidlsp/language_servers/powershell_language_server.py @@ -14,12 +14,14 @@ import platform import shutil import tempfile import threading +from collections.abc import Hashable from pathlib import Path from overrides import override -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import FileUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo @@ -199,6 +201,30 @@ class PowerShellLanguageServer(SolidLanguageServer): ) self.server_ready = threading.Event() + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + original_name = symbol["name"] + symbol_kind = symbol.get("kind") + + # normalize class declarations + if symbol_kind == SymbolKind.Class: + return original_name.removeprefix("class ").split("{", 1)[0].strip() + + # normalize method signatures + if symbol_kind == SymbolKind.Method: + return original_name.split("(", 1)[0].rsplit(None, 1)[-1] + + # normalize function declarations + if symbol_kind == SymbolKind.Function: + return original_name.removeprefix("function ").split("(", 1)[0].strip() + + return original_name + @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ diff --git a/src/solidlsp/language_servers/sourcekit_lsp.py b/src/solidlsp/language_servers/sourcekit_lsp.py index ba55e35b..8d30daeb 100644 --- a/src/solidlsp/language_servers/sourcekit_lsp.py +++ b/src/solidlsp/language_servers/sourcekit_lsp.py @@ -3,12 +3,14 @@ import os import pathlib import subprocess import time +from collections.abc import Hashable from overrides import override from solidlsp import ls_types -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_types import SymbolKind from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings @@ -56,6 +58,23 @@ class SourceKitLSP(SolidLanguageServer): self._did_sleep_before_requesting_references = False self._initialization_timestamp: float | None = None + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + original_name = symbol["name"] + + if symbol.get("kind") not in (SymbolKind.Function, SymbolKind.Method, SymbolKind.Constructor): + return original_name + + if "(" not in original_name: + return original_name + + return original_name.split("(", 1)[0].strip() + @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ diff --git a/test/conftest.py b/test/conftest.py index d02c22be..48443484 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -42,6 +42,7 @@ _LANGUAGE_REPO_ALIASES: dict[Language, Language] = { Language.PHP_PHPACTOR: Language.PHP, Language.PYTHON_JEDI: Language.PYTHON, Language.RUBY_SOLARGRAPH: Language.RUBY, + Language.PYTHON_TY: Language.PYTHON, } diff --git a/test/resources/repos/go/test_repo/main.go b/test/resources/repos/go/test_repo/main.go index b38c31ff..a5716358 100644 --- a/test/resources/repos/go/test_repo/main.go +++ b/test/resources/repos/go/test_repo/main.go @@ -15,6 +15,10 @@ type DemoStruct struct { Field int } +func (d *DemoStruct) Value() int { + return d.Field +} + func UsingHelper() { Helper() } diff --git a/test/resources/repos/powershell/test_repo/main.ps1 b/test/resources/repos/powershell/test_repo/main.ps1 index 6829fda4..f6d331f7 100644 --- a/test/resources/repos/powershell/test_repo/main.ps1 +++ b/test/resources/repos/powershell/test_repo/main.ps1 @@ -105,5 +105,11 @@ function Main { Write-Output "Script completed successfully" } +class PersonFormatter { + [string] FormatName([string]$Name) { + return "Formatted: $Name" + } +} + # Run main function Main @args diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index e9dc6b2e..51a0227c 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -194,7 +194,7 @@ class TestSerenaAgent: pytest.param(Language.PHP, "helperFunction", "Function", "helper.php", marks=pytest.mark.php), pytest.param(Language.CLOJURE, "greet", "Function", clj.CORE_PATH, marks=pytest.mark.clojure), pytest.param(Language.CSHARP, "Calculator", "Class", "Program.cs", marks=pytest.mark.csharp), - pytest.param(Language.POWERSHELL, "function Greet-User ()", "Function", "main.ps1", marks=pytest.mark.powershell), + pytest.param(Language.POWERSHELL, "Greet-User", "Function", "main.ps1", marks=pytest.mark.powershell), pytest.param(Language.CPP_CCLS, "add", "Function", "b.cpp", marks=pytest.mark.cpp), pytest.param(Language.LEAN4, "add", "Method", "Helper.lean", marks=pytest.mark.lean4), ], @@ -295,7 +295,7 @@ class TestSerenaAgent: marks=pytest.mark.clojure, ), pytest.param(Language.CSHARP, "Calculator", "Program.cs", "Program.cs", marks=pytest.mark.csharp), - pytest.param(Language.POWERSHELL, "function Greet-User ()", "main.ps1", "main.ps1", marks=pytest.mark.powershell), + pytest.param(Language.POWERSHELL, "Greet-User", "main.ps1", "main.ps1", marks=pytest.mark.powershell), pytest.param(Language.CPP_CCLS, "add", "b.cpp", "a.cpp", marks=pytest.mark.cpp), pytest.param(Language.LEAN4, "add", "Helper.lean", "Main.lean", marks=pytest.mark.lean4), ], diff --git a/test/serena/test_symbol.py b/test/serena/test_symbol.py index 60b936da..08188f49 100644 --- a/test/serena/test_symbol.py +++ b/test/serena/test_symbol.py @@ -5,7 +5,7 @@ import pytest from serena.jetbrains.jetbrains_types import SymbolDTO, SymbolDTOKey from serena.project import Project from serena.symbol import LanguageServerSymbol, LanguageServerSymbolRetriever, NamePathComponent, NamePathMatcher -from solidlsp.ls_config import Language +from test.solidlsp.conftest import PYTHON_BACKEND_LANGUAGES class TestSymbolNameMatching: @@ -235,7 +235,7 @@ class TestSymbolNameMatching: @pytest.mark.python class TestLanguageServerSymbolRetriever: - @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project_with_ls", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_info(self, project_with_ls: Project): symbol_retriever = LanguageServerSymbolRetriever(project_with_ls) create_user_method_symbol = symbol_retriever.find("UserService/create_user", within_relative_path="test_repo/services.py")[0] @@ -280,7 +280,7 @@ def _make_mock_symbols(count: int, *, relative_path: str = "test_repo/services.p class TestHoverBudget: """Tests for symbol_info_budget time budget behavior.""" - @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project_with_ls", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_budget_not_exceeded_all_lookups_performed(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """With a large budget, all hover lookups are performed.""" # Create symbol retriever with a mock agent that has large budget @@ -309,7 +309,7 @@ class TestHoverBudget: assert all(info is not None for info in result.values()) assert len(result) == 3 - @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project_with_ls", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_budget_exceeded_partial_info(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """With a small budget, hover lookups stop and remaining symbols get None info.""" project_with_ls.serena_config.symbol_info_budget = 0.1 @@ -353,7 +353,7 @@ class TestHoverBudget: assert result_list[3] is None assert result_list[4] is None - @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project_with_ls", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_budget_zero_means_unlimited(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """With budget=0, all hover lookups proceed (no early stopping).""" project_with_ls.serena_config.symbol_info_budget = 0.0 @@ -380,7 +380,7 @@ class TestHoverBudget: assert call_count == 5 assert all(info is not None for info in result.values()) - @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project_with_ls", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_project_budget_overrides_global(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """Project-level budget overrides global budget.""" # Create symbol retriever with global budget 10.0 but project budget 0.05 @@ -418,7 +418,7 @@ class TestHoverBudget: # So 2 calls succeed (proving project budget 0.05 overrode global 10.0) assert call_count == 2 - @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project_with_ls", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_project_null_inherits_global(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """When project budget is None, global budget is used.""" # Create symbol retriever with project budget=None (inherit global) diff --git a/test/solidlsp/al/test_al_basic.py b/test/solidlsp/al/test_al_basic.py index c226421f..a52e2f14 100644 --- a/test/solidlsp/al/test_al_basic.py +++ b/test/solidlsp/al/test_al_basic.py @@ -6,8 +6,10 @@ from serena.symbol import LanguageServerSymbol from solidlsp import SolidLanguageServer from solidlsp.language_servers.al_language_server import ALLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils from test.conftest import language_tests_enabled +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols pytestmark = [pytest.mark.al, pytest.mark.skipif(not language_tests_enabled(Language.AL), reason="AL tests are disabled")] @@ -556,3 +558,38 @@ class TestALPathNormalization: f"Hover for {symbol_name} should have injection with mixed paths. Got: {value[:200]}" ) break + + @pytest.mark.parametrize("language_server", [Language.AL], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + is_al_display_symbol = ( + s["name"].endswith((".Codeunit", ".Page", ".Table", ".TableExt", ".Enum", ".Interface")) + or (s["name"].startswith('"') and s["name"].endswith('"')) + or s["name"].startswith( + ( + "Enum Name ", + "Area ", + "Group ", + "Field ", + "Part ", + "SystemPart ", + "Repeater ", + "ActionRef ", + "Key ", + "FieldGroup ", + ) + ) + or ":" in s["name"] + ) + if not is_al_display_symbol and has_malformed_name( + s, + whitespace_allowed=s["kind"] in {SymbolKind.Class, SymbolKind.Struct, SymbolKind.Interface, SymbolKind.Enum}, + ): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/ansible/test_ansible_basic.py b/test/solidlsp/ansible/test_ansible_basic.py index 154b2231..bf6dc6ca 100644 --- a/test/solidlsp/ansible/test_ansible_basic.py +++ b/test/solidlsp/ansible/test_ansible_basic.py @@ -13,6 +13,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.skipif(platform.system() == "Windows", reason="ansible-language-server does not support native Windows operation") @@ -51,3 +52,17 @@ class TestAnsibleLanguageServerBasics: assert len(result) > 0, "Expected non-empty completion list" labels = [item["completionText"] for item in result if "completionText" in item] assert labels, f"Expected completions with completionText, got: {result[:3]}" + + @pytest.mark.xfail(reason="Seems like ansible LS lacks basic functionality at the moment, textDocument/documentSymbol doesn't work") + @pytest.mark.parametrize("language_server", [Language.ANSIBLE], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/bash/test_bash_basic.py b/test/solidlsp/bash/test_bash_basic.py index a3ead08f..c7715a29 100644 --- a/test/solidlsp/bash/test_bash_basic.py +++ b/test/solidlsp/bash/test_bash_basic.py @@ -9,6 +9,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.bash @@ -119,3 +120,16 @@ class TestBashLanguageServerBasics: # Verify total counts match expectations assert len(main_functions) >= 3, f"Should find at least 3 functions in main.sh, found {len(main_functions)}" assert len(utils_functions) >= 8, f"Should find at least 8 functions in utils.sh, found {len(utils_functions)}" + + @pytest.mark.parametrize("language_server", [Language.BASH], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/clojure/test_clojure_basic.py b/test/solidlsp/clojure/test_clojure_basic.py index 1a199673..c42067ac 100644 --- a/test/solidlsp/clojure/test_clojure_basic.py +++ b/test/solidlsp/clojure/test_clojure_basic.py @@ -3,8 +3,9 @@ import pytest from serena.project import Project from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language -from solidlsp.ls_types import UnifiedSymbolInformation +from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation from test.conftest import language_tests_enabled +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols from . import CORE_PATH, UTILS_PATH @@ -187,6 +188,8 @@ class TestLanguageServerBasics: assert found_relevant_references, f"Should have found calculate-area referencing multiply, but got: {result}" +@pytest.mark.skipif(not language_tests_enabled(Language.CLOJURE), reason="Clojure tests are disabled") +@pytest.mark.clojure class TestProjectBasics: @pytest.mark.parametrize("project", [Language.CLOJURE], indirect=True) def test_retrieve_content_around_line(self, project: Project): @@ -221,3 +224,20 @@ class TestProjectBasics: assert result is not None, "Should find require statements" utils_matches = [match for match in result if match.source_file_path and "utils.clj" in match.source_file_path] assert len(utils_matches) > 0, "Should find require statement in utils.clj" + + @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + # clojure-lsp exposes namespace and dependency/container entries in addition to real vars/functions. + # Those qualified names are not the target of this regression check. + if s["kind"] in {SymbolKind.Namespace, SymbolKind.Struct}: + continue + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/conftest.py b/test/solidlsp/conftest.py new file mode 100644 index 00000000..08bb6a01 --- /dev/null +++ b/test/solidlsp/conftest.py @@ -0,0 +1,56 @@ +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation + +PYTHON_BACKEND_LANGUAGES = [Language.PYTHON, Language.PYTHON_TY] + + +def has_malformed_name( + symbol: UnifiedSymbolInformation, + whitespace_allowed: bool = False, + period_allowed: bool = False, + colon_allowed: bool = False, + brace_allowed: bool = False, + parenthesis_allowed: bool = False, + comma_allowed: bool = False, +) -> bool: + forbidden_chars: list[str] = [] + + if not whitespace_allowed: + forbidden_chars.append(" ") + if not period_allowed: + forbidden_chars.append(".") + if not colon_allowed: + forbidden_chars.append(":") + if not brace_allowed: + forbidden_chars.append("{") + if not parenthesis_allowed: + forbidden_chars.append("(") + if not comma_allowed: + forbidden_chars.append(",") + + return any(separator in symbol["name"] for separator in forbidden_chars) + + +def request_all_symbols(language_server: SolidLanguageServer) -> list[UnifiedSymbolInformation]: + result: list[UnifiedSymbolInformation] = [] + + def visit(symbol: UnifiedSymbolInformation) -> None: + result.append(symbol) + for child in symbol.get("children", []): + visit(child) + + symbols = language_server.request_full_symbol_tree() + for symbol in symbols: + visit(symbol) + + return result + + +def format_symbol_for_assert(symbol: UnifiedSymbolInformation) -> str: + relative_path = symbol.get("location", {}).get("relativePath", "") + try: + kind = SymbolKind(symbol["kind"]).name + except ValueError: + kind = str(symbol["kind"]) + return f"{symbol['name']} [{kind}] ({relative_path})" diff --git a/test/solidlsp/cpp/test_cpp_basic.py b/test/solidlsp/cpp/test_cpp_basic.py index a8767949..3ca52693 100644 --- a/test/solidlsp/cpp/test_cpp_basic.py +++ b/test/solidlsp/cpp/test_cpp_basic.py @@ -15,6 +15,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols def _ccls_available() -> bool: @@ -142,3 +143,16 @@ int use_add() { # Clean up the new file if os.path.exists(new_file_abs_path): os.remove(new_file_abs_path) + + @pytest.mark.parametrize("language_server", _cpp_servers, indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/csharp/test_csharp_basic.py b/test/solidlsp/csharp/test_csharp_basic.py index 3d538061..91f67781 100644 --- a/test/solidlsp/csharp/test_csharp_basic.py +++ b/test/solidlsp/csharp/test_csharp_basic.py @@ -15,8 +15,10 @@ from solidlsp.language_servers.csharp_language_server import ( find_solution_or_project_file, ) from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils from solidlsp.settings import SolidLSPSettings +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.csharp @@ -359,3 +361,16 @@ class TestCSharpSolutionProjectOpening: # Verify the file actually exists assert os.path.exists(result) + + @pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, period_allowed=s["kind"] == SymbolKind.Namespace): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/dart/test_dart_basic.py b/test/solidlsp/dart/test_dart_basic.py index cdd98a97..0a5b69cf 100644 --- a/test/solidlsp/dart/test_dart_basic.py +++ b/test/solidlsp/dart/test_dart_basic.py @@ -8,6 +8,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.dart @@ -361,3 +362,16 @@ class TestDartLanguageServer: if main_dart_refs: for ref in main_dart_refs: assert "range" in ref or "location" in ref + + @pytest.mark.parametrize("language_server", [Language.DART], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/elixir/test_elixir_basic.py b/test/solidlsp/elixir/test_elixir_basic.py index fe18cd34..11b983ca 100644 --- a/test/solidlsp/elixir/test_elixir_basic.py +++ b/test/solidlsp/elixir/test_elixir_basic.py @@ -11,6 +11,8 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols from . import EXPERT_UNAVAILABLE, EXPERT_UNAVAILABLE_REASON @@ -110,3 +112,29 @@ class TestElixirBasic: for _ in range(3): symbols = language_server.request_document_symbols("lib/services.ex").get_all_symbols_and_roots() assert symbols is not None + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if s["kind"] in {SymbolKind.Module, SymbolKind.Namespace, SymbolKind.Struct}: + continue + + allow_test_style_name = s["name"].startswith(('test "', 'describe "')) + allow_struct_name = s["name"].startswith("%") + allow_type_name = s["name"].startswith("@type ") + if has_malformed_name( + s, + whitespace_allowed=allow_test_style_name or allow_type_name, + period_allowed=allow_struct_name, + colon_allowed=allow_test_style_name, + brace_allowed=allow_test_style_name or allow_struct_name, + comma_allowed=allow_test_style_name, + ): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/elm/test_elm_basic.py b/test/solidlsp/elm/test_elm_basic.py index 6283d914..5ff6fe22 100644 --- a/test/solidlsp/elm/test_elm_basic.py +++ b/test/solidlsp/elm/test_elm_basic.py @@ -1,12 +1,16 @@ import os +import shutil import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +@pytest.mark.skipif(shutil.which("elm") is None and not is_ci, reason="Elm compiler is not available") @pytest.mark.elm class TestElmLanguageServer: @pytest.mark.parametrize("language_server", [Language.ELM], indirect=True) @@ -52,3 +56,16 @@ class TestElmLanguageServer: # Verify that at least one reference is in Main.elm (where formatMessage is used) assert any("Main.elm" in ref.get("relativePath", "") for ref in refs), "Expected to find usage of formatMessage in Main.elm" + + @pytest.mark.parametrize("language_server", [Language.ELM], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/erlang/test_erlang_basic.py b/test/solidlsp/erlang/test_erlang_basic.py index 50ab3197..a1dba06a 100644 --- a/test/solidlsp/erlang/test_erlang_basic.py +++ b/test/solidlsp/erlang/test_erlang_basic.py @@ -9,6 +9,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols from . import ERLANG_LS_UNAVAILABLE, ERLANG_LS_UNAVAILABLE_REASON @@ -41,3 +42,16 @@ class TestErlangLanguageServerBasics: pytest.skip("Erlang language server not fully initialized") else: raise + + @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/fortran/test_fortran_basic.py b/test/solidlsp/fortran/test_fortran_basic.py index 1554e959..e7a45235 100644 --- a/test/solidlsp/fortran/test_fortran_basic.py +++ b/test/solidlsp/fortran/test_fortran_basic.py @@ -11,6 +11,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols # Mark all tests in this module as fortran tests pytestmark = pytest.mark.fortran @@ -272,3 +273,16 @@ class TestFortranLanguageServer: _refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) # refs might be empty if Point3D isn't used elsewhere, but the call should not fail # The important thing is that it doesn't error due to wrong character position + + @pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/fsharp/test_fsharp_basic.py b/test/solidlsp/fsharp/test_fsharp_basic.py index 8e6ff221..d0f52443 100644 --- a/test/solidlsp/fsharp/test_fsharp_basic.py +++ b/test/solidlsp/fsharp/test_fsharp_basic.py @@ -8,6 +8,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols # Currently, most F# tests fail, there seems to be a regression or instability. @@ -183,3 +184,16 @@ class TestFSharpLanguageServer: # This is a successful test - FsAutoComplete is working with F# files assert True, "F# language server can handle files successfully" + + @pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/go/test_go_basic.py b/test/solidlsp/go/test_go_basic.py index 8d83298c..93fcd98e 100644 --- a/test/solidlsp/go/test_go_basic.py +++ b/test/solidlsp/go/test_go_basic.py @@ -3,9 +3,11 @@ from pathlib import Path import pytest +from serena.symbol import LanguageServerSymbol from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.go @@ -17,6 +19,20 @@ class TestGoLanguageServer: 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_symbol_matches_go_method_by_bare_name(self, language_server: SolidLanguageServer) -> None: + symbols = language_server.request_full_symbol_tree(within_relative_path="main.go") + + assert SymbolUtils.symbol_tree_contains_name(symbols, "Value"), "Expected Go method name to be normalized to bare name" + assert not SymbolUtils.symbol_tree_contains_name(symbols, "(*DemoStruct).Value"), ( + "Expected receiver-qualified Go method name to be normalized away" + ) + + bare_name_matches = [match for root in symbols for match in LanguageServerSymbol(root).find("Value")] + + assert bare_name_matches, "Expected a Go method to match by bare name" + assert all(match.name == "Value" for match in bare_name_matches) + @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: file_path = os.path.join("main.go") @@ -192,3 +208,16 @@ class TestGoBuildTags: # A cache miss should repopulate and mark caches modified. _assert_caches_modified(ls_foo) + + @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/groovy/test_groovy_basic.py b/test/solidlsp/groovy/test_groovy_basic.py index 57e0baf3..47f22ed8 100644 --- a/test/solidlsp/groovy/test_groovy_basic.py +++ b/test/solidlsp/groovy/test_groovy_basic.py @@ -8,6 +8,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_utils import SymbolUtils from solidlsp.settings import SolidLSPSettings +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.groovy @@ -103,3 +104,16 @@ class TestGroovyLanguageServer: assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "ModelUser"), "ModelUser missing from overview" + + def test_bare_symbol_names(self) -> None: + assert self.language_server is not None + all_symbols = request_all_symbols(self.language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/haskell/test_haskell_basic.py b/test/solidlsp/haskell/test_haskell_basic.py index b080a306..67452151 100644 --- a/test/solidlsp/haskell/test_haskell_basic.py +++ b/test/solidlsp/haskell/test_haskell_basic.py @@ -19,6 +19,8 @@ import pytest from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.haskell @@ -217,3 +219,24 @@ class TestHaskellLanguageServer: assert any("Main.hs" in path or "Calculator.hs" in path for path in calc_ref_paths), ( f"Expected Calculator to be referenced in Main.hs or Calculator.hs, got: {calc_ref_paths}" ) + + @pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if s["kind"] == SymbolKind.Module: + continue + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + diagnostics = [ + { + "formatted": format_symbol_for_assert(sym), + "name": sym["name"], + "kind": SymbolKind(sym["kind"]).name, + "detail": sym.get("detail"), + } + for sym in malformed_symbols + ] + pytest.fail(f"Found malformed symbols: {diagnostics}", pytrace=False) diff --git a/test/solidlsp/hlsl/test_hlsl_basic.py b/test/solidlsp/hlsl/test_hlsl_basic.py index 7034579d..90d1c864 100644 --- a/test/solidlsp/hlsl/test_hlsl_basic.py +++ b/test/solidlsp/hlsl/test_hlsl_basic.py @@ -12,7 +12,9 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_exceptions import SolidLSPException +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols def _find_symbol_by_name(language_server: SolidLanguageServer, file_path: str, name: str) -> dict[str, Any] | None: @@ -158,3 +160,16 @@ class TestHlslHover: hover_info = language_server.request_hover("common.hlsl", 3, 7) assert hover_info is not None, "Hover should return information for VertexInput" assert "contents" in hover_info, "Hover should have contents" + + @pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, period_allowed=s["kind"] == SymbolKind.File): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/java/test_java_basic.py b/test/solidlsp/java/test_java_basic.py index 0810dac2..56b3bbf4 100644 --- a/test/solidlsp/java/test_java_basic.py +++ b/test/solidlsp/java/test_java_basic.py @@ -6,6 +6,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils from test.conftest import language_tests_enabled +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols pytestmark = [pytest.mark.java, pytest.mark.skipif(not language_tests_enabled(Language.JAVA), reason="Java tests disabled")] @@ -50,3 +51,16 @@ class TestJavaLanguageServer: 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" + + @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/julia/test_julia_basic.py b/test/solidlsp/julia/test_julia_basic.py index 99bf720c..44adb43d 100644 --- a/test/solidlsp/julia/test_julia_basic.py +++ b/test/solidlsp/julia/test_julia_basic.py @@ -1,9 +1,14 @@ +import shutil + import pytest from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language +from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +@pytest.mark.skipif(shutil.which("julia") is None and not is_ci, reason="Julia is not available") @pytest.mark.julia class TestJuliaLanguageServer: @pytest.mark.parametrize("language_server", [Language.JULIA], indirect=True) @@ -48,3 +53,16 @@ class TestJuliaLanguageServer: reference_paths = [ref["relativePath"] for ref in references] # The reference might be in either file (definition or usage) assert "main.jl" in reference_paths or "lib/helper.jl" in reference_paths + + @pytest.mark.parametrize("language_server", [Language.JULIA], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/kotlin/test_kotlin_basic.py b/test/solidlsp/kotlin/test_kotlin_basic.py index 762c391c..9a990458 100644 --- a/test/solidlsp/kotlin/test_kotlin_basic.py +++ b/test/solidlsp/kotlin/test_kotlin_basic.py @@ -4,8 +4,10 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols # Kotlin LSP (IntelliJ-based, pre-alpha v261) crashes on JVM restart under CI resource constraints @@ -55,3 +57,16 @@ class TestKotlinLanguageServer: 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" + + @pytest.mark.parametrize("language_server", [Language.KOTLIN], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, period_allowed=s["kind"] == SymbolKind.File): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/lua/test_lua_basic.py b/test/solidlsp/lua/test_lua_basic.py index ab58ec5e..f140881b 100644 --- a/test/solidlsp/lua/test_lua_basic.py +++ b/test/solidlsp/lua/test_lua_basic.py @@ -10,6 +10,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.lua @@ -253,3 +254,21 @@ class TestLuaLanguageServer: # The test file should have some content that references calculator symbol_list = test_symbols[0] if isinstance(test_symbols, tuple) else test_symbols assert len(symbol_list) > 0, "test_calculator.lua should have symbols" + + @pytest.mark.parametrize("language_server", [Language.LUA], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name( + s, + whitespace_allowed=s["name"] == " ", + period_allowed="." in s["name"], + colon_allowed=":" in s["name"], + ): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/luau/test_luau_basic.py b/test/solidlsp/luau/test_luau_basic.py index 264ace92..4564e257 100644 --- a/test/solidlsp/luau/test_luau_basic.py +++ b/test/solidlsp/luau/test_luau_basic.py @@ -9,6 +9,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.luau @@ -151,3 +152,16 @@ class TestLuauLanguageServer: assert definition["uri"].endswith("init.luau"), f"Definition should be in init.luau, got: {definition['uri']}" # createConfig is defined at line 8 (0-indexed): `local function createConfig(...)` assert definition["range"]["start"]["line"] == 8, f"Definition should be at line 8, got line {definition['range']['start']['line']}" + + @pytest.mark.parametrize("language_server", [Language.LUAU], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/markdown/test_markdown_basic.py b/test/solidlsp/markdown/test_markdown_basic.py index 5a3c65f8..34f57533 100644 --- a/test/solidlsp/markdown/test_markdown_basic.py +++ b/test/solidlsp/markdown/test_markdown_basic.py @@ -11,6 +11,7 @@ from serena.symbol import LanguageServerSymbol from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.markdown @@ -93,3 +94,16 @@ class TestMarkdownLanguageServerBasics: for symbol in all_symbols: assert symbol["kind"] == SymbolKind.Namespace, f"Nested heading '{symbol['name']}' should be remapped to Namespace" + + @pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, whitespace_allowed=True, parenthesis_allowed=True): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/matlab/test_matlab_basic.py b/test/solidlsp/matlab/test_matlab_basic.py index a6c423c8..71724ff7 100644 --- a/test/solidlsp/matlab/test_matlab_basic.py +++ b/test/solidlsp/matlab/test_matlab_basic.py @@ -16,6 +16,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols # Skip all tests if MATLAB is not available pytestmark = pytest.mark.matlab @@ -115,3 +116,16 @@ class TestMatlabLanguageServerReferences: # Should find references in both main.m and Calculator.m assert references is not None + + @pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/nix/test_nix_basic.py b/test/solidlsp/nix/test_nix_basic.py index 42d4d414..23f4311a 100644 --- a/test/solidlsp/nix/test_nix_basic.py +++ b/test/solidlsp/nix/test_nix_basic.py @@ -10,7 +10,9 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols # Skip all Nix tests on Windows as Nix doesn't support Windows pytestmark = pytest.mark.skipif(platform.system() == "Windows", reason="Nix and nil are not available on Windows") @@ -218,3 +220,33 @@ class TestNixLanguageServer: root = symbols[0] assert isinstance(root, dict), "Root should be a dict" assert "name" in root, "Root should have a name" + + @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + # nixd can surface anonymous expression nodes using synthetic display labels + # like "(anonymous lambda)", "(dynamic string)", "{anonymous}", and + # "(dynamic attribute name)". It also emits literal value nodes such as + # strings and arrays. These do not correspond to named declarations, so + # there is no bare identifier we could or should normalize them to in the + # Nix wrapper. This test is only meant to enforce bare names for symbols + # that actually have declaration names. + if s["kind"] in {SymbolKind.String, SymbolKind.Array}: + continue + if s["name"] in {"(anonymous lambda)", "(dynamic string)", "{anonymous}", "(dynamic attribute name)"}: + continue + if s["kind"] in {SymbolKind.Field, SymbolKind.Property} and "." in s["name"]: + continue + if has_malformed_name( + s, + whitespace_allowed=s["name"] == "(anonymous lambda)", + parenthesis_allowed=s["name"] == "(anonymous lambda)", + ): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/ocaml/test_ocaml_basic.py b/test/solidlsp/ocaml/test_ocaml_basic.py index 82af5fa9..f3dca2eb 100644 --- a/test/solidlsp/ocaml/test_ocaml_basic.py +++ b/test/solidlsp/ocaml/test_ocaml_basic.py @@ -1,12 +1,16 @@ import os +import shutil import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +@pytest.mark.skipif(shutil.which("opam") is None and not is_ci, reason="OPAM is not available") @pytest.mark.ocaml class TestOCamlLanguageServer: @pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True) @@ -49,8 +53,6 @@ class TestOCamlLanguageServer: def test_reason_file_patterns(self) -> None: """Test that OCaml language configuration recognizes Reason file extensions""" - from solidlsp.ls_config import Language - ocaml_lang = Language.OCAML file_matcher = ocaml_lang.get_source_fn_matcher() @@ -150,3 +152,19 @@ class TestOCamlLanguageServer: file_path = os.path.join("bin", "main.ml") symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() assert len(symbols) > 0, "Should find symbols in main.ml that use opened modules" + + @pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + # happens for lambdas a la let () = ... + if s["name"] == "()": + continue + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/pascal/test_pascal_basic.py b/test/solidlsp/pascal/test_pascal_basic.py index d926207a..e9636e94 100644 --- a/test/solidlsp/pascal/test_pascal_basic.py +++ b/test/solidlsp/pascal/test_pascal_basic.py @@ -10,12 +10,15 @@ Uses genericptr/pascal-language-server which returns SymbolInformation[] format: - Method names don't include parent class prefix; uses containerName instead """ +import shutil + import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind -from test.conftest import language_tests_enabled +from test.conftest import is_ci, language_tests_enabled +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols pytestmark = [ pytest.mark.pascal, @@ -24,6 +27,7 @@ pytestmark = [ @pytest.mark.pascal +@pytest.mark.skipif(shutil.which("fpc") is None and not is_ci, reason="Free Pascal compiler is not available") class TestPascalLanguageServerBasics: """Test basic functionality of the Pascal language server.""" @@ -193,3 +197,18 @@ class TestPascalLanguageServerBasics: # Should contain the doc comment assert "Calculates the sum" in value, f"Hover should include doc comment. Got: {value[:500]}" + + @pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if s["kind"] == SymbolKind.Package: + continue + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/perl/test_perl_basic.py b/test/solidlsp/perl/test_perl_basic.py index 59dbcc5f..41682e3f 100644 --- a/test/solidlsp/perl/test_perl_basic.py +++ b/test/solidlsp/perl/test_perl_basic.py @@ -5,6 +5,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.perl @@ -74,3 +75,16 @@ class TestPerlLanguageServer: main_pl_lines = sorted([ref["range"]["start"]["line"] for ref in main_pl_refs]) assert 17 in main_pl_lines, f"Expected reference at line 18 (0-indexed 17), found: {main_pl_lines}" assert 20 in main_pl_lines, f"Expected reference at line 21 (0-indexed 20), found: {main_pl_lines}" + + @pytest.mark.parametrize("language_server", [Language.PERL], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/php/test_php_basic.py b/test/solidlsp/php/test_php_basic.py index bc59d471..aab824f3 100644 --- a/test/solidlsp/php/test_php_basic.py +++ b/test/solidlsp/php/test_php_basic.py @@ -1,3 +1,4 @@ +import subprocess from pathlib import Path import pytest @@ -5,10 +6,18 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from test.conftest import is_ci, is_windows, language_tests_enabled +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols + + +def _php_supports_phpactor() -> bool: + completed_process = subprocess.run(["php", "-m"], capture_output=True, text=True, check=False) + installed_extensions = {line.strip().lower() for line in completed_process.stdout.splitlines()} + return "openssl" in installed_extensions + _php_servers: list[Language] = [Language.PHP] if language_tests_enabled(Language.PHP_PHPACTOR): - if not (is_windows and is_ci): # TODO: Phpactor tests are flaky in Windows CI and can even cause hangs #1040 + if not (is_windows and is_ci) and _php_supports_phpactor(): _php_servers.append(Language.PHP_PHPACTOR) @@ -256,3 +265,16 @@ class TestPhpLanguageServers: dog_root = next((s for s in symbols if s.get("name") == "Dog"), None) if dog_root is not None: assert SymbolUtils.symbol_tree_contains_name([dog_root], "greet"), "greet should be nested under Dog in symbol tree" + + @pytest.mark.parametrize("language_server", _php_servers, indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/powershell/test_powershell_basic.py b/test/solidlsp/powershell/test_powershell_basic.py index 3d6ef7a1..bf1b9e46 100644 --- a/test/solidlsp/powershell/test_powershell_basic.py +++ b/test/solidlsp/powershell/test_powershell_basic.py @@ -9,6 +9,8 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.powershell @@ -130,6 +132,14 @@ class TestPowerShellLanguageServerBasics: assert len(main_functions) >= 3, f"Should find at least 3 functions in main.ps1, found {len(main_functions)}" assert len(utils_functions) >= 8, f"Should find at least 8 functions in utils.ps1, found {len(utils_functions)}" + @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True) + def test_powershell_class_method_symbols_use_bare_method_name(self, language_server: SolidLanguageServer) -> None: + """Test whether PSES already reports class methods with bare names.""" + symbols = language_server.request_full_symbol_tree(within_relative_path="main.ps1") + + assert SymbolUtils.symbol_tree_contains_name(symbols, "PersonFormatter"), "Should find PersonFormatter class in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "FormatName"), "Expected PowerShell method to be exposed with bare name" + @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True) def test_powershell_find_references_within_file(self, language_server: SolidLanguageServer) -> None: """Test finding references to a function within the same file.""" @@ -172,3 +182,16 @@ class TestPowerShellLanguageServerBasics: assert any("utils.ps1" in loc.get("uri", "") for loc in definition_locations), ( f"Should find definition in utils.ps1, got {definition_locations}" ) + + @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, colon_allowed=s["name"].startswith("$")): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/python/test_python_basic.py b/test/solidlsp/python/test_python_basic.py index faeca67a..f57b37bd 100644 --- a/test/solidlsp/python/test_python_basic.py +++ b/test/solidlsp/python/test_python_basic.py @@ -12,14 +12,14 @@ import pytest from serena.project import Project from serena.util.text_utils import LineType from solidlsp import SolidLanguageServer -from solidlsp.ls_config import Language +from test.solidlsp.conftest import PYTHON_BACKEND_LANGUAGES, format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.python class TestPythonLanguageServerBasics: """Test basic functionality of the language server.""" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_references_user_class(self, language_server: SolidLanguageServer) -> None: """Test request_references on the User class.""" # Get references to the User class in models.py @@ -34,7 +34,7 @@ class TestPythonLanguageServerBasics: 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) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_references_item_class(self, language_server: SolidLanguageServer) -> None: """Test request_references on the Item class.""" # Get references to the Item class in models.py @@ -50,7 +50,7 @@ class TestPythonLanguageServerBasics: 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) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_references_function_parameter(self, language_server: SolidLanguageServer) -> None: """Test request_references on a function parameter.""" # Get references to the id parameter in get_user method @@ -65,7 +65,7 @@ class TestPythonLanguageServerBasics: 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) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_references_create_user_method(self, language_server: SolidLanguageServer) -> None: # Get references to the create_user method in UserService file_path = os.path.join("test_repo", "services.py") @@ -81,7 +81,7 @@ class TestPythonLanguageServerBasics: class TestProjectBasics: - @pytest.mark.parametrize("project", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_retrieve_content_around_line(self, project: Project) -> None: """Test retrieve_content_around_line functionality with various scenarios.""" file_path = os.path.join("test_repo", "models.py") @@ -188,7 +188,7 @@ class TestProjectBasics: else: assert line.match_type == LineType.AFTER_MATCH - @pytest.mark.parametrize("project", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_search_files_for_pattern(self, project: Project) -> None: """Test search_files_for_pattern with various patterns and glob filters.""" # Test 1: Search for class definitions across all files @@ -233,3 +233,16 @@ class TestProjectBasics: no_match_pattern = r"def\s+this_method_does_not_exist\s*\([^)]*\):" matches = project.search_source_files_for_pattern(no_match_pattern) assert len(matches) == 0 + + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/python/test_retrieval_with_ignored_dirs.py b/test/solidlsp/python/test_retrieval_with_ignored_dirs.py index 031f185c..9c12af8d 100644 --- a/test/solidlsp/python/test_retrieval_with_ignored_dirs.py +++ b/test/solidlsp/python/test_retrieval_with_ignored_dirs.py @@ -6,6 +6,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from test.conftest import start_ls_context +from test.solidlsp.conftest import PYTHON_BACKEND_LANGUAGES # This mark will be applied to all tests in this module pytestmark = pytest.mark.python @@ -19,7 +20,7 @@ def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]: yield ls -@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True) +@pytest.mark.parametrize("ls_with_ignored_dirs", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): """Tests that request_full_symbol_tree ignores the configured directory.""" root = ls_with_ignored_dirs.request_full_symbol_tree()[0] @@ -28,7 +29,7 @@ def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): assert children_names == {"test_repo", "examples"} -@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True) +@pytest.mark.parametrize("ls_with_ignored_dirs", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): """Tests that find_references ignores the configured directory.""" # Location of Item, which is referenced in scripts @@ -42,7 +43,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): assert not any("scripts" in ref["relativePath"] for ref in references) -@pytest.mark.parametrize("repo_path", [Language.PYTHON], indirect=True) +@pytest.mark.parametrize("repo_path", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: """Tests that refs and symbols with glob patterns are ignored.""" ignored_paths = ["*ipts", "custom_t*"] diff --git a/test/solidlsp/python/test_symbol_retrieval.py b/test/solidlsp/python/test_symbol_retrieval.py index ff98bd70..3baff0a0 100644 --- a/test/solidlsp/python/test_symbol_retrieval.py +++ b/test/solidlsp/python/test_symbol_retrieval.py @@ -12,8 +12,8 @@ import pytest from serena.symbol import LanguageServerSymbol from solidlsp import SolidLanguageServer -from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind +from test.solidlsp.conftest import PYTHON_BACKEND_LANGUAGES pytestmark = pytest.mark.python @@ -21,7 +21,7 @@ pytestmark = pytest.mark.python class TestLanguageServerSymbols: """Test the language server's symbol-related functionality.""" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol for a function.""" # Test for a position inside the create_user method @@ -36,7 +36,7 @@ class TestLanguageServerSymbols: if "body" in containing_symbol: assert containing_symbol["body"].get_text().strip().startswith("def create_user(self") - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_references_to_variables(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a variable.""" file_path = os.path.join("test_repo", "variables.py") @@ -51,7 +51,7 @@ class TestLanguageServerSymbols: assert "dataclass_instance" in ref_names assert "second_dataclass" in ref_names - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol for a class.""" # Test for a position inside the UserService class but outside any method @@ -64,7 +64,7 @@ class TestLanguageServerSymbols: assert containing_symbol["name"] == "UserService" assert containing_symbol["kind"] == SymbolKind.Class - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol with nested scopes.""" # Test for a position inside a method which is inside a class @@ -90,7 +90,7 @@ class TestLanguageServerSymbols: assert parent_symbol["name"] == "UserService" assert parent_symbol["kind"] == SymbolKind.Class - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol for a position with no containing symbol.""" # Test for a position outside any function/class (e.g., in imports) @@ -101,7 +101,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) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a function.""" # Test referencing symbols for create_user function @@ -125,7 +125,7 @@ class TestLanguageServerSymbols: assert "start" in symbol["location"]["range"] assert "end" in symbol["location"]["range"] - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_referencing_symbols_class(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a class.""" # Test referencing symbols for User class @@ -146,7 +146,7 @@ class TestLanguageServerSymbols: ] assert len(services_references) > 0, "No referencing symbols from services.py for User (selectionRange)" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_referencing_symbols_parameter(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a function parameter.""" # Test referencing symbols for id parameter in get_user @@ -167,7 +167,7 @@ class TestLanguageServerSymbols: ] assert len(method_refs) > 0, "No referencing symbols within method body for get_user (selectionRange)" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a position with no symbol.""" # For positions with no symbol, the method might throw an error or return None/empty list @@ -185,7 +185,7 @@ class TestLanguageServerSymbols: pass # Tests for request_defining_symbol - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a variable usage.""" # Test finding the definition of a symbol in the create_user method @@ -204,7 +204,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) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for an imported class.""" # Test finding the definition of the 'User' class used in the UserService.create_user method @@ -216,7 +216,7 @@ class TestLanguageServerSymbols: assert defining_symbol is not None assert defining_symbol.get("name") == "User" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_defining_symbol_method_call(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a method call.""" # Create an example file path for a file that calls UserService.create_user @@ -241,7 +241,7 @@ class TestLanguageServerSymbols: warnings.warn("Could not verify method call definition - file structure may differ from expected") - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a position with no symbol.""" # Test for a position with no symbol (e.g., whitespace or comment) @@ -252,7 +252,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) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_containing_symbol_variable(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol where the symbol is a variable.""" # Test for a position inside a variable definition @@ -265,7 +265,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) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a nested function or closure.""" # Use the existing nested.py file which contains nested classes and methods @@ -290,15 +290,8 @@ class TestLanguageServerSymbols: # Test 3: Find definition of a method-local function defining_symbol = language_server.request_defining_symbol(file_path, 9, 15) # Position inside func_within_func - # This is challenging for many language servers and may fail - try: - assert defining_symbol is not None - assert defining_symbol.get("name") == "func_within_func" - except (AssertionError, TypeError, KeyError): - # This is expected to potentially fail in many implementations - import warnings - - warnings.warn("Could not resolve nested class method definition - implementation limitation") + assert defining_symbol is not None + assert defining_symbol.get("name") == "func_within_func" # Test 2: Find definition of the nested class defining_symbol = language_server.request_defining_symbol(file_path, 15, 18) # Position of NestedClass @@ -314,9 +307,9 @@ class TestLanguageServerSymbols: # This is challenging for many language servers and may fail assert defining_symbol is not None assert defining_symbol.get("name") == "func_within_func" - assert defining_symbol.get("kind") == SymbolKind.Function.value + assert defining_symbol.get("kind") in [SymbolKind.Function.value, SymbolKind.Method.value] - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None: """Test the integration between different symbol-related methods.""" # This test demonstrates using the various symbol methods together @@ -365,7 +358,7 @@ class TestLanguageServerSymbols: warnings.warn("Could not verify container hierarchy - implementation detail") - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None: """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file @@ -395,7 +388,7 @@ class TestLanguageServerSymbols: ).get_all_symbols_and_roots() assert user_management_roots == user_management_node["children"] - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_symbol_tree_structure_subdir(self, language_server: SolidLanguageServer) -> None: """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file @@ -418,7 +411,7 @@ class TestLanguageServerSymbols: ).get_all_symbols_and_roots() assert user_management_roots == user_management_node["children"] - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None: """Test that request_dir_overview returns correct symbol information for files in a directory.""" # Get overview of the examples directory @@ -442,7 +435,7 @@ class TestLanguageServerSymbols: retrieved_symbols = {symbol["name"] for symbol in services_symbols if "name" in symbol} assert expected_symbols.issubset(retrieved_symbols) - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_document_overview(self, language_server: SolidLanguageServer) -> None: """Test that request_document_overview returns correct symbol information for a file.""" # Get overview of the user_management.py file @@ -451,25 +444,3 @@ class TestLanguageServerSymbols: # Verify that we have entries for both files symbol_names = {LanguageServerSymbol(s_info).name 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: SolidLanguageServer) -> None: - """Test that the containing symbol of a variable is the file itself.""" - # Get the containing symbol of a variable in a file - file_path = os.path.join("test_repo", "services.py") - # import of typing - references_to_typing = [ - ref.symbol - for ref in language_server.request_referencing_symbols(file_path, 4, 6, include_imports=False, include_file_symbols=True) - ] - assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File} - - # now include bodies - references_to_typing = [ - ref.symbol - for ref in language_server.request_referencing_symbols( - file_path, 4, 6, include_imports=False, include_file_symbols=True, include_body=True - ) - ] - assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File} - assert references_to_typing[0]["body"] diff --git a/test/solidlsp/r/test_r_basic.py b/test/solidlsp/r/test_r_basic.py index 647ff6c1..76f82602 100644 --- a/test/solidlsp/r/test_r_basic.py +++ b/test/solidlsp/r/test_r_basic.py @@ -3,14 +3,18 @@ Basic tests for R Language Server integration """ import os +import shutil from pathlib import Path import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +@pytest.mark.skipif(shutil.which("R") is None and not is_ci, reason="R is not available") @pytest.mark.r class TestRLanguageServer: """Test basic functionality of the R language server.""" @@ -82,8 +86,6 @@ class TestRLanguageServer: def test_file_matching(self): """Test that R files are properly matched.""" - from solidlsp.ls_config import Language - matcher = Language.R.get_source_fn_matcher() assert matcher.is_relevant_filename("script.R") @@ -95,3 +97,16 @@ class TestRLanguageServer: """Test R language enum value.""" assert Language.R == "r" assert str(Language.R) == "r" + + @pytest.mark.parametrize("language_server", [Language.R], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/rego/test_rego_basic.py b/test/solidlsp/rego/test_rego_basic.py index 79155a0c..ce8028c2 100644 --- a/test/solidlsp/rego/test_rego_basic.py +++ b/test/solidlsp/rego/test_rego_basic.py @@ -1,14 +1,19 @@ """Tests for Rego language server (Regal) functionality.""" import os +import shutil import pytest from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils +from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +@pytest.mark.skipif(shutil.which("regal") is None and not is_ci, reason="Regal is not available") @pytest.mark.rego class TestRegoLanguageServer: """Test Regal language server functionality for Rego.""" @@ -132,3 +137,18 @@ class TestRegoLanguageServer: assert "validate_user_input" in symbol_names, "validate_user_input rule not found" assert "has_valid_credentials" in symbol_names, "has_valid_credentials function not found" assert "validate_request" in symbol_names, "validate_request rule not found" + + @pytest.mark.parametrize("language_server", [Language.REGO], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if s["kind"] == SymbolKind.Package: + continue + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/ruby/test_ruby_basic.py b/test/solidlsp/ruby/test_ruby_basic.py index b7a4cf75..2a76b4f8 100644 --- a/test/solidlsp/ruby/test_ruby_basic.py +++ b/test/solidlsp/ruby/test_ruby_basic.py @@ -6,6 +6,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.ruby @@ -37,8 +38,36 @@ class TestRubyLanguageServer: str(repo_path / "main.rb"), 16, 17 ) # add method at line 17 (0-indexed 16), position 17 - assert len(definition_location_list) == 1 - definition_location = definition_location_list[0] + unique_locations = { + ( + location["relativePath"], + location["range"]["start"]["line"], + location["range"]["start"]["character"], + location["range"]["end"]["line"], + location["range"]["end"]["character"], + ): location + for location in definition_location_list + } + + assert len(unique_locations) == 1 + definition_location = next(iter(unique_locations.values())) print(f"Found definition: {definition_location}") assert definition_location["uri"].endswith("lib.rb") assert definition_location["range"]["start"]["line"] == 1 # add method on line 2 (0-indexed 1) + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name( + s, + whitespace_allowed=s["name"] == "<< self", + period_allowed=s["name"].startswith("self."), + ): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/rust/test_rust_basic.py b/test/solidlsp/rust/test_rust_basic.py index 8d84d736..814892d0 100644 --- a/test/solidlsp/rust/test_rust_basic.py +++ b/test/solidlsp/rust/test_rust_basic.py @@ -5,6 +5,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.rust @@ -55,3 +56,16 @@ class TestRustLanguageServer: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add missing from overview" + + @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/solidity/test_solidity_basic.py b/test/solidlsp/solidity/test_solidity_basic.py index 348c460b..e095871f 100644 --- a/test/solidlsp/solidity/test_solidity_basic.py +++ b/test/solidlsp/solidity/test_solidity_basic.py @@ -13,6 +13,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols def _find_identifier_position(file_path: Path, symbol_name: str) -> Optional[tuple[int, int]]: @@ -163,3 +164,16 @@ class TestSolidityLanguageServerBasics: ref_files = {ref.get("uri", "") for ref in references} assert any("Token.sol" in uri for uri in ref_files), "IERC20.transfer references should include Token.sol" + + @pytest.mark.parametrize("language_server", [Language.SOLIDITY], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/swift/test_swift_basic.py b/test/solidlsp/swift/test_swift_basic.py index 3f713f87..5a0d8bd0 100644 --- a/test/solidlsp/swift/test_swift_basic.py +++ b/test/solidlsp/swift/test_swift_basic.py @@ -15,6 +15,7 @@ from serena.util.text_utils import LineType from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols # Skip Swift tests on Windows due to complex GitHub Actions configuration WINDOWS_SKIP = platform.system() == "Windows" @@ -224,3 +225,16 @@ class TestSwiftProjectBasics: # Should find Status enum status_matches = [m for m in matches if "Status" in str(m)] assert len(status_matches) > 0, "Should find Status enum" + + @pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/systemverilog/test_systemverilog_basic.py b/test/solidlsp/systemverilog/test_systemverilog_basic.py index b3a8a4c5..4653a84c 100644 --- a/test/solidlsp/systemverilog/test_systemverilog_basic.py +++ b/test/solidlsp/systemverilog/test_systemverilog_basic.py @@ -5,6 +5,7 @@ This module tests Language.SYSTEMVERILOG using verible-verilog-ls. Tests are skipped if the language server is not available. """ +import shutil from typing import Any import pytest @@ -12,6 +13,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols def _find_symbol_by_name(language_server: SolidLanguageServer, file_path: str, name: str) -> dict[str, Any] | None: @@ -262,3 +264,17 @@ class TestSystemVerilogRename: for uri, file_edits in changes.items(): for edit in file_edits: assert edit["newText"] == "my_counter", f"Expected 'my_counter', got {edit['newText']}" + + @pytest.mark.skipif(shutil.which("verible-verilog-ls") is None, reason="verible-verilog-ls is not available") + @pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/terraform/test_terraform_basic.py b/test/solidlsp/terraform/test_terraform_basic.py index d608e4e5..612e3f9a 100644 --- a/test/solidlsp/terraform/test_terraform_basic.py +++ b/test/solidlsp/terraform/test_terraform_basic.py @@ -5,12 +5,18 @@ These tests validate the functionality of the language server APIs like request_references using the test repository. """ +import shutil + import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind +from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +@pytest.mark.skipif(shutil.which("terraform") is None and not is_ci, reason="Terraform CLI is not available") @pytest.mark.terraform class TestLanguageServerBasics: """Test basic functionality of the Terraform language server.""" @@ -51,3 +57,18 @@ class TestLanguageServerBasics: sel_start = var_symbol["selectionRange"]["start"] references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) assert len(references) >= 1, "variable should be referenced at least once" + + @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if s["kind"] == SymbolKind.Class: + continue + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/toml/test_toml_basic.py b/test/solidlsp/toml/test_toml_basic.py index b217aef8..522f8407 100644 --- a/test/solidlsp/toml/test_toml_basic.py +++ b/test/solidlsp/toml/test_toml_basic.py @@ -11,6 +11,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.toml @@ -233,3 +234,16 @@ class TestTomlLanguageServerBasics: strict_symbol = next((s for s in pyproject_symbols if s.get("name") == "strict"), None) if strict_symbol: assert strict_symbol.get("kind") == 17, "'strict' should have kind 17 (boolean)" + + @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/typescript/test_typescript_basic.py b/test/solidlsp/typescript/test_typescript_basic.py index 58c560f0..ba86ba04 100644 --- a/test/solidlsp/typescript/test_typescript_basic.py +++ b/test/solidlsp/typescript/test_typescript_basic.py @@ -5,6 +5,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.typescript @@ -31,3 +32,16 @@ class TestTypescriptLanguageServer: assert any("index.ts" in ref.get("relativePath", "") for ref in refs), ( "index.ts should reference helperFunction (tried all positions in selectionRange)" ) + + @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/vue/test_vue_basic.py b/test/solidlsp/vue/test_vue_basic.py index ca6a5e19..d3e05356 100644 --- a/test/solidlsp/vue/test_vue_basic.py +++ b/test/solidlsp/vue/test_vue_basic.py @@ -5,6 +5,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.vue @@ -360,3 +361,25 @@ class TestVueEdgeCases: f"Composable '{expected_composable}' should appear exactly once in directory overview. " f"Found {len(matching_files)} matches. All files: {list(composables_overview.keys())}" ) + + @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + is_callback = s["name"].endswith(" callback") + is_vue_selector = "." in s["name"] or ":" in s["name"] + is_vue_block = s["name"] in {"script setup", "style scoped"} + if has_malformed_name( + s, + whitespace_allowed=is_callback or is_vue_selector or is_vue_block, + period_allowed=is_vue_selector, + colon_allowed=is_vue_selector, + parenthesis_allowed=is_callback or is_vue_selector, + ): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/yaml_ls/test_yaml_basic.py b/test/solidlsp/yaml_ls/test_yaml_basic.py index ded7f01a..da60e487 100644 --- a/test/solidlsp/yaml_ls/test_yaml_basic.py +++ b/test/solidlsp/yaml_ls/test_yaml_basic.py @@ -11,6 +11,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.yaml @@ -175,3 +176,16 @@ class TestYAMLLanguageServerBasics: app_port = next((s for s in port_symbols if s["range"]["start"]["line"] == 4), None) assert app_port is not None, "Should find 'port' under 'app'" assert app_port["range"]["start"]["character"] == 2, "'port' should be indented 2 spaces" + + @pytest.mark.parametrize("language_server", [Language.YAML], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/zig/test_zig_basic.py b/test/solidlsp/zig/test_zig_basic.py index 12e23f8c..51b23650 100644 --- a/test/solidlsp/zig/test_zig_basic.py +++ b/test/solidlsp/zig/test_zig_basic.py @@ -13,6 +13,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.zig @@ -341,3 +342,16 @@ class TestZigLanguageServer: root = symbols[0] assert isinstance(root, dict), "Root should be a dict" assert "name" in root, "Root should have a name" + + @pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, whitespace_allowed=True): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/uv.lock b/uv.lock index 83e62d87..4151f3ae 100644 --- a/uv.lock +++ b/uv.lock @@ -216,6 +216,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] +[[package]] +name = "bottle" +version = "0.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/71/cca6167c06d00c81375fd668719df245864076d284f7cb46a694cbeb5454/bottle-0.13.4.tar.gz", hash = "sha256:787e78327e12b227938de02248333d788cfe45987edca735f8f88e03472c3f47", size = 98717, upload-time = "2025-06-15T10:08:59.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/f6/b55ec74cfe68c6584163faa311503c20b0da4c09883a41e8e00d6726c954/bottle-0.13.4-py2.py3-none-any.whl", hash = "sha256:045684fbd2764eac9cdeb824861d1551d113e8b683d8d26e296898d3dd99a12e", size = 103807, upload-time = "2025-06-15T10:08:57.691Z" }, +] + [[package]] name = "cachecontrol" version = "0.14.4" @@ -414,6 +423,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] +[[package]] +name = "clr-loader" +version = "0.2.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/61/cf819f8e8bb4d4c74661acf2498ba8d4a296714be3478d21eaabf64f5b9b/clr_loader-0.2.10-py3-none-any.whl", hash = "sha256:ebbbf9d511a7fe95fa28a95a4e04cd195b097881dfe66158dc2c281d3536f282", size = 56483, upload-time = "2026-01-03T23:13:05.439Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -1730,6 +1751,93 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, +] + [[package]] name = "platformdirs" version = "4.9.4" @@ -1773,6 +1881,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] +[[package]] +name = "proxy-tools" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/cf/77d3e19b7fabd03895caca7857ef51e4c409e0ca6b37ee6e9f7daa50b642/proxy_tools-0.1.0.tar.gz", hash = "sha256:ccb3751f529c047e2d8a58440d86b205303cf0fe8146f784d1cbcd94f0a28010", size = 2978, upload-time = "2014-05-05T21:02:24.606Z" } + [[package]] name = "psutil" version = "7.0.0" @@ -2065,11 +2179,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/10/e8192be5f38f3e8e7e046716de4cae33d56fd5ae08927a823bb916be36c1/pyjwt-2.12.0.tar.gz", hash = "sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02", size = 102511, upload-time = "2026-03-12T17:15:30.831Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, + { url = "https://files.pythonhosted.org/packages/15/70/70f895f404d363d291dcf62c12c85fdd47619ad9674ac0f53364d035925a/pyjwt-2.12.0-py3-none-any.whl", hash = "sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e", size = 29700, upload-time = "2026-03-12T17:15:29.257Z" }, ] [package.optional-dependencies] @@ -2077,6 +2191,104 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pyobjc-core" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, + { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, + { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, + { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, +] + +[[package]] +name = "pyobjc-framework-quartz" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ef/dcd22b743e38b3c430fce4788176c2c5afa8bfb01085b8143b02d1e75201/pyobjc_framework_quartz-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19f99ac49a0b15dd892e155644fe80242d741411a9ed9c119b18b7466048625a", size = 217795, upload-time = "2025-11-14T09:59:46.922Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9b/780f057e5962f690f23fdff1083a4cfda5a96d5b4d3bb49505cac4f624f2/pyobjc_framework_quartz-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7730cdce46c7e985535b5a42c31381af4aa6556e5642dc55b5e6597595e57a16", size = 218798, upload-time = "2025-11-14T10:00:01.236Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206, upload-time = "2025-11-14T10:00:15.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317, upload-time = "2025-11-14T10:00:30.703Z" }, + { url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558, upload-time = "2025-11-14T10:00:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" }, +] + +[[package]] +name = "pyobjc-framework-security" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/aa/796e09a3e3d5cee32ebeebb7dcf421b48ea86e28c387924608a05e3f668b/pyobjc_framework_security-12.1.tar.gz", hash = "sha256:7fecb982bd2f7c4354513faf90ba4c53c190b7e88167984c2d0da99741de6da9", size = 168044, upload-time = "2025-11-14T10:22:06.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/3d/8d3a39cd292d7c76ab76233498189bc7170fc80f573b415308464f68c7ee/pyobjc_framework_security-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b2d8819f0fb7b619ec7627a0d8c1cac1a57c5143579ce8ac21548165680684b", size = 41287, upload-time = "2025-11-14T10:02:54.491Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/5160c0f938fc0515fe8d9af146aac1b093f7ef285ce797fedae161b6c0e8/pyobjc_framework_security-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab42e55f5b782332be5442750fcd9637ee33247d57c7b1d5801bc0e24ee13278", size = 41280, upload-time = "2025-11-14T10:02:58.097Z" }, + { url = "https://files.pythonhosted.org/packages/32/48/b294ed75247c5cfa00d51925a10237337d24f54961d49a179b20a4307642/pyobjc_framework_security-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:afc36661cc6eb98cd794bed1d6668791e96557d6f72d9ac70aa49022d26af1d4", size = 41284, upload-time = "2025-11-14T10:03:01.722Z" }, + { url = "https://files.pythonhosted.org/packages/ef/57/0d3ef78779cf5c3bba878b2f824137e50978ad4a21dabe65d8b5ae0fc0d1/pyobjc_framework_security-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9510c98ab56921d1d416437372605cc1c1f6c1ad8d3061ee56b17bf423dd5427", size = 42162, upload-time = "2025-11-14T10:03:05.337Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/63c15f9449c191e7448a05ff8af4a82c39a51bb627bc96dc9697586c0f79/pyobjc_framework_security-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6319a34508fd87ab6ca3cda6f54e707196197a65b792b292705af967e225438a", size = 41348, upload-time = "2025-11-14T10:03:08.926Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d8/5aaa2a8124ed04a9d6ca7053dc0fa64e42be51497ed8263a24b744a95598/pyobjc_framework_security-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:03d166371cefdef24908825148eb848f99ee2c0b865870a09dcbb94334dd3e0a", size = 42908, upload-time = "2025-11-14T10:03:13.01Z" }, +] + +[[package]] +name = "pyobjc-framework-uniformtypeidentifiers" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/b8/dd9d2a94509a6c16d965a7b0155e78edf520056313a80f0cd352413f0d0b/pyobjc_framework_uniformtypeidentifiers-12.1.tar.gz", hash = "sha256:64510a6df78336579e9c39b873cfcd03371c4b4be2cec8af75a8a3d07dff607d", size = 17030, upload-time = "2025-11-14T10:23:02.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/5f/1f10f5275b06d213c9897850f1fca9c881c741c1f9190cea6db982b71824/pyobjc_framework_uniformtypeidentifiers-12.1-py2.py3-none-any.whl", hash = "sha256:ec5411e39152304d2a7e0e426c3058fa37a00860af64e164794e0bcffee813f2", size = 4901, upload-time = "2025-11-14T10:05:51.532Z" }, +] + +[[package]] +name = "pyobjc-framework-webkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/10/110a50e8e6670765d25190ca7f7bfeecc47ec4a8c018cb928f4f82c56e04/pyobjc_framework_webkit-12.1.tar.gz", hash = "sha256:97a54dd05ab5266bd4f614e41add517ae62cdd5a30328eabb06792474b37d82a", size = 284531, upload-time = "2025-11-14T10:23:40.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/37/5082a0bbe12e48d4ffa53b0c0f09c77a4a6ffcfa119e26fa8dd77c08dc1c/pyobjc_framework_webkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3db734877025614eaef4504fadc0fbbe1279f68686a6f106f2e614e89e0d1a9d", size = 49970, upload-time = "2025-11-14T10:07:01.413Z" }, + { url = "https://files.pythonhosted.org/packages/db/67/64920c8d201a7fc27962f467c636c4e763b43845baba2e091a50a97a5d52/pyobjc_framework_webkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:af2c7197447638b92aafbe4847c063b6dd5e1ed83b44d3ce7e71e4c9b042ab5a", size = 50084, upload-time = "2025-11-14T10:07:05.868Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/80d36280164c69220ce99372f7736a028617c207e42cb587716009eecb88/pyobjc_framework_webkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1da0c428c9d9891c93e0de51c9f272bfeb96d34356cdf3136cb4ad56ce32ec2d", size = 50096, upload-time = "2025-11-14T10:07:10.027Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7a/03c29c46866e266b0c705811c55c22625c349b0a80f5cf4776454b13dc4c/pyobjc_framework_webkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1a29e334d5a7dd4a4f0b5647481b6ccf8a107b92e67b2b3c6b368c899f571965", size = 50572, upload-time = "2025-11-14T10:07:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ac/924878f239c167ffe3bfc643aee4d6dd5b357e25f6b28db227e40e9e6df3/pyobjc_framework_webkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:99d0d28542a266a95ee2585f51765c0331794bca461aaf4d1f5091489d475179", size = 50210, upload-time = "2025-11-14T10:07:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/637cda4983dc0936b73a385f3906256953ac434537b812814cb0b6d231a2/pyobjc_framework_webkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1aaa3bf12c7b68e1a36c0b294d2728e06f2cc220775e6dc4541d5046290e4dc8", size = 50680, upload-time = "2025-11-14T10:07:23.331Z" }, +] + [[package]] name = "pyright" version = "1.1.403" @@ -2090,6 +2302,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" }, ] +[[package]] +name = "pystray" +version = "0.19.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, + { name = "python-xlib", marker = "sys_platform == 'linux'" }, + { name = "six" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/64/927a4b9024196a4799eba0180e0ca31568426f258a4a5c90f87a97f51d28/pystray-0.19.5-py2.py3-none-any.whl", hash = "sha256:a0c2229d02cf87207297c22d86ffc57c86c227517b038c0d3c59df79295ac617", size = 49068, upload-time = "2023-09-17T13:44:26.872Z" }, +] + [[package]] name = "pytest" version = "8.4.1" @@ -2161,6 +2387,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] +[[package]] +name = "python-xlib" +version = "0.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185, upload-time = "2022-12-25T18:52:58.662Z" }, +] + +[[package]] +name = "pythonnet" +version = "3.1.0rc0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "clr-loader" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/59/4295652a9a93a2bd6755ecce2dc32e3d3c704e7b8ef301946ee65c818a59/pythonnet-3.1.0rc0.tar.gz", hash = "sha256:5f0f505e100233d70864b00dc5f5dd760603cfd94ac64da5c8b0e15ed95af939", size = 241025, upload-time = "2026-02-25T10:46:53.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/3d/a378aa74085fc6f72923d5e39fc30efbc97541cfd49b3fe0a35f20d09ff7/pythonnet-3.1.0rc0-py3-none-any.whl", hash = "sha256:286ccf23d08e1cc2b5200bb91d0ee649436d2e8386fec36a85193c66d0ff8b5b", size = 293150, upload-time = "2026-02-25T10:46:48.535Z" }, +] + +[[package]] +name = "pywebview" +version = "6.2.dev50+g345596c1f" +source = { git = "https://github.com/r0x0r/pywebview?rev=345596c1f4fd59fd26c7aa02d4176c5362bcbabe#345596c1f4fd59fd26c7aa02d4176c5362bcbabe" } +dependencies = [ + { name = "bottle" }, + { name = "proxy-tools" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-uniformtypeidentifiers", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-webkit", marker = "sys_platform == 'darwin'" }, + { name = "pythonnet", marker = "sys_platform == 'win32'" }, + { name = "qtpy", marker = "sys_platform == 'openbsd6'" }, + { name = "typing-extensions" }, +] + [[package]] name = "pywin32" version = "311" @@ -2273,6 +2541,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] +[[package]] +name = "qtpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982, upload-time = "2025-02-11T15:09:25.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" }, +] + [[package]] name = "referencing" version = "0.37.0" @@ -2645,8 +2925,11 @@ dependencies = [ { name = "psutil" }, { name = "pydantic" }, { name = "pyright" }, + { name = "pystray" }, { name = "python-dotenv" }, { name = "python-multipart" }, + { name = "pythonnet", marker = "sys_platform == 'win32'" }, + { name = "pywebview" }, { name = "pyyaml" }, { name = "regex" }, { name = "requests" }, @@ -2720,13 +3003,16 @@ requires-dist = [ { name = "pyasn1", marker = "extra == 'dev'", specifier = "==0.6.3" }, { name = "pydantic", specifier = "==2.12.5" }, { name = "pyinstrument", marker = "extra == 'dev'", specifier = "==5.1.1" }, - { name = "pyjwt", marker = "extra == 'dev'", specifier = "==2.10.1" }, + { name = "pyjwt", marker = "extra == 'dev'", specifier = "==2.12.0" }, { name = "pyright", specifier = "==1.1.403" }, + { name = "pystray", specifier = "==0.19.5" }, { name = "pytest", marker = "extra == 'dev'", specifier = "==8.4.1" }, { name = "pytest-timeout", marker = "extra == 'dev'", specifier = "==2.4.0" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = "==3.8.0" }, { name = "python-dotenv", specifier = "==1.2.1" }, { name = "python-multipart", specifier = "==0.0.22" }, + { name = "pythonnet", marker = "sys_platform == 'win32'", specifier = "==3.1.0rc0" }, + { name = "pywebview", git = "https://github.com/r0x0r/pywebview?rev=345596c1f4fd59fd26c7aa02d4176c5362bcbabe" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "regex", specifier = "==2026.2.28" }, { name = "requests", specifier = "==2.33.0" },