Skip to main content

pytest-bdd-language-server 🥒

Python Version

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

🎯 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.cfparse steps, matched with the same libraries pytest-bdd itself uses
  • parsers.re steps, matched with Python's own re module directly
  • And/But/* steps, resolved to their effective Given/When/Then keyword first

🔍 Find References

Works from either end:

  • From a step definition's def line, find every .feature step that resolves to it
  • From a Gherkin step, find every other place that same step is used
  • include_declaration is 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/cfparse patterns are offered as real LSP snippets — {start:d} becomes a tabstop you fill in, not literal placeholder text
  • parsers.re patterns 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 supports window.workDoneProgress, so a large project doesn't feel like it's hanging
  • window/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 file
  • workspace/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: parse and parse_type — again, pytest-bdd's own dependencies, not a reimplementation
  • Step discovery: Python's ast module — 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 that extra_types custom 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

  • cfparse with extra_types can't be matched statically — flagged in diagnostics/hover as unverifiable rather than silently mismatched.
  • references/definition from the Python side only recognize the cursor on the def line 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 .py or .feature change 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:

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

pytest_bdd_language_server-1.0.0.tar.gz (35.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_bdd_language_server-1.0.0-py3-none-any.whl (45.9 kB view details)

Uploaded Python 3

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

Hashes for pytest_bdd_language_server-1.0.0.tar.gz
Algorithm Hash digest
SHA256 09f40efa48f1475e418a2b24f3a5b9b49ad44d367aa70f3acf53a64da52fa9b6
MD5 b7511ea0ca4eff1120e2e539e4ea9cc4
BLAKE2b-256 0a420cc8329b45128f674971074df692aba302d230dd18fb0200adffb7092bb0

See more details on using hashes here.

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

Hashes for pytest_bdd_language_server-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 56b4a3f030d8e06b76133eec5887eb8621eb953e9d30ef3734774c50f0c162f1
MD5 1efd4ba7ad8c97e5d18ecc0c0f85c4c8
BLAKE2b-256 ed8ad99cec9f9cdc87400eacd837f4b8a625883a4b1554a930b0a06658018376

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page