astral-sh / ruff

An extremely fast Python linter and code formatter, written in Rust.
https://docs.astral.sh/ruff
MIT License
30.92k stars 1.02k forks source link

[FAST002] FastAPI dependency without `Annotated` unsafe fix error #12982

Closed mbrulatout closed 2 weeks ago

mbrulatout commented 3 weeks ago

Hello,

Using ruff 0.6.1, I keep getting an error on FAST002 autofix.

I tried to simplify as much as i could both the code and config

datacenter_state_router = APIRouter()

@datacenter_state_router.patch(
    "/{name}",
)
async def datacenter_state_patch(
    current_state: DatacenterState = PermissionDepends(Permission.WRITE, get_datacenter_state),
    session: Session = Depends(database_manager.get_session),
) -> DatacenterState:
    pass

pyproject.toml

[tool.ruff]
target-version = 'py311'
line-length = 120
exclude = [
    "criteo/ingress/api/lib/models/alembic/"
]

[tool.ruff.format]
preview = true

[tool.ruff.lint]
# See complete list : https://beta.ruff.rs/docs/rules
select = [
    "FAST",  # fastapi
]

fixable = [
    "FAST002",
]

Removing that PermissionDepends arg (a wrapper around an actual dependency) fixes it.


class Permission(Enum):
    READ = "read"
    WRITE = "write"

def permission_dependency(
    permission: Permission, resource: Callable[..., Any], user: User = Depends(get_current_user)
) -> Any:
    dependable_resource = Depends(resource)

    def resource_call(_resource: OwnedResource = dependable_resource, _user: User = user) -> Any:
        return _resource

    return Depends(resource_call)

# An alias to mimic FastAPI Dependency (https://fastapi.tiangolo.com/reference/dependencies/#fastapi.Depends)
# This is usable with the additional permission, the contract being the following:
# PermissionDepends(Permission, Callable[..., OwnedResource])
PermissionDepends = permission_dependency
MichaReiser commented 3 weeks ago

For reproduction, it's important that the code has the relevant imports:

from fastapi import Depends, FastAPI, APIRouter

datacenter_state_router = APIRouter()

@datacenter_state_router.patch(
    "/{name}",
)
async def datacenter_state_patch(
    current_state: DatacenterState = PermissionDepends(
        Permission.WRITE, get_datacenter_state
    ),
    session: Session = Depends(database_manager.get_session),
) -> DatacenterState:
    pass

Playground

MichaReiser commented 3 weeks ago

The fix transforms the example into

from fastapi import Depends, FastAPI, APIRouter
from typing import Annotated

datacenter_state_router = APIRouter()

@datacenter_state_router.patch()
async def datacenter_state_patch(
    current_state: DatacenterState = PermissionDepends(
        Permission.WRITE, get_datacenter_state
    ),
    session: Annotated[Session, Depends(database_manager.get_session)],
) -> DatacenterState:
    pass

and the problem is that session now no longer has a default value but comes after a parameter with a default value.

The easiest fix is to disable the fix if the parameter comes after a parameter with a default value.