Saltar al contenido

Clases como Dependencias

Antes de profundizar en el sistema de Dependency Injection, mejoremos el ejemplo anterior.

Un dict del ejemplo anterior

En el ejemplo anterior, estábamos retornando un dict desde nuestra dependencia ("dependable"):

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons
🤓 Otras versiones y variantes

Consejo

Preferible usar la versión con Annotated si es posible.

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

Pero entonces obtenemos un dict en el parámetro commons de la path operation function.

Y sabemos que los editores no pueden proporcionar mucho soporte (como autocompletado) para los dicts, porque no pueden conocer sus claves y tipos de valores.

Podemos hacerlo mejor...

Qué conforma una dependencia

Hasta ahora has visto dependencias declaradas como funciones.

Pero esa no es la única forma de declarar dependencias (aunque probablemente sea la más común).

El factor clave es que una dependencia debe ser un "callable".

Un "callable" en Python es cualquier cosa que Python pueda "llamar" como una función.

Entonces, si tienes un objeto something (que podría no ser una función) y puedes "llamarlo" (ejecutarlo) así:

something()

o

something(some_argument, some_keyword_argument="foo")

entonces es un "callable".

Clases como dependencias

Puede que notes que para crear una instancia de una clase de Python, usas esa misma sintaxis.

Por ejemplo:

class Cat:
    def __init__(self, name: str):
        self.name = name


fluffy = Cat(name="Mr Fluffy")

En este caso, fluffy es una instancia de la clase Cat.

Y para crear fluffy, estás "llamando" a Cat.

Entonces, una clase de Python es también un callable.

Entonces, en FastAPI, podrías usar una clase de Python como dependencia.

Lo que FastAPI realmente verifica es que sea un "callable" (función, clase o cualquier otra cosa) y los parámetros definidos.

Si pasas un "callable" como dependencia en FastAPI, analizará los parámetros de ese "callable", y los procesará de la misma manera que los parámetros de una path operation function. Incluyendo sub-dependencias.

Eso también aplica a callables sin ningún parámetro. Igual que sería para path operation functions sin parámetros.

Entonces, podemos cambiar la dependencia "dependable" common_parameters de arriba por la clase CommonQueryParams:

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response
🤓 Otras versiones y variantes

Consejo

Preferible usar la versión con Annotated si es posible.

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

Presta atención al método __init__ usado para crear la instancia de la clase:

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response
🤓 Otras versiones y variantes

Consejo

Preferible usar la versión con Annotated si es posible.

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

...tiene los mismos parámetros que nuestro common_parameters anterior:

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons
🤓 Otras versiones y variantes

Consejo

Preferible usar la versión con Annotated si es posible.

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

Esos parámetros son los que FastAPI usará para "resolver" la dependencia.

En ambos casos, tendrá:

  • Un parámetro de query opcional q que es un str.
  • Un parámetro de query skip que es un int, con un valor por defecto de 0.
  • Un parámetro de query limit que es un int, con un valor por defecto de 100.

En ambos casos los datos serán convertidos, validados, documentados en el esquema OpenAPI, etc.

Usarlo

Ahora puedes declarar tu dependencia usando esta clase.

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response
🤓 Otras versiones y variantes

Consejo

Preferible usar la versión con Annotated si es posible.

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

FastAPI llama a la clase CommonQueryParams. Esto crea una "instancia" de esa clase y la instancia será pasada como el parámetro commons a tu función.

Anotación de tipo vs Depends

Nota cómo escribimos CommonQueryParams dos veces en el código anterior:

commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]

Consejo

Preferible usar la versión con Annotated si es posible.

commons: CommonQueryParams = Depends(CommonQueryParams)

El último CommonQueryParams, en:

... Depends(CommonQueryParams)

...es lo que FastAPI realmente usará para saber cuál es la dependencia.

Es de esta de la que FastAPI extraerá los parámetros declarados y es lo que FastAPI realmente llamará.


En este caso, el primer CommonQueryParams, en:

commons: Annotated[CommonQueryParams, ...

Consejo

Preferible usar la versión con Annotated si es posible.

commons: CommonQueryParams ...

...no tiene ningún significado especial para FastAPI. FastAPI no lo usará para conversión de datos, validación, etc. (ya que está usando Depends(CommonQueryParams) para eso).

En realidad podrías escribir simplemente:

commons: Annotated[Any, Depends(CommonQueryParams)]

Consejo

Preferible usar la versión con Annotated si es posible.

commons = Depends(CommonQueryParams)

...como en:

from typing import Annotated, Any

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response
🤓 Otras versiones y variantes

Consejo

Preferible usar la versión con Annotated si es posible.

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons=Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

Pero se recomienda declarar el tipo ya que de esa manera tu editor sabrá qué se pasará como el parámetro commons, y entonces podrá ayudarte con autocompletado de código, verificación de tipos, etc:

Atajo

Pero ves que tenemos algo de repetición de código aquí, escribiendo CommonQueryParams dos veces:

commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]

Consejo

Preferible usar la versión con Annotated si es posible.

commons: CommonQueryParams = Depends(CommonQueryParams)

FastAPI proporciona un atajo para estos casos, en donde la dependencia es específicamente una clase que FastAPI "llamará" para crear una instancia de la clase misma.

Para esos casos específicos, puedes hacer lo siguiente:

En lugar de escribir:

commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]

Consejo

Preferible usar la versión con Annotated si es posible.

commons: CommonQueryParams = Depends(CommonQueryParams)

...escribes:

commons: Annotated[CommonQueryParams, Depends()]

Consejo

Preferible usar la versión con Annotated si es posible.

commons: CommonQueryParams = Depends()

Declaras la dependencia como el tipo del parámetro, y usas Depends() sin ningún parámetro, en lugar de tener que escribir la clase completa otra vez dentro de Depends(CommonQueryParams).

El mismo ejemplo entonces se vería así:

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends()]):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response
🤓 Otras versiones y variantes

Consejo

Preferible usar la versión con Annotated si es posible.

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends()):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

...y FastAPI sabrá qué hacer.

Consejo

Si te parece más confuso que útil, ignóralo, no lo necesitas.

Es solo un atajo. Porque a FastAPI le importa ayudarte a minimizar la repetición de código.