From a802d7a0ea2bc39e1fc3329cd85b9575f97b6a08 Mon Sep 17 00:00:00 2001 From: Hengky Kurniawan <84961022+hengky-kurniawan-1@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:47:13 +0700 Subject: [PATCH] [`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``` --- .../test/fixtures/fastapi/FAST002_2.py | 87 +++++ crates/ruff_linter/src/rules/fastapi/mod.rs | 2 + .../rules/fastapi_non_annotated_dependency.rs | 37 ++- ...non-annotated-dependency_FAST002_2.py.snap | 303 ++++++++++++++++++ ...nnotated-dependency_FAST002_2.py_py38.snap | 303 ++++++++++++++++++ 5 files changed, 725 insertions(+), 7 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/fastapi/FAST002_2.py create mode 100644 crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_2.py.snap create mode 100644 crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_2.py_py38.snap diff --git a/crates/ruff_linter/resources/test/fixtures/fastapi/FAST002_2.py b/crates/ruff_linter/resources/test/fixtures/fastapi/FAST002_2.py new file mode 100644 index 0000000000..aa56943f28 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/fastapi/FAST002_2.py @@ -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}" diff --git a/crates/ruff_linter/src/rules/fastapi/mod.rs b/crates/ruff_linter/src/rules/fastapi/mod.rs index 17b3cb663c..51ff84c5bc 100644 --- a/crates/ruff_linter/src/rules/fastapi/mod.rs +++ b/crates/ruff_linter/src/rules/fastapi/mod.rs @@ -18,6 +18,7 @@ mod tests { #[test_case(Rule::FastApiRedundantResponseModel, Path::new("FAST001.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_2.py"))] #[test_case(Rule::FastApiUnusedPathParameter, Path::new("FAST003.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { 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 #[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_2.py"))] fn rules_py38(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}_py38", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs index 8d0fa8b280..9baf036ad7 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs @@ -275,19 +275,42 @@ fn create_diagnostic( .collect::>() .join(", "); - seen_default = true; - format!( - "{parameter_name}: {binding}[{annotation}, {default_}({kwarg_list})] \ - = {default_value}", + // Check if the default argument is ellipsis (...), which in FastAPI means "required" + let is_default_argument_ellipsis = matches!( + dependency_call.default_argument.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, annotation = checker.locator().slice(parameter.annotation.range()), default_ = checker .locator() .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() - .slice(dependency_call.default_argument.value().range()), - ) + .slice(dependency_call.default_argument.value().range()); + format!("{base_format} = {default_value}") + } } _ => { if seen_default { diff --git a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_2.py.snap b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_2.py.snap new file mode 100644 index 0000000000..24f2a87c00 --- /dev/null +++ b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_2.py.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_2.py_py38.snap b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_2.py_py38.snap new file mode 100644 index 0000000000..3adcae1955 --- /dev/null +++ b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_2.py_py38.snap @@ -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