mirror of https://github.com/astral-sh/ruff
[`fastapi`] Handle ellipsis defaults in FAST002 autofix (`FAST002`) (#20810)
## Summary Implement handling of ellipsis (`...`) defaults in the `FAST002` autofix to correctly differentiate between required and optional parameters in FastAPI route definitions. Previously, the autofix did not properly handle cases where parameters used `...` as a default value (to indicate required parameters). This could lead to incorrect transformations when applying the autofix. This change updates the `FAST002` autofix logic to: - Correctly recognize `...` as a valid FastAPI required default. - Preserve the semantics of required parameters while still applying other autofix improvements. - Avoid incorrectly substituting or removing ellipsis defaults. Fixes https://github.com/astral-sh/ruff/issues/20800 ## Test Plan Added a new test fixture at: ```crates/ruff_linter/resources/test/fixtures/fastapi/FAST002_2.py```
This commit is contained in:
parent
692b7d7f0c
commit
a802d7a0ea
|
|
@ -0,0 +1,87 @@
|
||||||
|
"""Test FAST002 ellipsis handling."""
|
||||||
|
|
||||||
|
from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
# Cases that should be fixed - ellipsis should be removed
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/test1")
|
||||||
|
async def test_ellipsis_query(
|
||||||
|
# This should become: param: Annotated[str, Query(description="Test param")]
|
||||||
|
param: str = Query(..., description="Test param"),
|
||||||
|
) -> str:
|
||||||
|
return param
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/test2")
|
||||||
|
async def test_ellipsis_header(
|
||||||
|
# This should become: auth: Annotated[str, Header(description="Auth header")]
|
||||||
|
auth: str = Header(..., description="Auth header"),
|
||||||
|
) -> str:
|
||||||
|
return auth
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/test3")
|
||||||
|
async def test_ellipsis_body(
|
||||||
|
# This should become: data: Annotated[dict, Body(description="Request body")]
|
||||||
|
data: dict = Body(..., description="Request body"),
|
||||||
|
) -> dict:
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/test4")
|
||||||
|
async def test_ellipsis_cookie(
|
||||||
|
# This should become: session: Annotated[str, Cookie(description="Session ID")]
|
||||||
|
session: str = Cookie(..., description="Session ID"),
|
||||||
|
) -> str:
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/test5")
|
||||||
|
async def test_simple_ellipsis(
|
||||||
|
# This should become: id: Annotated[str, Query()]
|
||||||
|
id: str = Query(...),
|
||||||
|
) -> str:
|
||||||
|
return id
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/test6")
|
||||||
|
async def test_multiple_kwargs_with_ellipsis(
|
||||||
|
# This should become: param: Annotated[str, Query(description="Test", min_length=1, max_length=10)]
|
||||||
|
param: str = Query(..., description="Test", min_length=1, max_length=10),
|
||||||
|
) -> str:
|
||||||
|
return param
|
||||||
|
|
||||||
|
|
||||||
|
# Cases with actual default values - these should preserve the default
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/test7")
|
||||||
|
async def test_with_default_value(
|
||||||
|
# This should become: param: Annotated[str, Query(description="Test")] = "default"
|
||||||
|
param: str = Query("default", description="Test"),
|
||||||
|
) -> str:
|
||||||
|
return param
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/test8")
|
||||||
|
async def test_with_default_none(
|
||||||
|
# This should become: param: Annotated[str | None, Query(description="Test")] = None
|
||||||
|
param: str | None = Query(None, description="Test"),
|
||||||
|
) -> str:
|
||||||
|
return param or "empty"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/test9")
|
||||||
|
async def test_mixed_parameters(
|
||||||
|
# First param should be fixed with default preserved
|
||||||
|
optional_param: str = Query("default", description="Optional"),
|
||||||
|
# Second param should not be fixed because of the preceding default
|
||||||
|
required_param: str = Query(..., description="Required"),
|
||||||
|
# Third param should be fixed with default preserved
|
||||||
|
another_optional_param: int = Query(42, description="Another optional"),
|
||||||
|
) -> str:
|
||||||
|
return f"{required_param}-{optional_param}-{another_optional_param}"
|
||||||
|
|
@ -18,6 +18,7 @@ mod tests {
|
||||||
#[test_case(Rule::FastApiRedundantResponseModel, Path::new("FAST001.py"))]
|
#[test_case(Rule::FastApiRedundantResponseModel, Path::new("FAST001.py"))]
|
||||||
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_0.py"))]
|
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_0.py"))]
|
||||||
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_1.py"))]
|
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_1.py"))]
|
||||||
|
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_2.py"))]
|
||||||
#[test_case(Rule::FastApiUnusedPathParameter, Path::new("FAST003.py"))]
|
#[test_case(Rule::FastApiUnusedPathParameter, Path::new("FAST003.py"))]
|
||||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
|
||||||
|
|
@ -56,6 +57,7 @@ mod tests {
|
||||||
// since `typing.Annotated` was added in Python 3.9
|
// since `typing.Annotated` was added in Python 3.9
|
||||||
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_0.py"))]
|
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_0.py"))]
|
||||||
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_1.py"))]
|
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_1.py"))]
|
||||||
|
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_2.py"))]
|
||||||
fn rules_py38(rule_code: Rule, path: &Path) -> Result<()> {
|
fn rules_py38(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
let snapshot = format!("{}_{}_py38", rule_code.name(), path.to_string_lossy());
|
let snapshot = format!("{}_{}_py38", rule_code.name(), path.to_string_lossy());
|
||||||
let diagnostics = test_path(
|
let diagnostics = test_path(
|
||||||
|
|
|
||||||
|
|
@ -275,19 +275,42 @@ fn create_diagnostic(
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
seen_default = true;
|
// Check if the default argument is ellipsis (...), which in FastAPI means "required"
|
||||||
format!(
|
let is_default_argument_ellipsis = matches!(
|
||||||
"{parameter_name}: {binding}[{annotation}, {default_}({kwarg_list})] \
|
dependency_call.default_argument.value(),
|
||||||
= {default_value}",
|
ast::Expr::EllipsisLiteral(_)
|
||||||
|
);
|
||||||
|
|
||||||
|
if is_default_argument_ellipsis && seen_default {
|
||||||
|
// For ellipsis after a parameter with default, can't remove the default
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_default_argument_ellipsis {
|
||||||
|
// For actual default values, mark that we've seen a default
|
||||||
|
seen_default = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let base_format = format!(
|
||||||
|
"{parameter_name}: {binding}[{annotation}, {default_}({kwarg_list})]",
|
||||||
parameter_name = parameter.name,
|
parameter_name = parameter.name,
|
||||||
annotation = checker.locator().slice(parameter.annotation.range()),
|
annotation = checker.locator().slice(parameter.annotation.range()),
|
||||||
default_ = checker
|
default_ = checker
|
||||||
.locator()
|
.locator()
|
||||||
.slice(map_callable(parameter.default).range()),
|
.slice(map_callable(parameter.default).range()),
|
||||||
default_value = checker
|
);
|
||||||
|
|
||||||
|
if is_default_argument_ellipsis {
|
||||||
|
// For ellipsis, don't add a default value since the parameter
|
||||||
|
// should remain required after conversion to Annotated
|
||||||
|
base_format
|
||||||
|
} else {
|
||||||
|
// For actual default values, preserve them
|
||||||
|
let default_value = checker
|
||||||
.locator()
|
.locator()
|
||||||
.slice(dependency_call.default_argument.value().range()),
|
.slice(dependency_call.default_argument.value().range());
|
||||||
)
|
format!("{base_format} = {default_value}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if seen_default {
|
if seen_default {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,303 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/fastapi/mod.rs
|
||||||
|
---
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:14:5
|
||||||
|
|
|
||||||
|
12 | async def test_ellipsis_query(
|
||||||
|
13 | # This should become: param: Annotated[str, Query(description="Test param")]
|
||||||
|
14 | param: str = Query(..., description="Test param"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
15 | ) -> str:
|
||||||
|
16 | return param
|
||||||
|
|
|
||||||
|
help: Replace with `typing.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
12 | @app.get("/test1")
|
||||||
|
13 | async def test_ellipsis_query(
|
||||||
|
14 | # This should become: param: Annotated[str, Query(description="Test param")]
|
||||||
|
- param: str = Query(..., description="Test param"),
|
||||||
|
15 + param: Annotated[str, Query(description="Test param")],
|
||||||
|
16 | ) -> str:
|
||||||
|
17 | return param
|
||||||
|
18 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:22:5
|
||||||
|
|
|
||||||
|
20 | async def test_ellipsis_header(
|
||||||
|
21 | # This should become: auth: Annotated[str, Header(description="Auth header")]
|
||||||
|
22 | auth: str = Header(..., description="Auth header"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
23 | ) -> str:
|
||||||
|
24 | return auth
|
||||||
|
|
|
||||||
|
help: Replace with `typing.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
20 | @app.get("/test2")
|
||||||
|
21 | async def test_ellipsis_header(
|
||||||
|
22 | # This should become: auth: Annotated[str, Header(description="Auth header")]
|
||||||
|
- auth: str = Header(..., description="Auth header"),
|
||||||
|
23 + auth: Annotated[str, Header(description="Auth header")],
|
||||||
|
24 | ) -> str:
|
||||||
|
25 | return auth
|
||||||
|
26 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:30:5
|
||||||
|
|
|
||||||
|
28 | async def test_ellipsis_body(
|
||||||
|
29 | # This should become: data: Annotated[dict, Body(description="Request body")]
|
||||||
|
30 | data: dict = Body(..., description="Request body"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
31 | ) -> dict:
|
||||||
|
32 | return data
|
||||||
|
|
|
||||||
|
help: Replace with `typing.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
28 | @app.post("/test3")
|
||||||
|
29 | async def test_ellipsis_body(
|
||||||
|
30 | # This should become: data: Annotated[dict, Body(description="Request body")]
|
||||||
|
- data: dict = Body(..., description="Request body"),
|
||||||
|
31 + data: Annotated[dict, Body(description="Request body")],
|
||||||
|
32 | ) -> dict:
|
||||||
|
33 | return data
|
||||||
|
34 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:38:5
|
||||||
|
|
|
||||||
|
36 | async def test_ellipsis_cookie(
|
||||||
|
37 | # This should become: session: Annotated[str, Cookie(description="Session ID")]
|
||||||
|
38 | session: str = Cookie(..., description="Session ID"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
39 | ) -> str:
|
||||||
|
40 | return session
|
||||||
|
|
|
||||||
|
help: Replace with `typing.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
36 | @app.get("/test4")
|
||||||
|
37 | async def test_ellipsis_cookie(
|
||||||
|
38 | # This should become: session: Annotated[str, Cookie(description="Session ID")]
|
||||||
|
- session: str = Cookie(..., description="Session ID"),
|
||||||
|
39 + session: Annotated[str, Cookie(description="Session ID")],
|
||||||
|
40 | ) -> str:
|
||||||
|
41 | return session
|
||||||
|
42 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:46:5
|
||||||
|
|
|
||||||
|
44 | async def test_simple_ellipsis(
|
||||||
|
45 | # This should become: id: Annotated[str, Query()]
|
||||||
|
46 | id: str = Query(...),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
47 | ) -> str:
|
||||||
|
48 | return id
|
||||||
|
|
|
||||||
|
help: Replace with `typing.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
44 | @app.get("/test5")
|
||||||
|
45 | async def test_simple_ellipsis(
|
||||||
|
46 | # This should become: id: Annotated[str, Query()]
|
||||||
|
- id: str = Query(...),
|
||||||
|
47 + id: Annotated[str, Query()],
|
||||||
|
48 | ) -> str:
|
||||||
|
49 | return id
|
||||||
|
50 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:54:5
|
||||||
|
|
|
||||||
|
52 | async def test_multiple_kwargs_with_ellipsis(
|
||||||
|
53 | # This should become: param: Annotated[str, Query(description="Test", min_length=1, max_length=10)]
|
||||||
|
54 | param: str = Query(..., description="Test", min_length=1, max_length=10),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
55 | ) -> str:
|
||||||
|
56 | return param
|
||||||
|
|
|
||||||
|
help: Replace with `typing.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
52 | @app.get("/test6")
|
||||||
|
53 | async def test_multiple_kwargs_with_ellipsis(
|
||||||
|
54 | # This should become: param: Annotated[str, Query(description="Test", min_length=1, max_length=10)]
|
||||||
|
- param: str = Query(..., description="Test", min_length=1, max_length=10),
|
||||||
|
55 + param: Annotated[str, Query(description="Test", min_length=1, max_length=10)],
|
||||||
|
56 | ) -> str:
|
||||||
|
57 | return param
|
||||||
|
58 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:65:5
|
||||||
|
|
|
||||||
|
63 | async def test_with_default_value(
|
||||||
|
64 | # This should become: param: Annotated[str, Query(description="Test")] = "default"
|
||||||
|
65 | param: str = Query("default", description="Test"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
66 | ) -> str:
|
||||||
|
67 | return param
|
||||||
|
|
|
||||||
|
help: Replace with `typing.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
63 | @app.get("/test7")
|
||||||
|
64 | async def test_with_default_value(
|
||||||
|
65 | # This should become: param: Annotated[str, Query(description="Test")] = "default"
|
||||||
|
- param: str = Query("default", description="Test"),
|
||||||
|
66 + param: Annotated[str, Query(description="Test")] = "default",
|
||||||
|
67 | ) -> str:
|
||||||
|
68 | return param
|
||||||
|
69 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:73:5
|
||||||
|
|
|
||||||
|
71 | async def test_with_default_none(
|
||||||
|
72 | # This should become: param: Annotated[str | None, Query(description="Test")] = None
|
||||||
|
73 | param: str | None = Query(None, description="Test"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
74 | ) -> str:
|
||||||
|
75 | return param or "empty"
|
||||||
|
|
|
||||||
|
help: Replace with `typing.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
71 | @app.get("/test8")
|
||||||
|
72 | async def test_with_default_none(
|
||||||
|
73 | # This should become: param: Annotated[str | None, Query(description="Test")] = None
|
||||||
|
- param: str | None = Query(None, description="Test"),
|
||||||
|
74 + param: Annotated[str | None, Query(description="Test")] = None,
|
||||||
|
75 | ) -> str:
|
||||||
|
76 | return param or "empty"
|
||||||
|
77 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:81:5
|
||||||
|
|
|
||||||
|
79 | async def test_mixed_parameters(
|
||||||
|
80 | # First param should be fixed with default preserved
|
||||||
|
81 | optional_param: str = Query("default", description="Optional"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
82 | # Second param should not be fixed because of the preceding default
|
||||||
|
83 | required_param: str = Query(..., description="Required"),
|
||||||
|
|
|
||||||
|
help: Replace with `typing.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
79 | @app.get("/test9")
|
||||||
|
80 | async def test_mixed_parameters(
|
||||||
|
81 | # First param should be fixed with default preserved
|
||||||
|
- optional_param: str = Query("default", description="Optional"),
|
||||||
|
82 + optional_param: Annotated[str, Query(description="Optional")] = "default",
|
||||||
|
83 | # Second param should not be fixed because of the preceding default
|
||||||
|
84 | required_param: str = Query(..., description="Required"),
|
||||||
|
85 | # Third param should be fixed with default preserved
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:83:5
|
||||||
|
|
|
||||||
|
81 | optional_param: str = Query("default", description="Optional"),
|
||||||
|
82 | # Second param should not be fixed because of the preceding default
|
||||||
|
83 | required_param: str = Query(..., description="Required"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
84 | # Third param should be fixed with default preserved
|
||||||
|
85 | another_optional_param: int = Query(42, description="Another optional"),
|
||||||
|
|
|
||||||
|
help: Replace with `typing.Annotated`
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:85:5
|
||||||
|
|
|
||||||
|
83 | required_param: str = Query(..., description="Required"),
|
||||||
|
84 | # Third param should be fixed with default preserved
|
||||||
|
85 | another_optional_param: int = Query(42, description="Another optional"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
86 | ) -> str:
|
||||||
|
87 | return f"{required_param}-{optional_param}-{another_optional_param}"
|
||||||
|
|
|
||||||
|
help: Replace with `typing.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
83 | # Second param should not be fixed because of the preceding default
|
||||||
|
84 | required_param: str = Query(..., description="Required"),
|
||||||
|
85 | # Third param should be fixed with default preserved
|
||||||
|
- another_optional_param: int = Query(42, description="Another optional"),
|
||||||
|
86 + another_optional_param: Annotated[int, Query(description="Another optional")] = 42,
|
||||||
|
87 | ) -> str:
|
||||||
|
88 | return f"{required_param}-{optional_param}-{another_optional_param}"
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
@ -0,0 +1,303 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/fastapi/mod.rs
|
||||||
|
---
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:14:5
|
||||||
|
|
|
||||||
|
12 | async def test_ellipsis_query(
|
||||||
|
13 | # This should become: param: Annotated[str, Query(description="Test param")]
|
||||||
|
14 | param: str = Query(..., description="Test param"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
15 | ) -> str:
|
||||||
|
16 | return param
|
||||||
|
|
|
||||||
|
help: Replace with `typing_extensions.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing_extensions import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
12 | @app.get("/test1")
|
||||||
|
13 | async def test_ellipsis_query(
|
||||||
|
14 | # This should become: param: Annotated[str, Query(description="Test param")]
|
||||||
|
- param: str = Query(..., description="Test param"),
|
||||||
|
15 + param: Annotated[str, Query(description="Test param")],
|
||||||
|
16 | ) -> str:
|
||||||
|
17 | return param
|
||||||
|
18 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:22:5
|
||||||
|
|
|
||||||
|
20 | async def test_ellipsis_header(
|
||||||
|
21 | # This should become: auth: Annotated[str, Header(description="Auth header")]
|
||||||
|
22 | auth: str = Header(..., description="Auth header"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
23 | ) -> str:
|
||||||
|
24 | return auth
|
||||||
|
|
|
||||||
|
help: Replace with `typing_extensions.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing_extensions import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
20 | @app.get("/test2")
|
||||||
|
21 | async def test_ellipsis_header(
|
||||||
|
22 | # This should become: auth: Annotated[str, Header(description="Auth header")]
|
||||||
|
- auth: str = Header(..., description="Auth header"),
|
||||||
|
23 + auth: Annotated[str, Header(description="Auth header")],
|
||||||
|
24 | ) -> str:
|
||||||
|
25 | return auth
|
||||||
|
26 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:30:5
|
||||||
|
|
|
||||||
|
28 | async def test_ellipsis_body(
|
||||||
|
29 | # This should become: data: Annotated[dict, Body(description="Request body")]
|
||||||
|
30 | data: dict = Body(..., description="Request body"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
31 | ) -> dict:
|
||||||
|
32 | return data
|
||||||
|
|
|
||||||
|
help: Replace with `typing_extensions.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing_extensions import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
28 | @app.post("/test3")
|
||||||
|
29 | async def test_ellipsis_body(
|
||||||
|
30 | # This should become: data: Annotated[dict, Body(description="Request body")]
|
||||||
|
- data: dict = Body(..., description="Request body"),
|
||||||
|
31 + data: Annotated[dict, Body(description="Request body")],
|
||||||
|
32 | ) -> dict:
|
||||||
|
33 | return data
|
||||||
|
34 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:38:5
|
||||||
|
|
|
||||||
|
36 | async def test_ellipsis_cookie(
|
||||||
|
37 | # This should become: session: Annotated[str, Cookie(description="Session ID")]
|
||||||
|
38 | session: str = Cookie(..., description="Session ID"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
39 | ) -> str:
|
||||||
|
40 | return session
|
||||||
|
|
|
||||||
|
help: Replace with `typing_extensions.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing_extensions import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
36 | @app.get("/test4")
|
||||||
|
37 | async def test_ellipsis_cookie(
|
||||||
|
38 | # This should become: session: Annotated[str, Cookie(description="Session ID")]
|
||||||
|
- session: str = Cookie(..., description="Session ID"),
|
||||||
|
39 + session: Annotated[str, Cookie(description="Session ID")],
|
||||||
|
40 | ) -> str:
|
||||||
|
41 | return session
|
||||||
|
42 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:46:5
|
||||||
|
|
|
||||||
|
44 | async def test_simple_ellipsis(
|
||||||
|
45 | # This should become: id: Annotated[str, Query()]
|
||||||
|
46 | id: str = Query(...),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
47 | ) -> str:
|
||||||
|
48 | return id
|
||||||
|
|
|
||||||
|
help: Replace with `typing_extensions.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing_extensions import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
44 | @app.get("/test5")
|
||||||
|
45 | async def test_simple_ellipsis(
|
||||||
|
46 | # This should become: id: Annotated[str, Query()]
|
||||||
|
- id: str = Query(...),
|
||||||
|
47 + id: Annotated[str, Query()],
|
||||||
|
48 | ) -> str:
|
||||||
|
49 | return id
|
||||||
|
50 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:54:5
|
||||||
|
|
|
||||||
|
52 | async def test_multiple_kwargs_with_ellipsis(
|
||||||
|
53 | # This should become: param: Annotated[str, Query(description="Test", min_length=1, max_length=10)]
|
||||||
|
54 | param: str = Query(..., description="Test", min_length=1, max_length=10),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
55 | ) -> str:
|
||||||
|
56 | return param
|
||||||
|
|
|
||||||
|
help: Replace with `typing_extensions.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing_extensions import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
52 | @app.get("/test6")
|
||||||
|
53 | async def test_multiple_kwargs_with_ellipsis(
|
||||||
|
54 | # This should become: param: Annotated[str, Query(description="Test", min_length=1, max_length=10)]
|
||||||
|
- param: str = Query(..., description="Test", min_length=1, max_length=10),
|
||||||
|
55 + param: Annotated[str, Query(description="Test", min_length=1, max_length=10)],
|
||||||
|
56 | ) -> str:
|
||||||
|
57 | return param
|
||||||
|
58 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:65:5
|
||||||
|
|
|
||||||
|
63 | async def test_with_default_value(
|
||||||
|
64 | # This should become: param: Annotated[str, Query(description="Test")] = "default"
|
||||||
|
65 | param: str = Query("default", description="Test"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
66 | ) -> str:
|
||||||
|
67 | return param
|
||||||
|
|
|
||||||
|
help: Replace with `typing_extensions.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing_extensions import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
63 | @app.get("/test7")
|
||||||
|
64 | async def test_with_default_value(
|
||||||
|
65 | # This should become: param: Annotated[str, Query(description="Test")] = "default"
|
||||||
|
- param: str = Query("default", description="Test"),
|
||||||
|
66 + param: Annotated[str, Query(description="Test")] = "default",
|
||||||
|
67 | ) -> str:
|
||||||
|
68 | return param
|
||||||
|
69 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:73:5
|
||||||
|
|
|
||||||
|
71 | async def test_with_default_none(
|
||||||
|
72 | # This should become: param: Annotated[str | None, Query(description="Test")] = None
|
||||||
|
73 | param: str | None = Query(None, description="Test"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
74 | ) -> str:
|
||||||
|
75 | return param or "empty"
|
||||||
|
|
|
||||||
|
help: Replace with `typing_extensions.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing_extensions import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
71 | @app.get("/test8")
|
||||||
|
72 | async def test_with_default_none(
|
||||||
|
73 | # This should become: param: Annotated[str | None, Query(description="Test")] = None
|
||||||
|
- param: str | None = Query(None, description="Test"),
|
||||||
|
74 + param: Annotated[str | None, Query(description="Test")] = None,
|
||||||
|
75 | ) -> str:
|
||||||
|
76 | return param or "empty"
|
||||||
|
77 |
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:81:5
|
||||||
|
|
|
||||||
|
79 | async def test_mixed_parameters(
|
||||||
|
80 | # First param should be fixed with default preserved
|
||||||
|
81 | optional_param: str = Query("default", description="Optional"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
82 | # Second param should not be fixed because of the preceding default
|
||||||
|
83 | required_param: str = Query(..., description="Required"),
|
||||||
|
|
|
||||||
|
help: Replace with `typing_extensions.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing_extensions import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
79 | @app.get("/test9")
|
||||||
|
80 | async def test_mixed_parameters(
|
||||||
|
81 | # First param should be fixed with default preserved
|
||||||
|
- optional_param: str = Query("default", description="Optional"),
|
||||||
|
82 + optional_param: Annotated[str, Query(description="Optional")] = "default",
|
||||||
|
83 | # Second param should not be fixed because of the preceding default
|
||||||
|
84 | required_param: str = Query(..., description="Required"),
|
||||||
|
85 | # Third param should be fixed with default preserved
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
|
|
||||||
|
FAST002 FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:83:5
|
||||||
|
|
|
||||||
|
81 | optional_param: str = Query("default", description="Optional"),
|
||||||
|
82 | # Second param should not be fixed because of the preceding default
|
||||||
|
83 | required_param: str = Query(..., description="Required"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
84 | # Third param should be fixed with default preserved
|
||||||
|
85 | another_optional_param: int = Query(42, description="Another optional"),
|
||||||
|
|
|
||||||
|
help: Replace with `typing_extensions.Annotated`
|
||||||
|
|
||||||
|
FAST002 [*] FastAPI dependency without `Annotated`
|
||||||
|
--> FAST002_2.py:85:5
|
||||||
|
|
|
||||||
|
83 | required_param: str = Query(..., description="Required"),
|
||||||
|
84 | # Third param should be fixed with default preserved
|
||||||
|
85 | another_optional_param: int = Query(42, description="Another optional"),
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
86 | ) -> str:
|
||||||
|
87 | return f"{required_param}-{optional_param}-{another_optional_param}"
|
||||||
|
|
|
||||||
|
help: Replace with `typing_extensions.Annotated`
|
||||||
|
1 | """Test FAST002 ellipsis handling."""
|
||||||
|
2 |
|
||||||
|
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
|
||||||
|
4 + from typing_extensions import Annotated
|
||||||
|
5 |
|
||||||
|
6 | app = FastAPI()
|
||||||
|
7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
83 | # Second param should not be fixed because of the preceding default
|
||||||
|
84 | required_param: str = Query(..., description="Required"),
|
||||||
|
85 | # Third param should be fixed with default preserved
|
||||||
|
- another_optional_param: int = Query(42, description="Another optional"),
|
||||||
|
86 + another_optional_param: Annotated[int, Query(description="Another optional")] = 42,
|
||||||
|
87 | ) -> str:
|
||||||
|
88 | return f"{required_param}-{optional_param}-{another_optional_param}"
|
||||||
|
note: This is an unsafe fix and may change runtime behavior
|
||||||
Loading…
Reference in New Issue