IMPORTANT: Never commit or push without explicit user confirmation. Always ask first.
pytest-language-server is a Rust LSP for pytest fixtures providing go-to-definition, find-references, hover, completions, diagnostics, and more.
- Language: Rust (Edition 2021, MSRV 1.85)
- Framework:
tower-lsp-server+rustpython-parser - Run tests:
cargo test - Lint:
cargo clippy - Debug:
RUST_LOG=debug cargo run
src/
├── main.rs # LanguageServer trait impl + CLI entry point
├── lib.rs # Library exports
├── config/mod.rs # Config from pyproject.toml [tool.pytest-language-server]
├── fixtures/ # Core analysis engine
│ ├── mod.rs # FixtureDatabase struct (DashMap-based concurrent storage)
│ │ # + get_name_to_import_map() (cached, content-hash invalidated)
│ ├── types.rs # FixtureDefinition, FixtureUsage, TypeImportSpec, etc.
│ ├── analyzer.rs # Python AST parsing, fixture extraction, return-type import resolution
│ ├── import_analysis.rs # Shared import layout analysis (AST + string fallback):
│ │ # ImportLayout, ImportGroup, ImportKind (Future/Stdlib/ThirdParty),
│ │ # parse_import_layout(), classify_import_statement(),
│ │ # adapt_type_for_consumer(), import_sort_key(), find_sorted_insert_position()
│ ├── imports.rs # Import handling, is_stdlib_module(), build_name_to_import_map(), file_path_to_module_path()
│ ├── resolver.rs # Fixture resolution with pytest priority rules
│ ├── scanner.rs # Workspace + venv scanning
│ └── cli.rs # CLI commands (fixtures list/unused)
└── providers/ # LSP handlers (one file per feature)
├── mod.rs # Backend struct, URI/path helpers
├── code_action.rs # Code actions: quickfix, source.pytest-ls, source.fixAll.pytest-ls
│ # Uses import_analysis for layout + adapt; TextEdit production stays here
├── inlay_hint.rs # Inlay hints with import-context-aware type display (adapt_type_for_consumer)
├── definition.rs, references.rs, hover.rs, completion.rs, ...
Key pattern: FixtureDatabase in src/fixtures/ handles all data; Backend in src/providers/ delegates LSP requests to it.
The code action provider (src/providers/code_action.rs) emits three kinds:
| Kind | Trigger | Behaviour |
|---|---|---|
quickfix |
undeclared-fixture diagnostic |
Adds missing fixture param with type annotation + import |
source.pytest-ls |
Cursor on unannotated fixture param | Adds : ReturnType + import for that fixture |
source.fixAll.pytest-ls |
Anywhere in file | Adds all missing type annotations + imports in one edit |
Import insertion is isort/ruff-aware:
parse_import_layout()(inimport_analysis.rs) parses the file via AST (or string fallback on syntax errors), returning anImportLayoutwith classifiedImportGroups,ParsedFromImports, andParsedBareImports.ImportKindnow has three variants:Future,Stdlib,ThirdParty.emit_kind_import_edits()inserts into the correct group with proper blank-line separators. Merging into multiline parenthesised imports is now supported (AST path only).ImportLayout::find_matching_from_import()finds existingfrom X import Ylines (single-line or multiline) for merge;can_merge_into()guards against merging fallback multiline entries whose names are unknown.build_import_edits()orchestrates deduplication, skip-if-already-imported, and group routing.
TypeImportSpec (in types.rs) captures check_name + import_statement for each type used in a fixture's return annotation. Resolved at analysis time:
build_name_to_import_map()(inimports.rs) builds a name→spec map from all imports in the fixture file (including stdlib/typing)resolve_return_type_imports()(inanalyzer.rs) tokenises the return type string, skips builtins, looks up each identifier in the import map, and falls back to locally-defined names viafile_path_to_module_path()- Results are stored in
FixtureDefinition::return_type_importsfor use by code actions
is_stdlib_module() is a free function in imports.rs, used internally by import_analysis.rs
for classification. It is no longer re-exported from mod.rs since all callers outside
fixtures/ now go through classify_import_statement() in import_analysis.rs.
inlay_hint.rs calls adapt_type_for_consumer() (from import_analysis.rs) before emitting each
hint, so the displayed type matches the consumer file's import style:
- If the consumer has
from pathlib import Path, the hint shows: Pathnot: pathlib.Path. - If the consumer has
import pathlib, the hint shows: pathlib.Pathnot: Path. The returnedVec<TypeImportSpec>is discarded — hints are display-only.
Builds (and caches by content hash) a HashMap<String, TypeImportSpec> for a file's imports.
Used by both code_action and inlay_hint to avoid re-parsing the AST on every request.
Cache is cleared in cleanup_file_cache() and evict_cache_if_needed().
- Same file (highest)
- Closest conftest.py (walk up directory tree)
- Plugin fixtures (pytest11 entry points, e.g. workspace editable installs)
- Third-party from venv site-packages (lowest)
@pytest.fixture
def cli_runner(cli_runner): # Parameter refers to PARENT fixture
return cli_runnerPosition matters: cursor on function name → child; cursor on parameter → parent. Uses start_char/end_char in FixtureUsage.
- LSP uses 0-based lines
- Internal storage uses 1-based lines
- Use
lsp_line_to_internal()/internal_line_to_lsp()helpers
Never hold .get() references across analyze_file() calls. Scope references in blocks:
// CORRECT
{
let entry = db.definitions.get("name").unwrap();
// use entry
} // Reference dropped
db.analyze_file(...); // Safe- Add capability in
main.rsinitialize()→ServerCapabilities - Create
src/providers/new_feature.rs - Add
pub mod new_feature;tosrc/providers/mod.rs - Implement handler method in new file
- Wire up in
main.rsLanguageServer trait impl - Add tests in
tests/test_lsp.rs
- Register the kind in
main.rsinitialize()→code_action_kinds - Add a
constfor the kind insrc/providers/code_action.rs - Gate the new logic behind
kind_requested(&context.only, &YOUR_KIND) - Build
TextEdits for the action; usebuild_import_edits()if imports are needed - Add unit tests in the
mod testsblock insidecode_action.rs - Add integration tests in
tests/test_lsp.rs
Always use the script (updates Cargo.toml, pyproject.toml, extensions):
./bump-version.sh X.Y.ZWhen adding new LSP features, update the feature lists in all extension READMEs:
extensions/vscode-extension/README.mdextensions/intellij-plugin/README.mdextensions/zed-extension/README.md
Keep them in sync with the main README.md features section.
Fixtures imported via star imports in conftest.py are discovered:
# conftest.py
from .fixtures import * # Fixtures from fixtures.py are now availableThe scanner:
- First scans
conftest.pyand test files - Then iteratively discovers modules imported by conftest files
- Handles transitive imports (A → B → C)
Performance optimizations:
imported_fixtures_cachestores results with dual invalidation (content hash + definitions version)is_standard_library_module()uses O(1) HashSet lookup instead of linear array search- Iterative module scanning prevents redundant AST parsing
- Fixtures defined inside
ifblocks are not detected - Only scans
conftest.py,test_*.py,*_test.pyfiles (but also scans modules imported by conftest)
Run cargo test. Test files:
Integration tests (tests/):
tests/test_fixtures.rs- FixtureDatabase unit teststests/test_lsp.rs- LSP protocol tests (includes code action, hover, TypeImportSpec tests)tests/test_lsp_performance.rs- LSP performance/stress teststests/test_e2e.rs- End-to-end CLI teststests/test_config.rs- Configuration loading teststests/test_decorators.rs- Decorator recognition teststests/test_project/- Sample pytest project for testing
Inline unit tests (#[cfg(test)] mod tests):
src/fixtures/import_analysis.rs-parse_import_layout(AST + fallback),ImportKindclassification (includingFuture),find_matching_from_import(including multiline),can_merge_into, sort keys,find_sorted_insert_position,adapt_type_for_consumersrc/providers/code_action.rs-build_import_edits/emit_kind_import_edits(TextEdit generation, isort group routing, multiline merge, Future-import skipping)src/providers/completion.rs- Completion context detectionsrc/fixtures/imports.rs-file_path_to_module_path, import extractionsrc/fixtures/scanner.rs- Workspace/venv scanningsrc/fixtures/string_utils.rs- Parameter annotation parsingsrc/config/mod.rs- Config parsing from pyproject.toml