Add completion for request#129
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #129 +/- ##
==========================================
+ Coverage 77.08% 77.16% +0.07%
==========================================
Files 28 28
Lines 4338 4357 +19
==========================================
+ Hits 3344 3362 +18
- Misses 994 995 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR special-cases pytest’s built-in request fixture by synthesizing a FixtureDefinition in the FixtureDatabase, so it can participate in completion/hover/inlay hints/code actions even though it isn’t declared via @pytest.fixture. It also includes broader LSP/code-action and analysis infrastructure changes (matching what’s described in PR #126).
Changes:
- Register a synthetic
requestfixture during pytest internal/venv scanning, includingFixtureRequestreturn type + import spec. - Add return-type import resolution plumbing (
TypeImportSpec, import-map building, type-alias expansion) to support typed code actions and isort-aware import insertion. - Extract
LanguageServer for Backendinto a shared module for integration testing, add extensive tests/docs, and gate forced-exit shutdown behavior behind a feature.
Reviewed changes
Copilot reviewed 22 out of 24 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/fixtures/scanner.rs |
Registers synthetic request fixture after scanning _pytest internals. |
src/fixtures/analyzer.rs |
Adds return-type import resolution + is_parameter tracking for usages. |
src/fixtures/types.rs |
Introduces TypeImportSpec and extends FixtureDefinition/FixtureUsage. |
src/fixtures/imports.rs |
Adds free is_stdlib_module() and build_name_to_import_map() support. |
src/providers/code_action.rs |
Adds new code-action kinds + import insertion/adaptation logic. |
src/providers/inlay_hint.rs |
Skips non-parameter usages for type inlay hints. |
src/providers/language_server.rs / src/providers/mod.rs / src/main.rs / src/lib.rs |
Moves LanguageServer impl into providers for lib+bin builds and testing. |
tests/test_fixtures.rs |
Adds request-specific tests and is_parameter behavior tests. |
tests/test_language_server.rs |
Adds integration coverage for LanguageServer for Backend methods. |
Cargo.toml / pyproject.toml / .github/workflows/ci.yml |
Adds lsp_force_exit feature and updates build steps. |
README.md + extension READMEs + CLAUDE.md |
Documents expanded code-action capabilities and architecture. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Cargo.toml
Outdated
| [[bin]] | ||
| name = "pytest-language-server" | ||
| path = "src/main.rs" | ||
| required-features = ["lsp_force_exit"] |
There was a problem hiding this comment.
required-features = ["lsp_force_exit"] on the [[bin]] target means cargo build / cargo install will no longer produce the pytest-language-server binary unless users explicitly pass --features lsp_force_exit. If the goal is only to avoid process::exit() when used as a library/test dependency, consider an approach that keeps the binary buildable by default (e.g. move the forced-exit behavior into main.rs, or gate it via a runtime flag/env var), so downstream tooling doesn’t unexpectedly lose the binary.
| // `request` is not defined via @pytest.fixture anywhere in _pytest/ — | ||
| // pytest injects it programmatically via FixtureManager. Register a | ||
| // synthetic definition so that hover, inlay hints, completion and code | ||
| // actions all know its type. | ||
| self.register_request_builtin_fixture(&pytest_internal); | ||
| } |
There was a problem hiding this comment.
PR description says this change only special-cases the request fixture, but this PR also includes large unrelated changes (e.g. LanguageServer impl extraction, new code-action kinds/import insertion, binary feature gating). If those are intended to come from #126, it may be better to rebase onto/merge that PR first (as noted in the description) or split this PR so the request fix can be reviewed/landed independently.
| - **Code actions** to automatically add missing fixture parameters | ||
| - **Code actions** to automatically add missing fixture parameters with type annotations | ||
| - Intelligent parameter insertion (handles both empty and existing parameter lists) | ||
| - Works with both single-line and multi-line function signatures |
There was a problem hiding this comment.
This documentation claims diagnostics/quick-fix insertion works for multi-line function signatures, but the current quick-fix implementation in handle_code_action still relies on finding "):" on a single line and won’t handle multi-line signatures. Either adjust the implementation to support multi-line signatures, or soften this statement so docs match actual behavior.
| - Works with both single-line and multi-line function signatures | |
| - Supports single-line function signatures (multi-line support may be limited) |
src/providers/code_action.rs
Outdated
| let Some(func_line_content) = lines.get(function_line as usize) else { | ||
| warn!( | ||
| "Function line {} is out of range in {:?}", | ||
| function_line, file_path | ||
| ); | ||
| continue; | ||
| }; | ||
|
|
||
| // Locate the closing `):` of the function signature. | ||
| let Some(paren_pos) = func_line_content.find("):") else { | ||
| continue; | ||
| }; | ||
|
|
||
| if !func_line_content[..paren_pos].contains('(') { | ||
| continue; | ||
| } | ||
|
|
||
| let param_start = match func_line_content.find('(') { | ||
| Some(pos) => pos + 1, | ||
| None => { | ||
| warn!( | ||
| "Invalid function signature at {:?}:{}", | ||
| file_path, function_line | ||
| ); | ||
| continue; | ||
| } | ||
| }; | ||
|
|
||
| let params_section = &func_line_content[param_start..paren_pos]; | ||
| let has_params = !params_section.trim().is_empty(); | ||
|
|
There was a problem hiding this comment.
The quick-fix parameter insertion logic here only works when the entire function signature (including the closing "):") is on the same line as fixture.function_line. For multi-line signatures this will fail to offer/apply the quick fix. Consider reusing FixtureDatabase::get_function_param_insertion_info() (already used by completion auto-add) so code actions support multi-line signatures consistently and avoid duplicating signature-parsing logic.
| let Some(func_line_content) = lines.get(function_line as usize) else { | |
| warn!( | |
| "Function line {} is out of range in {:?}", | |
| function_line, file_path | |
| ); | |
| continue; | |
| }; | |
| // Locate the closing `):` of the function signature. | |
| let Some(paren_pos) = func_line_content.find("):") else { | |
| continue; | |
| }; | |
| if !func_line_content[..paren_pos].contains('(') { | |
| continue; | |
| } | |
| let param_start = match func_line_content.find('(') { | |
| Some(pos) => pos + 1, | |
| None => { | |
| warn!( | |
| "Invalid function signature at {:?}:{}", | |
| file_path, function_line | |
| ); | |
| continue; | |
| } | |
| }; | |
| let params_section = &func_line_content[param_start..paren_pos]; | |
| let has_params = !params_section.trim().is_empty(); | |
| // Use shared helper logic (also used by completion auto-add) so we | |
| // correctly handle multi-line function signatures and avoid | |
| // duplicating parsing logic here. | |
| let Some(insertion_info) = backend | |
| .fixture_db() | |
| .get_function_param_insertion_info(&file_path, fixture.function_line) | |
| else { | |
| warn!( | |
| "Could not determine parameter insertion info for fixture at {:?}:{}", | |
| file_path, function_line | |
| ); | |
| continue; | |
| }; | |
| // Map helper result into the local variables expected by the rest | |
| // of this function. | |
| let param_start = insertion_info.param_start_col as usize; | |
| let paren_pos = insertion_info.insertion_col as usize; | |
| let has_params = insertion_info.has_existing_params; |
8cd1ec2 to
0ad2b90
Compare
|
@benediktziegler I merged the previous MR, can you rebase this one so I can review it (and also trigger copilot to do the same) I'll release both together when this gets merged |
0ad2b90 to
cb86f3d
Compare
pytest's `request` fixture is injected programmatically by `FixtureManager` and never appears as a `@pytest.fixture`-decorated function, so the AST scanner cannot discover it. A synthetic `FixtureDefinition` is now registered at venv-scan time, enabling hover, inlay hints, completions, go-to-definition, and code actions to work with it correctly.
|
done |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Guard: skip registration if an identical synthetic definition | ||
| // (same file_path) is already present. This prevents duplicate | ||
| // entries when scan_venv_fixtures / scan_pytest_internal_fixtures | ||
| // is called more than once (e.g. on workspace reload). | ||
| if let Some(existing) = self.definitions.get("request") { | ||
| if existing.iter().any(|d| d.file_path == file_path) { | ||
| debug!( | ||
| "Synthetic 'request' fixture already registered for {:?}, skipping", | ||
| file_path | ||
| ); | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
The duplicate-prevention guard only skips when an existing request definition has the exact same file_path. If a first scan registers the sentinel path (because fixtures.py is missing) and a later scan finds fixtures.py, this will register a second synthetic request definition and both will be returned in completion/hover resolution. Consider replacing any prior synthetic request definitions (or de-duplicating by name + is_plugin/is_third_party) rather than keying solely on file_path.
| self.record_fixture_definition(definition); | ||
|
|
||
| // Record each dependency as a usage | ||
| for arg in Self::all_args(args) { | ||
| let arg_name = arg.def.arg.as_str(); | ||
|
|
There was a problem hiding this comment.
This block is now recording all parameters (including request) as usages, not just dependencies. The leading comment still says “Record each dependency as a usage”, which is misleading after the change; please update it to reflect the actual behavior to avoid future regressions.
| for arg in Self::all_args(args) { | ||
| let arg_name = arg.def.arg.as_str(); | ||
|
|
||
| if arg_name != "self" && arg_name != "request" { | ||
| // `request` is excluded from *dependencies* (it is a special pytest | ||
| // injection, not a regular fixture), but we DO record it as a usage | ||
| // so that inlay hints and type-annotation code actions work on it. | ||
| if arg_name != "self" { |
There was a problem hiding this comment.
After widening the condition to include request, the surrounding wording/logging still frames these as “dependencies” (e.g. the loop comment and the later info!("Found fixture dependency: ...")). Since request is explicitly excluded from the dependencies list above, consider renaming the comment/log to “parameter usage” (or only logging as dependency when arg_name != "request") to keep behavior and diagnostics consistent.
| #[timeout(30000)] | ||
| fn test_request_completion_available() { | ||
| // `request` must appear in the list of available fixtures for a test file | ||
| // once the venv has been scanned (simulated here by direct registration). |
There was a problem hiding this comment.
The test comment says venv scanning is “simulated here by direct registration”, but this test actually creates a .venv/.../site-packages/_pytest tree and calls scan_workspace. Please adjust the comment to match what the test is doing to reduce confusion during future maintenance.
| // once the venv has been scanned (simulated here by direct registration). | |
| // once `scan_workspace` discovers the venv's `_pytest` package. |
|
@benediktziegler some remarks from copilot 🙏🏼 |
This PR adds the
requestfixture manually to theFixtureDatabase.Fixes #128
Merge #126 first.