From 4ccacc80f917fd00c83c4b1b31b10b7776e711c1 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 20 Nov 2024 10:48:26 +0000 Subject: [PATCH] [ruff-0.8] [`FAST`] Further improve docs for `fast-api-non-annotated-depencency` (`FAST002`) (#14467) --- crates/ruff_linter/src/rules/fastapi/mod.rs | 16 + .../rules/fastapi_non_annotated_dependency.rs | 23 +- ...i-non-annotated-dependency_FAST002.py.snap | 25 +- ...-annotated-dependency_FAST002.py_py38.snap | 332 ++++++++++++++++++ 4 files changed, 380 insertions(+), 16 deletions(-) create mode 100644 crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002.py_py38.snap diff --git a/crates/ruff_linter/src/rules/fastapi/mod.rs b/crates/ruff_linter/src/rules/fastapi/mod.rs index 7d7f00300a..257b8ddee2 100644 --- a/crates/ruff_linter/src/rules/fastapi/mod.rs +++ b/crates/ruff_linter/src/rules/fastapi/mod.rs @@ -25,4 +25,20 @@ mod tests { assert_messages!(snapshot, diagnostics); Ok(()) } + + // FAST002 autofixes use `typing_extensions` on Python 3.8, + // since `typing.Annotated` was added in Python 3.9 + #[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002.py"))] + fn rules_py38(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}_py38", rule_code.as_ref(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("fastapi").join(path).as_path(), + &settings::LinterSettings { + target_version: settings::types::PythonVersion::Py38, + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } } 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 07d4d8b859..8475d3977a 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 @@ -20,6 +20,10 @@ use crate::settings::types::PythonVersion; /// everywhere helps ensure consistency and clarity in defining dependencies /// and parameters. /// +/// `Annotated` was added to the `typing` module in Python 3.9; however, +/// the third-party [`typing_extensions`] package provides a backport that can be +/// used on older versions of Python. +/// /// ## Example /// /// ```python @@ -58,8 +62,11 @@ use crate::settings::types::PythonVersion; /// /// [fastAPI documentation]: https://fastapi.tiangolo.com/tutorial/query-params-str-validations/?h=annotated#advantages-of-annotated /// [typing.Annotated]: https://docs.python.org/3/library/typing.html#typing.Annotated +/// [typing_extensions]: https://typing-extensions.readthedocs.io/en/stable/ #[violation] -pub struct FastApiNonAnnotatedDependency; +pub struct FastApiNonAnnotatedDependency { + py_version: PythonVersion, +} impl Violation for FastApiNonAnnotatedDependency { const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; @@ -70,7 +77,12 @@ impl Violation for FastApiNonAnnotatedDependency { } fn fix_title(&self) -> Option { - Some("Replace with `Annotated`".to_string()) + let title = if self.py_version >= PythonVersion::Py39 { + "Replace with `typing.Annotated`" + } else { + "Replace with `typing_extensions.Annotated`" + }; + Some(title.to_string()) } } @@ -137,7 +149,12 @@ fn create_diagnostic( parameter: &ast::ParameterWithDefault, safe_to_update: bool, ) { - let mut diagnostic = Diagnostic::new(FastApiNonAnnotatedDependency, parameter.range); + let mut diagnostic = Diagnostic::new( + FastApiNonAnnotatedDependency { + py_version: checker.settings.target_version, + }, + parameter.range, + ); if safe_to_update { if let (Some(annotation), Some(default)) = diff --git a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002.py.snap b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002.py.snap index 680f5d57c2..e05eaf7195 100644 --- a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002.py.snap +++ b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/fastapi/mod.rs -snapshot_kind: text --- FAST002.py:24:5: FAST002 [*] FastAPI dependency without `Annotated` | @@ -11,7 +10,7 @@ FAST002.py:24:5: FAST002 [*] FastAPI dependency without `Annotated` 25 | some_security_param: str = Security(get_oauth2_user), 26 | ): | - = help: Replace with `Annotated` + = help: Replace with `typing.Annotated` ℹ Unsafe fix 12 12 | Security, @@ -40,7 +39,7 @@ FAST002.py:25:5: FAST002 [*] FastAPI dependency without `Annotated` 26 | ): 27 | pass | - = help: Replace with `Annotated` + = help: Replace with `typing.Annotated` ℹ Unsafe fix 12 12 | Security, @@ -69,7 +68,7 @@ FAST002.py:32:5: FAST002 [*] FastAPI dependency without `Annotated` 33 | some_path_param: str = Path(), 34 | some_body_param: str = Body("foo"), | - = help: Replace with `Annotated` + = help: Replace with `typing.Annotated` ℹ Unsafe fix 12 12 | Security, @@ -98,7 +97,7 @@ FAST002.py:33:5: FAST002 [*] FastAPI dependency without `Annotated` 34 | some_body_param: str = Body("foo"), 35 | some_cookie_param: str = Cookie(), | - = help: Replace with `Annotated` + = help: Replace with `typing.Annotated` ℹ Unsafe fix 12 12 | Security, @@ -127,7 +126,7 @@ FAST002.py:34:5: FAST002 [*] FastAPI dependency without `Annotated` 35 | some_cookie_param: str = Cookie(), 36 | some_header_param: int = Header(default=5), | - = help: Replace with `Annotated` + = help: Replace with `typing.Annotated` ℹ Unsafe fix 12 12 | Security, @@ -156,7 +155,7 @@ FAST002.py:35:5: FAST002 [*] FastAPI dependency without `Annotated` 36 | some_header_param: int = Header(default=5), 37 | some_file_param: UploadFile = File(), | - = help: Replace with `Annotated` + = help: Replace with `typing.Annotated` ℹ Unsafe fix 12 12 | Security, @@ -185,7 +184,7 @@ FAST002.py:36:5: FAST002 [*] FastAPI dependency without `Annotated` 37 | some_file_param: UploadFile = File(), 38 | some_form_param: str = Form(), | - = help: Replace with `Annotated` + = help: Replace with `typing.Annotated` ℹ Unsafe fix 12 12 | Security, @@ -214,7 +213,7 @@ FAST002.py:37:5: FAST002 [*] FastAPI dependency without `Annotated` 38 | some_form_param: str = Form(), 39 | ): | - = help: Replace with `Annotated` + = help: Replace with `typing.Annotated` ℹ Unsafe fix 12 12 | Security, @@ -243,7 +242,7 @@ FAST002.py:38:5: FAST002 [*] FastAPI dependency without `Annotated` 39 | ): 40 | # do stuff | - = help: Replace with `Annotated` + = help: Replace with `typing.Annotated` ℹ Unsafe fix 12 12 | Security, @@ -272,7 +271,7 @@ FAST002.py:47:5: FAST002 [*] FastAPI dependency without `Annotated` 48 | ): 49 | pass | - = help: Replace with `Annotated` + = help: Replace with `typing.Annotated` ℹ Unsafe fix 12 12 | Security, @@ -301,7 +300,7 @@ FAST002.py:53:5: FAST002 [*] FastAPI dependency without `Annotated` 54 | skip: int = 0, 55 | limit: int = 10, | - = help: Replace with `Annotated` + = help: Replace with `typing.Annotated` ℹ Unsafe fix 12 12 | Security, @@ -330,4 +329,4 @@ FAST002.py:67:5: FAST002 FastAPI dependency without `Annotated` 68 | ): 69 | pass | - = help: Replace with `Annotated` + = help: Replace with `typing.Annotated` diff --git a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002.py_py38.snap b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002.py_py38.snap new file mode 100644 index 0000000000..0d6342b9ad --- /dev/null +++ b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002.py_py38.snap @@ -0,0 +1,332 @@ +--- +source: crates/ruff_linter/src/rules/fastapi/mod.rs +--- +FAST002.py:24:5: FAST002 [*] FastAPI dependency without `Annotated` + | +22 | @app.get("/items/") +23 | def get_items( +24 | current_user: User = Depends(get_current_user), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002 +25 | some_security_param: str = Security(get_oauth2_user), +26 | ): + | + = help: Replace with `typing_extensions.Annotated` + +ℹ Unsafe fix +12 12 | Security, +13 13 | ) +14 14 | from pydantic import BaseModel + 15 |+from typing_extensions import Annotated +15 16 | +16 17 | app = FastAPI() +17 18 | router = APIRouter() +-------------------------------------------------------------------------------- +21 22 | +22 23 | @app.get("/items/") +23 24 | def get_items( +24 |- current_user: User = Depends(get_current_user), + 25 |+ current_user: Annotated[User, Depends(get_current_user)], +25 26 | some_security_param: str = Security(get_oauth2_user), +26 27 | ): +27 28 | pass + +FAST002.py:25:5: FAST002 [*] FastAPI dependency without `Annotated` + | +23 | def get_items( +24 | current_user: User = Depends(get_current_user), +25 | some_security_param: str = Security(get_oauth2_user), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002 +26 | ): +27 | pass + | + = help: Replace with `typing_extensions.Annotated` + +ℹ Unsafe fix +12 12 | Security, +13 13 | ) +14 14 | from pydantic import BaseModel + 15 |+from typing_extensions import Annotated +15 16 | +16 17 | app = FastAPI() +17 18 | router = APIRouter() +-------------------------------------------------------------------------------- +22 23 | @app.get("/items/") +23 24 | def get_items( +24 25 | current_user: User = Depends(get_current_user), +25 |- some_security_param: str = Security(get_oauth2_user), + 26 |+ some_security_param: Annotated[str, Security(get_oauth2_user)], +26 27 | ): +27 28 | pass +28 29 | + +FAST002.py:32:5: FAST002 [*] FastAPI dependency without `Annotated` + | +30 | @app.post("/stuff/") +31 | def do_stuff( +32 | some_query_param: str | None = Query(default=None), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002 +33 | some_path_param: str = Path(), +34 | some_body_param: str = Body("foo"), + | + = help: Replace with `typing_extensions.Annotated` + +ℹ Unsafe fix +12 12 | Security, +13 13 | ) +14 14 | from pydantic import BaseModel + 15 |+from typing_extensions import Annotated +15 16 | +16 17 | app = FastAPI() +17 18 | router = APIRouter() +-------------------------------------------------------------------------------- +29 30 | +30 31 | @app.post("/stuff/") +31 32 | def do_stuff( +32 |- some_query_param: str | None = Query(default=None), + 33 |+ some_query_param: Annotated[str | None, Query(default=None)], +33 34 | some_path_param: str = Path(), +34 35 | some_body_param: str = Body("foo"), +35 36 | some_cookie_param: str = Cookie(), + +FAST002.py:33:5: FAST002 [*] FastAPI dependency without `Annotated` + | +31 | def do_stuff( +32 | some_query_param: str | None = Query(default=None), +33 | some_path_param: str = Path(), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002 +34 | some_body_param: str = Body("foo"), +35 | some_cookie_param: str = Cookie(), + | + = help: Replace with `typing_extensions.Annotated` + +ℹ Unsafe fix +12 12 | Security, +13 13 | ) +14 14 | from pydantic import BaseModel + 15 |+from typing_extensions import Annotated +15 16 | +16 17 | app = FastAPI() +17 18 | router = APIRouter() +-------------------------------------------------------------------------------- +30 31 | @app.post("/stuff/") +31 32 | def do_stuff( +32 33 | some_query_param: str | None = Query(default=None), +33 |- some_path_param: str = Path(), + 34 |+ some_path_param: Annotated[str, Path()], +34 35 | some_body_param: str = Body("foo"), +35 36 | some_cookie_param: str = Cookie(), +36 37 | some_header_param: int = Header(default=5), + +FAST002.py:34:5: FAST002 [*] FastAPI dependency without `Annotated` + | +32 | some_query_param: str | None = Query(default=None), +33 | some_path_param: str = Path(), +34 | some_body_param: str = Body("foo"), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002 +35 | some_cookie_param: str = Cookie(), +36 | some_header_param: int = Header(default=5), + | + = help: Replace with `typing_extensions.Annotated` + +ℹ Unsafe fix +12 12 | Security, +13 13 | ) +14 14 | from pydantic import BaseModel + 15 |+from typing_extensions import Annotated +15 16 | +16 17 | app = FastAPI() +17 18 | router = APIRouter() +-------------------------------------------------------------------------------- +31 32 | def do_stuff( +32 33 | some_query_param: str | None = Query(default=None), +33 34 | some_path_param: str = Path(), +34 |- some_body_param: str = Body("foo"), + 35 |+ some_body_param: Annotated[str, Body("foo")], +35 36 | some_cookie_param: str = Cookie(), +36 37 | some_header_param: int = Header(default=5), +37 38 | some_file_param: UploadFile = File(), + +FAST002.py:35:5: FAST002 [*] FastAPI dependency without `Annotated` + | +33 | some_path_param: str = Path(), +34 | some_body_param: str = Body("foo"), +35 | some_cookie_param: str = Cookie(), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002 +36 | some_header_param: int = Header(default=5), +37 | some_file_param: UploadFile = File(), + | + = help: Replace with `typing_extensions.Annotated` + +ℹ Unsafe fix +12 12 | Security, +13 13 | ) +14 14 | from pydantic import BaseModel + 15 |+from typing_extensions import Annotated +15 16 | +16 17 | app = FastAPI() +17 18 | router = APIRouter() +-------------------------------------------------------------------------------- +32 33 | some_query_param: str | None = Query(default=None), +33 34 | some_path_param: str = Path(), +34 35 | some_body_param: str = Body("foo"), +35 |- some_cookie_param: str = Cookie(), + 36 |+ some_cookie_param: Annotated[str, Cookie()], +36 37 | some_header_param: int = Header(default=5), +37 38 | some_file_param: UploadFile = File(), +38 39 | some_form_param: str = Form(), + +FAST002.py:36:5: FAST002 [*] FastAPI dependency without `Annotated` + | +34 | some_body_param: str = Body("foo"), +35 | some_cookie_param: str = Cookie(), +36 | some_header_param: int = Header(default=5), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002 +37 | some_file_param: UploadFile = File(), +38 | some_form_param: str = Form(), + | + = help: Replace with `typing_extensions.Annotated` + +ℹ Unsafe fix +12 12 | Security, +13 13 | ) +14 14 | from pydantic import BaseModel + 15 |+from typing_extensions import Annotated +15 16 | +16 17 | app = FastAPI() +17 18 | router = APIRouter() +-------------------------------------------------------------------------------- +33 34 | some_path_param: str = Path(), +34 35 | some_body_param: str = Body("foo"), +35 36 | some_cookie_param: str = Cookie(), +36 |- some_header_param: int = Header(default=5), + 37 |+ some_header_param: Annotated[int, Header(default=5)], +37 38 | some_file_param: UploadFile = File(), +38 39 | some_form_param: str = Form(), +39 40 | ): + +FAST002.py:37:5: FAST002 [*] FastAPI dependency without `Annotated` + | +35 | some_cookie_param: str = Cookie(), +36 | some_header_param: int = Header(default=5), +37 | some_file_param: UploadFile = File(), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002 +38 | some_form_param: str = Form(), +39 | ): + | + = help: Replace with `typing_extensions.Annotated` + +ℹ Unsafe fix +12 12 | Security, +13 13 | ) +14 14 | from pydantic import BaseModel + 15 |+from typing_extensions import Annotated +15 16 | +16 17 | app = FastAPI() +17 18 | router = APIRouter() +-------------------------------------------------------------------------------- +34 35 | some_body_param: str = Body("foo"), +35 36 | some_cookie_param: str = Cookie(), +36 37 | some_header_param: int = Header(default=5), +37 |- some_file_param: UploadFile = File(), + 38 |+ some_file_param: Annotated[UploadFile, File()], +38 39 | some_form_param: str = Form(), +39 40 | ): +40 41 | # do stuff + +FAST002.py:38:5: FAST002 [*] FastAPI dependency without `Annotated` + | +36 | some_header_param: int = Header(default=5), +37 | some_file_param: UploadFile = File(), +38 | some_form_param: str = Form(), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002 +39 | ): +40 | # do stuff + | + = help: Replace with `typing_extensions.Annotated` + +ℹ Unsafe fix +12 12 | Security, +13 13 | ) +14 14 | from pydantic import BaseModel + 15 |+from typing_extensions import Annotated +15 16 | +16 17 | app = FastAPI() +17 18 | router = APIRouter() +-------------------------------------------------------------------------------- +35 36 | some_cookie_param: str = Cookie(), +36 37 | some_header_param: int = Header(default=5), +37 38 | some_file_param: UploadFile = File(), +38 |- some_form_param: str = Form(), + 39 |+ some_form_param: Annotated[str, Form()], +39 40 | ): +40 41 | # do stuff +41 42 | pass + +FAST002.py:47:5: FAST002 [*] FastAPI dependency without `Annotated` + | +45 | skip: int, +46 | limit: int, +47 | current_user: User = Depends(get_current_user), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002 +48 | ): +49 | pass + | + = help: Replace with `typing_extensions.Annotated` + +ℹ Unsafe fix +12 12 | Security, +13 13 | ) +14 14 | from pydantic import BaseModel + 15 |+from typing_extensions import Annotated +15 16 | +16 17 | app = FastAPI() +17 18 | router = APIRouter() +-------------------------------------------------------------------------------- +44 45 | def get_users( +45 46 | skip: int, +46 47 | limit: int, +47 |- current_user: User = Depends(get_current_user), + 48 |+ current_user: Annotated[User, Depends(get_current_user)], +48 49 | ): +49 50 | pass +50 51 | + +FAST002.py:53:5: FAST002 [*] FastAPI dependency without `Annotated` + | +51 | @app.get("/users/") +52 | def get_users( +53 | current_user: User = Depends(get_current_user), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002 +54 | skip: int = 0, +55 | limit: int = 10, + | + = help: Replace with `typing_extensions.Annotated` + +ℹ Unsafe fix +12 12 | Security, +13 13 | ) +14 14 | from pydantic import BaseModel + 15 |+from typing_extensions import Annotated +15 16 | +16 17 | app = FastAPI() +17 18 | router = APIRouter() +-------------------------------------------------------------------------------- +50 51 | +51 52 | @app.get("/users/") +52 53 | def get_users( +53 |- current_user: User = Depends(get_current_user), + 54 |+ current_user: Annotated[User, Depends(get_current_user)], +54 55 | skip: int = 0, +55 56 | limit: int = 10, +56 57 | ): + +FAST002.py:67:5: FAST002 FastAPI dependency without `Annotated` + | +65 | skip: int = 0, +66 | limit: int = 10, +67 | current_user: User = Depends(get_current_user), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002 +68 | ): +69 | pass + | + = help: Replace with `typing_extensions.Annotated`