Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Pyrefly's current development status is [stable](https://github.com/facebook/pyr
- **Fast.** Pyrefly checks over 1.85 million lines of code per second, type checking projects like PyTorch 15x faster than Mypy and Pyright. In the IDE, rechecks typically complete in under 10 milliseconds after saving a file.
- **Production-proven at scale.** Pyrefly is the default type checker for Instagram's 20-million-line Python codebase at Meta, and has been adopted by large open source projects including PyTorch and JAX.
- **Full-featured language server.** Code navigation, autocomplete, hover information, inlay hints, semantic highlighting, and more, with consistent results across the CLI and your editor of choice.
- **Understands real-world Python.** Built-in support for frameworks like [Pydantic](https://pyrefly.org/en/docs/pydantic/) and [Django](https://pyrefly.org/en/docs/django/), with model validation, field types, and autocomplete that work out of the box.
- **Understands real-world Python.** Built-in support for frameworks and tools like [Pydantic](https://pyrefly.org/en/docs/pydantic/), [Django](https://pyrefly.org/en/docs/django/), and [pytest](https://pyrefly.org/en/docs/pytest/), with model validation, field types, fixture navigation, and autocomplete that work out of the box.
- **Adoption-ready.** Migrate from Mypy or Pyright with `pyrefly init`, silence existing errors with `pyrefly suppress`, and generate type annotations with `pyrefly infer`. Start with one file and expand at your own pace.

### Getting Started
Expand Down
83 changes: 47 additions & 36 deletions pyrefly/lib/lsp/non_wasm/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4534,41 +4534,60 @@ impl Server {
{
return Ok((!actions.is_empty()).then_some(actions));
}
if allow_refactor {
let mut push_refactor_actions = |refactors: Vec<LocalRefactorCodeAction>| {
for action in refactors {
let mut changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();
for (module, edit_range, new_text) in action.edits {
let Some(lsp_location) = self.to_lsp_location(&TextRangeWithModule {
module,
range: edit_range,
}) else {
continue;
};
changes.entry(lsp_location.uri).or_default().push(TextEdit {
range: lsp_location.range,
new_text,
});
}
if changes.is_empty() {
let mut push_local_actions = |local_actions: Vec<LocalRefactorCodeAction>| {
for action in local_actions {
let mut changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();
for (module, edit_range, new_text) in action.edits {
let Some(lsp_location) = self.to_lsp_location(&TextRangeWithModule {
module,
range: edit_range,
}) else {
continue;
}
actions.push(CodeActionOrCommand::CodeAction(CodeAction {
title: action.title,
kind: Some(action.kind),
edit: Some(WorkspaceEdit {
changes: Some(changes),
..Default::default()
}),
};
changes.entry(lsp_location.uri).or_default().push(TextEdit {
range: lsp_location.range,
new_text,
});
}
if changes.is_empty() {
continue;
}
actions.push(CodeActionOrCommand::CodeAction(CodeAction {
title: action.title,
kind: Some(action.kind),
edit: Some(WorkspaceEdit {
changes: Some(changes),
..Default::default()
}));
}),
..Default::default()
}));
}
};
macro_rules! timed_local_action {
($name:expr, $call:expr) => {{
let start = Instant::now();
if let Some(local_actions) = $call {
push_local_actions(local_actions);
}
};
record_code_action_telemetry($name, start);
}};
}
if allow_quickfix {
timed_local_action!(
"pytest_fixture_type_annotation",
transaction.pytest_fixture_type_annotation_code_actions(
&handle,
range,
import_format
)
);
}
if allow_refactor {
macro_rules! timed_refactor_action {
($name:expr, $call:expr) => {{
let start = Instant::now();
if let Some(refactors) = $call {
push_refactor_actions(refactors);
push_local_actions(refactors);
}
record_code_action_telemetry($name, start);
}};
Expand Down Expand Up @@ -4637,14 +4656,6 @@ impl Server {
"convert_dict",
transaction.convert_dict_code_actions(&handle, range)
);
timed_refactor_action!(
"pytest_fixture_type_annotation",
transaction.pytest_fixture_type_annotation_code_actions(
&handle,
range,
import_format
)
);
let start = Instant::now();
if let Some(action) =
convert_module_package_code_actions(&self.initialize_params.capabilities, uri)
Expand Down
4 changes: 0 additions & 4 deletions pyrefly/lib/state/lsp/quick_fixes/pytest_fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,6 @@ pub(crate) fn pytest_fixture_type_annotation_code_actions(
});
}

if candidates.is_empty() {
return None;
}

let module = module_info.dupe();
let selection_matches_fixtures = candidates
.iter()
Expand Down
34 changes: 34 additions & 0 deletions pyrefly/lib/test/lsp/code_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4816,6 +4816,40 @@ def test_one(answer: int, user: str):
assert_eq!(expected.trim(), updated_all.trim());
}

#[test]
fn pytest_fixture_parameter_annotation_from_annotated_fixture() {
let code = r#"
import pytest # type: ignore

@pytest.fixture
def enabled() -> bool:
return True

def test_feature(enabled):
print(enabled)
"#;
let (handles, state) =
mk_multi_file_state_assert_no_errors(&[("main", code)], Require::Everything);
let handle = handles.get("main").unwrap();
let transaction = state.transaction();
let module_info = transaction.get_module_info(handle).unwrap();
let cursor = TextSize::try_from(code.find("enabled):").unwrap()).unwrap();
let selection = TextRange::new(cursor, cursor);

let actions = transaction
.pytest_fixture_type_annotation_code_actions(handle, selection, ImportFormat::Absolute)
.unwrap_or_default();
let action = actions
.iter()
.find(|action| action.title == "Add pytest fixture parameter type annotation")
.expect("missing fixture parameter annotation action");
let updated = apply_refactor_edits_for_module(&module_info, &action.edits);
assert!(
updated.contains("def test_feature(enabled: bool):"),
"expected fixture parameter annotation from annotated fixture return"
);
}

/// Returns the edits of the "Add `@override` decorator" quick fix for the method
/// at the last `def foo` in `code`, or `None` if the fix is not offered.
fn add_override_quickfix_edits(
Expand Down
159 changes: 159 additions & 0 deletions website/docs/pytest.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
title: Pytest Support
description: Pyrefly support for pytest fixtures and test navigation.
---

{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}

# Pytest Support

Pyrefly includes built-in support for [pytest](https://docs.pytest.org/), the
popular Python testing framework. This support focuses on fixtures and IDE
workflows for navigating, annotating, and running tests.

> **Note:** Pyrefly's pytest support is automatic when a file imports `pytest`.
> Some IDE features also depend on selecting a Python interpreter where pytest is
> installed.

### Feedback

We welcome your feedback and suggestions. Please share your thoughts and ideas
by opening an issue on [GitHub](https://github.com/facebook/pyrefly/issues).

---

## What is pytest?

pytest is a Python testing framework where tests are usually functions or
methods named `test_*`. Its fixture system lets tests request reusable setup
objects by naming fixture parameters.

```python
import pytest

@pytest.fixture
def username() -> str:
return "Ada"

def test_user(username):
assert username == "Ada"
```

In this example, pytest injects the `username` fixture into the test function
based on the parameter name.

---

## How Pyrefly Supports pytest

Pyrefly recognizes pytest imports and fixture decorators without requiring a
plugin or manual configuration. It:

- Recognizes fixtures declared with `@pytest.fixture`, `@pytest.fixture(...)`,
and imported fixture aliases such as `from pytest import fixture`.
- Resolves fixture parameters in tests and fixtures to their fixture
definitions.
- Finds references from a fixture definition to the test and fixture parameters
that request it.
- Shows optional inlay hints for pytest fixture parameter types.
- Provides quick fixes that add inferred return annotations to fixtures.
- Provides quick fixes that add inferred parameter annotations to tests that use
fixtures.
- Adds runnable CodeLens actions for pytest tests in supported editors.

---

## Fixture Navigation

Pyrefly understands pytest's fixture-by-name convention, so IDE navigation works
across fixture definitions and fixture parameters in the same module.

```python
import pytest

@pytest.fixture
def database_url() -> str:
return "sqlite://"

def test_connection(database_url):
assert database_url.startswith("sqlite")
```

Go-to-definition on the `database_url` parameter in `test_connection` jumps to
the fixture definition. Find-references on the fixture name includes parameters
that request that fixture.

Pyrefly also handles fixture aliases:

```python
from pytest import fixture as reusable

@reusable
def user_id() -> int:
return 42

def test_lookup(user_id):
assert user_id > 0
```

---

## Fixture Type Hints and Quick Fixes

Pyrefly can infer fixture return types and offer quick fixes that make those
types explicit.

```python
import pytest

@pytest.fixture
def enabled():
return True

def test_feature(enabled):
assert enabled
```

In the editor, these quick fixes appear as code actions:

- Place the cursor inside an unannotated fixture to use
`Add pytest fixture type annotation`, which updates the fixture to
`def enabled() -> bool:`.
- Place the cursor on an unannotated test parameter that matches a known fixture
to use `Add pytest fixture parameter type annotation`, which updates the test
to `def test_feature(enabled: bool):`.
- When more than one matching annotation is available, Pyrefly can also offer
batch code actions that annotate all eligible fixtures or fixture parameters
in the file.

---

## Running pytest Tests from the Editor

In supported editors, Pyrefly shows runnable CodeLens actions above pytest tests.
For example, a test method can be run using a pytest node id such as:

```bash
python -m pytest path/to/test_file.py::TestClass::test_method
```

This feature uses the Python interpreter selected for the workspace. If Pyrefly
cannot import pytest from that interpreter, select the correct interpreter or
install pytest in the active environment.

---

## How to Use

You do not need to enable a Pyrefly plugin or add Pyrefly-specific configuration
for pytest support.

1. Install `pytest` in the Python environment used by your editor.
1. Install `pyrefly`.
1. Write pytest fixtures and tests as usual.
1. Run Pyrefly or use the Pyrefly language server in your editor.
5 changes: 5 additions & 0 deletions website/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ let docsSidebar = [
},
],
},
{
type: 'doc' as const,
id: 'pytest',
label: 'Pytest Support',
},
{
type: 'doc' as const,
id: 'django',
Expand Down
Loading