All skills
oaustegard avatar

/python-lsp

@6154d36

Semantic Python code queries via a stdio LSP client driving pyright-langserver. Provides binding-resolved go-to-definition, find-references, hover types, type diagnostics, file symbol outlines, and project-wide symbol search — name resolution and type inference that tree-sitter and ripgrep cannot do. Use when you need to follow an import to a definition, find all real uses of a symbol (excluding same-named-but-unrelated ones), get an inferred type, surface type errors, outline a file, or search symbols across a project. Triggers on "go to definition", "find references", "what type is", "resolve this symbol", "symbol outline", "find symbol in project", "pyright", "type-check this file".

Use this Skill: https://skilld.dev/gh/oaustegard/claude-skills/python-lsp

This session only. Nothing lands on disk.

SKILL.md

≈177 tokens always: the name and description. ≈1.7k when used: this file. ≈134 more on demand in 1 file.

python-lsp

A thin, dependency-free Python client that owns the LSP lifecycle against pyright-langserver --stdio and exposes high-value semantic queries.

Why, over tree-sitter / ripgrep: tree-sitter gives a CST — structural queries, call-site enumeration by name. It cannot do name resolution, type inference, or cross-file binding. ripgrep matches text, so it false-positives on shadowed / same-named symbols. pyright resolves bindings. This client is that semantic overlay.

Setup (self-installing)

The client bootstraps pyright on first use. Run the bootstrap explicitly, or let LSPClient do it via ensure_pyright():

sh /mnt/skills/user/python-lsp/scripts/bootstrap.sh
# or, equivalently, the one-liner it wraps:
command -v pyright-langserver >/dev/null || uv tool install pyright

pyright wheels vendor the langserver JS bundle and run it on system node — no npm install, no separate fetch when node is present. Measured cold (caches wiped): uv tool install pyright ~0.7s, first working server ~1.8s total; warm sub-second.

Node prerequisite. The clean path assumes system node (v18+) is present. With no node, pyright-python falls back to downloading node from nodejs.org, which may be blocked in locked-down containers. The bootstrap detects node and fails loudly (exit 1, clear message) rather than hanging.

Usage: CLI

LSP=/mnt/skills/user/python-lsp/scripts/lsp_client.py

python3 $LSP bootstrap                              # ensure pyright installed
python3 $LSP <root> definition  <file> <line> <col>
python3 $LSP <root> references  <file> <line> <col>
python3 $LSP <root> hover       <file> <line> <col>
python3 $LSP <root> diagnostics <file>
python3 $LSP <root> symbols     <file>             # documentSymbol outline
python3 $LSP <root> wsymbols    <query>            # workspace/symbol search

Positions are zero-based line/character (LSP spec). <file> is relative to <root> or absolute.

Usage: library

The scripts/ module lands on the boot .pth, so it is importable directly.

import sys; sys.path.insert(0, "/mnt/skills/user/python-lsp/scripts")
from lsp_client import LSPClient

with LSPClient("/path/to/repo") as c:        # context manager reaps the subprocess
    c.open_all("pkg/service.py", "pkg/models.py")
    c.wait_for_index()                       # REQUIRED before querying — see below
    defs  = c.definition("pkg/service.py", 4, 8)   # -> [Location], follows imports
    refs  = c.references("pkg/models.py", 8, 4)     # -> [Location], binding-resolved
    typ   = c.hover("pkg/service.py", 4, 4)         # -> "(variable) u: User"
    diags = c.diagnostics("pkg/bad.py")             # -> [diagnostic dicts]
    outln = c.document_symbols("pkg/models.py")     # -> [SymbolInfo], file outline
    hits  = c.workspace_symbols("User")             # -> [SymbolInfo], project-wide

Location has .path, .start_line, .start_char, .end_line, .end_char (all zero-based) and .as_dict(). Convert 1-based UI input with Position.from_one_based(line, col).

The root you pass is split into two roles. Relative <file> arguments resolve against it (call that the scope), but pyright is rooted at the enclosing project root — by default LSPClient climbs out of any package the scope sits inside (every ancestor with an __init__.py) via find_project_root. This is what makes a query scoped to a sub-package (scipy/optimize) still resolve the project's own absolute imports (from scipy.optimize._x import Y); without it, references silently undercount — same blindness as a text grep, opposite direction. The promotion is announced on stderr. Pass auto_root=False to pin pyright at the scope verbatim.

Methods

Method Returns Notes
definition(file, line, col) list[Location] Go-to-definition across files/imports.
references(file, line, col) list[Location] Binding-resolved — the win over ripgrep. Excludes same-named, unrelated symbols.
hover(file, line, col) str | None Inferred type / signature string.
diagnostics(file) list[dict] pyright type/error diagnostics for the file.
document_symbols(file) list[SymbolInfo] One file's outline (classes/functions/methods); nesting via .container.
workspace_symbols(query) list[SymbolInfo] Project-wide fuzzy symbol search. Empty query = every symbol (expensive).

SymbolInfo has .name, .kind (int), .kind_name (e.g. "Class"), .location (a Location), .container, and .as_dict().

The lifecycle gotchas (the parts that bite)

  1. Wait for indexing before querying. Querying mid-index returns empty results — the most common silent failure. wait_for_index() blocks on pyright's $/progress begin/end cycle (with a diagnostics-arrival fallback). Always call it after did_open / open_all and before any query.
  2. Don't advertise workspace.configuration or workspace.workspaceFolders. If the client claims either capability, pyright defers all analysis until the corresponding negotiation completes — the server starts its service instance and then goes silent (no diagnostics, no progress, queries hang). start() advertises neither, so pyright uses its defaults and analyzes open files immediately — no didChangeConfiguration nudge needed. Relevant if you reimplement the lifecycle or add capabilities. (Bisected against the fixture; workspace.symbol is safe to advertise.)
  3. Reap the subprocess. Use the context manager (or call stop()) so sessions don't leak pyright-langserver processes. stop() sends shutdown + exit, then waits/terminates/kills as needed.
  4. Open the files you query. Queries auto-did_open their target file, but for cross-file references open all relevant files first so pyright has built their models.
  5. Root at the project, not the sub-package. pyright resolves absolute intra-project imports only when rooted where the top-level package is importable. Rooted at a sub-package, those imports fail and references undercount with no error. LSPClient auto-detects this (climbs out of the enclosing package; see find_project_root) and prints the promotion to stderr; auto_root=False opts out. This was a live silent-undercount bug: --refs ScalarFunction over scipy/optimize returned 3 references rooted at the sub-package vs 30 rooted at the project root.

Tests

cd /mnt/skills/user/python-lsp
python3 -m pytest tests/test_lsp_client.py -v

Round-trips against tests/fixture/ (a small multi-file package): definition follows an import, references excludes an unrelated same-named symbol, hover returns an inferred type, diagnostics flags an intentional type error, the indexing-wait is verified deterministic, and subprocess cleanup is checked for orphans.

Source: SKILL.md on GitHub

1 warning2mo3 checks · Risk SAFE
  • Gen Agent Trust Hub2mo

    This skill provides semantic Python code analysis by wrapping the Microsoft Pyright language server. It automates the installation of Pyright using standard package managers (uv or pipx) and executes the server locally to resolve code definitions, references, and type information within a project.

  • Socket2mo

    No alerts

  • Snyk2mo

    Risk: MEDIUM · 1 issue

Signed by skilld at 6154d36. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 4 months ago
metadata
{
  "version": "0.3.0"
}

README badge

README badge for oaustegard/claude-skills/python-lsp