diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 55932a4..4eabeeb 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -6,6 +6,7 @@ on: - main paths: - "playground/**" + - ".github/workflows/playground.yml" workflow_dispatch: permissions: @@ -36,6 +37,9 @@ jobs: uses: actions/configure-pages@v6 - run: npm ci + - name: Build pinned WASM backends + run: npm run fetch-wasm + - run: npm run test:wasm - run: npm run build - uses: actions/upload-pages-artifact@v5 diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 5e48725..856d3a0 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -17,6 +17,10 @@ jobs: fail-fast: false matrix: python-version: ["3.12", "3.13", "3.14"] + pyright-package: ["basedpyright", "pyright"] + env: + PYRIGHT_PACKAGE: ${{ matrix.pyright-package }} + UV_PYTHON: ${{ matrix.python-version }} steps: - uses: actions/checkout@v7 @@ -27,12 +31,12 @@ jobs: - name: Set up Python ${{ matrix.python-version }} run: uv python install ${{ matrix.python-version }} - name: Install dependencies - run: uv sync --all-extras + run: uv sync --all-extras --locked - name: Setup Node.js environment uses: actions/setup-node@v7.0.0 - - name: Install basedpyright globally - run: npm install -g basedpyright + - name: Install Pyright distribution + run: npm install -g "$PYRIGHT_PACKAGE" env: NPM_CONFIG_PREFIX: ~/.npm-global diff --git a/CLAUDE.md b/CLAUDE.md index d7217ff..7ab6bcc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Maintenance + +Follow `docs/FEATURE_VERIFICATION.md` for release updates, including separate +Microsoft Pyright/basedpyright checks and browser WASM verification. Keep the README +concise; dated evidence and detailed comparisons belong in `docs/`. + ## Essential Commands Always use `uv` for Python operations: @@ -118,7 +124,7 @@ This is a minimal-dependency Python library providing typed LSP (Language Server **Generation Process:** 1. `download_schemas.py`: Fetches latest schemas from upstream 2. `datamodel-codegen`: Converts JSON schema to TypedDict definitions, pinned to - `--formatters black isort`, then ruff-formats its own output + `--formatters black isort --disable-timestamp`, then ruff-formats its own output 3. `generate.py`: Orchestrates final type file generation with utilities in `assets/scripts/utils/` 4. Every file in the Makefile's `GENERATED_FILES` is ruff-formatted and `--fix`ed so regenerating produces no spurious diff diff --git a/Makefile b/Makefile index debcc32..f2c0dac 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,7 @@ generate-lsp-schema: --use-field-description \ --use-schema-description \ --use-double-quotes \ + --disable-timestamp \ --formatters black isort uvx ruff format assets/scripts/lsp_schema.py @@ -56,6 +57,7 @@ generate-pyright-schema: --use-field-description \ --use-schema-description \ --use-double-quotes \ + --disable-timestamp \ --formatters black isort uvx ruff format lsp_types/pyright/config_schema.py diff --git a/README.md b/README.md index 09dae41..faa5e44 100644 --- a/README.md +++ b/README.md @@ -5,192 +5,96 @@ [![Tests](https://github.com/Mazyod/lsp-python-types/actions/workflows/python-tests.yml/badge.svg)](https://github.com/Mazyod/lsp-python-types/actions/workflows/python-tests.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -_Publish the excellent work of [Sublime LSP](https://github.com/sublimelsp/lsp-python-types) as a PyPI package._ - -lsp-types-splash - - -__LSP Types__ is a Python package that aims to provide a fully typed interface to Language Server Protocol (LSP) interactions. It can be used to simply utilize the types, or to interact with an LSP server over stdio. - -The library has minimal dependencies (`tomlkit` for TOML config serialization). - -## Installation +Typed Python interfaces for the Language Server Protocol, with async sessions +and process pooling for Python language servers. Built on +[Sublime LSP’s generated types](https://github.com/sublimelsp/lsp-python-types). +Python 3.12+; one runtime dependency, `tomlkit`. + +## Meet your party + +Pixel-art party: Pyright the blue sentinel, Pyrefly the coral artificer, ty the green scout, and Zuban the purple diplomat. + +- **[Pyright — the sentinel](docs/research/landscape.md#pyright--the-veteran).** + Broad typing support and configurable execution environments. Equip the + **basedpyright** fork for extra diagnostics, baselines and semantic highlighting; + those extras are not part of Microsoft Pyright. +- **[Pyrefly — the artificer](docs/research/pyrefly.md).** + A growing toolbelt: configurable regex and `mock.patch` checks, framework knowledge, + and experimental tensor/DataFrame analysis. Some tools need explicit settings; + experimental APIs can change. +- **[ty — the scout](docs/research/landscape.md#ty--the-swift-scout).** + Built for quick incremental feedback, explanatory diagnostics and precise type + narrowing. In this adapter, hover favors the type alone and completion resolution + is unavailable. +- **[Zuban — the diplomat](docs/research/landscape.md#zuban--the-bridge-builder).** + Bridges Mypy workflows and editor inference for untyped code. Compatibility modes + are its specialty; value-constrained generic bodies and unused ignores remain + checking blind spots. + +These are personalities, not speed rankings. The linked field notes separate +upstream features from what this library actually tests. + +## Start a session ```sh -pip install lsp-types -``` - -## Usage - -Using the LSP types: - -```python -import lsp_types - -# Use the types +pip install "lsp-types[pyrefly]" # Or [ty] / [zuban] ``` -Using an LSP process through stdio: - -> [!TIP] -> Recommend using [basedpyright](https://github.com/DetachHead/basedpyright) for extended features. - ```python -from lsp_types.process import LSPProcess, ProcessLaunchInfo - -process_info = ProcessLaunchInfo(cmd=[ - "pyright-langserver", "--stdio" -]) +import asyncio +from pathlib import Path +from tempfile import TemporaryDirectory -async with LSPProcess(process_info) as process: - # Initialize the process - ... - - # Grab a typed listener - diagnostics_listener = process.notify.on_publish_diagnostics(timeout=1.0) - - # Send a notification (`await` is optional. It ensures messages have been drained) - await process.notify.did_open_text_document(...) - - # Wait for diagnostics to come in - diagnostics = await diagnostics_listener -``` - -`LSPProcess.stop()` is terminal — including the implicit stop() when the `async with` -block exits. Calling `start()` on a stopped process raises `RuntimeError` instead -of relaunching the server, and requests and notifications sent through it raise -`RuntimeError` too (notifications are no longer dropped with a warning). The -messages name the state they came from (`LSP process has been stopped` vs. `LSP -process has not been started`). Construct a new `LSPProcess` when you need to -restart a server. - -## LSPs - -The following LSPs are available out of the box: - -- [Pyright](https://github.com/microsoft/pyright) -- [Pyrefly](https://github.com/facebook/pyrefly) -- [ty](https://github.com/astral-sh/ty) - Astral's fast Python type checker -- [Zuban](https://github.com/zubanls/zuban) - Rust-based type checker + LSP by the author of Jedi - -### Pyrefly CLI tools - -This library drives Pyrefly's LSP server (`pyrefly lsp`), but Pyrefly also ships a broader -standalone CLI worth knowing about (verified with Pyrefly 1.2.0): - -| Command | What it does | -|---------|--------------| -| `pyrefly init` | Scaffold a `pyrefly.toml` (or `[tool.pyrefly]` in `pyproject.toml`), or **migrate an existing mypy/pyright config** to Pyrefly | -| `pyrefly check` | Full type check of a file or project | -| `pyrefly snippet ` | Type-check an inline code snippet | -| `pyrefly infer` | Automatically add inferred type annotations to a file or directory | -| `pyrefly coverage` | Type-coverage reporting commands | -| `pyrefly suppress` | Add ignore comments for existing errors, or remove unused ignores | -| `pyrefly stubgen` | Generate `.pyi` stub files from Python source | -| `pyrefly dump-config` | Print Pyrefly's resolved configuration | -| `pyrefly tsp` | Start a TSP (Type Server Protocol) server (new in 1.2.x) | - -Run `pyrefly --help` for details, or see the [Pyrefly docs](https://pyrefly.org/). - -## Feature Support Matrix - -### Legend - -| Symbol | Meaning | -|--------|---------| -| :white_check_mark: | Fully supported | -| :warning: | Partial support (see notes) | -| :x: | Not supported | -| :grey_question: | Not tested / Not exposed in API | - -### Features by Backend - -> Last verified: Pyrefly 1.2.0, ty 0.0.75, Zuban 0.9.2 (basedpyright: CI only, unpinned `npm install -g basedpyright`) - -| Feature | Pyright | Pyrefly | ty | Zuban | Notes | -|---------|:-------:|:-------:|:--:|:-----:|-------| -| Diagnostics | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | -| Hover | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | ty shows type only, not variable name | -| Completion | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | -| Completion Resolution | :white_check_mark: | :x: | :x: | :white_check_mark: | Pyrefly: no-op (returns item unchanged); ty: not supported (`-32601`) | -| Signature Help | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | -| Rename | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | -| Semantic Tokens | :white_check_mark:\* | :white_check_mark:\*\* | :white_check_mark: | :white_check_mark: | \*basedpyright recommended; \*\*Pyrefly: legend not advertised (see docs) | -| Go to Definition | :grey_question: | :grey_question: | :grey_question: | :grey_question: | Not exposed in Session API | -| Find References | :grey_question: | :grey_question: | :grey_question: | :grey_question: | Not exposed in Session API | -| Code Actions | :grey_question: | :grey_question: | :grey_question: | :grey_question: | Not exposed in Session API | -| Formatting | :grey_question: | :grey_question: | :grey_question: | :grey_question: | Not exposed in Session API | - -> See [Feature Verification Guide](docs/FEATURE_VERIFICATION.md) for methodology on maintaining this table. - -For detailed documentation: -- [Semantic Tokens Reference](docs/SEMANTIC_TOKENS.md) - Token types and modifiers for Monaco/editor integration -- [Pyrefly Known Limitations](lsp_types/pyrefly/KNOWN_LIMITATIONS.md) -- [ty Known Limitations](lsp_types/ty/KNOWN_LIMITATIONS.md) -- [Zuban Known Limitations](lsp_types/zuban/KNOWN_LIMITATIONS.md) - -### Pyright Example - -```python from lsp_types import Session -from lsp_types.pyright.backend import PyrightBackend - -async def test_pyright_session(): - code = """\ -def greet(name: str) -> str: - return 123 -""" - - session = await Session.create(PyrightBackend(), initial_code=code) - diagnostics = await session.get_diagnostics() +from lsp_types.pyrefly.backend import PyreflyBackend + +async def main(): + with TemporaryDirectory() as workspace: + session = await Session.create( + PyreflyBackend(), + base_path=Path(workspace), + initial_code='answer: int = "oops"', + ) + try: + print(await session.get_diagnostics()) + await session.update_code("answer: int = 42") + print(await session.get_diagnostics()) # [] + finally: + await session.shutdown() + +asyncio.run(main()) +``` - assert diagnostics != [] +Swap in `TyBackend`, `ZubanBackend`, or `PyrightBackend`. For Pyright, install +Node.js and `npm install -g pyright` (or `basedpyright`) separately. +Sessions write backend configuration into `base_path`; use a dedicated workspace +as above. For types alone, `import lsp_types`; no server is needed. - code = """\ -def greet(name: str) -> str: - return f"Hello, {name}" -""" +## What works here - await session.update_code(code) - diagnostics = await session.get_diagnostics() - assert diagnostics == [] +Diagnostics, hover, completion, signature help and rename pass across all four +backends. Semantic tokens work with **basedpyright**, Pyrefly, ty and Zuban; +Microsoft Pyright does not provide them. Completion resolution enriches results +with Pyright/basedpyright and Zuban; Pyrefly echoes the item, while ty rejects it. - await session.shutdown() -``` +Verified **2026-09-11**: Pyright **1.1.414**, basedpyright **1.40.1**, +Pyrefly **1.3.0**, ty **0.0.80**, Zuban **0.9.3**. -After `shutdown()`, a session's operational methods raise `RuntimeError`; its -captured server and semantic-token metadata remain readable. Calling -`shutdown()` while other operations are in flight is safe: it waits up to five -seconds for them to finish, and if any are still running it stops the language -server process instead of returning it to the pool, keeping stale operations -out of the next session's protocol stream. (One narrow exception: cancelling -an operation ends its in-flight accounting even if a notification write it -already queued is still being flushed.) +[Feature evidence & maintenance runbook](docs/FEATURE_VERIFICATION.md) · +[Semantic tokens](docs/SEMANTIC_TOKENS.md) · +[Low-level API & lifecycle](docs/USAGE.md) · +[Maintenance results](docs/MAINTENANCE_2026-09-11.md) ## Development -- Requires Python 3.12+. -- Requires `uv` for dev dependencies. - -Generate latest types in one go: ```sh +uv sync --all-extras --locked +npm install -g basedpyright +uv run pytest +uvx pyright --pythonpath .venv/bin/python +uvx ruff check . make generate-latest-types ``` -Download the latest json schema: -```sh -make download-schemas -``` - -Generate the types: -```sh -make generate-types -``` - -Copy the `lsp_types/types.py` file to your project. - -NOTE: Do not import types that begin with `__`. These types are internal types and are not meant to be used. - -### TODOs - -- Support server request handlers. +The [runbook](docs/FEATURE_VERIFICATION.md) covers testing Microsoft Pyright +separately, regenerating schemas and updating the browser playground. diff --git a/assets/images/README.md b/assets/images/README.md new file mode 100644 index 0000000..b7d9088 --- /dev/null +++ b/assets/images/README.md @@ -0,0 +1,9 @@ +# LSP party artwork + +`lsp-party.png` was generated with the built-in imagegen tool on 2026-09-11. +The characters are editorial metaphors, not official mascots or benchmark scores. +Left to right: Pyright the sentinel, Pyrefly the artificer, ty the scout, Zuban the diplomat. + +## Generation prompt + +Use case: stylized-concept. Asset type: compact GitHub README banner for a Python LSP library. Create a polished pixel-art adventuring party, exactly four equally prominent friendly fantasy characters in a single horizontal row, full bodies, crisp square pixel edges, carefully limited 16-bit palette, dark midnight-blue backdrop with subtle tiny stars and one shared ground line. Wide 3:1 composition, no cards, no grids, no stats, no title. Left to right: (1) blue-and-silver sentinel with shield and a small book, steady confident posture; (2) coral-and-gold artificer with goggles, tool belt and glowing tiny mechanical firefly; (3) mint-and-teal hooded scout with light boots, compass and flowing scarf, energetic posture; (4) purple-and-cream diplomat scholar holding two differently colored scrolls joined by a ribbon, warm expression. Give each a distinct silhouette. Pixel lettering below each, exact text: "Pyright", "Pyrefly", "ty", "Zuban". No other text, logos, watermark, rankings or health bars. Generous margins and strong legibility when displayed at 760px wide; charming, professional indie-game sprite artwork. diff --git a/assets/images/lsp-party.png b/assets/images/lsp-party.png new file mode 100644 index 0000000..4c9f6f9 Binary files /dev/null and b/assets/images/lsp-party.png differ diff --git a/assets/lsprotocol/lsp.json b/assets/lsprotocol/lsp.json index fc68d8d..85244c4 100644 --- a/assets/lsprotocol/lsp.json +++ b/assets/lsprotocol/lsp.json @@ -3899,6 +3899,10 @@ { "kind": "reference", "name": "WorkDoneProgressParams" + }, + { + "kind": "reference", + "name": "PartialResultParams" } ], "documentation": "A parameter literal used in inline value requests.\n\n@since 3.17.0", @@ -3950,6 +3954,10 @@ { "kind": "reference", "name": "WorkDoneProgressParams" + }, + { + "kind": "reference", + "name": "PartialResultParams" } ], "documentation": "A parameter literal used in inlay hint requests.\n\n@since 3.17.0", @@ -4358,6 +4366,10 @@ { "kind": "reference", "name": "WorkDoneProgressParams" + }, + { + "kind": "reference", + "name": "PartialResultParams" } ], "documentation": "A parameter literal used in inline completion requests.\n\n@since 3.18.0", diff --git a/assets/lsps/pyrefly-guide.md b/assets/lsps/pyrefly-guide.md index d184c12..319752f 100644 --- a/assets/lsps/pyrefly-guide.md +++ b/assets/lsps/pyrefly-guide.md @@ -1,382 +1,69 @@ -# Pyrefly: A Developer's Guide to Programmatic Usage and LSP Integration +# Pyrefly integration reference -This guide documents practical insights gained from implementing a proof of concept with Pyrefly, Facebook's fast Python type checker and Language Server Protocol (LSP) server written in Rust. - -## Overview - -Pyrefly is a modern alternative to traditional Python type checkers like mypy, offering: - -- **Performance**: Written in Rust for speed on large codebases -- **LSP Support**: Built-in Language Server Protocol implementation -- **Editor Integration**: Works with VS Code, Neovim, Emacs, Vim, and other LSP-compatible editors -- **Type Checking**: Advanced static analysis with comprehensive diagnostics - -## Command Line Interface - -### Basic Usage - -Pyrefly provides several commands accessible via its CLI: - -```bash -# Type check a single file -pyrefly check main.py - -# Type check entire project -pyrefly check - -# Start LSP server -pyrefly lsp - -# Check a code snippet -pyrefly snippet "def hello(name: str) -> str: return f'Hello {name}'" - -# Initialize configuration -pyrefly init - -# Display configuration -pyrefly dump-config -``` - -### Key CLI Options - -- `--verbose`: Enable detailed logging -- `--threads N`: Control parallelization (0 = auto) -- `--color`: Control colored output (auto/always/never) - -## LSP Server Implementation - -### Starting the LSP Server - -```bash -# Basic LSP server -pyrefly lsp - -# With verbose logging -pyrefly lsp --verbose - -# With specific indexing mode -pyrefly lsp --indexing-mode lazy-non-blocking-background -``` - -### Indexing Modes - -Pyrefly supports different indexing strategies: - -- `none`: Disable indexing (disables find-refs, etc.) -- `lazy-non-blocking-background`: Index in background thread (default) -- `lazy-blocking`: Index in main thread (blocks IDE services) - -### LSP Communication Protocol - -The LSP server communicates via JSON-RPC over stdin/stdout. Here's the basic flow: - -#### 1. Initialize Connection - -```json -{ - "jsonrpc": "2.0", - "id": 1, - "method": "initialize", - "params": { - "processId": 12345, - "rootUri": "file:///path/to/project", - "capabilities": { - "textDocument": { - "hover": {"contentFormat": ["markdown", "plaintext"]}, - "completion": {}, - "definition": {}, - "references": {} - } - } - } -} -``` - -#### 2. Send Initialized Notification - -```json -{ - "jsonrpc": "2.0", - "method": "initialized", - "params": {} -} -``` - -#### 3. Open Document - -```json -{ - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": { - "textDocument": { - "uri": "file:///path/to/file.py", - "languageId": "python", - "version": 1, - "text": "def hello(name: str) -> str:\n return f'Hello {name}'" - } - } -} -``` - -#### 4. Request Features - -```json -// Hover information -{ - "jsonrpc": "2.0", - "id": 2, - "method": "textDocument/hover", - "params": { - "textDocument": {"uri": "file:///path/to/file.py"}, - "position": {"line": 0, "character": 4} - } -} - -// Code completion -{ - "jsonrpc": "2.0", - "id": 3, - "method": "textDocument/completion", - "params": { - "textDocument": {"uri": "file:///path/to/file.py"}, - "position": {"line": 1, "character": 10} - } -} -``` - -### Python LSP Client Implementation - -Here's a minimal Python client for interacting with Pyrefly LSP: - -```python -import subprocess -import json -import os - -class PyreflyLSPClient: - def __init__(self, project_root: str): - self.project_root = project_root - self.process = None - - def start_server(self): - """Start Pyrefly LSP server process.""" - self.process = subprocess.Popen( - ["pyrefly", "lsp", "--verbose"], - cwd=self.project_root, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - bufsize=0 - ) - - def send_message(self, message: dict): - """Send JSON-RPC message to LSP server.""" - content = json.dumps(message) - content_length = len(content.encode('utf-8')) - - # LSP requires Content-Length header - full_message = f"Content-Length: {content_length}\r\n\r\n{content}" - - self.process.stdin.write(full_message) - self.process.stdin.flush() - - def initialize(self): - """Initialize LSP connection.""" - init_msg = { - "jsonrpc": "2.0", - "id": 1, - "method": "initialize", - "params": { - "processId": os.getpid(), - "rootUri": f"file://{self.project_root}", - "capabilities": { - "textDocument": { - "hover": {"contentFormat": ["markdown"]}, - "completion": {}, - "definition": {}, - "references": {} - } - } - } - } - - self.send_message(init_msg) - - # Send initialized notification - self.send_message({ - "jsonrpc": "2.0", - "method": "initialized", - "params": {} - }) -``` - -## Programmatic Diagnostics - -### Running Type Checks Programmatically - -```python -import subprocess -import json - -def run_pyrefly_check(file_path: str) -> tuple[int, str, str]: - """Run Pyrefly type check and return results.""" - result = subprocess.run( - ["pyrefly", "check", file_path], - capture_output=True, - text=True - ) - - return result.returncode, result.stdout, result.stderr - -# Usage -exit_code, stdout, stderr = run_pyrefly_check("main.py") -if exit_code == 0: - print("No type errors found") -else: - print(f"Type errors detected:\n{stderr}") -``` - -### Parsing Diagnostic Output - -Pyrefly outputs diagnostics in a structured format. Here's how to parse them: - -```python -import re -from typing import List, Dict, Any - -def parse_pyrefly_diagnostics(stderr: str) -> List[Dict[str, Any]]: - """Parse Pyrefly diagnostic output into structured data.""" - diagnostics = [] - - # Pattern for Pyrefly error messages - error_pattern = r'(.+):(\d+):(\d+): (.+)' - - for line in stderr.strip().split('\n'): - if 'ERROR' in line or 'WARN' in line: - match = re.search(error_pattern, line) - if match: - diagnostics.append({ - 'file': match.group(1), - 'line': int(match.group(2)), - 'column': int(match.group(3)), - 'message': match.group(4), - 'severity': 'error' if 'ERROR' in line else 'warning' - }) - - return diagnostics -``` +Verified with **Pyrefly 1.3.0 on 2026-09-11**. The backend's typed configuration +is maintained in `lsp_types/pyrefly/config_schema.py`; this document is a short +reference, not a generated or exhaustive upstream schema. ## Configuration -### Project Configuration - -Pyrefly looks for configuration in `pyrefly.toml` at the project root. However, during testing, many standard configuration options weren't recognized, suggesting the configuration format is still evolving. - -### Working Configuration Approach - -Instead of relying on configuration files, use command-line options for consistent behavior: - -```bash -# Recommended approach -pyrefly lsp --verbose --threads 4 -``` - -### Environment Variables +Pyrefly reads `pyrefly.toml` or `[tool.pyrefly]` in `pyproject.toml`. +`PyreflyBackend` writes `pyrefly.toml`, converting top-level Python snake_case +keys to TOML kebab-case. Error-code keys inside `errors` should already use +their upstream spelling. For example: -Pyrefly respects these environment variables: - -- `PYREFLY_THREADS`: Number of threads for parallelization -- `PYREFLY_COLOR`: Color output control -- `PYREFLY_VERBOSE`: Enable verbose logging - -## Editor Integration Examples - -### VS Code - -Install the official Pyrefly extension from the marketplace, or configure manually: - -```json -{ - "python.languageServer": "Pyrefly", - "pyrefly.args": ["--verbose"] -} -``` - -### Neovim (with nvim-lspconfig) - -```lua -require('lspconfig').pyrefly.setup{ - cmd = {"pyrefly", "lsp"}, - settings = { - pyrefly = { - -- Add any Pyrefly-specific settings here - } - } +```python +from lsp_types import Session +from lsp_types.pyrefly.backend import PyreflyBackend +from lsp_types.pyrefly.config_schema import Model + +options: Model = { + "preset": "strict", + "python_version": "3.13", + "search_path": ["src"], + "check_unannotated_defs": True, + "infer_return_types": "checked", + "errors": {"regex": "error", "missing-attribute-patch-target": "warn"}, } +# In an async function: +# session = await Session.create(PyreflyBackend(), options=options) ``` -### Emacs (with Eglot) - -```elisp -(add-to-list 'eglot-server-programs - '(python-mode . ("pyrefly" "lsp"))) -``` +Errors accept severity strings (`error`, `warn`, `info`, `ignore`) and legacy +booleans. `untyped_def_behavior` remains accepted but is deprecated upstream; +use `check_unannotated_defs` and `infer_return_types` for new configurations. +Plain dictionaries passed to `Session.create(options=...)` can carry options +not yet represented in `Model`. -## Performance Considerations +An unconfigured CLI project may use the minimal `basic` preset or migrate +nearby mypy/Pyright configuration in memory. Explicit configuration makes +comparisons reproducible. `Session.create()` writes a configuration file, +including an empty file when given no options, so its behavior need not match +an unconfigured CLI invocation. See the +[configuration reference](https://pyrefly.org/en/docs/configuration/) and +[1.3.0 configuration implementation](https://github.com/facebook/pyrefly/blob/1.3.0/crates/pyrefly_config/src/base.rs). -### Indexing Strategy +## LSP and CLI -Choose indexing mode based on your needs: +The library starts `pyrefly lsp` and communicates over JSON-RPC stdio. +The backend forwards `verbose`, `threads`, and `indexing_mode` as launch flags. +Indexing modes are `none`, `lazy-non-blocking-background` (default), and +`lazy-blocking` (useful for deterministic testing). Disabling indexing limits +features such as references. Pyrefly supports virtual documents opened with +`textDocument/didOpen`; files need not already exist on disk. -- **Development**: Use `lazy-non-blocking-background` for responsiveness -- **CI/Testing**: Use `lazy-blocking` for deterministic behavior -- **Large Projects**: Consider `none` if find-references isn't critical +The standalone CLI also offers configuration migration (`init`), annotation +inference (`infer`), type coverage (`coverage`), ignore management (`suppress`), +stub generation (`stubgen`), and an independent Type Server Protocol (`tsp`) +server. These are not wrapped by this library's `Session` API. -### Threading - -- Default (0) uses automatic thread detection -- Set to 1 for sequential execution -- Higher values can improve performance on large codebases - -## Troubleshooting - -### Common Issues - -1. **Module Import Errors**: Ensure Pyrefly can find your Python environment -2. **LSP Connection Issues**: Check that stdin/stdout aren't being used by other processes -3. **Configuration Warnings**: Many config options may not be supported yet -4. **Performance**: Adjust indexing mode and thread count for your workflow - -### Debugging LSP Communication - -Enable verbose mode and monitor stderr for detailed protocol messages: +Use structured output for programmatic CLI diagnostics: ```bash -pyrefly lsp --verbose 2> lsp-debug.log +pyrefly check --output-format=json main.py +pyrefly dump-config main.py +pyrefly lsp --help ``` -## Key Learnings - -1. **Fast Performance**: Pyrefly is noticeably faster than traditional Python type checkers -2. **LSP-First Design**: Built from the ground up with LSP support in mind -3. **Minimal Configuration**: Works well with minimal setup, unlike some alternatives -4. **Active Development**: Configuration options and features are still evolving -5. **Editor Agnostic**: Works with any LSP-compatible editor -6. **Rust Performance**: Benefits from Rust's performance characteristics for large codebases - -## Best Practices - -1. Start with minimal configuration and add complexity as needed -2. Use verbose mode during development and debugging -3. Choose appropriate indexing mode for your workflow -4. Monitor performance with different thread settings -5. Keep the LSP server process isolated from your main application -6. Use programmatic access for CI/CD integration - -## Resources - -- [Pyrefly GitHub Repository](https://github.com/facebook/pyrefly) -- [Language Server Protocol Specification](https://microsoft.github.io/language-server-protocol/) -- [LSP Client Libraries](https://microsoft.github.io/language-server-protocol/implementors/sdks/) \ No newline at end of file +See [IDE features](https://pyrefly.org/en/docs/IDE-features/), +[integration limitations](../../lsp_types/pyrefly/KNOWN_LIMITATIONS.md), and +[the 1.3.0 release](https://github.com/facebook/pyrefly/releases/tag/1.3.0). diff --git a/assets/scripts/lsp_schema.py b/assets/scripts/lsp_schema.py index 65c966c..ce93e1e 100644 --- a/assets/scripts/lsp_schema.py +++ b/assets/scripts/lsp_schema.py @@ -1,6 +1,5 @@ # generated by datamodel-codegen: # filename: lsp.schema.json -# timestamp: 2026-08-30T19:54:45+00:00 from __future__ import annotations diff --git a/docs/FEATURE_VERIFICATION.md b/docs/FEATURE_VERIFICATION.md index 2611d7e..72d38cd 100644 --- a/docs/FEATURE_VERIFICATION.md +++ b/docs/FEATURE_VERIFICATION.md @@ -1,101 +1,159 @@ -# Feature Verification Guide +# Feature verification and maintenance runbook -This document describes how to verify and update the feature support matrix in the README. +Run this after backend releases, dependency updates or changes to `Session`. +Keep the README to a short overview; put the evidence and qualifications here. -## Verification Process +## 1. Update and record versions -### Step 1: Run the Test Suite +Check live PyPI/npm metadata and tagged upstream release notes. Search indexes can +lag a release. Prefer stable releases; distinguish an LSP binary from a CLI, +editor extension, TSP server or browser WASM package. -```bash -uv run pytest tests/test_session.py -v +```sh +uv sync --all-extras --upgrade +uv run pyrefly --version +uv run ty --version +uv run zuban --version +npm view pyright version +npm view basedpyright version ``` -Tests are parametrized across all backends (Pyright, Pyrefly, ty, Zuban). Key indicators: -- **PASSED**: Feature works for that backend -- **XFAIL**: Known limitation, documented with reason -- **FAILED**: Regression or new issue +Commit `uv.lock` with the dependency changes. Minimum supported versions in +`pyproject.toml` are compatibility floors, not the versions verified in this run; +raise them only if a change requires it. Generate schemas from upstream with: -### Step 2: Review xfail Markers - -In `tests/`, search for `xfail` to find documented limitations: - -```python -# Example from test_pool.py::test_session_warmup_on_recycle: -if backend_name == "ty": - pytest.xfail("ty hover doesn't include variable names in output") +```sh +make generate-latest-types ``` -Each xfail message explains why the feature is limited. - -### Step 3: Check Backend Capabilities - -Each backend declares its LSP capabilities in `get_lsp_capabilities()`: -- `lsp_types/pyright/backend.py` -- `lsp_types/pyrefly/backend.py` -- `lsp_types/ty/backend.py` -- `lsp_types/zuban/backend.py` - -Features declared here indicate what the client advertises to the server. - -### Step 4: Review Known Limitations - -- [Pyrefly Known Limitations](../lsp_types/pyrefly/KNOWN_LIMITATIONS.md) -- [ty Known Limitations](../lsp_types/ty/KNOWN_LIMITATIONS.md) -- [Zuban Known Limitations](../lsp_types/zuban/KNOWN_LIMITATIONS.md) - -## Updating the Feature Table +If `make` is unavailable, execute its recipes directly (`uv run` / `uvx`). +Review the schema and generated diff; don't edit generated types manually. Repeat +generation to check reproducibility. The recipes disable timestamps and explicitly +select Black/isort before Ruff formatting. -### When to Update +## 2. Test both Pyright distributions -1. **After adding new Session API methods**: Add test, run it, update table -2. **After upgrading backend versions**: Re-run tests, check for improvements, update version line -3. **After backend releases announce new features**: Test and document +`PyrightBackend` launches `pyright-langserver`. Both npm distributions provide +that executable, so install them in **separate directories**: -### Updating Version Numbers - -Update the "Last verified" line in README when re-testing: - -```bash -# Check installed versions -pyright --version # or basedpyright --version -pyrefly --version -ty --version -zuban --version +```sh +npm install --prefix /tmp/lsp-basedpyright basedpyright@1.40.1 +npm install --prefix /tmp/lsp-pyright pyright@1.1.414 +PATH=/tmp/lsp-basedpyright/node_modules/.bin:$PATH uv run pytest tests -q +PYRIGHT_PACKAGE=pyright PATH=/tmp/lsp-pyright/node_modules/.bin:$PATH \ + uv run pytest tests -k Pyright -q +uvx pyright --pythonpath .venv/bin/python . +uvx ruff check . ``` -### Status Symbols - -| Symbol | Meaning | When to Use | -|--------|---------|-------------| -| :white_check_mark: | Fully supported | Test passes without xfail | -| :warning: | Partial support | Test has xfail or conditional skip | -| :x: | Not supported | Feature fails or is documented as unsupported | -| :grey_question: | Unknown | Not exposed in Session API or not tested | - -### Adding Notes +Update those exact npm versions on the next maintenance run. `PYRIGHT_PACKAGE` +selects the tests' expected Microsoft behavior; it does not choose the executable. +The default test expectation is basedpyright. CI tests both distributions on Python +3.12, 3.13 and 3.14; npm versions float there, while Python installs use the lock. + +Review every failure, xfail and skip. A passing echo of a completion item does not +prove resolution adds documentation. Similarly, client capabilities describe what +**we advertise**, not what the server implements. Inspect `initialize` responses +and send a real request with a control case when validating a claim. + +## Verified Session features + +Snapshot **2026-09-11**: Pyright 1.1.414, basedpyright 1.40.1, Pyrefly 1.3.0, +ty 0.0.80, Zuban 0.9.3. “Yes” means the named integration test passes for its +fixture, not comprehensive conformance for every Python program. + +| Feature | Pyright | basedpyright | Pyrefly | ty | Zuban | +|---|---|---|---|---|---| +| Diagnostics | Yes | Yes | Yes | Yes | Yes | +| Hover | Yes | Yes | Yes | Type only | Yes | +| Completion | Yes | Yes | Yes | Yes | Yes | +| Completion resolution | Adds docs | Adds docs | Echo | Error -32601 | Adds docs | +| Signature help | Yes | Yes | Yes | Yes | Yes | +| Rename | Yes | Yes | Yes | Yes | Yes | +| Semantic tokens | Error -32601 | Yes | Fallback legend | Yes | Yes | + +Evidence lives in `tests/test_session.py`: `test_session_diagnostics`, +`test_session_hover`, `test_session_completion`, `test_session_signature_help`, +`test_session_rename`, and the `test_session_semantic_tokens*` tests. Completion +resolution checks actual added docstrings, the Pyrefly echo and ty's error. +Microsoft Pyright omits optional `serverInfo` and semantic-token metadata; +metadata/recycling tests explicitly cover that behavior. + +The single expected failure is `test_session_warmup_on_recycle[ty]`, whose +variable-name assertion does not match ty's type-only hover. Normal hover works. + +Go to definition, references, code actions, formatting and broader upstream +features are **not verified by this Session suite**. The low-level `LSPProcess` +API exposes requests, but exposure alone does not establish server support. + +## 3. Recheck the integration boundaries + +```sh +PATH=/tmp/lsp-basedpyright/node_modules/.bin:$PATH \ + uv run python examples/extract_semantic_legends.py +rg -n 'xfail|skip' tests +``` -- Keep notes concise (under 50 characters) -- Reference the specific limitation (e.g., "shows type only, not variable name") -- Use semicolons to separate multiple backend notes +Compare legends with [the token reference](SEMANTIC_TOKENS.md) and tagged source. +Pyrefly 1.3 still omits its provider from initialization; it needs the fallback +legend. `tests/test_semantic_tokens.py` checks that its five new string modifiers +survive normalization, including a live server fixture. Append canonical entries +so existing editor indices stay stable. ty 0.0.80 appends `operator` and `regexp` +token types, already covered by the canonical legend. + +Reprobe versioned limitations with positive controls before advancing their dates: +[Pyrefly](../lsp_types/pyrefly/KNOWN_LIMITATIONS.md), +[ty](../lsp_types/ty/KNOWN_LIMITATIONS.md), +[Zuban](../lsp_types/zuban/KNOWN_LIMITATIONS.md). +Check manual config schemas against tagged source/docs, including severity values, +nested sections and renamed/deprecated settings. Keep historical evidence labeled. + +## 4. Refresh the browser playground + +From `playground/`: + +```sh +npm update +npm audit +npm run fetch-wasm +npm run test:wasm +npm run build +``` -## Evidence Mapping +Review the pinned releases/checksums in `fetch-wasm.sh`; `npm update` alone cannot +update WASM or the browser-basedpyright CDN worker. ty's source build needs a Rust +toolchain, the WASM target and a native compiler. Pyrefly has a release WASM archive. +Verify actual browser diagnostics, hover and edits for every engine; successful +TypeScript compilation does not prove workers or WASM load. Test the configured +GitHub Pages base path and confirm the build includes `dist/pyrefly` and `dist/ty` assets. + +For the optional browser regression check, install Playwright outside the project +and run the production preview in another terminal: + +```sh +npm install --prefix /tmp/lsp-browser-check playwright +/tmp/lsp-browser-check/node_modules/.bin/playwright install chromium +npm run preview -- --host 127.0.0.1 +# In another terminal, from playground/: +PLAYWRIGHT_MODULE=/tmp/lsp-browser-check/node_modules/playwright/index.mjs \ + node browser.test.mjs +``` -| Feature | Test Function | What to Check | -|---------|---------------|---------------| -| Diagnostics | `test_session_diagnostics` | All backends pass; virtual documents work everywhere (ty since 0.0.16) | -| Hover | `test_session_hover` | All backends pass; `if backend_name != "ty"` guards the variable-name assertion (ty shows type only) | -| Completion | `test_session_completion` | All backends pass | -| Completion Resolution | `test_session_completion` | `if backend_name != "ty"` skips resolution for ty only (it errors `-32601`). Pyrefly is invoked but only its echo is asserted, which is why the README still marks it unsupported | -| Signature Help | `test_session_signature_help` | No xfails expected | -| Rename | `test_session_rename` | All backends pass; Pyrefly rename was fixed in 1.1.1 (the former xfail is removed) | -| Semantic Tokens | `test_session_semantic_tokens` | No xfails expected | +The deterministic concurrency check uses Vite's dev modules to control completion +order, including late diagnostics, edits during debounce and adapter disposal: -## Untested Features +```sh +npm run dev -- --host 127.0.0.1 +# In another terminal, from playground/: +PLAYWRIGHT_MODULE=/tmp/lsp-browser-check/node_modules/playwright/index.mjs \ + node concurrency.test.mjs +``` -Features declared in backend capabilities but not exposed in Session API: -- Go to Definition (Pyrefly, ty, Zuban declare it) -- Find References (Pyrefly, ty, Zuban declare it) -- Code Actions -- Formatting +## 5. Publish the evidence in the docs -To test these, use the low-level `LSPProcess` API directly. +Update the README versions and short profiles, this feature snapshot, semantic +legends and limitation dates. Save a dated maintenance report with commands, +results, unresolved issues and environment details. Keep broader capabilities, +release sources and benchmark caveats in the [field guide](research/landscape.md) +and [Pyrefly notes](research/pyrefly.md). Do not turn upstream marketing benchmarks +or test-suite elapsed time into a speed leaderboard. diff --git a/docs/INTEGRATION_NOTES.md b/docs/INTEGRATION_NOTES.md index bbd08a7..bac08bc 100644 --- a/docs/INTEGRATION_NOTES.md +++ b/docs/INTEGRATION_NOTES.md @@ -136,10 +136,18 @@ new monaco.lsp.MonacoLspClient(transport); - Per-backend implementations (~200 lines each) - Dependencies: `vscode-languageserver-protocol`, `vscode-jsonrpc` +**Current artifact check (2026-09-11):** installed `monaco-editor` is still +0.56.0. The public declaration still exposes only `constructor(transport)` and +no `dispose()`; the shipped LSP client hardcodes `rootUri: null`, omits +`initializationOptions`, and discards the feature disposable store. The two +migration blockers below remain. The playground's existing adapters were built +and smoke-tested in Chromium; the other issue statuses below are historical, +not freshly verified. + **Blockers / caveats (re-verified against v0.56.0 — 2026-08-30):** -Verification was documentary only: there is no npm on the machine, so nothing was -installed or executed. Evidence came from the published npm artifacts (`monaco.d.ts` +The August 30 verification was documentary only: npm was unavailable on that +machine, so nothing was installed or executed in that earlier run. Evidence came from the published npm artifacts (`monaco.d.ts` and the shipped `esm/external/monaco-lsp-client/out/index.js` for 0.55.1 and 0.56.0, fetched via CDN), the `monaco-lsp-client/` source at `main`, and the issue trackers. The decisive check: diffing the shipped LSP bundle 0.55.1 -> 0.56.0 yields 50 lines — diff --git a/docs/MAINTENANCE_2026-09-11.md b/docs/MAINTENANCE_2026-09-11.md new file mode 100644 index 0000000..7e95632 --- /dev/null +++ b/docs/MAINTENANCE_2026-09-11.md @@ -0,0 +1,99 @@ +# Maintenance — September 11, 2026 + +## Release snapshot + +Checked live registries and tagged releases, rather than cached search results. +Python lockfile: Pyrefly **1.2.0 → 1.3.0**, ty **0.0.75 → 0.0.80**, +Zuban **0.9.2 → 0.9.3**. Separately installed Microsoft Pyright **1.1.414** and +basedpyright **1.40.1**. Refreshed all resolvable Python dependencies, including +datamodel-code-generator **0.76.0 → 0.79.0**, and declared its formatter extras. +Compatibility floors remain unchanged; the lock records the tested versions. + +Browser packages: browser-basedpyright **1.28.1 → 1.40.1**, TypeScript **7.0.2**, +Vite **8.3.0**, JSON-RPC **9.0.2**, LSP protocol **3.18.3**, DOMPurify **3.4.15**. +Monaco remains **0.56.0**. Pyrefly WASM is pinned to **1.3.0** and ty's WASM source +to **0.0.80**, both with SHA-256 checks. The browser UI correctly names basedpyright. + +Release sources and comparisons: [field guide](research/landscape.md), +[Pyrefly investigation](research/pyrefly.md). + +## What the investigation changed + +- **Highlighting:** Pyrefly 1.3 emits five new string modifiers. Appended them to + its fallback legend and the canonical legend, preserving existing indices. + A wire-bit regression check and live server test cover all five. ty now + advertises `operator` and `regexp` token types; the canonical legend already + handles both. Updated the [token reference](SEMANTIC_TOKENS.md). +- **Configuration:** updated Pyrefly severities, presets, inference, multi-platform + settings and baselines; ty import-analysis controls, strictness, per-file + overrides, script exclusion and output formats; Zuban `auto` mode. Refreshed + backend limitation documents with positive-control probes and explicitly + marked older evidence. Removed obsolete Pyrefly configuration advice. +- **Pyright distinction:** Microsoft Pyright lacks semantic tokens and omits + optional server metadata that basedpyright supplies. Four old test assumptions + were specific to the fork. Tests now check the correct behavior for each; + CI explicitly selects both distributions and each Python interpreter. +- **Protocol generation:** upstream adds partial-result tokens to inline values, + inlay hints and inline completion requests. Regenerated the public types and + removed generated timestamps so repeated builds produce identical files. +- **Browser integration:** fixed WASM copying/URLs, repeated ty logger setup, + inconsistent Python targets and diagnostic races around edits/backend switches. + CI now fetches/builds and smoke-tests pinned WASM before producing the site. +- **Presentation:** replaced the long README with a ~425-word overview and one + pixel-art party banner. Usage details and the feature matrix moved into docs. + [Artwork and generation prompt](../assets/images/README.md) are in the repo. + +Pyrefly's configured regex/mock-target checks caught two errors that strict +Pyright did not in the same CLI probe. This is evidence for specific extra +checks, not an overall winner. ty's incremental architecture and type narrowing, +Zuban's Mypy migration/editor inference, and basedpyright's controls provide more +useful personalities than invented performance or reliability scores. + +## Validation + +Host: Linux x86_64, Python **3.12.14**. Commands use `uv`; npm servers were installed +in separate temporary prefixes, with the intended binary first in `PATH`. + +- Full suite with basedpyright: **251 passed, 1 xfailed**. The expected exception + is ty's variable-name hover assertion; ordinary hover passes. +- Microsoft Pyright separately: **35 passed** (`PYRIGHT_PACKAGE=pyright`, `-k Pyright`). +- `uvx pyright --pythonpath .venv/bin/python .`: **0 errors, warnings or information**. +- Ruff lint and formatting checks: **passed**; `git diff --check`: **passed**. +- `uv build`: wheel and sdist built successfully; no package version bump or publication. +- Schema generation: all five generated files reproduced **byte-for-byte**. + `make` was unavailable, so the Makefile's recipes were run directly. +- Backend probes confirmed completion-documentation enrichment with Pyright, + basedpyright and Zuban, Pyrefly's echo even after stripping metadata, and ty's + `-32601` rejection. The suite now checks these behaviors explicitly. +- WASM tests: **2 passed**, exercising errors, hover and clearing after edits. +- Playground production build: **passed**; `npm audit`: **0 vulnerabilities**; + `npm outdated`: **no entries**. +- Chromium **153** / Playwright **1.63.0**: rendered diagnostics, hover and clearing + passed across basedpyright → Pyrefly → ty → Pyrefly → ty → basedpyright, including + repeated WASM initialization, with no console/page errors. Rapid backend + selection also preserved the final choice. +- `concurrency.test.mjs`: **passed** controlled late-result, edit-during-debounce, + adapter-switch/detach, diagnostic-version, superseded-request, timeout and + disposal checks. Timed-out requests no longer return old cached diagnostics. + +The [runbook](FEATURE_VERIFICATION.md) contains reproducible commands, including +the optional browser regression script without a new project dependency. + +## Limits and follow-up + +Local Python testing used 3.12; the revised CI covers 3.12, 3.13 and 3.14, but a +remote CI run was not triggered. The artwork and browser assets were inspected +locally; no deployment was performed. basedpyright's browser worker still loads +from pinned jsDelivr URLs. ty WASM requires Rust and a native compiler; this run +built it in an isolated Node 24 container. Its optimized WASM is about 18 MB, +and Pyrefly's about 14 MB; generated modules stay ignored and CI rebuilds them. + +The existing Monaco dynamic-import bundler warning remains harmless. The +code generator also emits a formatter deprecation warning even with the extras +explicitly declared; generated output and validation succeed. Monaco's native +LSP client still lacks configurable initialization and disposal in the installed +0.56.0 artifact, so migration remains deferred (see [integration notes](INTEGRATION_NOTES.md)). + +These checks establish the tested integration behaviors, not full conformance or +an independent speed leaderboard. CLI, LSP, TSP and browser WASM features remain +separate in the evidence. No backend is labeled universally fastest or fragile. diff --git a/docs/SEMANTIC_TOKENS.md b/docs/SEMANTIC_TOKENS.md index 5ebb003..c451ba1 100644 --- a/docs/SEMANTIC_TOKENS.md +++ b/docs/SEMANTIC_TOKENS.md @@ -49,9 +49,13 @@ See `examples/extract_semantic_legends.py` for a complete working example. ## Token Legends by Backend -### Pyright (basedpyright) +Microsoft Pyright 1.1.414 does **not** provide semantic tokens. The Pyright-family +legend below belongs to the separate basedpyright fork, which uses the same backend. +These are LSP legends, independent of the playground’s WASM APIs. -> Last verified: basedpyright 1.36.2 +### basedpyright (through PyrightBackend) + +> Last verified: basedpyright 1.40.1 (2026-09-11) #### Token Types @@ -91,8 +95,8 @@ See `examples/extract_semantic_legends.py` for a complete working example. ### Pyrefly -> Last verified: Pyrefly 1.2.0 (2026-08-30) -> Legend source: [semantic_tokens.rs](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/state/semantic_tokens.rs) +> Last verified: Pyrefly 1.3.0 (2026-09-11) +> Legend source: [semantic_tokens.rs](https://github.com/facebook/pyrefly/blob/1.3.0/pyrefly/lib/state/semantic_tokens.rs) Pyrefly does not advertise its legend via LSP initialization, but the token mappings are defined in source code. @@ -139,12 +143,17 @@ Pyrefly does not advertise its legend via LSP initialization, but the token mapp | 8 | `documentation` | | 9 | `defaultLibrary` | | 10 | `selfParameter` | +| 11 | `byteString` | +| 12 | `formatString` | +| 13 | `rawString` | +| 14 | `stringPrefix` | +| 15 | `templateString` | --- ### ty -> Last verified: ty 0.0.75 (2026-08-30) +> Last verified: ty 0.0.80 (2026-09-11) #### Token Types @@ -165,6 +174,8 @@ Pyrefly does not advertise its legend via LSP initialization, but the token mapp | 12 | `decorator` | | 13 | `builtinConstant` | | 14 | `typeParameter` | +| 15 | `operator` | +| 16 | `regexp` | #### Token Modifiers @@ -179,7 +190,7 @@ Pyrefly does not advertise its legend via LSP initialization, but the token mapp ### Zuban -> Last verified: Zuban 0.9.2 (2026-08-30) +> Last verified: Zuban 0.9.3 (2026-09-11) Zuban advertises its legend via LSP initialization (follows LSP 3.17 standard ordering for the 23 token types it emits). @@ -308,10 +319,11 @@ The canonical legend follows LSP standard ordering, with backend-specific tokens - 23: label (LSP standard) - 24-26: Backend-specific (selfParameter, clsParameter, builtinConstant) -**Token Modifiers (bit 0-13):** +**Token Modifiers (bit 0-18):** - 0-9: LSP standard modifiers (declaration, definition, readonly, static, deprecated, abstract, async, modification, documentation, defaultLibrary) - 10-12: Backend-specific from Pyright (builtin, classMember, parameter) - 13: Backend-specific from Pyrefly (selfParameter) +- 14-18: Pyrefly 1.3 string modifiers (byteString, formatString, rawString, stringPrefix, templateString); appended so existing indices stay stable --- diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..896e915 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,50 @@ +# Usage details + +## Low-level stdio + +> [!TIP] +> Recommend using [basedpyright](https://github.com/DetachHead/basedpyright) for extended features. + +```python +from lsp_types.process import LSPProcess, ProcessLaunchInfo + +process_info = ProcessLaunchInfo(cmd=[ + "pyright-langserver", "--stdio" +]) + +async with LSPProcess(process_info) as process: + # Initialize the process + ... + + # Grab a typed listener + diagnostics_listener = process.notify.on_publish_diagnostics(timeout=1.0) + + # Send a notification (`await` is optional. It ensures messages have been drained) + await process.notify.did_open_text_document(...) + + # Wait for diagnostics to come in + diagnostics = await diagnostics_listener +``` + +`LSPProcess.stop()` is terminal — including the implicit stop() when the `async with` +block exits. Calling `start()` on a stopped process raises `RuntimeError` instead +of relaunching the server, and requests and notifications sent through it raise +`RuntimeError` too (notifications are no longer dropped with a warning). The +messages name the state they came from (`LSP process has been stopped` vs. `LSP +process has not been started`). Construct a new `LSPProcess` when you need to +restart a server. + + +## Session lifecycle + +After `shutdown()`, a session's operational methods raise `RuntimeError`; its +captured server and semantic-token metadata remain readable. Calling +`shutdown()` while other operations are in flight is safe: it waits up to five +seconds for them to finish, and if any are still running it stops the language +server process instead of returning it to the pool, keeping stale operations +out of the next session's protocol stream. (One narrow exception: cancelling +an operation ends its in-flight accounting even if a notification write it +already queued is still being flushed.) + + +Internal generated types whose names start with `__` are not public API. diff --git a/docs/research/landscape.md b/docs/research/landscape.md new file mode 100644 index 0000000..09cb094 --- /dev/null +++ b/docs/research/landscape.md @@ -0,0 +1,150 @@ +# Python language servers: a field guide + +Research snapshot: **2026-09-11**. These are upstream capabilities and design +choices, not a claim that every feature is exposed by this library's `Session` +API. See [feature verification](../FEATURE_VERIFICATION.md) for local evidence. +The README's characters are playful descriptions of priorities, not performance +ratings or predictions of reliability. + +## Release snapshot + +- **Pyright 1.1.414**, September 9: typing fixes, type-equality optimization, + and publication of `pyright-typeserver` to npm. + [Release notes](https://github.com/microsoft/pyright/releases/tag/1.1.414). +- **basedpyright 1.40.1**, September 10: merges Pyright 1.1.414 and improves + multiline builtin docstring display. + [Release notes](https://github.com/DetachHead/basedpyright/releases/tag/v1.40.1). +- **ty 0.0.80**, September 9: fixes hangs during bursts of inlay-hint requests, + gives autofixes more descriptive names, and improves typing and memory usage. + Since the previous local 0.0.75 snapshot, 0.0.76 also added a preview rule for + missing direct dependencies and improved PEP 723 script environments. + [0.0.80](https://github.com/astral-sh/ty/releases/tag/0.0.80), + [0.0.76](https://github.com/astral-sh/ty/releases/tag/0.0.76). +- **Zuban 0.9.3**, September 2: deterministic file processing and a fix for + quadratic behavior involving literals. + [Release](https://github.com/zubanls/zuban/releases/tag/v0.9.3), + [changelog](https://docs.zubanls.com/en/latest/changelog.html). + +The versions above were checked against live package registries and explicit +release tags. Cached GitHub `/releases/latest` pages returned older versions for +Zuban and basedpyright during this run; search-result dates alone are insufficient. + +## Pyright — the veteran + +Pyright remains a broad, configurable type checker. Its execution environments +let different project subdirectories target different Python versions, platforms, +and import paths. Its language server includes call hierarchy, navigation, +rename, completions and stub generation. These make a useful established +reference without implying that it wins every comparison. +[Pyright features at 1.1.414](https://github.com/microsoft/pyright/blob/1.1.414/docs/features.md). + +**Tradeoff:** Pyright, Pylance and basedpyright are different products. In +particular, semantic highlighting, inlay hints and import quick fixes are among +the Pylance features independently implemented by basedpyright; a successful +basedpyright test does not establish vanilla Pyright support. +[basedpyright's LSP additions](https://docs.basedpyright.com/latest/benefits-over-pyright/pylance-features/). + +## basedpyright — the guardian with adjustable armor + +This fork adds stricter defaults and finer diagnostic controls to the Pyright +family. Its recommended mode enables all diagnostic rules, uses warnings for +some checks, and checks all platforms by default. Expect adoption to surface +more issues unless the project config relaxes those defaults. +[Defaults](https://docs.basedpyright.com/latest/benefits-over-pyright/better-defaults/). + +Its baseline records existing errors so new work can face stricter checks +without first fixing the whole codebase. Baselines work in both CLI and LSP; +matching is imperfect when code moves. Editor additions include enum and +non-string literal completions, automatic `@override` insertion, and configurable +hint severity. These are concrete customization strengths, not just parity. +[Baseline](https://docs.basedpyright.com/latest/benefits-over-pyright/baseline/), +[editor improvements](https://docs.basedpyright.com/latest/benefits-over-pyright/language-server-improvements/). + +## ty — the swift scout + +ty's architecture prioritizes fine-grained incremental analysis: edits should +invalidate only the computations that depend on them. Its diagnostics emphasize +context and explanations. That makes quick feedback and understandable errors +a more useful identity than simply “written in Rust.” +[Astral's design and benchmark discussion](https://astral.sh/blog/ty). + +The type system also has its own character: it permits variable redeclarations, +uses intersections for narrowing, and reasons about reachability using inferred +types. Its `hasattr` analysis accounts for subclasses adding an attribute. +Explicit `ty_extensions.Intersection` annotations are ty-specific and currently +available only during type checking; the internal inference benefits do not +require adopting those annotations. +[Type-system examples](https://docs.astral.sh/ty/features/type-system/). + +Configuration offers per-file rule and analysis overrides, selective treatment +of third-party imports, and opt-in stricter equality and generic narrowing. +The last two default to false: extra theoretical precision can produce types +that are less convenient for everyday code. +[Configuration reference](https://docs.astral.sh/ty/reference/configuration/). + +**Tradeoff:** this integration has its own documented completion-resolution, +hover-format and file-watching constraints. A missing client feature is not a +general verdict on ty's editor support. Consult the +[locally verified limitations](../../lsp_types/ty/KNOWN_LIMITATIONS.md). + +## Zuban — the bridge builder + +Zuban's distinctive combination is Mypy migration and help with untyped code. +Its Mypy mode preserves familiar configuration and behavior, while native mode +checks untyped functions and infers their returns. The LSP's automatic mode +selection is influenced by project configuration; this library writes +`[tool.zuban]`, which selects native mode unless explicitly overridden. +[Modes and configuration](https://docs.zubanls.com/en/latest/usage.html#modes). + +Since 0.8.0, editor heuristics follow call sites to improve completion, hover, +navigation and signatures even where the checker still sees `Any`. This is an +explicit distinction between editor assistance and type-checking semantics. +Earlier releases added Django model support, notebook support and completion +documentation resolution. +[Changelog](https://docs.zubanls.com/en/latest/changelog.html). + +**Tradeoff:** upstream still documents unchecked bodies of functions with +value-constrained TypeVars and missing unused-ignore diagnostics. General Mypy +plugin compatibility is not planned; targeted library support is different. +Zuban currently uses one CPU core and targets low memory consumption. Call/type +hierarchy and file-rename import updates are absent from its documented LSP +capabilities. These are more actionable limits than calling it “less capable.” +[Capabilities and missing features](https://docs.zubanls.com/en/latest/features.html). + +## Reading performance claims fairly + +Astral's December 2025 announcement reports separate cold CLI and incremental +LSP measurements: Home Assistant checking without cache, and diagnostic +recomputation after an edit in PyTorch on an M4. Its striking multipliers describe +those workloads and historical versions; they are not current universal rankings. +[Benchmark context](https://astral.sh/blog/ty). + +Zuban's homepage claims substantial speedups over Mypy and lower CPU/memory than +ty and Pyrefly. These are upstream claims, not measurements made by this project. +Likewise, passing over 95% of the *relevant Mypy tests* is a specific compatibility +measure, not a score for LSP quality or all Python programs. +[Zuban's overview](https://docs.zubanls.com/en/latest/). + +A useful local comparison must fix versions, interpreter, config and workload; +separate process startup, cold project checking and warm edits; check equivalent +diagnostic work; and report repeated measurements with machine details. The +integration suite establishes functionality, not a speed leaderboard. No README +character implies “fastest,” “slow,” or “breaks easily.” + +## Maintenance findings + +The upstream review led to these maintenance changes: + +- ty's typed config now includes analysis import controls and strictness options, + per-file analysis overrides and script exclusions. Its output-format choices + and warning-exit default were refreshed against the + [reference](https://docs.astral.sh/ty/reference/configuration/). +- Zuban's typed mode choices now include `auto`, added in 0.9.0. Fresh live probes + confirmed that its constrained-TypeVar and unused-ignore limitations remain. + [Changelog](https://docs.zubanls.com/en/latest/changelog.html). +- Vanilla Pyright and basedpyright now have independent test runs. Editor + extension features are no longer credited to vanilla Pyright through a shared + backend name. +- TSP is separate from this library's LSP integration. Pyright's newest release + also publishes a type server, so that category is not exclusive to Pyrefly. + [Pyright 1.1.414](https://github.com/microsoft/pyright/releases/tag/1.1.414). diff --git a/docs/research/pyrefly.md b/docs/research/pyrefly.md new file mode 100644 index 0000000..2b7c6ec --- /dev/null +++ b/docs/research/pyrefly.md @@ -0,0 +1,91 @@ +# Pyrefly: the artificer with a growing toolkit + +Research checked **2026-09-11**. Character metaphor describes specialization, +not a performance ranking or a claim about reliability. + +## Release and direction + +**1.3.0** is the current stable release. Its notes say September 10; GitHub +published it September 11 at 00:42 UTC, following the first PyPI wheel at +00:41 UTC. Cached search results still showed 1.2.0, so the live +[PyPI metadata](https://pypi.org/pypi/pyrefly/json) and +[GitHub release metadata](https://api.github.com/repos/facebook/pyrefly/releases/latest) +were checked directly. + +The distinctive direction is broader code understanding. Version 1.3 adds +literal-regex and `mock.patch` validation, same-file Django reverse relations, +SQLAlchemy update checks, and PyTorch registered attributes. Its LSP adds Change +Signature, wider workspace symbol search, and cross-file hierarchy/reference +support without opening each file. Tensor-shape checking for JAX/NumPy/PyTorch +and DataFrame schema checking remain **experimental**; the previous +`@shaped_array` API was removed. These are upstream release claims, not all +features independently tested here. [1.3.0 release notes](https://github.com/facebook/pyrefly/releases/tag/1.3.0) + +## A small, reproducible difference + +This CLI probe was run with Pyrefly **1.3.0** and Pyright **1.1.414**, using +temporary files and explicit configurations: + +```python +import re +from unittest.mock import patch +re.compile("[") +patch("math.nonexistent_symbol") +``` + +With the following `pyrefly.toml`, `pyrefly check probe.py` reported both an +invalid regex and a missing patch target: + +```toml +[errors] +regex = "error" +missing-attribute-patch-target = "error" +``` + +`pyright --project . probe.py`, with `{"typeCheckingMode": "strict"}` in +`pyrightconfig.json`, reported **0 errors and 0 warnings**. This demonstrates +two particular checks, not comparative correctness across Python. An +unconfigured Pyrefly snippet reported no errors: enable the intended checks +explicitly when reproducing the comparison. These results concern CLI +diagnostics; the same fixture was not tested through LSP in this research. + +## Keep the interfaces separate + +The installed `pyrefly --help` confirms `infer`, `coverage`, `suppress`, +`stubgen`, configuration migration via `init`, and `tsp` alongside `lsp`. +This library starts **`pyrefly lsp`**; it does not wrap those CLI commands or +TSP. Upstream advertises richer editor refactorings and navigation than this +library's high-level `Session` methods currently expose; low-level LSP access +is a separate path. [IDE feature documentation](https://pyrefly.org/en/docs/IDE-features/) + +The release's striking TSP speedups measure repeated requests in a captured +Pylance session. They are neither Pyrefly-versus-Pyright timings nor this +library's LSP latency. Do not turn them into README speed multipliers. +[Performance notes](https://github.com/facebook/pyrefly/releases/tag/1.3.0) + +## Upgrade findings for this integration + +- This maintenance extends the hardcoded semantic-token and canonical modifier lists with + `byteString`, `formatString`, `rawString`, `stringPrefix`, `templateString`; + the previous run already detected these in 1.3 prereleases. A fresh 1.3.0 + LSP probe confirmed the missing advertised legend and completion-resolution + echo, including when detail and documentation were stripped first. +- The typed configuration now includes current severity strings in + `ErrorConfig`, `preset`, `check_unannotated_defs`, `infer_return_types`, + `replace_untyped_imports_with_any`, and useful baseline settings. + `untyped_def_behavior` remains for compatibility but is marked deprecated. The tagged + implementation explicitly names its replacements. + [1.3.0 base configuration](https://github.com/facebook/pyrefly/blob/1.3.0/crates/pyrefly_config/src/base.rs), + [baseline configuration](https://github.com/facebook/pyrefly/blob/1.3.0/crates/pyrefly_config/src/config.rs), + [severity definitions](https://github.com/facebook/pyrefly/blob/1.3.0/crates/pyrefly_config/src/error_kind.rs) +- The historical `assets/lsps/pyrefly-guide.md` advice that config files + are unreliable has been replaced. The supported files are `pyrefly.toml` and + `[tool.pyrefly]` in `pyproject.toml`; CLI JSON output also makes its regex + stderr parser unnecessary. More inference can analyze more dependency + modules, so depth has a cost. [Configuration documentation](https://pyrefly.org/en/docs/configuration/) + +## Character choice + +Pyrefly's artificer carries a toolbelt and glowing firefly: a metaphor for broader +code analysis and specialized tools, with experimental features clearly marked. +It is not a score for speed, robustness or overall correctness. diff --git a/lsp_types/pyrefly/KNOWN_LIMITATIONS.md b/lsp_types/pyrefly/KNOWN_LIMITATIONS.md index dd34b63..e061f06 100644 --- a/lsp_types/pyrefly/KNOWN_LIMITATIONS.md +++ b/lsp_types/pyrefly/KNOWN_LIMITATIONS.md @@ -1,49 +1,46 @@ -# Pyrefly Backend - Known Limitations +# Pyrefly backend: known limitations -This document describes known limitations and behavioral differences when using the Pyrefly backend compared to other LSP backends (Pyright, ty). +Verified with **Pyrefly 1.3.0 on 2026-09-11** using this library's LSP client. +Release notes are dated September 10; PyPI and GitHub publication occurred +September 11 UTC. [Release](https://github.com/facebook/pyrefly/releases/tag/1.3.0) -## 1. Completion Item Resolution Is a No-op +## Completion resolution is an echo -**Limitation**: Pyrefly accepts the `completionItem/resolve` LSP request but returns the item unchanged. +`completionItem/resolve` returns the submitted item unchanged, despite +advertising `completionProvider.resolveProvider: true`. A live probe resolved +a method completion both normally and with `detail`/`documentation` removed; +both responses exactly matched their respective inputs. -**Behavior**: Calling `resolve_completion()` does not raise (unlike ty), but the resolved item carries no additional `detail`, `documentation`, or other metadata beyond what the initial completion already provided. +Initial completions already include type details and documentation, so ordinary +completion remains useful. Calling `resolve_completion()` succeeds but does +not retrieve additional metadata. -**Impact**: Completion items won't gain extended documentation from resolution. Basic completion works fine. +## Semantic-token legend is not advertised -## 2. Configuration Key Format +The initialize response omits `semanticTokensProvider` entirely, while +`textDocument/semanticTokens/full` still returns tokens. The backend therefore +supplies `PYREFLY_LEGEND` instead of discovering a legend from the server. -**Note**: Pyrefly uses TOML configuration (`pyrefly.toml`) with kebab-case keys (e.g., `python-version`, `search-path`). The backend automatically converts snake_case Python keys to kebab-case when writing the config file. +Pyrefly 1.3.0 adds five string modifiers: `byteString`, `formatString`, +`rawString`, `stringPrefix`, and `templateString` (bits 11–15). This maintenance +updates both the fallback legend and canonical modifiers so normalization +preserves those bits. Token types are unchanged. See +[semantic-token documentation](../../docs/SEMANTIC_TOKENS.md). ---- +## Configuration and API boundaries -## Previously Documented, Now Resolved +The backend writes kebab-case TOML keys from top-level snake_case Python keys. +Nested error-code keys should use the upstream names (`bad-assignment`, etc.). +The typed schema covers common options; a plain `Session.create(options=...)` +dictionary can carry other upstream settings. -- **Rename operations disabled for external files** (documented for Pyrefly 0.32.0): Earlier Pyrefly versions treated session files as "external" and returned no rename edits, so `get_rename_edits()` was marked `xfail`. As of Pyrefly 1.1.1 rename returns proper edits and the test is now a regular passing case. +Upstream CLI tools, TSP, and editor refactorings extend beyond the high-level +`Session` API. Their availability upstream does not imply a matching Session +method. Tensor-shape and DataFrame schema extensions remain experimental. ---- +## Previously resolved -## Version Information - -These limitations were last verified with Pyrefly 1.2.0 (verified 2026-08-30; -1.2.0 is the newest stable release on PyPI, released 2026-08-01): - -- `completionItem/resolve` remains a no-op. The resolved item comes back - byte-identical, and an item with `detail`/`documentation` stripped before the - request comes back still stripped — a pure echo, not an already-complete - result. Pyrefly nonetheless advertises `completionProvider.resolveProvider: - true`. Little is lost in practice: Pyrefly front-loads `detail` and - `documentation` into the initial completion items. -- The semantic-tokens legend is still not advertised: `semanticTokensProvider` - is absent from the initialize result entirely (not merely missing its - `legend`), even though the server answers `textDocument/semanticTokens/full`. - The hardcoded `PYREFLY_LEGEND` therefore remains required. ty and Zuban both - advertise a legend. -- Rename returns proper edits for the virtual session document and spans - on-disk sibling modules; the fix has not regressed. - -**Forward-looking:** Pyrefly `main` (heading to 1.3.0) appends five token -modifiers after `selfParameter` — `byteString`, `formatString`, `rawString`, -`stringPrefix`, `templateString` (bits 11-15). Verified against 1.3.0.dev3: -those bits are emitted and silently dropped by `normalize_tokens()` because -they are absent from both `PYREFLY_LEGEND` and `CANONICAL_TOKEN_MODIFIERS`. -Both lists must be extended when 1.3.0 ships. Token *types* are unchanged. +Rename previously failed for session files classified as external. It has +worked since 1.1.1 and remains covered by the regular rename integration test; +the former expected failure is gone. Virtual documents work without on-disk +mirroring. diff --git a/lsp_types/pyrefly/config_schema.py b/lsp_types/pyrefly/config_schema.py index 72cef4b..b9df971 100644 --- a/lsp_types/pyrefly/config_schema.py +++ b/lsp_types/pyrefly/config_schema.py @@ -1,6 +1,7 @@ # Pyrefly configuration schema # Based on official Pyrefly documentation: https://pyrefly.org/en/docs/configuration/ # CLI reference: https://github.com/facebook/pyrefly +# Reviewed against Pyrefly 1.3.0 (2026-09-11). # # Note: Field names use snake_case (Python convention) but are automatically # converted to kebab-case when written to pyrefly.toml (official format). @@ -19,22 +20,21 @@ "skip-and-infer-return-any", ] -# Error severity configuration (error-code -> enabled/disabled) -ErrorConfig = dict[str, bool] +ErrorSeverity = Literal["error", "warn", "info", "ignore"] +# Boolean values remain accepted by Pyrefly for compatibility. +ErrorConfig = dict[str, bool | ErrorSeverity] class Model(TypedDict): """ Pyrefly Configuration Schema - Comprehensive type definitions for all Pyrefly configuration options. - Field names use snake_case following Python conventions. Pyrefly accepts - both snake_case and kebab-case in TOML configuration files. + Common Pyrefly configuration options and backend launch settings. + Field names use snake_case; the backend writes kebab-case TOML keys. All fields are NotRequired for maximum flexibility. For arbitrary fields not yet in this schema: - - Use cast(dict, config) to add extra keys - - Or pass plain dict to write_config (accepts Mapping[str, Any]) + pass a plain dictionary to Session.create(options=...). Official Documentation: https://pyrefly.org/en/docs/configuration/ """ @@ -94,8 +94,8 @@ class Model(TypedDict): python_version: NotRequired[str] """Python version for sys.version checks, e.g. "3.13.0" (USER REQUESTED)""" - python_platform: NotRequired[str] - """Platform for sys.platform checks, e.g. "linux", "darwin", "win32" """ + python_platform: NotRequired[str | list[str]] + """One platform, multiple platforms, or "all" for sys.platform checks.""" conda_environment: NotRequired[str] """Conda environment name for querying Python configuration""" @@ -116,8 +116,23 @@ class Model(TypedDict): typeshed_path: NotRequired[str] """Override bundled typeshed with custom path""" + preset: NotRequired[Literal["off", "basic", "legacy", "default", "strict", "all"]] + """Select the starting set of diagnostics and checking behavior.""" + untyped_def_behavior: NotRequired[UntypedDefBehavior] - """How to handle untyped function definitions (default: check-and-infer-return-type)""" + """Deprecated upstream; use check_unannotated_defs and infer_return_types.""" + + check_unannotated_defs: NotRequired[bool] + """Check unannotated function bodies (default: true with the default preset).""" + + infer_return_types: NotRequired[Literal["never", "annotated", "checked"]] + """Infer returns for no functions, annotated functions, or all checked functions.""" + + treat_all_caps_as_final: NotRequired[bool] + """Reject reassignment of ALL_CAPS names (opt-in).""" + + required_version: NotRequired[str] + """PEP 440 constraint on the Pyrefly version, e.g. ">=1.3,<1.4".""" infer_with_first_use: NotRequired[bool] """Infer container types from first usage patterns (default: true)""" @@ -138,6 +153,9 @@ class Model(TypedDict): replace_imports_with_any: NotRequired[list[str]] """Module globs to unconditionally replace with typing.Any""" + replace_untyped_imports_with_any: NotRequired[list[str]] + """Replace matching installed packages lacking stubs or py.typed with Any.""" + ignore_missing_imports: NotRequired[list[str]] """Module globs to replace with typing.Any when not found""" @@ -149,4 +167,16 @@ class Model(TypedDict): # ======================================================================== errors: NotRequired[ErrorConfig] - """Error severity configuration: {"error-code": bool, ...}""" + """Error code to severity (or legacy enabled/disabled boolean).""" + + baseline: NotRequired[str] + """Path to a baseline file of existing diagnostics.""" + + baseline_error_level: NotRequired[ErrorSeverity] + """Severity for diagnostics matching the baseline (default: ignore).""" + + baseline_matching_mode: NotRequired[Literal["column", "concise-description"]] + """Match baseline entries by source column or concise diagnostic description.""" + + baseline_format: NotRequired[Literal["full", "minimal"]] + """Amount of metadata written to baseline entries.""" diff --git a/lsp_types/pyright/config_schema.py b/lsp_types/pyright/config_schema.py index bbdbc81..92578b5 100644 --- a/lsp_types/pyright/config_schema.py +++ b/lsp_types/pyright/config_schema.py @@ -1,6 +1,5 @@ # generated by datamodel-codegen: # filename: pyright.schema.json -# timestamp: 2026-08-30T19:54:45+00:00 from __future__ import annotations diff --git a/lsp_types/semantic_tokens.py b/lsp_types/semantic_tokens.py index c82012f..dd4a2a5 100644 --- a/lsp_types/semantic_tokens.py +++ b/lsp_types/semantic_tokens.py @@ -55,6 +55,11 @@ "classMember", # bit 11 (pyright) "parameter", # bit 12 (pyright - modifier, not to be confused with type) "selfParameter", # bit 13 (pyrefly - modifier for self/cls parameters) + "byteString", # bit 14 (pyrefly 1.3+) + "formatString", # bit 15 + "rawString", # bit 16 + "stringPrefix", # bit 17 + "templateString", # bit 18 ] # The canonical legend for Monaco/editor integration @@ -72,7 +77,7 @@ } # Pyrefly legend (server doesn't advertise it via LSP) -# Source: https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/state/semantic_tokens.rs +# Source: https://github.com/facebook/pyrefly/blob/1.3.0/pyrefly/lib/state/semantic_tokens.rs PYREFLY_LEGEND: types.SemanticTokensLegend = { "tokenTypes": [ "namespace", # 0 @@ -111,6 +116,11 @@ "documentation", # bit 8 "defaultLibrary", # bit 9 "selfParameter", # bit 10 (for self/cls parameters) + "byteString", # bit 11 (1.3+; appended, compatible with older servers) + "formatString", # bit 12 + "rawString", # bit 13 + "stringPrefix", # bit 14 + "templateString", # bit 15 ], } diff --git a/lsp_types/ty/KNOWN_LIMITATIONS.md b/lsp_types/ty/KNOWN_LIMITATIONS.md index 5fe5a86..e79c79a 100644 --- a/lsp_types/ty/KNOWN_LIMITATIONS.md +++ b/lsp_types/ty/KNOWN_LIMITATIONS.md @@ -21,13 +21,14 @@ unhandled, so sending it produces a warning and no effect. Configuration does not have to live in `ty.toml`, however. ty also reads `initializationOptions.configuration` at initialization, which accepts inline ty config using kebab-case keys (`{"configuration": {"rules": {"unresolved-import": -"ignore"}}}` was verified to suppress that diagnostic with no `ty.toml` present; +"ignore"}}}` was verified to suppress that diagnostic with an empty `ty.toml`; the snake_case spelling is silently ignored). Pass it through the public `Session.create(..., initialize_params={"initializationOptions": {...}})` parameter. This mirrors Zuban, which honors its own `initializationOptions` — several backends in this repo accept LSP-time configuration, so "file-based only" -is the wrong mental model. What remains true for ty is that configuration cannot -be changed *after* the session starts: to change it, create a new session. +is the wrong mental model. This integration applies configuration when creating +the session. Recreate the session to change it reliably; its default null +configuration replies and lack of file watching do not provide settings updates. ## 2. Hover Format Differs @@ -48,13 +49,15 @@ be changed *after* the session starts: to change it, create a new session. **Impact**: None on functionality. Configuration reaches ty through two channels rather than the command line: `ty.toml` (what `TyBackend.write_config()` writes), -and `initializationOptions` at LSP initialization — see limitation 1. Two keys -were verified to have a real effect there: `logLevel` (changes server log +and `initializationOptions` at LSP initialization — see limitation 1. The August +30 probe verified two keys with a real effect: `logLevel` (changes server log verbosity) and `configuration` (applies inline ty config). Others are accepted without a warning, but their effect was not confirmed and should not be assumed: -`diagnosticMode`, `disableLanguageServices`, `configuration-file`, `inlayHints`, +`diagnosticMode`, `disableLanguageServices`, `inlayHints`, `completions`, `pythonExtension`, `workspaceTrust`, `experimental`, -`showSyntaxErrors`. +`showSyntaxErrors`. The current documented setting for an explicit TOML path is +`configurationFile`; the older probe used `configuration-file` and did not +establish its behavior. See the [editor settings reference](https://docs.astral.sh/ty/reference/editor-settings/). ty warns loudly on unrecognized *top-level* initialization-option keys, so a typo there is visible. That does not extend to nested keys: a misspelled rule name @@ -81,21 +84,17 @@ adjusts its warning to how much watching the client claims to support. ``` WARN Your LSP client doesn't support file watching: You may see stale results when files change outside the editor ``` -Advertising `workspace.didChangeWatchedFiles.dynamicRegistration` narrows this to -"...doesn't support file watching **outside of project**: You may see stale results -when **dependencies change**". Additionally advertising `relativePatternSupport` -removes the warning entirely. This tiering is not new — it behaves identically at -ty 0.0.70. - -**Why the warning is left in place**: silencing it by advertising those -capabilities would be dishonest, not a fix. When `dynamicRegistration` is -advertised, ty replies with a `client/registerCapability` *request*, and this -client cannot answer it: the read loop tests `if "method" in payload` before -`elif "id" in payload` (`lsp_types/process.py:480`), so a server-initiated -request — which carries both — is routed to the notification listeners and never -answered. Advertising a capability we do not implement and then leaving ty's -request hanging is worse than the warning. A real fix needs server-request -replies plus actual file watching. +**Historical probe (0.0.70 and 0.0.75, August 30):** Advertising +`workspace.didChangeWatchedFiles.dynamicRegistration` narrowed the warning to +watching outside the project; also advertising `relativePatternSupport` removed +it. These capability variants were not rerun in the September maintenance. + +**Why the warning is left in place:** this library has no file watchers. The +process now answers server requests: its default handler returns null +configuration entries, acknowledges registration requests, and replies +`-32601` for unknown methods. That prevents protocol stalls, but acknowledging a +registration does not install a watcher. Advertising watching support would +still promise behavior this client does not provide. **Impact**: Files modified outside the LSP session (by external tools, a build step, or a dependency install) may not be picked up until the session is @@ -111,7 +110,8 @@ are unaffected. Unknown request: completionItem/resolve (-32601) ``` -**Impact**: Completion items won't have extended documentation or additional metadata that resolution typically provides. Basic completion works fine. +**Impact**: Clients must use the initial completion response; they cannot fetch +additional details through a separate resolution request. Basic completion works. --- @@ -123,31 +123,26 @@ Unknown request: completionItem/resolve (-32601) ## Version Information -These limitations were documented based on ty version 0.0.11 (January 2026), last -verified with ty 0.0.75 (August 30, 2026), which is the newest release on PyPI. - -All six entries were probed directly against 0.0.75, each with a control case -proving the probe could detect the opposite result. **None of the six was fixed.** -The identical probe suite was then re-run against a pinned ty 0.0.70 in a throwaway -virtualenv: behaviour was byte-identical on all six. Nothing regressed and nothing -was fixed in 0.0.70..0.0.75, and the release notes for 0.0.71-0.0.75 contain no LSP -change bearing on these entries. The `ty>=0.0.16` floor in `pyproject.toml` remains -correct. - -Two entries read differently than their original wording suggests: - -- Limitation 4 is a gap in *this client*, not in ty. Supplying `workspaceFolders` - in the initialize params (URI matching `base_path`) removes the warning with no - loss of function: diagnostics stay correct and `ty.toml` is still applied. - `Session.create()` does not currently send the field. A folder URI that is not a - real directory is worse than sending none — ty falls back to default settings and - reports no diagnostics at all. -- Limitation 5 is tiered by client capability and should be left alone; see that - section for why silencing it would be a regression in honesty, not a fix. - -Limitation 1 is narrower than originally written: the notification is still -unhandled, but ty does accept configuration over LSP at initialization via -`initializationOptions`. Configuration still cannot be changed after a session -starts. - -Future versions may address some of these limitations. +The September 11, 2026 maintenance used **ty 0.0.80**. Fresh temporary +LSP sessions confirmed: + +- `workspace/didChangeConfiguration` still logs an unhandled-notification + warning. An unresolved import remained after sending a suppressing setting + and editing the document; inline initialization with the correct kebab-case + rule suppressed it, while an unrelated assignment error remained. The + snake_case rule spelling did not suppress it. +- Hover over `result: str = "ok"` returned `Literal["ok"]`, without its name. +- `ty server --help` lists only `-h/--help`. +- The default initialization emits both missing-workspace and missing-watcher + warnings. Supplying a valid `workspaceFolders` removed the former while + retaining correct diagnostics. Session does not currently supply this field. +- Resolving a real completion item returned `-32601`. +- All diagnostic and hover probes used virtual documents, with no `.py` file on + disk. New analysis configuration serialized and loaded successfully; an + `allowed_unresolved_imports` entry suppressed its matching unresolved import + without suppressing the assignment control. + +Historical August 30 probes compared 0.0.70 and 0.0.75 and found identical behavior +for the six entries. That run also checked log-level changes, invalid workspace +paths and watcher-capability variants. Those details remain historical evidence, +not claims that every variant was repeated at 0.0.80. diff --git a/lsp_types/ty/config_schema.py b/lsp_types/ty/config_schema.py index 5333dcf..c079974 100644 --- a/lsp_types/ty/config_schema.py +++ b/lsp_types/ty/config_schema.py @@ -15,7 +15,7 @@ PythonPlatform = Literal["win32", "darwin", "android", "ios", "linux", "all"] # Output format options -OutputFormat = Literal["full", "concise"] +OutputFormat = Literal["full", "concise", "github", "gitlab", "junit"] class EnvironmentConfig(TypedDict, total=False): @@ -61,6 +61,9 @@ class SrcConfig(TypedDict, total=False): respect_ignore_files: NotRequired[bool] """Auto-exclude files listed in .gitignore. Default: true.""" + exclude_scripts: NotRequired[bool] + """Exclude PEP 723 scripts unless explicitly passed to the CLI. Default: false.""" + class AnalysisConfig(TypedDict, total=False): """ @@ -72,6 +75,18 @@ class AnalysisConfig(TypedDict, total=False): respect_type_ignore_comments: NotRequired[bool] """Whether 'type: ignore' comments suppress errors. Default: true.""" + allowed_unresolved_imports: NotRequired[list[str]] + """Module glob patterns exempt from unresolved-import diagnostics.""" + + replace_imports_with_any: NotRequired[list[str]] + """Module glob patterns whose imports become Any, even when resolvable.""" + + strict_equality_semantics: NotRequired[bool] + """Use strict equality inference and narrowing semantics. Default: false.""" + + strict_generic_narrowing: NotRequired[bool] + """Use strict narrowing for unspecialized generic classes. Default: false.""" + class TerminalConfig(TypedDict, total=False): """ @@ -81,10 +96,10 @@ class TerminalConfig(TypedDict, total=False): """ error_on_warning: NotRequired[bool] - """Exit with code 1 when warnings are emitted. Default: false.""" + """Exit with code 1 when warnings are emitted. Default: true.""" output_format: NotRequired[OutputFormat] - """Diagnostic message format: 'full' or 'concise'. Default: full.""" + """Diagnostic output format for the CLI. Default: full.""" class OverrideConfig(TypedDict, total=False): @@ -103,12 +118,15 @@ class OverrideConfig(TypedDict, total=False): rules: NotRequired[dict[str, RuleSeverity]] """Rule overrides for matched files.""" + analysis: NotRequired[AnalysisConfig] + """Analysis overrides for matched files.""" + class Model(TypedDict, total=False): """ ty Configuration Schema - Comprehensive type definitions for all ty configuration options. + Type definitions for documented ty configuration options. Field names use snake_case following Python conventions but are automatically converted to kebab-case when written to ty.toml. diff --git a/lsp_types/types.py b/lsp_types/types.py index f9df720..77c0075 100644 --- a/lsp_types/types.py +++ b/lsp_types/types.py @@ -1615,6 +1615,9 @@ class InlineValueParams(TypedDict): requested.""" workDoneToken: NotRequired["ProgressToken"] """An optional token that a server can use to report work done progress.""" + partialResultToken: NotRequired["ProgressToken"] + """An optional token that a server can use to report partial results (e.g. streaming) to + the client.""" class InlineValueRegistrationOptions(TypedDict): @@ -1641,6 +1644,9 @@ class InlayHintParams(TypedDict): """The document range for which inlay hints should be computed.""" workDoneToken: NotRequired["ProgressToken"] """An optional token that a server can use to report work done progress.""" + partialResultToken: NotRequired["ProgressToken"] + """An optional token that a server can use to report partial results (e.g. streaming) to + the client.""" class InlayHint(TypedDict): @@ -1874,6 +1880,9 @@ class InlineCompletionParams(TypedDict): """The position inside the text document.""" workDoneToken: NotRequired["ProgressToken"] """An optional token that a server can use to report work done progress.""" + partialResultToken: NotRequired["ProgressToken"] + """An optional token that a server can use to report partial results (e.g. streaming) to + the client.""" class InlineCompletionList(TypedDict): diff --git a/lsp_types/zuban/KNOWN_LIMITATIONS.md b/lsp_types/zuban/KNOWN_LIMITATIONS.md index 75ab736..7fb97ee 100644 --- a/lsp_types/zuban/KNOWN_LIMITATIONS.md +++ b/lsp_types/zuban/KNOWN_LIMITATIONS.md @@ -8,7 +8,7 @@ This document describes known limitations and behavioral differences when using **Why `pyproject.toml` and not a dedicated file**: Unlike Pyright (`pyrightconfig.json`), Pyrefly (`pyrefly.toml`), and ty (`ty.toml`), Zuban has no dedicated config file in its native "default" mode. Selecting Zuban's PyRight-like mode is done via `pyproject.toml`'s `[tool.zuban]` table (some settings can also be passed as LSP `initializationOptions` — see entry 4). -**Why `[tool.zuban]` and not `[tool.mypy]`**: Presence of `[tool.zuban]` puts Zuban into its recommended `default` mode (PyRight-like). `[tool.mypy]` would force the Mypy-compatible mode, which is less capable. +**Why `[tool.zuban]` and not `[tool.mypy]`**: Presence of `[tool.zuban]` puts Zuban into its recommended `default` mode (PyRight-like). `[tool.mypy]` would force the Mypy-compatible mode, which preserves different Mypy-compatible defaults. **Impact**: Re-invoking `write_config` replaces the previous `[tool.zuban]` table in place; every other parsed value and section is preserved. @@ -24,14 +24,14 @@ that preserved values but stripped every comment.) **The table is written even when `options` is empty**, and must be: its *presence* is what selects Zuban's `default` mode. A project carrying -`[tool.mypy]` but no `[tool.zuban]` runs in the weaker Mypy-compatible mode, so -skipping the write would silently downgrade it. Note this is observable only +`[tool.mypy]` but no `[tool.zuban]` uses Mypy-compatible defaults, so +skipping the write would silently change its mode. Note this is observable only through `zuban server` — the `zuban check` subcommand pins `default` mode regardless of configuration, which makes the caveat easy to mis-verify. ## 2. Unused `# type: ignore` Comments Not Reported -**Limitation**: Zuban does not yet report unused `# type: ignore` comments (upstream limitation still present as of Zuban 0.9.2, per the [features documentation](https://docs.zubanls.com/en/latest/features.html)). +**Limitation**: Zuban does not yet report unused `# type: ignore` comments (upstream limitation still present as of Zuban 0.9.3, per the [features documentation](https://docs.zubanls.com/en/latest/features.html)). **Impact**: Code that accumulates stale `# type: ignore` comments will not be flagged when using this backend. @@ -41,14 +41,14 @@ regardless of configuration, which makes the caveat easy to mis-verify. *value-constrained* `TypeVar` — `TypeVar("T", str, bytes)` or the PEP 695 form `[T: (str, bytes)]`. Upstream lists this under "Missing Features" in the [features documentation](https://docs.zubanls.com/en/latest/features.html), still -present as of Zuban 0.9.2. +present as of Zuban 0.9.3. **Not affected**: *upper-bounded* TypeVars — `TypeVar("T", bound=str)` — are checked normally. The distinction is constraints (a tuple of alternatives) versus a bound (a single upper limit); only the former disables body checking. **Impact**: Type errors inside value-constrained generic functions do not surface -via diagnostics. Verified at 0.9.2: `bad: int = "definitely not an int"` inside a +via diagnostics. Historical August 30 probe at 0.9.2: `bad: int = "definitely not an int"` inside a `TypeVar("T", str, bytes)` body (line 7) and inside a `[T: (str, bytes)]` body (line 2) produced no diagnostic, while the identical statement in a plain `def plain(x: str) -> str` in the same file (lines 13 and 8 respectively) was @@ -80,12 +80,12 @@ callers can supply them via `Session.create`'s public `initialize_params`: initialize_params={"initializationOptions": {"diagnosticMode": "workspace"}}, ) -Confirmed to change behavior at 0.9.2 by diffing the initialize response: +Reconfirmed at 0.9.3 by diffing the initialize response: `diagnosticMode="workspace"` flips `diagnosticProvider.workspaceDiagnostics` to `true`; `typeCheckingMode="off"` drops `diagnosticProvider` entirely; -`disableLanguageServices=true` drops `hoverProvider` and sets -`completionProvider: false`. An unknown key left the response identical to -baseline, confirming these are read rather than ignored. +`disableLanguageServices=true` drops `hoverProvider` and removes +`completionProvider`. The historical 0.9.2 probe also found that an unknown key +left the response identical to baseline; that control was not repeated at 0.9.3. **Caveat**: for the two options probed this way, the change was to what Zuban *advertises*, not to what it *answers*. With `typeCheckingMode="off"`, @@ -99,29 +99,29 @@ client that gates requests on advertised capabilities sees a behavior change; ## Version Information -These limitations were documented based on Zuban version 0.7.0 (April 2026), last -verified on 2026-08-30 with Zuban 0.9.2 (released 2026-08-26, the newest release on -PyPI). All four entries were re-probed directly against a live `zuban server` -reporting `serverInfo {"name": "zuban", "version": "0.9.2"}`: - -- **1** — a pre-existing `pyproject.toml` containing `[project]`, - `[project.scripts]`, `[build-system]`, `[tool.ruff]`, and `[tool.foo]` (with an - inline table and a `[[tool.foo.item]]` array-of-tables) survived a `write_config` - call with every parsed value intact; only `[tool.zuban]` was replaced. Since - v0.22.1 the edit is format-preserving (`tomlkit`), so comments, inline tables, - arrays-of-tables and whitespace elsewhere in the file also survive. -- **2** — a file containing two unnecessary `# type: ignore` comments produced zero - diagnostics, while a plain type error in a control file was reported. -- **3** — a type error inside a value-constrained `TypeVar("T", str, bytes)` body - (line 7) and inside a PEP 695 `[T: (str, bytes)]` body (line 2) went unreported, - while the identical error in a plain function in the same file (lines 13 and 8) - was caught. An upper-bounded `TypeVar("T", bound=str)` body (line 7) *was* - checked. -- **4** — `zuban server --help` still lists only `-h/--help`, and - `zuban server --mode default` fails with `error: unexpected argument '--mode' - found`. Zuban does honor LSP `initializationOptions`; see that section. - -Upstream still lists entries 2 and 3 verbatim under "Missing Features" in the -[features documentation](https://docs.zubanls.com/en/latest/features.html), and the -0.9.2 changelog records only bugfixes and conformance-test fixes. Future versions -may address some of these limitations. +The September 11, 2026 maintenance used **Zuban 0.9.3**, released September 2. +Fresh temporary LSP sessions reporting that version confirmed: + +- The typed `mode="auto"` setting serialized and loaded successfully. +- One virtual document contained assignment errors in four functions. Errors + were reported in the upper-bounded TypeVar and plain functions, but not in + either value-constrained TypeVar syntax. Its unnecessary `# type: ignore` + was not reported either. +- `zuban server --help` lists only `-h/--help`. +- `diagnosticMode="workspace"` changed advertised workspace diagnostics to true; + `typeCheckingMode="off"` removed the diagnostic provider; + `disableLanguageServices=true` removed hover and completion providers. + Nevertheless, direct diagnostic, hover and completion requests still returned + results with the corresponding advertised services disabled. +- The 18 config tests, including preservation of comments, other sections, + inline tables and CRLF, passed. This is library behavior verified separately + from upstream capability claims. + +The detailed line-numbered 0.9.2 examples above are historical August 30 evidence. +The newer combined probe reached the same constrained-versus-bounded conclusion. +No fresh behavioral probe was made for `pythonExecutable` or `inlayHintMode`. + +Upstream continues to list unused-ignore reporting and constrained generic +bodies under [missing features](https://docs.zubanls.com/en/latest/features.html). +The [0.9.3 changelog](https://docs.zubanls.com/en/latest/changelog.html) records +deterministic file processing and a fix for quadratic literal handling. diff --git a/lsp_types/zuban/config_schema.py b/lsp_types/zuban/config_schema.py index b3c8dce..ecd19f0 100644 --- a/lsp_types/zuban/config_schema.py +++ b/lsp_types/zuban/config_schema.py @@ -16,8 +16,8 @@ from typing import Literal, NotRequired, TypedDict -# Zuban's two operating modes. `default` is PyRight-like and recommended. -ZubanMode = Literal["default", "mypy"] +# `auto` selects native or Mypy-compatible behavior from project configuration. +ZubanMode = Literal["default", "mypy", "auto"] # Controls how Zuban infers untyped function return types. # - `any`: behave like Mypy (return type is `Any`). @@ -36,7 +36,7 @@ class Model(TypedDict, total=False): """ mode: NotRequired[ZubanMode] - """Selects `default` (PyRight-like, recommended) or `mypy` (Mypy-compatible).""" + """Selects native `default`, Mypy-compatible `mypy`, or config-based `auto`.""" mypy_path: NotRequired[list[str]] """Additional import search paths (equivalent to Mypy's `mypy_path` / `MYPYPATH`).""" diff --git a/playground/browser.test.mjs b/playground/browser.test.mjs new file mode 100644 index 0000000..2fb4356 --- /dev/null +++ b/playground/browser.test.mjs @@ -0,0 +1,71 @@ +import assert from "node:assert/strict"; +// Keep Playwright outside the project's dependencies, e.g. install it under /tmp. +// Run against "npm run preview" with PLAYWRIGHT_MODULE=/absolute/path/to/playwright/index.mjs. +const { chromium } = await import( + process.env.PLAYWRIGHT_MODULE || "playwright" +); +const browser = await chromium.launch({ headless: true }); +const page = await browser.newPage(); +const errors = []; +page.on("pageerror", (e) => errors.push(e.message)); +page.on("console", (m) => { + if (m.type() === "error") errors.push(m.text()); +}); +try { + await page.goto("http://127.0.0.1:4173/lsp-python-types/"); + for (const name of [ + "basedpyright", + "Pyrefly", + "ty", + "Pyrefly", + "ty", + "basedpyright", + ]) { + await page.getByRole("button", { name, exact: true }).click(); + await page.waitForFunction( + (name) => document.querySelector("#status").textContent === name, + name, + ); + await page.locator(".view-lines").click({ position: { x: 90, y: 12 } }); + await page.keyboard.press("Control+KeyA"); + await page.keyboard.insertText('value: int = "wrong"\n'); + await page.waitForFunction( + () => document.querySelectorAll(".squiggly-error").length === 1, + ); + await page.keyboard.press("Control+Home"); + await page.keyboard.press("ArrowRight"); + await page.keyboard.press("Control+KeyK"); + await page.keyboard.press("Control+KeyI"); + await page.locator(".monaco-hover:visible").waitFor({ state: "visible" }); + assert.match( + await page.locator(".monaco-hover:visible").innerText(), + name === "ty" ? /Literal/ : /int/, + ); + await page.keyboard.press("Escape"); + await page.keyboard.press("Control+KeyA"); + await page.keyboard.insertText("value: int = 42\n"); + await page.waitForFunction( + () => document.querySelectorAll(".squiggly-error").length === 0, + ); + console.log(`${name}: production markers, hover and clearing errors PASS`); + } + // Do not wait for initialization between selections: only the final choice may win. + await page.evaluate(() => { + for (const name of ["Pyrefly", "basedpyright", "ty"]) { + document.querySelector(`button[data-backend="${name}"]`).click(); + } + }); + await page.waitForFunction( + () => document.querySelector("#status").textContent === "ty", + ); + await page.waitForTimeout(1500); + assert.equal(await page.locator("#status").textContent(), "ty"); + assert.equal( + await page.locator("#backend-selector .active").textContent(), + "ty", + ); + console.log("Rapid backend selections preserve the final choice: PASS"); + assert.deepEqual(errors, []); +} finally { + await browser.close(); +} diff --git a/playground/concurrency.test.mjs b/playground/concurrency.test.mjs new file mode 100644 index 0000000..b60d337 --- /dev/null +++ b/playground/concurrency.test.mjs @@ -0,0 +1,147 @@ +import assert from "node:assert/strict"; +// Run against "npm run dev"; use the same external Playwright module as browser.test.mjs. +const { chromium } = await import( + process.env.PLAYWRIGHT_MODULE || "playwright" +); +const browser = await chromium.launch({ headless: true }); +try { + const page = await browser.newPage(); + await page.goto("http://127.0.0.1:5173/lsp-python-types/"); + await page.waitForFunction( + () => document.querySelector("#status").textContent === "basedpyright", + ); + const snapshots = await page.evaluate(async () => { + const mainSource = await (await fetch("/lsp-python-types/src/main.ts")).text(); + const editorUrl = mainSource.match(/from "([^"]*\/editor\.ts[^"]*)"/)[1]; + const { setAdapter } = await import(editorUrl); + // Import Vite's exact bundled Monaco instance, including its cache query. + const editorSource = await (await fetch(editorUrl)).text(); + const monaco = await import( + editorSource.match(/import \* as monaco from "([^"]+)"/)[1] + ); + const model = monaco.editor.getModels()[0]; + const nextTurn = () => new Promise((resolve) => setTimeout(resolve, 0)); + const pending = []; + const adapter = (name) => ({ + name, + updateCode: () => new Promise((resolve) => pending.push(resolve)), + getHover: async () => null, + dispose() {}, + }); + const diagnostic = (message) => [ + { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 2, + message, + severity: 8, + }, + ]; + const markers = () => + monaco.editor + .getModelMarkers({ resource: model.uri }) + .map((marker) => ({ owner: marker.owner, message: marker.message })); + setAdapter(adapter("first")); + model.setValue("value: int = 42\n"); + pending.shift()(diagnostic("before edit")); + await nextTurn(); + const duringDebounce = markers(); + setAdapter(adapter("old")); + const finishOld = pending.shift(); + setAdapter(adapter("new")); + pending.shift()(diagnostic("new result")); + await nextTurn(); + finishOld(diagnostic("old result")); + await nextTurn(); + const afterSwitch = markers(); + setAdapter(adapter("detached")); + setAdapter(null); + pending.shift()(diagnostic("after disposal")); + await nextTurn(); + return { duringDebounce, afterSwitch, afterDetach: markers() }; + }); + assert.deepEqual(snapshots, { + duringDebounce: [], + afterSwitch: [{ owner: "new", message: "new result" }], + afterDetach: [], + }); + console.log( + "Editor ignores results during debounce, after switching, and after detaching: PASS", + ); + + const diagnostics = await page.evaluate(async () => { + const { PyrightBackend } = + await import("/lsp-python-types/src/backends/pyright.ts"); + const adapter = new PyrightBackend(); + await adapter.initialize(); + // Control the real transport's incoming publications without asking the server to analyze. + adapter.connection.sendNotification = async () => {}; + const publish = (version, message) => + adapter.workers[0].dispatchEvent( + new MessageEvent("message", { + data: { + jsonrpc: "2.0", + method: "textDocument/publishDiagnostics", + params: { + uri: "file:///src/main.py", + version, + diagnostics: [ + { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 1 }, + }, + message, + severity: 1, + }, + ], + }, + }, + }), + ); + try { + const first = adapter.updateCode("first"); + let secondFinished = false; + const second = adapter.updateCode("second").then((result) => { + secondFinished = true; + return result; + }); + publish(1, "stale"); + await new Promise((resolve) => setTimeout(resolve, 0)); + const ignoredOldVersion = !secondFinished; + publish(2, "current"); + const results = { + first: await first, + second: (await second).map((item) => item.message), + ignoredOldVersion, + }; + const originalTimeout = window.setTimeout; + window.setTimeout = (callback, delay, ...args) => + originalTimeout(callback, delay === 10000 ? 0 : delay, ...args); + try { + results.timedOut = await adapter.updateCode("no response"); + } finally { + window.setTimeout = originalTimeout; + } + const disposed = adapter.updateCode("disposed"); + adapter.dispose(); + results.disposed = await disposed; + return results; + } finally { + adapter.dispose(); + } + }); + assert.deepEqual(diagnostics, { + first: [], + second: ["current"], + ignoredOldVersion: true, + timedOut: [], + disposed: [], + }); + console.log( + "basedpyright ignores stale versions and settles superseded, timed-out and disposed requests: PASS", + ); +} finally { + await browser.close(); +} diff --git a/playground/fetch-wasm.sh b/playground/fetch-wasm.sh index 1ef534f..4b48810 100755 --- a/playground/fetch-wasm.sh +++ b/playground/fetch-wasm.sh @@ -1,86 +1,61 @@ #!/usr/bin/env bash -# Fetches pre-built WASM modules for Pyrefly and ty from their GitHub releases. -# Run this before `npm run dev` if you want Pyrefly or ty backends. +# Fetch Pyrefly's release WASM and build ty from its matching release source. +# ty needs Rust (rustup) and a native compiler; wasm-pack runs through npm. +# Update release versions and their SHA256 sums together during maintenance. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WASM_DIR="${SCRIPT_DIR}/wasm" +PYREFLY_VERSION="1.3.0" +TY_VERSION="0.0.80" +WASM_PACK_VERSION="0.15.0" +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT -# --- Pyrefly --- -# Build from source using wasm-pack (requires Rust toolchain) fetch_pyrefly() { local dir="${WASM_DIR}/pyrefly" - mkdir -p "$dir" - - if [ -f "$dir/pyrefly_wasm_bg.wasm" ]; then - echo "Pyrefly WASM already exists, skipping." + if [ -f "$dir/pyrefly_wasm_bg.wasm" ] && [ -f "$dir/pyrefly_wasm.js" ] && + [ "$(cat "$dir/.version" 2>/dev/null || true)" = "$PYREFLY_VERSION" ]; then + echo "Pyrefly ${PYREFLY_VERSION} WASM already exists." return fi - echo "Building Pyrefly WASM from source..." - local tmpdir - tmpdir=$(mktemp -d) - trap 'rm -rf "$tmpdir"' RETURN - - git clone --depth=1 https://github.com/facebook/pyrefly.git "$tmpdir/pyrefly" - cd "$tmpdir/pyrefly/pyrefly_wasm" - - if ! command -v wasm-pack &>/dev/null; then - echo "Installing wasm-pack..." - cargo install wasm-pack - fi - - wasm-pack build --target web --out-dir "$dir" --no-typescript - echo "Pyrefly WASM built successfully." + echo "Downloading Pyrefly ${PYREFLY_VERSION} WASM..." + curl --fail --location --retry 3 \ + "https://github.com/facebook/pyrefly/releases/download/${PYREFLY_VERSION}/pyrefly-wasm.tar.gz" \ + -o "$TMP_DIR/pyrefly-wasm.tar.gz" + (cd "$TMP_DIR" && echo "6a70781b5e70ad505f18cade35bf64bfc83cfee14437e07fceafd374cc358fd0 pyrefly-wasm.tar.gz" | shasum -a 256 --check) + mkdir -p "$dir" + tar -xzf "$TMP_DIR/pyrefly-wasm.tar.gz" -C "$dir" + echo "$PYREFLY_VERSION" > "$dir/.version" } -# --- ty --- -# Build from source using wasm-pack (requires Rust toolchain) fetch_ty() { local dir="${WASM_DIR}/ty" - mkdir -p "$dir" - - if [ -f "$dir/ty_wasm_bg.wasm" ]; then - echo "ty WASM already exists, skipping." + if [ -f "$dir/ty_wasm_bg.wasm" ] && [ -f "$dir/ty_wasm.js" ] && + [ "$(cat "$dir/.version" 2>/dev/null || true)" = "$TY_VERSION" ]; then + echo "ty ${TY_VERSION} WASM already exists." return fi - echo "Building ty WASM from source..." - local tmpdir - tmpdir=$(mktemp -d) - trap 'rm -rf "$tmpdir"' RETURN - - git clone --depth=1 https://github.com/astral-sh/ruff.git "$tmpdir/ruff" - cd "$tmpdir/ruff" - - if ! command -v wasm-pack &>/dev/null; then - echo "Installing wasm-pack..." - cargo install wasm-pack - fi - - wasm-pack build --target web crates/ty_wasm --out-dir "$dir" --no-typescript - echo "ty WASM built successfully." + command -v cargo >/dev/null || { echo "Install Rust with rustup before building ty WASM." >&2; exit 1; } + echo "Building ty ${TY_VERSION} WASM from release source..." + curl --fail --location --retry 3 \ + "https://github.com/astral-sh/ty/releases/download/${TY_VERSION}/source.tar.gz" \ + -o "$TMP_DIR/ty-source.tar.gz" + (cd "$TMP_DIR" && echo "a039d7e66d362e1707fc494f2c69dfdd2eb5333dc8fa9ad582edc125932fa5fa ty-source.tar.gz" | shasum -a 256 --check) + tar -xzf "$TMP_DIR/ty-source.tar.gz" -C "$TMP_DIR" + # The source archive contains the exact Ruff workspace used for this ty release. + (cd "$TMP_DIR/ruff" && npm exec --yes --package="wasm-pack@${WASM_PACK_VERSION}" -- \ + wasm-pack build crates/ty_wasm --target web --out-dir "$dir" --no-typescript -- --locked) + echo "$TY_VERSION" > "$dir/.version" } -echo "=== Fetching WASM modules ===" -echo "" - case "${1:-all}" in - pyrefly) - fetch_pyrefly - ;; - ty) - fetch_ty - ;; - all) - fetch_pyrefly - fetch_ty - ;; - *) - echo "Usage: $0 [pyrefly|ty|all]" - exit 1 - ;; + pyrefly) fetch_pyrefly ;; + ty) fetch_ty ;; + all) fetch_pyrefly; fetch_ty ;; + *) echo "Usage: $0 [pyrefly|ty|all]" >&2; exit 1 ;; esac -echo "" -echo "Done! WASM modules are in ${WASM_DIR}/" +echo "WASM modules are in ${WASM_DIR}/" diff --git a/playground/package-lock.json b/playground/package-lock.json index fc149d8..a6ed84e 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -10,28 +10,28 @@ "dependencies": { "monaco-editor": "^0.56.0", "remote-web-worker": "^0.0.9", - "vscode-jsonrpc": "^9.0.0", - "vscode-languageserver-protocol": "^3.17.5" + "vscode-jsonrpc": "^9.0.2", + "vscode-languageserver-protocol": "^3.18.3" }, "devDependencies": { - "typescript": "^7.0.0", - "vite": "^8.0.0" + "typescript": "^7.0.2", + "vite": "^8.3.0" } }, "node_modules/@oxc-project/types": { - "version": "0.147.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", - "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", "dev": true, "license": "MIT", "funding": { - "url": "https://github.com/sponsors/Boshen" + "url": "https://github.com/sponsors/oxc-project" } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", - "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", "cpu": [ "arm" ], @@ -46,9 +46,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", - "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", "cpu": [ "arm64" ], @@ -63,9 +63,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", - "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", "cpu": [ "arm64" ], @@ -80,9 +80,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", - "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", "cpu": [ "x64" ], @@ -97,9 +97,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", - "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", "cpu": [ "x64" ], @@ -114,9 +114,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", - "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", "cpu": [ "arm" ], @@ -131,16 +131,13 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", - "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -151,16 +148,13 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", - "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -171,16 +165,13 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", - "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", "cpu": [ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -191,16 +182,13 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", - "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", "cpu": [ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -211,16 +199,13 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", - "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -231,16 +216,13 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", - "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -251,9 +233,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", - "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", "cpu": [ "arm64" ], @@ -268,9 +250,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", - "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", "cpu": [ "arm64" ], @@ -285,9 +267,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", - "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", "cpu": [ "x64" ], @@ -666,9 +648,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", - "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz", + "integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -850,9 +832,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -874,9 +853,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -898,9 +874,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -922,9 +895,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1003,9 +973,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -1029,9 +999,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -1042,9 +1012,9 @@ } }, "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -1062,7 +1032,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1077,13 +1047,13 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", - "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.147.0", + "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -1093,21 +1063,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.6", - "@rolldown/binding-android-arm64": "1.2.6", - "@rolldown/binding-darwin-arm64": "1.2.6", - "@rolldown/binding-darwin-x64": "1.2.6", - "@rolldown/binding-freebsd-x64": "1.2.6", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", - "@rolldown/binding-linux-arm64-gnu": "1.2.6", - "@rolldown/binding-linux-arm64-musl": "1.2.6", - "@rolldown/binding-linux-ppc64-gnu": "1.2.6", - "@rolldown/binding-linux-s390x-gnu": "1.2.6", - "@rolldown/binding-linux-x64-gnu": "1.2.6", - "@rolldown/binding-linux-x64-musl": "1.2.6", - "@rolldown/binding-openharmony-arm64": "1.2.6", - "@rolldown/binding-win32-arm64-msvc": "1.2.6", - "@rolldown/binding-win32-x64-msvc": "1.2.6" + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" } }, "node_modules/source-map-js": { @@ -1173,16 +1143,16 @@ } }, "node_modules/vite": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", - "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.26", - "rolldown": "~1.2.4", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "bin": { @@ -1199,7 +1169,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -1269,15 +1239,6 @@ "vscode-languageserver-types": "3.18.3" } }, - "node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz", - "integrity": "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/vscode-languageserver-types": { "version": "3.18.3", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.3.tgz", diff --git a/playground/package.json b/playground/package.json index ae04f3d..461d80e 100644 --- a/playground/package.json +++ b/playground/package.json @@ -7,19 +7,20 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "fetch-wasm": "bash fetch-wasm.sh" + "fetch-wasm": "bash fetch-wasm.sh", + "test:wasm": "node --test wasm.test.mjs" }, "dependencies": { "monaco-editor": "^0.56.0", "remote-web-worker": "^0.0.9", - "vscode-jsonrpc": "^9.0.0", - "vscode-languageserver-protocol": "^3.17.5" + "vscode-jsonrpc": "^9.0.2", + "vscode-languageserver-protocol": "^3.18.3" }, "devDependencies": { - "typescript": "^7.0.0", - "vite": "^8.0.0" + "typescript": "^7.0.2", + "vite": "^8.3.0" }, "overrides": { - "dompurify": "^3.4.11" + "dompurify": "^3.4.15" } } diff --git a/playground/src/backends/pyrefly.ts b/playground/src/backends/pyrefly.ts index 898a02d..896aa71 100644 --- a/playground/src/backends/pyrefly.ts +++ b/playground/src/backends/pyrefly.ts @@ -134,8 +134,11 @@ function mapPyreflySeverity( async function loadPyreflyWasm(): Promise { // Try loading from local static assets (built by CI or fetch-wasm.sh) try { - const baseUrl = new URL(/* @vite-ignore */ "../../wasm/pyrefly/", import.meta.url).href; - const jsUrl = `${baseUrl}pyrefly_wasm.js`; + // Absolute URLs keep Vite's dev server from transforming public WASM glue. + const jsUrl = new URL( + `${import.meta.env.BASE_URL}pyrefly/pyrefly_wasm.js`, + window.location.origin, + ).href; const mod = (await import(/* @vite-ignore */ jsUrl)) as PyreflyWasmModule; await mod.default(); diff --git a/playground/src/backends/pyright.ts b/playground/src/backends/pyright.ts index 86733c0..ff7f81f 100644 --- a/playground/src/backends/pyright.ts +++ b/playground/src/backends/pyright.ts @@ -16,7 +16,7 @@ import type { BackendAdapter, DiagnosticInfo, HoverInfo } from "./interface"; let MarkerSeverity: typeof import("monaco-editor").MarkerSeverity; const PACKAGE = "browser-basedpyright"; -const VERSION = "1.28.1"; +const VERSION = "1.40.1"; const WORKER_URL = `https://cdn.jsdelivr.net/npm/${PACKAGE}@${VERSION}/dist/pyright.worker.js`; const ROOT_PATH = "/src/"; @@ -33,11 +33,11 @@ const DEFAULT_CONFIG = JSON.stringify({ }); export class PyrightBackend implements BackendAdapter { - readonly name = "Pyright"; + readonly name = "basedpyright"; private connection: MessageConnection | null = null; private workers: Worker[] = []; private version = 0; - private latestDiagnostics: Diagnostic[] = []; + private diagnosticsVersion = 0; private diagnosticsResolve: ((diags: Diagnostic[]) => void) | null = null; async initialize(): Promise { @@ -84,8 +84,10 @@ export class PyrightBackend implements BackendAdapter { "textDocument/publishDiagnostics", (params: PublishDiagnosticsParams) => { if (params.uri === FILE_URI) { - this.latestDiagnostics = params.diagnostics; - if (this.diagnosticsResolve) { + if ( + this.diagnosticsResolve && + (params.version === undefined || params.version >= this.diagnosticsVersion) + ) { this.diagnosticsResolve(params.diagnostics); this.diagnosticsResolve = null; } @@ -139,6 +141,8 @@ export class PyrightBackend implements BackendAdapter { if (!this.connection) return []; const version = ++this.version; + this.diagnosticsResolve?.([]); + this.diagnosticsVersion = version; // Create a promise that resolves when we get diagnostics back const diagnosticsPromise = new Promise((resolve) => { @@ -147,7 +151,7 @@ export class PyrightBackend implements BackendAdapter { setTimeout(() => { if (this.diagnosticsResolve === resolve) { this.diagnosticsResolve = null; - resolve(this.latestDiagnostics); + resolve([]); } }, 10000); }); @@ -200,6 +204,9 @@ export class PyrightBackend implements BackendAdapter { } dispose(): void { + this.version++; + this.diagnosticsResolve?.([]); + this.diagnosticsResolve = null; if (this.connection) { this.connection.dispose(); this.connection = null; diff --git a/playground/src/backends/ty.ts b/playground/src/backends/ty.ts index fcdf99b..9b48467 100644 --- a/playground/src/backends/ty.ts +++ b/playground/src/backends/ty.ts @@ -43,7 +43,6 @@ type TyPositionClass = TyPosition; interface TyWasmModule { default(): Promise; - initLogging(level: number): void; Workspace: new ( root: string, encoding: number, @@ -51,7 +50,6 @@ interface TyWasmModule { ) => TyWorkspace; Position: new (line: number, column: number) => TyPositionClass; PositionEncoding: { Utf16: number }; - LogLevel: { Info: number }; } let MarkerSeverity: typeof import("monaco-editor").MarkerSeverity; @@ -73,7 +71,7 @@ export class TyBackend implements BackendAdapter { this.workspace = new this.tyModule.Workspace( "/", this.tyModule.PositionEncoding.Utf16, - {}, + { environment: { "python-version": "3.12" } }, ); this.fileHandle = this.workspace.openFile(FILENAME, ""); } @@ -87,8 +85,7 @@ export class TyBackend implements BackendAdapter { return diagnostics.map((d) => { const range = d.toRange(this.workspace!); return { - // ty uses 0-based positions; Monaco uses 1-based — but ty's playground - // shows range.start.line is already 1-based from toRange() + // ty's WASM Position is 1-based, like Monaco (unlike LSP positions). startLineNumber: range?.start.line ?? 1, startColumn: range?.start.column ?? 1, endLineNumber: range?.end.line ?? 1, @@ -153,16 +150,14 @@ function mapTySeverity(severity: number): monaco.MarkerSeverity { async function loadTyWasm(): Promise { try { - const baseUrl = new URL(/* @vite-ignore */ "../../wasm/ty/", import.meta.url).href; - const jsUrl = `${baseUrl}ty_wasm.js`; + // Absolute URLs keep Vite's dev server from transforming public WASM glue. + const jsUrl = new URL( + `${import.meta.env.BASE_URL}ty/ty_wasm.js`, + window.location.origin, + ).href; const mod = (await import(/* @vite-ignore */ jsUrl)) as TyWasmModule; await mod.default(); - try { - mod.initLogging(mod.LogLevel.Info); - } catch { - // initLogging may fail if already initialized - } return mod; } catch (e) { throw new Error( diff --git a/playground/src/editor.ts b/playground/src/editor.ts index 18e3ca0..cf0d92b 100644 --- a/playground/src/editor.ts +++ b/playground/src/editor.ts @@ -49,6 +49,7 @@ export function createEditor(): monaco.editor.IStandaloneCodeEditor { // Listen for content changes (debounced 500ms) — registered once editor.onDidChangeModelContent(() => { + currentVersion++; if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(() => runDiagnostics(), 500); }); @@ -56,7 +57,9 @@ export function createEditor(): monaco.editor.IStandaloneCodeEditor { return editor; } -export function setAdapter(adapter: BackendAdapter): void { +export function setAdapter(adapter: BackendAdapter | null): void { + currentVersion++; + if (debounceTimer) clearTimeout(debounceTimer); // Clear old markers and hover provider if (currentAdapter) { monaco.editor.setModelMarkers( @@ -71,17 +74,17 @@ export function setAdapter(adapter: BackendAdapter): void { } currentAdapter = adapter; - currentVersion = 0; + if (!adapter) return; // Register hover provider hoverDisposable = monaco.languages.registerHoverProvider("python", { provideHover: async (_model, position) => { - if (!currentAdapter) return null; - const info = await currentAdapter.getHover( + const version = currentVersion; + const info = await adapter.getHover( position.lineNumber, position.column, ); - if (!info) return null; + if (!info || adapter !== currentAdapter || version !== currentVersion) return null; return { range: info.range, contents: [{ value: info.contents }], @@ -94,17 +97,18 @@ export function setAdapter(adapter: BackendAdapter): void { } async function runDiagnostics(): Promise { - if (!currentAdapter) return; + const adapter = currentAdapter; + if (!adapter) return; const version = ++currentVersion; const code = editor.getValue(); - const diagnostics = await currentAdapter.updateCode(code); + const diagnostics = await adapter.updateCode(code); // Stale check — a newer version was triggered - if (version !== currentVersion) return; + if (version !== currentVersion || adapter !== currentAdapter) return; - setMarkers(currentAdapter.name, diagnostics); + setMarkers(adapter.name, diagnostics); } function setMarkers(owner: string, diagnostics: DiagnosticInfo[]): void { diff --git a/playground/src/main.ts b/playground/src/main.ts index a5993e6..629ded0 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -11,11 +11,12 @@ import { import type { BackendAdapter } from "./backends/interface"; let currentAdapter: BackendAdapter | null = null; +let switchVersion = 0; const failedBackends = new Set(); function createBackend(name: BackendName): BackendAdapter { switch (name) { - case "Pyright": + case "basedpyright": return new PyrightBackend(); case "Pyrefly": return new PyreflyBackend(); @@ -26,6 +27,8 @@ function createBackend(name: BackendName): BackendAdapter { async function switchBackend(name: BackendName): Promise { if (failedBackends.has(name)) return; + const version = ++switchVersion; + setAdapter(null); // Dispose current adapter if (currentAdapter) { @@ -35,13 +38,19 @@ async function switchBackend(name: BackendName): Promise { setStatus(`Loading ${name}...`, "loading"); + const adapter = createBackend(name); try { - const adapter = createBackend(name); await adapter.initialize(); + if (version !== switchVersion) { + adapter.dispose(); + return; + } currentAdapter = adapter; setAdapter(adapter); setStatus(name, "ready"); } catch (err) { + adapter.dispose(); + if (version !== switchVersion) return; console.error(`Failed to initialize ${name}:`, err); failedBackends.add(name); setBackendDisabled(name, true); @@ -57,7 +66,7 @@ async function main(): Promise { }); // Load default backend - await switchBackend("Pyright"); + await switchBackend("basedpyright"); } main(); diff --git a/playground/src/ui.ts b/playground/src/ui.ts index 4c90fac..4be9089 100644 --- a/playground/src/ui.ts +++ b/playground/src/ui.ts @@ -1,12 +1,12 @@ -export type BackendName = "Pyright" | "Pyrefly" | "ty"; +export type BackendName = "basedpyright" | "Pyrefly" | "ty"; -const ALL_BACKENDS: BackendName[] = ["Pyright", "Pyrefly", "ty"]; +const ALL_BACKENDS: BackendName[] = ["basedpyright", "Pyrefly", "ty"]; export interface UICallbacks { onBackendSelect: (name: BackendName) => void; } -let currentBackend: BackendName = "Pyright"; +let currentBackend: BackendName = "basedpyright"; export function initUI(callbacks: UICallbacks): void { const selector = document.getElementById("backend-selector")!; diff --git a/playground/vite.config.ts b/playground/vite.config.ts index e19664a..d1ae831 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from "vite"; export default defineConfig({ base: "/lsp-python-types/", + publicDir: "wasm", build: { target: "es2022", chunkSizeWarningLimit: 4000, // Monaco editor is large diff --git a/playground/wasm.test.mjs b/playground/wasm.test.mjs new file mode 100644 index 0000000..bfacad9 --- /dev/null +++ b/playground/wasm.test.mjs @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const invalid = 'value: int = "wrong"\n'; +const valid = "value: int = 42\n"; + +async function load(name) { + const module = await import(`./wasm/${name}/${name}_wasm.js`); + await module.default({ + module_or_path: await readFile( + new URL(`./wasm/${name}/${name}_wasm_bg.wasm`, import.meta.url), + ), + }); + return module; +} + +test("Pyrefly release WASM diagnoses edits and supplies hover", async () => { + const { State } = await load("pyrefly"); + const state = new State("3.12"); + try { + state.updateSandboxFiles({ "main.py": "" }, true); + state.setActiveFile("main.py"); + state.updateSingleFile("main.py", invalid); + const errors = state.getErrors(); + assert.ok( + errors.some( + (error) => error.startLineNumber === 1 && error.severity === 8, + ), + ); + assert.match(JSON.stringify(state.hover(1, 2)), /int/); + state.updateSingleFile("main.py", valid); + assert.equal(state.getErrors().length, 0); + } finally { + state.free(); + } +}); + +test("ty release WASM diagnoses edits and supplies 1-based hover", async () => { + const { Workspace, Position, PositionEncoding } = await load("ty"); + const workspace = new Workspace("/", PositionEncoding.Utf16, { + environment: { "python-version": "3.12" }, + }); + const file = workspace.openFile("main.py", ""); + try { + workspace.updateFile(file, invalid); + const diagnostics = workspace.checkFile(file); + assert.ok(diagnostics.some((diagnostic) => diagnostic.severity() === 2)); + const range = diagnostics[0].toRange(workspace); + assert.equal(range.start.line, 1); + assert.ok(range.start.column >= 1); + for (const diagnostic of diagnostics) diagnostic.free(); + const hover = workspace.hover(file, new Position(1, 2)); + assert.match(hover.markdown, /Literal\["wrong"\]/); + assert.equal(hover.range.start.line, 1); + workspace.updateFile(file, valid); + assert.equal(workspace.checkFile(file).length, 0); + } finally { + workspace.closeFile(file); + workspace.free(); + } +}); diff --git a/pyproject.toml b/pyproject.toml index d1dff2b..47e68df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dev = [ "pytest>=9.0.0,<10", "pytest-cov>=7.0.0,<8", "pytest-asyncio>=1.0.0", - "datamodel-code-generator>=0.53.0", + "datamodel-code-generator[black,isort]>=0.53.0", "httpx>=0.28.1", "rich>=14.2.0", ] diff --git a/tests/conftest.py b/tests/conftest.py index 1debc75..9018735 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,8 @@ from __future__ import annotations +import os + import pytest from lsp_types.pyrefly.backend import PyreflyBackend @@ -30,3 +32,9 @@ def lsp_backend(request: pytest.FixtureRequest) -> LSPBackend: def backend_name(lsp_backend: LSPBackend) -> str: """Helper fixture to get the backend name for test identification.""" return lsp_backend.__class__.__name__.replace("Backend", "").lower() + + +@pytest.fixture +def microsoft_pyright(backend_name: str) -> bool: + """CI runs the shared adapter against both npm distributions explicitly.""" + return backend_name == "pyright" and os.environ.get("PYRIGHT_PACKAGE") == "pyright" diff --git a/tests/test_semantic_tokens.py b/tests/test_semantic_tokens.py new file mode 100644 index 0000000..894ec31 --- /dev/null +++ b/tests/test_semantic_tokens.py @@ -0,0 +1,60 @@ +"""Regression coverage for Pyrefly 1.3 string highlighting.""" + +from pathlib import Path + +from lsp_types import Session, types +from lsp_types.pyrefly.backend import PyreflyBackend +from lsp_types.semantic_tokens import ( + CANONICAL_LEGEND, + PYREFLY_LEGEND, + build_modifier_mapping, + build_type_mapping, + normalize_tokens, +) + + +def test_pyrefly_string_modifier_bits_survive_normalization(): + # Check each wire bit separately as well as the combination, so swapped + # mappings cannot hide behind an identical aggregate mask. + for mask in (1, 2, 4, 8, 16, 31): + raw: types.SemanticTokens = { + "data": [0, 4, 3, 18, (mask << 11) | 4], + "resultId": "strings", + } + normalized = normalize_tokens( + raw, + build_type_mapping(PYREFLY_LEGEND), + build_modifier_mapping(PYREFLY_LEGEND), + ) + assert normalized == { + "data": [0, 4, 3, 18, (mask << 14) | 4], + "resultId": "strings", + } + assert raw["data"][-1] == (mask << 11) | 4 + + +async def test_pyrefly_live_string_highlighting(tmp_path: Path): + session = await Session.create( + PyreflyBackend(), + base_path=tmp_path, + initial_code='a = b"bytes"\nb = r"raw"\nc = f"{a}"\nd = t"{a}"\n', + options={"python_version": "3.14"}, + ) + try: + tokens = await session.get_semantic_tokens(normalize=True) + assert tokens is not None + modifiers = { + name + for mask in tokens["data"][4::5] + for bit, name in enumerate(CANONICAL_LEGEND["tokenModifiers"]) + if mask & (1 << bit) + } + assert { + "byteString", + "rawString", + "formatString", + "stringPrefix", + "templateString", + } <= modifiers + finally: + await session.shutdown() diff --git a/tests/test_session.py b/tests/test_session.py index 73a9493..c1e9d28 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -9,7 +9,7 @@ import lsp_types from lsp_types import session as session_module from lsp_types.pool import LSPProcessPool -from lsp_types.process import LSPProcess, ProcessLaunchInfo +from lsp_types.process import Error, LSPProcess, ProcessLaunchInfo from lsp_types.pyrefly.backend import PyreflyBackend from lsp_types.pyrefly.config_schema import Model as PyreflyConfig from lsp_types.pyright.backend import PyrightBackend @@ -370,7 +370,7 @@ async def test_session_completion(lsp_backend, backend_name, tmp_path: Path): code = """\ class MyClass: def my_method(self) -> None: - pass + 'Method documentation.' obj = MyClass() obj. @@ -399,11 +399,21 @@ def my_method(self) -> None: resolved = await session.resolve_completion(method_completion) assert resolved is not None assert resolved.get("label") == "my_method" + if backend_name == "pyrefly": + assert resolved == method_completion + else: + assert "Method documentation." in str(resolved.get("documentation")) + assert resolved.get("documentation") != method_completion.get( + "documentation" + ) + else: + with pytest.raises(Error, match="-32601"): + await session.resolve_completion(method_items[0]) await session.shutdown() -async def test_session_semantic_tokens(lsp_backend, tmp_path: Path): +async def test_session_semantic_tokens(lsp_backend, microsoft_pyright, tmp_path: Path): """Test semantic token retrieval""" code = """\ def greet(name: str) -> str: @@ -416,6 +426,13 @@ def greet(name: str) -> str: ) # Get semantic tokens + if microsoft_pyright: + try: + with pytest.raises(Error, match="-32601"): + await session.get_semantic_tokens() + finally: + await session.shutdown() + return tokens = await session.get_semantic_tokens() assert tokens is not None token_data = tokens.get("data", []) @@ -426,7 +443,9 @@ def greet(name: str) -> str: await session.shutdown() -async def test_session_semantic_tokens_normalized(lsp_backend, tmp_path: Path): +async def test_session_semantic_tokens_normalized( + lsp_backend, microsoft_pyright, tmp_path: Path +): """Test normalized semantic token retrieval with canonical legend""" code = """\ def greet(name: str) -> str: @@ -446,6 +465,14 @@ def greet(name: str) -> str: # Check that backend_legend is captured (Pyrefly uses hardcoded, others use server) backend_legend = session.backend_legend + if microsoft_pyright: + try: + assert backend_legend is None + with pytest.raises(Error, match="-32601"): + await session.get_semantic_tokens(normalize=True) + finally: + await session.shutdown() + return assert backend_legend is not None # Get raw tokens @@ -498,13 +525,21 @@ async def test_session_semantic_tokens_canonical_legend_consistency( await session.shutdown() -async def test_session_server_info(lsp_backend, backend_name, tmp_path: Path): +async def test_session_server_info( + lsp_backend, backend_name, microsoft_pyright, tmp_path: Path +): """Test that serverInfo from the initialize response is exposed on the session""" session = await lsp_types.Session.create( lsp_backend, base_path=tmp_path, initial_code="x = 1" ) server_info = session.server_info + if microsoft_pyright: + try: + assert server_info is None # Optional in LSP; Microsoft omits it. + finally: + await session.shutdown() + return assert server_info is not None, ( f"{backend_name} should report serverInfo in initialize response" ) @@ -518,7 +553,7 @@ async def test_session_server_info(lsp_backend, backend_name, tmp_path: Path): await session.shutdown() -async def test_session_recycling_basic(lsp_backend, tmp_path: Path): +async def test_session_recycling_basic(lsp_backend, microsoft_pyright, tmp_path: Path): """Test basic session recycling functionality""" pool = LSPProcessPool(max_size=2) @@ -532,8 +567,12 @@ async def test_session_recycling_basic(lsp_backend, tmp_path: Path): ) first_server_info = session1.server_info first_backend_legend = session1.backend_legend - assert first_server_info is not None - assert first_backend_legend is not None + if microsoft_pyright: + assert first_server_info is None + assert first_backend_legend is None + else: + assert first_server_info is not None + assert first_backend_legend is not None # Verify it works hover_info = await session1.get_hover_info( diff --git a/tests/test_zuban_config.py b/tests/test_zuban_config.py index e37a091..3aa2aff 100644 --- a/tests/test_zuban_config.py +++ b/tests/test_zuban_config.py @@ -45,7 +45,7 @@ def test_zuban_backend_write_config_creates_pyproject_toml(tmp_path: Path): backend = ZubanBackend() options: ZubanConfig = { - "mode": "default", + "mode": "auto", "untyped_strict_optional": True, } backend.write_config(tmp_path, options) @@ -56,7 +56,7 @@ def test_zuban_backend_write_config_creates_pyproject_toml(tmp_path: Path): parsed = tomllib.loads(config_path.read_text()) assert "tool" in parsed assert "zuban" in parsed["tool"] - assert parsed["tool"]["zuban"]["mode"] == "default" + assert parsed["tool"]["zuban"]["mode"] == "auto" assert parsed["tool"]["zuban"]["untyped_strict_optional"] is True @@ -291,9 +291,9 @@ def test_zuban_backend_write_config_empty_options_still_writes_table(tmp_path: P """An empty [tool.zuban] must still be written -- it selects Zuban's mode. The table's *presence* puts Zuban in its `default` (PyRight-like) mode. A - project carrying [tool.mypy] but no [tool.zuban] is driven into the weaker - Mypy-compatible mode, so skipping the write when `options` is empty would - silently downgrade those projects. See KNOWN_LIMITATIONS.md entry 1. + project carrying [tool.mypy] but no [tool.zuban] uses Mypy-compatible + behavior, so skipping the write when `options` is empty would silently + change the chosen mode. See KNOWN_LIMITATIONS.md entry 1. """ from lsp_types.zuban.backend import ZubanBackend diff --git a/uv.lock b/uv.lock index 8e720d4..60c003e 100644 --- a/uv.lock +++ b/uv.lock @@ -17,15 +17,15 @@ wheels = [ [[package]] name = "anyio" -version = "4.14.2" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] [[package]] @@ -197,7 +197,7 @@ wheels = [ [[package]] name = "datamodel-code-generator" -version = "0.76.0" +version = "0.79.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, @@ -209,9 +209,17 @@ dependencies = [ { name = "pydantic" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/65/27983214b172cf5463209ef55b68f7bb180eaeb114fbca477f709e2ca721/datamodel_code_generator-0.76.0.tar.gz", hash = "sha256:782ad3d17ea53f3a2300347d4058e281ba749ff57821712464f88c1ce75876c8", size = 2184419, upload-time = "2026-08-29T02:04:09.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/98/4b35f12e551d078e4297422e98cbc679f4792475bcf7da2e008308695d97/datamodel_code_generator-0.79.0.tar.gz", hash = "sha256:d1e4297c946cccb9c2f7f662167b5876d5554ce3884ac145042256af08f23924", size = 2661584, upload-time = "2026-09-10T17:56:30.318Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/72/801a441c9d3717c9fab3399a382ed2d146cc79df12b8ea45427ce26d55a3/datamodel_code_generator-0.76.0-py3-none-any.whl", hash = "sha256:cff9d19faa9072cfc19a04d11ac5e30d0e1a846bd1f843f05f37841941fe3e62", size = 643099, upload-time = "2026-08-29T02:04:07.519Z" }, + { url = "https://files.pythonhosted.org/packages/80/62/c4ab81535f7fd6d46040908c535248ef96fa53d90c7379913256ddcd3f6c/datamodel_code_generator-0.79.0-py3-none-any.whl", hash = "sha256:0aa827080096844a56135eb990e69d622e79f74bad2e5f006ce797bd74477d1f", size = 695513, upload-time = "2026-09-10T17:56:27.845Z" }, +] + +[package.optional-dependencies] +black = [ + { name = "black", marker = "sys_platform != 'emscripten'" }, +] +isort = [ + { name = "isort", marker = "sys_platform != 'emscripten'" }, ] [[package]] @@ -333,7 +341,7 @@ zuban = [ [package.dev-dependencies] dev = [ - { name = "datamodel-code-generator" }, + { name = "datamodel-code-generator", extra = ["black", "isort"] }, { name = "httpx" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -352,7 +360,7 @@ provides-extras = ["pyrefly", "ty", "zuban"] [package.metadata.requires-dev] dev = [ - { name = "datamodel-code-generator", specifier = ">=0.53.0" }, + { name = "datamodel-code-generator", extras = ["black", "isort"], specifier = ">=0.53.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "pytest", specifier = ">=9.0.0,<10" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, @@ -482,11 +490,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.11.5" +version = "4.11.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/06/cf1564dcc2e2261c8c8c6c05628dc8b418943bdae2a4e58640ceb2f770fa/platformdirs-4.11.5.tar.gz", hash = "sha256:e8b31f4f8bcbbedef91a6b57a706255e4f148d2a4e01648382a0a47342539173", size = 34823, upload-time = "2026-08-27T21:36:37.46Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/18/f3bb8ef0d3b930692343da8aa4d3cbcd6749477c053959395ac81965a6e9/platformdirs-4.11.8.tar.gz", hash = "sha256:f23abafea7dd4276d1f29104b83598d7dcc567cafd07c9c951e66665645437fc", size = 37182, upload-time = "2026-09-08T22:20:42.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/12/6f3fcd5067a9cbf4f8664b32957973498da8b083455203c8d9cab83a725c/platformdirs-4.11.5-py3-none-any.whl", hash = "sha256:89f8d42695853b89c7170bd49bc3dc593f98a71e695ede88e06a3b247bc4563b", size = 23900, upload-time = "2026-08-27T21:36:36.227Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e1/5b7b8bbb55084d1425bcb9bc823ff519e1b2be05f6ebb0089e2eacc38413/platformdirs-4.11.8-py3-none-any.whl", hash = "sha256:52f2f181bbfde907966932cc8312d967d02976422d66d537ea16092b8e291081", size = 24027, upload-time = "2026-09-08T22:20:41.537Z" }, ] [[package]] @@ -599,21 +607,21 @@ wheels = [ [[package]] name = "pyrefly" -version = "1.2.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/01/a86e9f24722b095c3f88e3616132b75a21b0df53804bdc6a45314dd4d93c/pyrefly-1.2.0.tar.gz", hash = "sha256:5485f960fc2481617068c918335c39ab1507ef90b6b5bd35bf57726e60e73185", size = 6243654, upload-time = "2026-08-01T02:56:27.592Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/82/df301034ff7705b51ab212039958910434e1e9bd4765cde435538221fc35/pyrefly-1.3.0.tar.gz", hash = "sha256:e96a0bf3abdc41cf40f7d2ef274bb610f4e6e4b387b366aea102902839073388", size = 6678024, upload-time = "2026-09-11T00:41:37.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/9d/3c0ef1d4843987b22f996ed381ec9cf5a3b1273e29804db276252e4c95eb/pyrefly-1.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7f46d983ac49ddd2b043694960a01dc6a19a5cfd8eec609d6bd9c42866f91b4e", size = 14026305, upload-time = "2026-08-01T02:56:02.611Z" }, - { url = "https://files.pythonhosted.org/packages/0a/06/03bbb78fbea54cdc65b626619f3597d5611aca4fdef11e72a4e8360e7e63/pyrefly-1.2.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:756f669b5555090f5c1a4fef30db1785fabe657764f7e4e6dc88994dfb8ca82d", size = 13463880, upload-time = "2026-08-01T02:56:04.93Z" }, - { url = "https://files.pythonhosted.org/packages/13/5a/7d8bc00a38e93bbc9c3e7bd14d305f7948717e667c9bcddeab9dd42fd255/pyrefly-1.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3465812ce5ef4781fb592edbf2724547296f0a3124be115d73c7e8b2401862d", size = 13907329, upload-time = "2026-08-01T02:56:07.104Z" }, - { url = "https://files.pythonhosted.org/packages/be/94/9e08b4bf799d0b8f36b55a2783c7ba5f51730cf0632a85a67b5b5ed876cd/pyrefly-1.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5de7b2ad2bba5c8055181681a84b74143eac2234a48ba5d1b7ed7e7a722b02bd", size = 15039020, upload-time = "2026-08-01T02:56:09.208Z" }, - { url = "https://files.pythonhosted.org/packages/5b/bd/bca5fd0c80f4daf8ee6903a29df9f3de1feb05ff0946b8f35ec8c5096b13/pyrefly-1.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25822ea9505f589ea8a725e4268b475132fb89e038fbf092e446510443ac142a", size = 14986199, upload-time = "2026-08-01T02:56:11.924Z" }, - { url = "https://files.pythonhosted.org/packages/97/f7/f07087f3d185ad2eced0c56cef89ca5474dfb4ff25f146cd50a861c97553/pyrefly-1.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90efe75e17491ef5d636e10469e9278d7d0256b3b4c5e1f4750069bf3ae0f5d1", size = 14393715, upload-time = "2026-08-01T02:56:14.143Z" }, - { url = "https://files.pythonhosted.org/packages/d3/70/0d142c320e284b9e3ce35e9b1e58b8ce2ee1f578f2a7234bc30e5022b94f/pyrefly-1.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:368aaf7eee4f511ddc0f8e564cf14e01ab2f10b0db9105c6d5b153bf498d07bf", size = 13933008, upload-time = "2026-08-01T02:56:16.525Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e8/e84f11b6e1f63fd453ad3654213b9a0f6f4de8cef6b58038eef2d0d5955d/pyrefly-1.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d52d5da7bc65fb7675fbaa80eda879d4f8787c494f04cac21603330d3abbdbbe", size = 14431827, upload-time = "2026-08-01T02:56:18.645Z" }, - { url = "https://files.pythonhosted.org/packages/0f/06/810d31380f66c75e1c0779a408d3b16117b1b368b57894f6aa66bef21686/pyrefly-1.2.0-py3-none-win32.whl", hash = "sha256:8c90751de8506d938e8f802659c74cf35bd7a0036510ee6c634a38eebb280bfa", size = 13229447, upload-time = "2026-08-01T02:56:20.921Z" }, - { url = "https://files.pythonhosted.org/packages/ed/98/4dafa3c7a1caed2dc8cc708dde09ba27963c7736508f55b626fff3024113/pyrefly-1.2.0-py3-none-win_amd64.whl", hash = "sha256:8a8964c224ccc4882730130955815de21ff443c1ac3f0b90685b19bf63848170", size = 14087387, upload-time = "2026-08-01T02:56:23.188Z" }, - { url = "https://files.pythonhosted.org/packages/1b/1c/df3cb0a2e5591660ded7a1836cd2f29dc48c91adb1c0a3a700a96f6d09e1/pyrefly-1.2.0-py3-none-win_arm64.whl", hash = "sha256:3a90bb8df39dfbac74b1f3b2e9d7c526b8f80568884c3944d955023a73ebf61e", size = 13430873, upload-time = "2026-08-01T02:56:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/36/98/47bf19b6caef77852c7e1c03d71ef48a5f452bf041503d883e0bca6fc4c8/pyrefly-1.3.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dd6c61e4c307eaaf3cf25fb329c4e7c9ff0376c42d5900362be2fd3333578a69", size = 15088089, upload-time = "2026-09-11T00:41:11.046Z" }, + { url = "https://files.pythonhosted.org/packages/4c/77/454615aa5db04903c99ced321295e32242c4b188be1a08f40b617ed72984/pyrefly-1.3.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:14c56defaa804747f18e03d11b539e71b71c8251a2b9f58e73dea80e2f6adfd9", size = 14431422, upload-time = "2026-09-11T00:41:13.61Z" }, + { url = "https://files.pythonhosted.org/packages/01/f2/142e190b9432f802682b0b0794f71e2010e936de0541492cc43678d647af/pyrefly-1.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64285b1261681933f97cb11f3ba2e4445ff02a266707e4e2b7a279db0424c9f2", size = 14880659, upload-time = "2026-09-11T00:41:15.947Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2f/60598fe38f29f22d11d9f4b5c7fee1cbbb0d5b0cb7db6b373761d57f5f75/pyrefly-1.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46a90d6e48415dfa5d8d9c94ea5b9b61d7149dab87348952f2e000cf45145664", size = 16116073, upload-time = "2026-09-11T00:41:18.547Z" }, + { url = "https://files.pythonhosted.org/packages/c2/0a/33e49756fe3341dbb13789622cb8c9b6350d2710ceb47b8b0521f134ceea/pyrefly-1.3.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0cc33b35204b8e1b09396313024580d25a041de2f2f527e6496fc099282b967", size = 16049648, upload-time = "2026-09-11T00:41:21.255Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/be59429726af4c0c3585951a937232d957182364cd2a621f74f4298a176e/pyrefly-1.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94ff692911596d257462320a2ef1435c93c20c80d631404961282f4cc22a6cc", size = 15439725, upload-time = "2026-09-11T00:41:23.572Z" }, + { url = "https://files.pythonhosted.org/packages/2b/73/bb834e88afea6c0de6960be2102c0cc1d817193c1efc3f6db9b450df191c/pyrefly-1.3.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5705bec5438aa4e3b65b0d2ea7613f6bdbdc57f8848bd4b7940bbab37f230d8c", size = 14900386, upload-time = "2026-09-11T00:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/c7/64/1adbc6ece9090bbd2e4ed1300918ab709166119426a21102639912ea65bb/pyrefly-1.3.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fe60cb6d7cdf6c9bb236c0a7efc4b9839cfe7ef834b07d637862f95a642699ef", size = 15478065, upload-time = "2026-09-11T00:41:28.049Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5c/e1351611d2e205718394237381220ab70f0e21cb297db024eafead19acf9/pyrefly-1.3.0-py3-none-win32.whl", hash = "sha256:4fb4cd5e5007e99208e44f749edaf37a03c0a56c98da7422c2dd60fde0f294b0", size = 14130445, upload-time = "2026-09-11T00:41:30.401Z" }, + { url = "https://files.pythonhosted.org/packages/3c/92/1d78e2ab1f9f1055ea43164d336903c95e608bede06ed182dc4b4f2c5390/pyrefly-1.3.0-py3-none-win_amd64.whl", hash = "sha256:f809fa5b447012adb8861efeb8f6435146788a39d9251d79dc38731a5d241893", size = 15067598, upload-time = "2026-09-11T00:41:32.582Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6f/459f56dc345c32f8597acef13675e7e67679fb56bc3fd8f0a280eaeb8c02/pyrefly-1.3.0-py3-none-win_arm64.whl", hash = "sha256:3a3fb7c07dfb9b43d4205d8e85a83aaefc85377d0437dc8469c132836a8a7e5d", size = 14347403, upload-time = "2026-09-11T00:41:35.01Z" }, ] [[package]] @@ -758,27 +766,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.75" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/d0/d0c96f898d6974a4a3569ab3efdf9512c04ad99f9203effb55f72497fe97/ty-0.0.75.tar.gz", hash = "sha256:4c5eead33dfbf6e2ebb4f400f74b51ffc9bab702a6f23ddb648a1cbb740387e3", size = 6868326, upload-time = "2026-08-26T20:23:40.399Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/6c/b12d03505f17581f0cfa3c12273fe34c1d67b36dfda1bc561a6bdc16512b/ty-0.0.75-py3-none-linux_armv6l.whl", hash = "sha256:e5409f50db2246fd4bd039d93d261e0cfa1daa554a4fb77256f91072c570349a", size = 12972606, upload-time = "2026-08-26T20:22:59.716Z" }, - { url = "https://files.pythonhosted.org/packages/d1/aa/30f11eecd9215a9f87e8fe8baaf48f3ce905f5d75b8e4aac70f0091f130c/ty-0.0.75-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5e7b8b3472fb9bb2eeab314984b265df08a7a9d518867a9e6020eebc06570be2", size = 12527158, upload-time = "2026-08-26T20:23:02.767Z" }, - { url = "https://files.pythonhosted.org/packages/f2/11/7fd7001b0b5c6610bfbad7357e47d5fe6f82d4e84e94c53776a478f5e9f8/ty-0.0.75-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c6ccf34169821fe0d23e3360deeef981d217963412f1d087b9bdd32ec57f7a57", size = 12400533, upload-time = "2026-08-26T20:23:04.965Z" }, - { url = "https://files.pythonhosted.org/packages/fd/7f/1e284ea3d348d7be02f12d83bc22ed9ef193033f863f05b64db99027f141/ty-0.0.75-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842ebb41e9c6c334b40768704e20b1a69d5c6b08805b289d5e0e2565f49f2de1", size = 12420592, upload-time = "2026-08-26T20:23:07.427Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ab/d813271543370c47fd74b5118f2066ab32b0983e907b1821f3f9a6d0fa7f/ty-0.0.75-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf7a5a723c5f1e0fab4ffbfe9bd95123a526ed48f206e5f25cb2161ca294007a", size = 12739219, upload-time = "2026-08-26T20:23:09.809Z" }, - { url = "https://files.pythonhosted.org/packages/31/5b/95b49cc5570fd92a7bf63732f649b31906158721e03c7fcb1b5be74ee3bf/ty-0.0.75-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54382f98e5da292fcd7104391afef5105c35bb2f312e29bea6f5fa419935255c", size = 13494046, upload-time = "2026-08-26T20:23:12.191Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0d/502d2dd68173cf020e1ad2bdbab9544c86776de0b0e2ed15f8c2fe006e3d/ty-0.0.75-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac13b180dc2aade2cd243f56b01650e78bf091a2e522ad3bc947245d7837c613", size = 13938899, upload-time = "2026-08-26T20:23:14.764Z" }, - { url = "https://files.pythonhosted.org/packages/20/5b/f3b12a25c07224456219fc2bd20db0ad7e40b304be0ff6aad728da0135f9/ty-0.0.75-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752df7951a443219d7f1ff817e3723c85d428565ff449e08a7a93ba821661526", size = 13656711, upload-time = "2026-08-26T20:23:17.145Z" }, - { url = "https://files.pythonhosted.org/packages/51/7b/f090ad306e2b15a07b332d647138c5264b89d9758855ecce8b8a10bcb153/ty-0.0.75-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd399feedf7cee816563c1baec45fc1c0b3c89f1ea42364920b688004b5b7da", size = 13093499, upload-time = "2026-08-26T20:23:19.489Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4b/f69b99aaaca0c7c65d5f114b186b26b21666f767b0c69eec99a2bdccc061/ty-0.0.75-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d7625f6f56c7dc1e873579fdc9e432a0e21e302afe847ab60704d2303442a92e", size = 13520580, upload-time = "2026-08-26T20:23:21.789Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e7/692c5f905c0345a15d2255fc74066d660f030254ae8dcdaf33f5a5c2f279/ty-0.0.75-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:89e7d527e95a2534b70cae29e94c104b84082760ea05927d23bb87280969c104", size = 12524095, upload-time = "2026-08-26T20:23:24.026Z" }, - { url = "https://files.pythonhosted.org/packages/ba/9a/f42b12cf265ea95344bf554764c4791cfb273bdd628aadd7c209af7cadc3/ty-0.0.75-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0843f134440740706e01bee5f88f4cfc10e9b018bddb9e4ef4c12dc9fc0c9aef", size = 12756591, upload-time = "2026-08-26T20:23:26.126Z" }, - { url = "https://files.pythonhosted.org/packages/7c/5e/9b180c133cb9cce48179a7d2bf9e1802d992aa8176a918e0e05205760b42/ty-0.0.75-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1bd0ec0e50ee1376875c88891efe6f549c3560fa5b2ddad79a425cd5a6218b9c", size = 12998754, upload-time = "2026-08-26T20:23:28.353Z" }, - { url = "https://files.pythonhosted.org/packages/39/f6/3c6ef5dd550103e29905121c67fb96a374564f31a2f44c6faa1af98c2d61/ty-0.0.75-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1f9eafd561f90110d5e29f589ec3e956c4686e2f6631348d99276436f5cbe4d1", size = 13316474, upload-time = "2026-08-26T20:23:30.857Z" }, - { url = "https://files.pythonhosted.org/packages/bb/52/12776337874c821076bd5368e352ccd9e67174790abe3b856f749cb3524b/ty-0.0.75-py3-none-win32.whl", hash = "sha256:05063a6fafe2154b794a7f964515d148e51acd186d72d4a3acd347ee9fa19336", size = 12316315, upload-time = "2026-08-26T20:23:33.528Z" }, - { url = "https://files.pythonhosted.org/packages/53/e6/bb51e16af5c7138c9f52f8f3d0a401a371c6798d092e3b74926f186a9814/ty-0.0.75-py3-none-win_amd64.whl", hash = "sha256:81cf1ba5f6b7536ad56747865214255d9bc8e80533a689dbb9ddeaad464b09f1", size = 12917267, upload-time = "2026-08-26T20:23:35.978Z" }, - { url = "https://files.pythonhosted.org/packages/39/73/4542f829107468b5de4231af67f29927c093bfad11f3c1e5b2c08fb1206b/ty-0.0.75-py3-none-win_arm64.whl", hash = "sha256:541c9af5b7a0ad23d15ec315a7da81150833c359f48124ed3789ff25eacd6f42", size = 12711024, upload-time = "2026-08-26T20:23:38.159Z" }, +version = "0.0.80" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/b0/6d1b10e0d422736a3c439e487c950ae785f401d71ff879d5be68bccb6d90/ty-0.0.80.tar.gz", hash = "sha256:fe86bc91327e45ff5e3593b7e306e7f57a44bc0608f38d647b8d97bd99c96013", size = 7183998, upload-time = "2026-09-09T21:18:47.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/c8/3c93195eca282936ebb574d0ba98843e4b147ee324006f88464777012a92/ty-0.0.80-py3-none-linux_armv6l.whl", hash = "sha256:738d1cfca466c577aea24547c348629c00c63548cf7da2c46b497b3840f85dee", size = 13606509, upload-time = "2026-09-09T21:18:10.476Z" }, + { url = "https://files.pythonhosted.org/packages/e7/48/bbb47f7001c97262a5109bcd92a45bcc8a2fbb21e994c214a9897630bfb7/ty-0.0.80-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:56060164bb8ee43770fa367524fbdba2611fc90f8923a02cc91f80eb37d5a1b0", size = 13203812, upload-time = "2026-09-09T21:18:12.796Z" }, + { url = "https://files.pythonhosted.org/packages/db/8d/fd141160567047b742e466be8ed09a4c4ed80fa045d37f946eb4f4532fee/ty-0.0.80-py3-none-macosx_11_0_arm64.whl", hash = "sha256:da4062e0fbf3923d9b71688157c5753394f243348f00732e9edbb27498397169", size = 13021896, upload-time = "2026-09-09T21:18:15.186Z" }, + { url = "https://files.pythonhosted.org/packages/1e/10/b4177faf9e71bc37bc4d08f512042269660a1ce0ec457c48f96e51fc91d7/ty-0.0.80-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9eb94b2659f506a3a07a9ea1415d5c18aaa1e554adc1612614d617a0ed320e1", size = 13083881, upload-time = "2026-09-09T21:18:17.616Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c2/192d48b9a9acbbe030fc9a4bd73af75671a5566e731949f77e912a68ce68/ty-0.0.80-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2f5e39a87da48c1af1a3b3135f02be7a83a93da30b0d6a5f5d27806e7c747ce", size = 13354874, upload-time = "2026-09-09T21:18:19.744Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/736dfd31efd98bed9dbe018ff91676e5ea83485b1e24afb635fd0247728f/ty-0.0.80-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e00149e7779c6b98f3ba12e10a631a861bd7ca1a42bbf064e2677ada7d2c0c2", size = 14204805, upload-time = "2026-09-09T21:18:21.999Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6f/557c726cb53401998e3589bb632c963a6887a70240e0a4386024f3731c81/ty-0.0.80-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a377268f359fb6e7a2cc9026a09223c764f58d020dec8e6baeaba9caba6ff829", size = 14630014, upload-time = "2026-09-09T21:18:24Z" }, + { url = "https://files.pythonhosted.org/packages/e9/19/7a4b18fe27f6b6bd4ffa56b67c8b09ecb76bb4312d1c64b73f65417b4dbd/ty-0.0.80-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4511dbf1b266b9ce5b73e9b28d94468096c2b6da00e2fe205168c32d3b352ca7", size = 14326946, upload-time = "2026-09-09T21:18:26.679Z" }, + { url = "https://files.pythonhosted.org/packages/f8/95/16dd90805fc7e53ec54ae9fdb1a8b74753fb159a55a170f60f49b9417824/ty-0.0.80-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe95feffa7156800c6f804195acb9fb5846a39671c7192c30fff23351aaf7c31", size = 13705988, upload-time = "2026-09-09T21:18:28.681Z" }, + { url = "https://files.pythonhosted.org/packages/b1/09/4e87992c23ab8a742c7d4914ac5fe66fb9301c8464a69fd2ecbc0be3fbcd/ty-0.0.80-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ba26b39f06bc8c3c2c5acd3a147239482b36620cdbca1b9d02e915294854f2cc", size = 14233729, upload-time = "2026-09-09T21:18:30.961Z" }, + { url = "https://files.pythonhosted.org/packages/36/e9/b8c11fda8e66a1d1cc1a7160ed719a33f4dc0107b1cef6a14e2673965763/ty-0.0.80-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2e6167320888c115a6fbe69893b63fd1c848f9121a454e75e5f612674528e3f3", size = 13173523, upload-time = "2026-09-09T21:18:33.266Z" }, + { url = "https://files.pythonhosted.org/packages/73/a7/512083fa540c5be1ac8642658f03bfb5bfa6fd168ea4f55e3c0aabe04166/ty-0.0.80-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7aecb62b4de70b479eab07d1779ea66da26dd8b068f50a9330867157312f103c", size = 13373014, upload-time = "2026-09-09T21:18:35.339Z" }, + { url = "https://files.pythonhosted.org/packages/5e/63/cd2f0ca81fd9b8cafef93bbcf022bf636bc4de8ee7465c6aafeec1d4d52b/ty-0.0.80-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4fed06adacd16b7e2d722f37449021b1419598d0440769701c0f4827faddde63", size = 13667827, upload-time = "2026-09-09T21:18:37.241Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5d/ebdfdcb2dc099ae74dc4b75511eef0dd398b2fa27006c543543974b4567f/ty-0.0.80-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:09329af6303ce611ec2cfffc995dc1ddf76c9f47194fc1a4d64a984759c25f5b", size = 13963866, upload-time = "2026-09-09T21:18:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/c286d5e1b560cfd16d6e91fdab683718aacf09fc88938ca06dcc9d1c8502/ty-0.0.80-py3-none-win32.whl", hash = "sha256:a81b3b512f7b4c68e42fbd1835633de658809666e9aafd5defa52bbc5197698d", size = 12881806, upload-time = "2026-09-09T21:18:41.507Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c8/3b3a8ac16d47a23ff34bf57c0d99a491860748aea96e0e886b4bfed54f9b/ty-0.0.80-py3-none-win_amd64.whl", hash = "sha256:8043f99878a2ae434781cb881c8a3840dff70c4a5b60bdc5ae84251f35ee063f", size = 13528883, upload-time = "2026-09-09T21:18:43.687Z" }, + { url = "https://files.pythonhosted.org/packages/2e/71/a6c697930fca76596d7d17f8853120f41a298fd9ea632c901cfca8ca869b/ty-0.0.80-py3-none-win_arm64.whl", hash = "sha256:e277ef034331da5319efc839c064968d24f35715b71c70369cf97d36fe4dcbdb", size = 13370135, upload-time = "2026-09-09T21:18:45.758Z" }, ] [[package]] @@ -816,19 +824,19 @@ wheels = [ [[package]] name = "zuban" -version = "0.9.2" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/c0/1c395a08b2a7c48fbbb1398818d65813032ad0acfec5a841db0e7142a6ac/zuban-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8922df80834c2d5cb1e2798192dc85d40b4dc3fa3149e7b2b89dccf3cdc110d8", size = 11377755, upload-time = "2026-08-26T00:19:21.022Z" }, - { url = "https://files.pythonhosted.org/packages/af/3c/b323802de4a86f71479b85d388294380205006362bca4414d3213469136a/zuban-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:112c9bd639ec417fe7881a7d99bc34e35ac6ba6e0afd4f33caf0774ea8ae84fc", size = 11101402, upload-time = "2026-08-26T00:19:24.548Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3b/71ee867aebe9175de1db0208c2eeee7f294821545e6140c28e5ed9f21888/zuban-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8d65dafe086a8e73cbb8e6d7e3b234a7d2e64cf794d49c5ca0ca901a2880bea", size = 28451467, upload-time = "2026-08-26T00:19:28.898Z" }, - { url = "https://files.pythonhosted.org/packages/bc/93/7331f9f096b1c4a9d575ed6e86572cf6c5ac9eab5dc1c68d0d875e78d549/zuban-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d8431a65ac7815af24e015d6f6c935518625e015e028b061899677a3d43c6349", size = 28687477, upload-time = "2026-08-26T00:19:33.102Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5a/de09de19a0ea8a4593632631c905751cf380cf8a92d1607632ad40c683ea/zuban-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d271a27a56ad89e3bb5942580aa903354c3861bfab29e5c655d44007d1d15a2", size = 29736764, upload-time = "2026-08-26T00:19:38.73Z" }, - { url = "https://files.pythonhosted.org/packages/cb/71/2e32dfafa5ac302b7b9cc48bbcfbc9da6bca221ccfd03da5b9d2a28240ee/zuban-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20a467e9d90f5dce53be3c760d9b65e768870a4e940a5256985f67155587eeb9", size = 31719649, upload-time = "2026-08-26T00:19:43.454Z" }, - { url = "https://files.pythonhosted.org/packages/b2/24/d437b54a088b2a35df948841d011a011ba5db59bdb4bad7c800dffcbbe2e/zuban-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a2c29e501de2f39690eadffab6369c9ac949bdab82aaab4c9fcbbfda0794d704", size = 28630746, upload-time = "2026-08-26T00:19:47.888Z" }, - { url = "https://files.pythonhosted.org/packages/04/12/a132d0ad526d8986475344546b22acfea0bd6856bb293b70e691fa9bf5d5/zuban-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:23e34cd5d3c2aad476506fc2a0d3f4fd314e87bb03c873ec2c319497b06f16df", size = 29122179, upload-time = "2026-08-26T00:19:52.937Z" }, - { url = "https://files.pythonhosted.org/packages/70/4e/ebbec923b02c69eee955cc65d227c4a1b6f5c6f2b509b92c9df02ad67e2b/zuban-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:40f4737d3f7c5926b986ebb4cb44767b2b72788072e833c49d2d3ad2f69ecbdb", size = 29624765, upload-time = "2026-08-26T00:19:57.778Z" }, - { url = "https://files.pythonhosted.org/packages/db/7f/8baa60499029d57bc0dbc254d75469f7b7860654671d0bf3683bc6c56f05/zuban-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a83d3c1f12cd131774a4f89b3c0d750619e7035d02dcd4f668fb43e5ba2fea", size = 28964675, upload-time = "2026-08-26T00:20:02.558Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d8/4a6f29958fa962c2b12ec32a0624708b9da93bec236ace0ac993632d05b1/zuban-0.9.2-py3-none-win32.whl", hash = "sha256:59d09caf2e488eb6d31a3bead1699349f66dafbfd595eb64bcd8e11b79d8e2d1", size = 10051150, upload-time = "2026-08-26T00:20:05.965Z" }, - { url = "https://files.pythonhosted.org/packages/af/c2/123e0f3054f688039290766fdbdd4cba96834537e6fd3f6c95ae460858bd/zuban-0.9.2-py3-none-win_amd64.whl", hash = "sha256:9bc429ef78d3f21a6aeec9cddffe8748235ef2374ef318a91f4aa089cafba75c", size = 10730401, upload-time = "2026-08-26T00:20:08.97Z" }, +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/97/a0ba462d87300de7d0488d368c35fb204b39fc4f4b5f58e55a211b74be63/zuban-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c479be7ec45c03c9a909a561f9c77475cb558b7b326626c7dccfc03006294c0e", size = 11391467, upload-time = "2026-09-02T15:28:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e5/fd0e96aa7d4a6d8aa8c892b1a96350e696c29577972409f94323faccafc9/zuban-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b24addd7f1fa95790381feaebef7cfc095b0c81a77f27457d0deaa4ba21f679", size = 11101353, upload-time = "2026-09-02T15:28:27.206Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5f/a64ce57bb76680028d39540f6fb3789509f5e718d44c1a6ee8451da8ec68/zuban-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5c575915e3018f82976f45ee0f46f9f15eb458a30b3e9c4241a82419fa59f1", size = 28569052, upload-time = "2026-09-02T15:28:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/dc/83/3a9adab0eb8805ba25e95683a728ec9e67cb3d146c16a1b464b6e7b1d6bb/zuban-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba146100486a899699ed61b4622eb7088e2c526f8d5fc1433647a3c50ebbf088", size = 28789112, upload-time = "2026-09-02T15:28:35.046Z" }, + { url = "https://files.pythonhosted.org/packages/7d/02/391f917472321cf944548bb3f0435115a7a8cc6815fe0c0960662872403a/zuban-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:716e4bd45bcd444ea80c56603f7ecb302afb6155c5e8615754921d1cbfb09dbc", size = 29883166, upload-time = "2026-09-02T15:28:38.989Z" }, + { url = "https://files.pythonhosted.org/packages/74/be/e3a79dfba2c6c204fe4d3c3102cd40ee2d45f04992d8ce8322938d6db400/zuban-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a99e797ceaccead3e6e5e4ba337dce822fedd9978579eb54a6d50364c962b5", size = 31934489, upload-time = "2026-09-02T15:28:43.526Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f6/3aea03d059b7d9f8eedd86ef8d77c992288a75d9ca254862e5a66712ced3/zuban-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9351787b9034316678bdb075fbb63344d66c4a13fe5616101ca37fe204dfb3db", size = 28743782, upload-time = "2026-09-02T15:28:47.411Z" }, + { url = "https://files.pythonhosted.org/packages/0c/69/62e3b373bc50dfe705c16b8593881e4c9f549d4a6d8140ade997dca12904/zuban-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f2249f2363415e5fc5ff2b89fc35ff650bf2a6e4337ea04e53dbf23984104a3f", size = 29226151, upload-time = "2026-09-02T15:28:51.298Z" }, + { url = "https://files.pythonhosted.org/packages/8b/73/00db674a103315370a68c601ac97a6e0818170fd8e4ce126689be7458496/zuban-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8274b0df6bfa87fea376a6f089a1a442acc59a981af2c2cd0397ba1385eca851", size = 29772524, upload-time = "2026-09-02T15:28:55.563Z" }, + { url = "https://files.pythonhosted.org/packages/44/6d/d8e43f95e7a06a4fe67a86d802eea755f8e1c478bfab60ef2bcfecdfb2ee/zuban-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c72470afba2d6cbeb20626452015a5bee1488248b3db8849da0df7d090f70386", size = 29138520, upload-time = "2026-09-02T15:29:00.248Z" }, + { url = "https://files.pythonhosted.org/packages/77/d8/59eeb2bfb1d215a1879d4b941a118937e4ac0010da67f21e4c0a5746b2bb/zuban-0.9.3-py3-none-win32.whl", hash = "sha256:7af0490c4e4b1bf6049a8313aa3dee286f12791d5e937d0af03fa1e49184aa84", size = 10030746, upload-time = "2026-09-02T15:29:03.853Z" }, + { url = "https://files.pythonhosted.org/packages/0f/0f/b9051319c9c884a28eb4d53c9f1b022301b912a1b80554d2b0516fc9f174/zuban-0.9.3-py3-none-win_amd64.whl", hash = "sha256:c10097d0b8cbc7525ed12ebc9abea279b7e03c7aeee8037a0edbd14fd967e83d", size = 10745279, upload-time = "2026-09-02T15:29:06.758Z" }, ]