[fastapi] Handle callable class dependencies with __call__ method (FAST003)#23553
Conversation
ntBre
left a comment
There was a problem hiding this comment.
Thanks! I had a couple of small questions/suggestions and then realized I had a follow up question on the issue itself.
crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs
Outdated
Show resolved
Hide resolved
|
stakeswky
left a comment
There was a problem hiding this comment.
Thanks for the thorough review! I've reworked the approach based on your question on the issue.
After testing, the key insight is:
Depends(Query)— FastAPI calls the class constructor, so params come from__init__Depends(Query())— FastAPI calls the instance, so params come from__call__
The original fix was wrong to conflate both cases. The new approach:
- Splits
from_depends_callto handle bothNameandCallexpressions from_dependency_name→ class reference → uses__init__from_dependency_instance→ class instance → uses__call__- Shared
class_params_from_methodhelper for both paths
The matches! nit no longer applies since the find() with OR is gone. The None → Unknown change is kept — when a class has no matching method, returning Unknown suppresses false positives (we know there's a dependency, just can't analyze it).
|
I'm going to wait to confirm my understanding with the issue author before reviewing this again, but I think this aligns with my current understanding. |
When a class is used as a FastAPI dependency via `Depends(SomeClass)` and the class has a `__call__` method but no `__init__`, the rule previously returned `None` from `from_dependency_name`, causing the dependency to be skipped entirely. This led to a false positive FAST003 diagnostic because the path parameter was not found in the (empty) dependency parameter list. Two changes: 1. Fall back to `__call__` when no `__init__` is found, so callable-class dependencies (as documented in FastAPI's advanced dependencies guide) are handled correctly. 2. Return `Some(Self::Unknown)` instead of `None` when neither `__init__` nor `__call__` is present, so we conservatively suppress the diagnostic rather than emitting a false positive. Fixes astral-sh#23526
Rework the dependency resolution to correctly distinguish between: - Depends(SomeClass) — class reference, FastAPI calls __init__ - Depends(SomeClass()) — instance, FastAPI calls __call__ Previously the code conflated both cases by checking __init__ OR __call__ in a single find(), which was both incorrect and misleading. Now: - from_dependency_name: handles Name refs, uses __init__ for classes - from_dependency_instance: handles Call exprs, uses __call__ for classes - class_params_from_method: shared helper for both paths Also applies the matches!() nit from review and restores the original 'Skip self parameter' comment.
Per ntBre's feedback, the else branch should remain `return None` as in the original code.
c90a5c7 to
ece6354
Compare
|
Addressed the review feedback:
Re the approach: the issue discussion confirmed that |
|
Thanks for checking! Confirmed — the PR already handles the two cases separately:
So |
|
Thanks for confirming on the issue! That aligns with the current implementation — the PR only checks |
ntBre
left a comment
There was a problem hiding this comment.
Thanks! There was a .snap.new file mistakenly added, so I pushed a commit removing that and also threw in a couple of non-Annotated test cases while I was here.
__call__ methodfastapi] Handle callable class dependencies with __call__ method (FAST003)
* main: [ty] Take myself out of the reviewer pool for the next few days (#23618) [ty] Fix bug where ty would think that a `Callable` with a variadic positional parameter could be a subtype of a `Callable` with a positional-or-keyword parameter (#23610) [`ruff`] Add fix for `none-not-at-end-of-union` (`RUF036`) (#22829) Bump cargo dist to 0.31 (#23614) [`pyflakes`] Fix false positive for names shadowing re-exports (`F811`) (#23356) [`fastapi`] Handle callable class dependencies with `__call__` method (`FAST003`) (#23553) [ty] Recurse into tuples and nested tuples when applying special-cased validation of `isinstance()` and `issubclass()` (#23607) Update typing conformance suite commit (#23606) [ty] Detect invalid uses of `@final` on non-methods (#23604) [ty] Move the type hierarchy request handlers to individual modules [ty] Wire up the type hierarchy implementation with the LSP [ty] Add routine for mapping from system path to vendored path [ty] Implement internal routines for providing the LSP "type hierarchy" feature [ty] Add some helper methods on `ClassLiteral` [ty] Move some module name helper routines to methods on `ModuleName` [ty] Bump version of `lsp-types` [ty] Refactor to support building constraint sets differently (#23600) [ty] Dataclass transform: neither frozen nor non-frozen (#23366) [ty] Add snapshot tests for advanced `invalid-assignment` scenarios (#23581) [ty] disallow negative narrowing on SubclassOf types (#23598)
Fixes #23526
Problem
When a class with a
__call__method (but no__init__) is used as a FastAPI dependency, FAST003 emits a false positive:Root Cause
In
from_dependency_name, theClassDefinitionbranch checked for Pydantic base models and__init__, but returnedNone(notSome(Self::Unknown)) when neither was found. This caused the dependency to be silently skipped in the caller, leaving the path parameter unmatched.Fix
Two changes:
Fall back to
__call__when no__init__is found. This correctly handles the callable instance pattern from FastAPI's docs, where an instance with__call__is passed toDepends.Return
Some(Self::Unknown)instead ofNonewhen neither__init__nor__call__exists, so we conservatively suppress the diagnostic rather than emitting a false positive.Tests
Added four new test cases:
__call__(self, thing_id)→ no diagnostic ✓__init__(self, thing_id)and__call__→ no diagnostic (uses__init__) ✓__call__→ FAST003 emitted ✓__init__, no__call__) → no diagnostic (Unknown) ✓