Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,23 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## 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/`.
Follow `docs/internal/FEATURE_VERIFICATION.md` for release updates, including
separate Microsoft Pyright/basedpyright checks and browser WASM verification.

## Documentation voice and audience

- Keep the README and every guide it links concise, direct and confident. State
what each backend does and give actionable instructions and concrete limits.
- Preserve the character banner, backend personalities and tasteful emojis.
- Keep run dates, tested-version inventories, commands/results, investigation
history and editorial feedback in `docs/internal/`. Public docs must not link
to those records or narrate internal verification and decision-making.
- Confidence must follow evidence: keep real limitations and experimental labels;
remove hedging, repetitive caveats and unsupported rankings.
- Write public guides for users, internal records for maintainers. Apply this
distinction to the full README link path, not just the README itself.

The durable feedback record is `docs/internal/DOCUMENTATION.md`.

## Essential Commands

Expand Down
71 changes: 30 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,21 @@ 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

<img src="assets/images/lsp-party.png" width="800" alt="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
## 🧙 Meet your party

<img src="https://raw.githubusercontent.com/Mazyod/lsp-python-types/main/assets/images/lsp-party.png" width="800" alt="Pixel-art party: Pyright the blue sentinel, Pyrefly the coral artificer, ty the green scout, and Zuban the purple diplomat." />

- 🛡️ **[Pyright — the sentinel](https://github.com/Mazyod/lsp-python-types/blob/main/docs/research/landscape.md#pyright--the-sentinel).**
Broad typing support and configurable execution environments.
Choose **basedpyright** for extra diagnostics, baselines and semantic highlighting.
- 🔧 **[Pyrefly — the artificer](https://github.com/Mazyod/lsp-python-types/blob/main/docs/research/pyrefly.md).**
Framework-aware analysis with opt-in regex and `mock.patch` checks.
- 🏹 **[ty — the scout](https://github.com/Mazyod/lsp-python-types/blob/main/docs/research/landscape.md#ty--the-scout).**
Incremental analysis, explanatory diagnostics and precise type narrowing.
- 🤝 **[Zuban — the diplomat](https://github.com/Mazyod/lsp-python-types/blob/main/docs/research/landscape.md#zuban--the-diplomat).**
Mypy-compatible configuration and editor inference for untyped code.

## 🚀 Start a session

```sh
pip install "lsp-types[pyrefly]" # Or [ty] / [zuban]
Expand Down Expand Up @@ -70,31 +60,30 @@ 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.

## What works here
## ✨ Features

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.
All four backends support diagnostics, hover, completion, signature help and rename.

Verified **2026-09-11**: Pyright **1.1.414**, basedpyright **1.40.1**,
Pyrefly **1.3.0**, ty **0.0.80**, Zuban **0.9.3**.
| Feature | Pyright | basedpyright | Pyrefly | ty | Zuban |
|---|---|---|---|---|---|
| Semantic highlighting | ❌ | ✅ | ✅ | ✅ | ✅ |
| Completion documentation via resolve | ✅ | ✅ | ❌ | ❌ | ✅ |

[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)
Pyrefly returns completion items unchanged on resolve; ty does not support the
request. ty hover returns the type without the symbol name.

## Development
📖 [API & lifecycle](https://github.com/Mazyod/lsp-python-types/blob/main/docs/USAGE.md) ·
🎨 [Semantic tokens](https://github.com/Mazyod/lsp-python-types/blob/main/docs/SEMANTIC_TOKENS.md) ·
🧭 [Backend guide](https://github.com/Mazyod/lsp-python-types/blob/main/docs/research/landscape.md) ·
🎮 [Browser playground](https://mazyod.com/lsp-python-types/)

## 🛠️ Development

```sh
uv sync --all-extras --locked
npm install -g basedpyright
uv run pytest
uv run pytest tests
uvx pyright --pythonpath .venv/bin/python
uvx ruff check .
make generate-latest-types
```

The [runbook](docs/FEATURE_VERIFICATION.md) covers testing Microsoft Pyright
separately, regenerating schemas and updating the browser playground.
273 changes: 54 additions & 219 deletions docs/INTEGRATION_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,219 +1,54 @@
# LSP Backend Integration Notes

This document captures frictions and enhancement opportunities discovered while integrating new LSP backends into lsp-python-types.

## ty Backend Integration (January 2026)

### Frictions Encountered

#### 1. Virtual Document Support (Resolved)

**Issue**: ty (as integrated at 0.0.11) required files to exist on disk before it could provide diagnostics, completion, and other features. Pyright and Pyrefly work with "virtual documents" opened via `didOpen` without requiring the file to exist on disk.

**Original workaround**: The `requires_file_on_disk()` flag was added to the `LSPBackend` protocol so `Session.create()`/`update_code()` could mirror the session code to disk for ty.

**Resolution (August 2026)**: Bisecting PyPI releases showed ty supports virtual documents from 0.0.16 onward (diagnostics, completion, and rename all verified with no file on disk). `TyBackend.requires_file_on_disk()` now returns `False` and the package floor is `ty>=0.0.16`. The protocol flag remains for any future backend that needs it.

#### 2. `workspace/didChangeConfiguration` Not Supported

**Issue**: ty logs `Received notification workspace/didChangeConfiguration which does not have a handler.` The Session class sends this notification after initialization to apply workspace settings, unless the backend opts out.

**Impact**: Runtime configuration changes via `didChangeConfiguration` don't work with ty. However, configuration written to `ty.toml` is respected.

**Resolution**: The `LSPBackend` protocol gained `consumes_did_change_configuration()` (default `True`). `TyBackend` and `ZubanBackend` return `False`, so `Session.create()` skips the notification entirely for them. No functional loss — both read their config from disk.

#### 3. Nested Configuration Structure

**Issue**: ty uses nested TOML sections (`[environment]`, `[src]`, `[rules]`) unlike Pyrefly's flat structure. This required implementing recursive key conversion.

**Solution**: Created `_convert_keys_to_kebab()` function in `lsp_types/ty/backend.py`:
```python
def _convert_keys_to_kebab(obj: t.Mapping[str, t.Any]) -> dict[str, t.Any]:
"""Recursively convert dict keys from snake_case to kebab-case."""
result: dict[str, t.Any] = {}
for key, value in obj.items():
kebab_key = key.replace("_", "-")
if isinstance(value, dict):
result[kebab_key] = _convert_keys_to_kebab(value)
elif isinstance(value, list):
result[kebab_key] = [
_convert_keys_to_kebab(v) if isinstance(v, dict) else v
for v in value
]
else:
result[kebab_key] = value
return result
```

**Potential Enhancement**: Extract this utility to a shared module (`lsp_types/utils.py`) since Pyrefly also uses TOML with kebab-case keys (though currently with flat structure).

#### 4. Hover Information Format Differences

**Issue**: ty's hover response shows just the type (`str`) rather than `variable_name: type` format used by Pyright and Pyrefly.

**Impact**: Test assertions checking for variable names in hover text fail for ty.

**Workaround**: Added backend-specific assertion in `test_session_hover`:
```python
if backend_name != "ty":
assert "result" in hover_text
assert "str" in hover_text
```

#### 5. No CLI Flags for LSP Server

**Issue**: Unlike Pyrefly which accepts `--verbose`, `--threads`, and `--indexing-mode` CLI flags, ty's `server` command accepts no configuration flags.

**Impact**: Configuration reaches ty via `ty.toml` or via `initializationOptions` at
LSP initialization, not via the command line. See ty's KNOWN_LIMITATIONS entries 1 and 3.

**Solution**: `create_process_launch_info()` simply returns `["ty", "server"]` without any conditional flag building.

---

## Enhancement Opportunities

### 1. Shared TOML Key Conversion Utility

Both Pyrefly and ty use TOML with kebab-case keys but Python code uses snake_case. Consider creating:

```python
# lsp_types/utils.py
def snake_to_kebab_recursive(obj: Mapping[str, Any]) -> dict[str, Any]:
"""Recursively convert dict keys from snake_case to kebab-case."""
# ... implementation
```

Then refactor both backends to use this shared utility.

### 2. Backend Capability Flags (implemented)

The `LSPBackend` protocol carries both as methods:
- `requires_file_on_disk() -> bool` — all four backends return `False`
- `consumes_did_change_configuration() -> bool` — `False` for ty and Zuban

`Session.create()` branches on both.

### 3. Common LSP Capabilities Base

Create a helper function for shared capabilities:

```python
def get_base_python_capabilities() -> types.ClientCapabilities:
"""Common LSP capabilities for Python type checkers."""
return {
"textDocument": {
"publishDiagnostics": {...},
"hover": {...},
"signatureHelp": {},
}
}
```

Backends could extend this base instead of duplicating the boilerplate.

### 4. Monaco Native LSP Client (`monaco.lsp`)

Monaco Editor v0.55.0 (November 2025) introduced a built-in LSP client under `monaco.lsp` that could significantly simplify the playground. The current playground manually handles JSON-RPC, the LSP handshake, position conversion, diagnostics, and hover registration (~200 lines per backend). With `monaco.lsp`, this reduces to ~10-15 lines per backend.

**What it provides:**
- `MonacoLspClient` — auto-registers 21 LSP features (completion, hover, diagnostics, semantic tokens, go-to-definition, rename, code actions, inlay hints, etc.)
- `WebSocketTransport` — connect via WebSocket
- `createTransportToWorker(worker)` — connect to a Web Worker
- `createTransportToIFrame(iframe)` — connect to an iframe

**Example usage:**
```typescript
const worker = new Worker(PYRIGHT_WORKER_URL);
const transport = monaco.lsp.createTransportToWorker(worker);
new monaco.lsp.MonacoLspClient(transport);
// All features auto-registered, including semantic tokens
```

**What it would replace in the playground:**
- `BackendAdapter` interface (diagnostics, hover, updateCode)
- `typeConversions.ts` (LSP-to-Monaco position mapping)
- 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):**

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 —
untrusted markdown, a stray `debugger;` removal, one import rename. The client is
functionally unchanged, so every blocker below still stands.

- **API still unstable** — `monaco-lsp-client/README.md` (current): "This package is
in alpha stage and might contain many bugs." 0.56.0 added typings (0.55.1 shipped
no `.d.ts` at all) but made no stability declaration.
- **No custom initialization params — still the main blocker.**
`constructor(transport: IMessageTransport)` is the entire public API; the client
hardcodes `{ processId: null, capabilities, rootUri: null }` and never sends
`initializationOptions`. Identical in 0.55.1, 0.56.0 and `main`.
- **Registration is global in practice for our backends** (the original wording was
too broad). Providers register against
`toMonacoLanguageSelector(capability.documentSelector)`, which falls back to
`{ language: "*" }` only when that selector is missing or empty — so capabilities
registered dynamically via `client/registerCapability` *are* scoped per-language.
But options derived from static server capabilities carry no document selector,
and Pyright, Pyrefly, ty and Zuban all advertise statically.
- **No reconnection** — `WebSocketTransport`'s `socket.onclose` only flips transport
state to `closed`; nothing subscribes to that state, and `reconnect` appears zero
times in the shipped bundle.
- **No `dispose()`** (microsoft/monaco-editor#5340, open) — new since this note was
written, and disqualifying on its own here. `createFeatures()` builds a
`DisposableStore` that the constructor discards, and `MonacoLspClient` exposes no
`dispose()`, so provider registrations outlive the transport for the page lifetime.
`playground/src/main.ts` disposes the adapter on every backend switch, so each
switch would leak a full set of providers.
- **Other open bugs to watch** — microsoft/monaco-editor#5224 and #5239 (document
URIs are case-mangled during text-document synchronisation) and #5342
(`textDocument/codeAction` drops the diagnostic `data`, `code` and `source` fields
servers need for quickfixes).

**Recommendation:** Do not migrate on monaco-editor 0.56.0. Re-evaluate only once
both a `MonacoLspClient` constructor accepting initialization options and a proper
`dispose()` have landed; #5340 is a required lifecycle fix, not merely something to
monitor. Treat 0.57+ as a release horizon to re-check, not an expectation that it
will be usable. If both land, migrating the playground would eliminate significant
boilerplate and gain features (completion, semantic tokens, rename, etc.) for free.

### 5. Backend Registry Pattern

For easier discovery and testing:

```python
_BACKENDS: dict[str, type[LSPBackend]] = {}

def register_backend(name: str):
def decorator(cls):
_BACKENDS[name] = cls
return cls
return decorator

@register_backend("ty")
class TyBackend(LSPBackend):
...
```

---

## Summary

The ty backend integration revealed that different LSP servers have varying requirements around file handling and configuration. Optional capability flags on backends have since shipped (`requires_file_on_disk()`, `consumes_did_change_configuration()`). The current abstraction works but could still benefit from:

1. Shared utilities for common patterns (TOML conversion, base capabilities)
2. Better documentation of backend-specific behaviors

The core `LSPBackend` protocol and `Session` class work well across all four backends (Pyright, Pyrefly, ty, Zuban) with minimal backend-specific handling needed in tests.
# 🔧 Backend integration

`Session` provides one API for Pyright, basedpyright, Pyrefly, ty, and Zuban.
Each backend supplies its launch command, configuration, and capabilities.
All supported backends analyze virtual documents without writing Python files
to disk.

## Configuration

`Session.create(options=...)` writes configuration in `base_path` before
launching the server. The default `base_path` is the current directory.

| Backend | Configuration file | Python option keys |
| --- | --- | --- |
| Pyright / basedpyright | `pyrightconfig.json` | Upstream camelCase |
| Pyrefly | `pyrefly.toml` | Top-level snake_case becomes kebab-case; nested keys keep their spelling |
| ty | `ty.toml` | Snake_case becomes kebab-case recursively |
| Zuban | `[tool.zuban]` in `pyproject.toml` | Snake_case, unchanged |

Pyright, Pyrefly, and ty replace their configuration files. Zuban replaces only
`[tool.zuban]`, preserving other sections and their formatting. Use a dedicated
`base_path` to keep session configuration separate from an existing project.

Pass server-specific LSP initialization settings through
`Session.create(initialize_params=...)`. These values override the corresponding
top-level initialization fields. ty and Zuban receive configuration at session
creation; recreate their sessions to apply changes.

## Capabilities and results

- **Semantic tokens:** basedpyright, Pyrefly, ty, and Zuban support them.
Microsoft Pyright does not. Use the [normalized token API](SEMANTIC_TOKENS.md)
for one editor legend across backends.
- **Hover:** content and formatting differ by server. Display the returned
content directly; ty's variable hover shows the type without the variable name.
- **Completion resolution:** Pyrefly returns the submitted item unchanged.
ty rejects resolution requests. Use their initial completion results.
- **External file changes:** the client does not watch files. Recreate a session
when files changed outside `update_code()` are not reflected in results.

See the backend-specific guidance for [Pyrefly](../lsp_types/pyrefly/KNOWN_LIMITATIONS.md),
[ty](../lsp_types/ty/KNOWN_LIMITATIONS.md), and
[Zuban](../lsp_types/zuban/KNOWN_LIMITATIONS.md).

## Adding a backend

Implement `LSPBackend` with the server's configuration writer, launch command,
client capabilities, workspace settings, and semantic-token legend fallback.
Use `None` for the fallback when the server advertises its legend.

Set `requires_file_on_disk()` to match the server's document requirements.
Set `consumes_did_change_configuration()` to `False` when the server does not
handle that notification. `Session` handles initialization, document updates,
requests, and shutdown.
Loading