pytest-bdd-language-server 🥒
A Language Server Protocol (LSP) implementation for Gherkin .feature files backed by pytest-bdd step definitions, with support for go to definition, find references, code completion, hover documentation, workspace symbols, diagnostics, and more!
Built with AI, reviewed with care 🤖
This project was built with the help of an AI coding agent, with each feature reviewed and tested (unit + end-to-end, via a real server subprocess) before moving to the next. If you find any issues, please open an issue or submit a pull request!
Table of Contents
- Features
- Installation
- Setup
- Configuration
- Supported Step Patterns
- CLI Commands
- Architecture
- Design Decisions
- Known Limitations
- Development
- Contributing
- License
- Acknowledgments
Features
🎯 Go to Definition
Jump directly from a Gherkin step to the Python function that implements it:
- Literal steps (
@given("I am an author")) parsers.parse/parsers.cfparsesteps, matched with the same libraries pytest-bdd itself usesparsers.resteps, matched with Python's ownremodule directlyAnd/But/*steps, resolved to their effective Given/When/Then keyword first
🔍 Find References
Works from either end:
- From a step definition's
defline, find every.featurestep that resolves to it - From a Gherkin step, find every other place that same step is used
include_declarationis respected on both ends
📚 Hover Documentation
Hover over a step to see which definition(s) match it, without jumping:
- Function name and source location
- The matched pattern and its kind (literal / parse / cfparse / re)
- A clear note when no definition matches, or when the only candidate can't be verified statically
✨ Code Completion
Autocomplete existing step patterns while writing a new step:
- Filtered by the effective keyword (Given/When/Then,
And/But/*resolved) parsers.parse/cfparsepatterns are offered as real LSP snippets —{start:d}becomes a tabstop you fill in, not literal placeholder textparsers.repatterns are intentionally excluded — a regex's source text isn't text it matches, so there's nothing sensible to insert
🔎 Workspace Symbols
Search across the whole project, not just step definitions:
- Features and Scenarios: search by name; matching a Feature's name surfaces every Scenario under it too
- Step definitions: search by keyword and pattern
- Powers editor-side pickers — see
pytest-bdd.nvim's Telescope integration for an example
The per-file counterpart, textDocument/documentSymbol, gives the same Feature/Scenario/step structure for the currently open document, nested rather than flat — this is what powers your editor's outline view, breadcrumbs, or sticky scroll.
⚠️ Diagnostics
Catches problems before you ever run pytest:
- Undefined step: a step with no matching definition anywhere — the same error pytest-bdd would raise at runtime, surfaced while you type instead
- Unverifiable step: the only candidate definition uses
parsers.cfparse(..., extra_types=...), whose custom converters live in your own code and can't be checked statically — reported as informational, not a warning, since it isn't necessarily wrong
🧹 Find Unused Steps
A custom pytest-bdd.unusedSteps command (via workspace/executeCommand, since no standard LSP method fits "list definitions with no usages") lists every step definition with no matching usage in any .feature file — likely dead code left behind after a scenario was reworded or removed. unresolved definitions are excluded, since there's no way to confirm they're truly unused rather than just unverifiable.
⏳ Progress & Scan Feedback
$/progress: workspace indexing reports progress if the client supportswindow.workDoneProgress, so a large project doesn't feel like it's hangingwindow/showMessage: files that couldn't be read or parsed are reported in a single, capped summary — not silently skipped, and not one popup per broken fileworkspace/didChangeWatchedFiles: the index refreshes automatically on external changes (git checkout, another process, a second editor window) if the client supports dynamic registration, in addition to on-save
Installation
# Using uv (recommended)
uv tool install pytest-bdd-language-server
# Or with pip, inside your project's virtualenv
pip install pytest-bdd-language-server
# Or with pipx (isolated environment)
pipx install pytest-bdd-language-server
Not yet published to PyPI — install from source in the meantime:
git clone https://framagit.org/RomainTT/pytest-bdd-language-server
cd pytest-bdd-language-server
uv tool install --editable .
Setup
Neovim
The pytest-bdd.nvim plugin handles this for you (filetype detection, virtualenv resolution, a Telescope picker). Without it, Neovim 0.11+'s native config works directly:
vim.filetype.add({ extension = { feature = "cucumber" } })
vim.lsp.config("pytest_bdd_language_server", {
cmd = { "pytest-bdd-language-server" },
filetypes = { "cucumber" },
root_markers = { ".git", "pyproject.toml", "setup.py", "setup.cfg" },
})
vim.lsp.enable("pytest_bdd_language_server")
VS Code
The pytest-bdd-vscode extension handles server resolution and adds two commands (pytest-bdd: Browse Steps, pytest-bdd: Find Unused Steps) built on workspace/symbol and the custom command above. Not yet published to the Marketplace — see its README for building a local .vsix.
Other Editors
Any editor with LSP support can use pytest-bdd-language-server. Point it at the pytest-bdd-language-server command over stdio, with a document selector matching .feature files.
Configuration
There are currently no user-configurable settings — no initializationOptions, no workspace/didChangeConfiguration handling. Behavior is entirely determined by the workspace folder(s) reported at initialize and the client's declared capabilities (for $/progress and the file watcher). See Known Limitations for what that means in practice.
Supported Step Patterns
All four of pytest-bdd's step decorator syntaxes are recognized and matched using the same libraries pytest-bdd itself relies on at runtime — not a reimplementation of their semantics:
from pytest_bdd import given, parsers
# Literal text
@given("I am an author")
def author():
...
# parsers.parse (matched via the `parse` library)
@given(parsers.parse("there are {count:d} cucumbers"))
def cucumbers(count):
...
# parsers.cfparse (matched via parse_type.cfparse)
@given(parsers.cfparse("there are {count:d} cucumber(s)"))
def cucumbers_cf(count):
...
# parsers.re (matched via Python's own `re`)
@given(parsers.re(r"there are (?P<count>\d+) cucumbers"))
def cucumbers_re(count):
...
parsers.cfparse(..., extra_types=...) is the one case that can't be statically verified — its custom converters live in your own code, so it's flagged as unresolved rather than guessed at (see Diagnostics).
CLI Commands
Beyond the LSP server itself (pytest-bdd-language-server serve, the default when no command is given — see Setup), the same package exposes standalone commands that don't speak LSP at all, reusing the exact same scanning and matching logic:
# List step definitions with no matching .feature usage anywhere.
# Exits 1 if any are found -- built for CI pipelines and pre-commit hooks.
pytest-bdd-language-server unused-steps [PATHS...] [--format text|json]
# List .feature step usages with no matching step definition -- the mirror
# of unused-steps. Exits 1 if any are found.
pytest-bdd-language-server undefined-steps [PATHS...] [--format text|json]
# List every step definition found, optionally filtered by keyword/pattern.
pytest-bdd-language-server list-steps [PATHS...] [--format text|json] [--query TEXT]
All three default to scanning the current directory if no PATHS are given. undefined-steps catches a gap pytest-bdd's own step resolution doesn't cover: StepDefinitionNotFoundError is only raised when a step actually runs, so an undefined step in a scenario that's skipped, filtered out (-k, markers, --lf, CI sharding), or simply never wired into a test file via scenarios(...) can go undetected indefinitely. This command checks every .feature file directly, independent of what a given pytest invocation happens to execute -- and without running any test code, fixtures, or setup to find out. Usages only matched by an unresolved cfparse+extra_types candidate are reported separately as unverifiable and don't affect the exit code, consistent with how Diagnostics treats the same situation.
Example, wired into CI:
pytest-bdd-language-server undefined-steps . || {
echo "Found .feature steps with no matching definition -- see above."
exit 1
}
pytest-bdd-language-server unused-steps . || {
echo "Found step definitions with no .feature usage -- see above."
exit 1
}
Architecture
- Language: Python 3.10+
- LSP framework: pygls
- Gherkin parsing: gherkin-official — the same parser pytest-bdd uses internally
- Step pattern matching:
parseandparse_type— again, pytest-bdd's own dependencies, not a reimplementation - Step discovery: Python's
astmodule — statically parses decorators rather than importing your code
Design Decisions
A few choices worth knowing if you're reading the source:
- Step definitions are discovered via
ast, not by importing your code. Importing would require resolving your project's virtualenv, running arbitrary module-level side effects, and handling failures in third-party code — none of which belong in a scan that runs on every save. The tradeoff is thatextra_typescustom converters can't be resolved (see Known Limitations). - Matching reuses pytest-bdd's own libraries rather than reimplementing their semantics, so a step either statically matches or doesn't, exactly the way pytest-bdd itself would decide at runtime.
- Workspace scans run in a thread executor, not directly on the event loop, so a large project doesn't block other LSP requests (hover, completion...) while indexing.
Known Limitations
cfparsewithextra_typescan't be matched statically — flagged in diagnostics/hover as unverifiable rather than silently mismatched.references/definitionfrom the Python side only recognize the cursor on thedefline itself, not the decorator line above it.- No rename, formatting, call hierarchy, code actions, or semantic tokens. Gherkin syntax highlighting and folding are expected to come from a Treesitter grammar on the client side, not this server.
- No incremental re-indexing. A
.pyor.featurechange triggers a full workspace rescan rather than a targeted update — fast enough for typical project sizes, but a known scaling limit.
Development
This project uses uv for dependency management, ruff for linting, and pytest (with pytest-lsp for end-to-end tests that drive a real server subprocess over LSP) for testing.
# Install dependencies (including dev dependencies)
uv sync
# Run the full test suite
uv run pytest
# Run only the fast, non-LSP unit tests
uv run pytest tests/unit
# Lint
uv run ruff check .
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
Licensed under Mozilla Public License v2
Acknowledgments
Built with:
- pygls — LSP framework
- gherkin-official — Gherkin parsing
- parse / parse_type — step pattern matching
Special thanks to the pytest-bdd team for the framework this server exists to support.
Built with AI assistance, reviewed with care.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytest_bdd_language_server-1.0.0.tar.gz.
File metadata
- Download URL: pytest_bdd_language_server-1.0.0.tar.gz
- Upload date:
- Size: 35.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.33 {"installer":{"name":"uv","version":"0.11.33","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
09f40efa48f1475e418a2b24f3a5b9b49ad44d367aa70f3acf53a64da52fa9b6
|
|
| MD5 |
b7511ea0ca4eff1120e2e539e4ea9cc4
|
|
| BLAKE2b-256 |
0a420cc8329b45128f674971074df692aba302d230dd18fb0200adffb7092bb0
|
File details
Details for the file pytest_bdd_language_server-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pytest_bdd_language_server-1.0.0-py3-none-any.whl
- Upload date:
- Size: 45.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.33 {"installer":{"name":"uv","version":"0.11.33","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
56b4a3f030d8e06b76133eec5887eb8621eb953e9d30ef3734774c50f0c162f1
|
|
| MD5 |
1efd4ba7ad8c97e5d18ecc0c0f85c4c8
|
|
| BLAKE2b-256 |
ed8ad99cec9f9cdc87400eacd837f4b8a625883a4b1554a930b0a06658018376
|