Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Are there plans to make class-based-views a first-class feature? #270

Closed
somada141 opened this issue May 29, 2019 · 38 comments
Closed

Are there plans to make class-based-views a first-class feature? #270

somada141 opened this issue May 29, 2019 · 38 comments
Labels
question Question or problem question-migrate

Comments

@somada141
Copy link

Description

Hi @tiangolo! First off kudos on FastAPI, beautiful stuff. My team (about a dozen backenders, pinging one here @gvbgduh) are in the midst of porting out Py2 services to Py3 and thought it'd be a good time to move away from Flask and Flask-RESTful and into something that makes better use of the Py3 features (predominantly async). Thus far FastAPI/Starlette is the top contender but the one feature we're missing the most is class-based-views (CBVs) as opposed to the defacto function-based-views (FBVs). Thus we were wondering if there's any plans to introduce CBVs as a first-class feature in FastAPI.

While we know Starlette plays well with CBVs we looove the automagical features FastAPI offers like validation, OpenAPI generation, etc, things we had to do in very round-about ways prior.

Way we see it CBVs have the following primary perks:

  • Centralised routing since you tend to declare the different routes in one place, typically after instantiating the application.
  • Code reusability since you can easily do OOP and inherit (eg through mixins) or compose common functionality around.
  • Would make it much easier to port existing CBVs to FastAPI since we'd be coming from Flask-RESTful.

Thus far we've found we can 'hack' CBVs into FastAPI as such:

from typing import Dict

import fastapi
from starlette.endpoints import HTTPEndpoint

from app.services.api.models import Something


app = fastapi.FastAPI(debug=True)


class ResourceRoot(HTTPEndpoint):
    def get(self):
        # do stuff


class ResourceSomething(HTTPEndpoint):

    def get(self, some_id: str) -> Dict:
        # do stuff

    def post(self, something: Something) -> None:
        # do stuff


resource_root = ResourceRoot(scope={"type": "http"})
resource_something = ResourceSomething(scope={"type": "http"})
app.add_api_route(
    path="/",
    endpoint=resource_root.get,
    methods=["GET"],
)
app.add_api_route(
    path="/something",
    endpoint=resource_something.post,
    methods=["POST"]
)
app.add_api_route(
    path="/something/{some_id}",
    endpoint=resource_something.get,
    methods=["GET"]
)

and as far as we can tell we're maintaining the FastAPI fanciness but it might be a little confusing for our devs. Is there a better way to do this?

@somada141 somada141 added the question Question or problem label May 29, 2019
@bs32g1038
Copy link

I also want this feature. Great, support!

@knyghty
Copy link

knyghty commented May 29, 2019

FWIW, I'm looking for this feature because I have a HTTP header I need to pass through to external APIs - right now I'm passing it through a bunch of different functions, would be nice to have a class so I can just do self.my_header = whatever and just pass in self.my_header where I need to.

@somada141
Copy link
Author

@knyghty wouldn't a middleware make more sense there?

@knyghty
Copy link

knyghty commented May 29, 2019

@somada141 it was my first thought, but I don't only need to replicate the header on the response, but also to other requests to external services I'm making.

@tiangolo
Copy link
Owner

Hi @tiangolo! First off kudos on FastAPI, beautiful stuff.

I'm glad you're liking it!

[...] CBVs [...]

Thanks for the request and the explanation @somada141 .

Help me understand something, in your example, the same ResourceSomething has two different URL paths, /something and /something/{some_id}.

From the examples I can see in Flask-restful and the ones for HTTPEndpoint in Starlette, I see that class-based views would only be able to handle one path operation at a time, right?

So, it would have to be a class for /something, I guess with get to read all the resources of that type and post to create new ones.

...and then another class for /something/{some_id}, with put to update an item and get to read a specific item.

Is that correct? How would you expect that to work?


Maybe to put it another way, how would you imagine the code you write using FastAPI with class-based views? How would it look like?

@somada141
Copy link
Author

@tiangolo you're right per the Flask-RESTful paradigm you'd have one path per resource-class though I've seen cases where different HTTP methods and paths were assigned to the same class. Nonetheless I wouldn't think that's necessary for a clean CBV approach.

A simplified approach would be:
1 endpoint - 1 path - 1 resource class - N methods (eg GETting, POSTing, PUTting an item etc)

@gvbgduh
Copy link

gvbgduh commented Jul 12, 2019

Hi @tiangolo, thanks for the great effort!

It's really great how it's hooked with pydantic!

But, yeah, CBV would be really great. It especially pays back while the project is growing. There're many use cases for them and advantages.

Say, you can have one endpoint/view/resource for the entity, like

class Employee:
    async def get(...)  # List
    async def post(...)
    async def put(...)
    async def delete(...)
    ...

That'll allow to cover all actions in the one endpoint for one route, say /employees (it doesn't prevent to define several routes to this endpoint) and the differences in payloads can be handled separate schemas, like it would be common to pass filter, limit, offset to the GET one, full dict for POST and partial one for PATCH.
The great adventure here is that response schema will be same and the context of the fields can be fully defined in the model of this entity.

Between entities there might a lot of logic shared (for pagination, filtering, creating, updating, deleting, etc.) and the difference might be only in the model reference (not always though), so that staff can be moved out as example

class EntitiesCollection:
    model = None
    async def(...):
        # query model with the given params
    async post(...):
    ...

So, the target model can become:

class Employee(EntitiesCollection):
    model = EmployeeORMModel

Sometimes, it's not aligned that well or some additional functionality needed that's logically separated from the model itself, so it's handy move to mixins, like
class PermissionRequiredMixin: or class LoginRequiredMixin: or JWTAuthMixin, so the parent class can become

class EntitiesCollection(JWTAuthMixin, PermissionRequiredMixin)

and all child classes will comply.

Or there can be a shared business logic, I'm making it up, but say there're also departments and as well as employees that should comply to some business rules, that can be implemented in the parent class or in the mixin and reused across related endpoints.

It's not impossible with function based views, but it's becoming quite verbose with the project growth.

It covers not all cases though, in terms of API flask-RESTful and Django REST Framework are quite bright examples, but they have a lot of stuff in it and it might be quite reckless to repeat all of that but generic support for HTTPEndpoint would be great, so dev can decide what they can implement and if it's required some structures of that can be implemented as add-ons to the fastapi.

starlette has starlette.endpoints.HTTPEndpoint and from what I got the support of it is dropped in fastapi.routing.APIRoute
with explicit restriction to the function or the method - https://github.com/tiangolo/fastapi/blob/master/fastapi/routing.py#L285
so it's processed accordingly bellow with request_response in https://github.com/tiangolo/fastapi/blob/master/fastapi/routing.py#L296

while startlette.routing.Route allows to handle HTTPEndpoint as well

        if inspect.isfunction(endpoint) or inspect.ismethod(endpoint):
            # Endpoint is function or method. Treat it as `func(request) -> response`.
            self.app = request_response(endpoint)
            if methods is None:
                methods = ["GET"]
        else:
            # Endpoint is a class. Treat it as ASGI.
            self.app = endpoint

https://github.com/encode/starlette/blob/master/starlette/routing.py#L152:L159

So, I wonder if could be possible to introduce it as well keeping the functionality fastapi already provides. That would be fantastic.

If you need some assistance I might be able to help to come up with a PR.

@tiangolo
Copy link
Owner

Thanks for the feedback and arguments! I'll check it thoroughly soon.

Right now I'm thinking about if the decorator should be on the class or on each method, as the decorator has the path and also the response model, etc

@gbgduh
Copy link

gbgduh commented Jul 15, 2019

yeah, I think I see the problem here, it's an interesting dilemma.
Logically, it would be nice to have it on the class as in general HTTPEndpoint.dispatch should take care of further flow, so like

@app.route("/some_url")
class SomeEndpoint(HTTPEndpoint):
    async def get(self, request):
        # some logic
    async def post(self, request):
        # some logic
    ...

and it would also allow to do it as

api_v1 = Router([
    Route('/some_url', endpoint=SomeEndpoint, methods=['GET', 'POST', 'PATCH', 'DELETE]),
    ...
])

# And if there're some apps
app = FastAPI()
app.mount('/api_v1', app=api_v1)

But indeed complication comes from the response schemas.

As inherited from starlette.routing.Route the call is made in APIRoute.__call__.
In general request_response does the same as HTTPEndpoint.dispatch.

Both request_response and HTTPEndpoint.__init__ comply with args/interface (scope: Scope, receive: Receive, send: Send).
In fastapi request_response is given enhanced get_app where all magic happens.

But the problem arises as get_app takes args in APIRoute.__init__ as part of the whole app instantiation, but the HTTPEnpoint is actually instantiated in the Route.__call__, where request_response has already prepared get_app func. It works well as request_response takes relevant scope every request, but the HTTPEnpoint class has to instantiated at the APIRoute.__call__ time.

But it's the time where APIRoute.__call__ has signature
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
and uses request_response or HTTPEnpoint.__init__ with those params.
https://github.com/encode/starlette/blob/master/starlette/routing.py#L199:L207

That's where I hit the wall.

@gbgduh
Copy link

gbgduh commented Jul 15, 2019

Hm, after all that long thought I think I might have an idea.

So at the APIRoute.__init__ step we need an Endpoint class (not an instance) that respects

    dependant: Dependant,
    body_field: Field = None,
    status_code: int = 200,
    response_class: Type[Response] = JSONResponse,
    response_field: Field = None,
    response_model_include: Set[str] = None,
    response_model_exclude: Set[str] = set(),
    response_model_by_alias: bool = True,
    response_model_skip_defaults: bool = False,
    dependency_overrides_provider: Any = None,

that can be later instantiated in the APIRoute.__call__ and use them in the HTTPEndpoint.dispatch (in the same way get_app does), ideally without overriding the inherited __call__ method. And having a fresh instance every request.

And that sounds to me like the metaclass recipe.
Now I wonder how far it can fly.

@somada141
Copy link
Author

I was mulling over this one myself yday. A method decorator would be convenient in terms of providing the same arguments for summary, description, responses, model, etc but would not cover the instantiation of the resource-class (e.g. in the way Flask-Restful does it, https://flask-restful.readthedocs.io/en/0.3.5/intermediate-usage.html#passing-constructor-parameters-into-resources).

At that point a method-decorator would effectively boil down to a solution similar to what I showed originally where instantiation happens outside of FastAPI having methods 'passed' to FastAPI individually. That would maintain the same functionality and preclude interface compliance tricks but might be confusing for developers. Arguably, a method decorator could 'imply' a FastAPI-managed instantiation of the encompassing class but it would be hard to guarantee a 1-N class-methods instantiations without keeping track of 'related' decorators.

Alternatively I suppose is would be possible to decorate both as in decorate the class designating it as a resource-class and have a decorator that is a subset of add_api_route defining parameters like the response model, etc on the method itself. I've never written decorator 'pairs' like this so I'm not sure what the repercussions would be though.

@dmontagu
Copy link
Collaborator

dmontagu commented Aug 4, 2019

I was playing around with this, and it looks you can implement class-based views today by annotating self as a Depends -- no need for any new features!

Well, to be clear, you still have to decorate the individual methods, but this enables you to:

  1. reuse dependency declarations
  2. reuse initialization logic
  3. get reusable cleanup logic without resorting to middleware (similar to the goals of a context-manager dependency, I believe)

And as far as I can tell, this causes no issues even with strict mypy.

Here is a self-contained example showing it basically works with everything:

from fastapi import FastAPI, Depends
from starlette.testclient import TestClient


async def async_dep():
    return 1


def sync_dep():
    return 2


app = FastAPI()


class DepClass:
    def __init__(self, int1: int = Depends(async_dep), int2: int = Depends(sync_dep)) -> None:
        print("Initializing DepClass instance")
        self.int1 = int1
        self.int2 = int2


class MyRoutes(DepClass):
    @app.get("/method_test_sync")
    def sync_route(self: DepClass = Depends()):
        print("sync")
        return self.int1, self.int2

    @app.get("/method_test_async")
    async def async_route(self: DepClass = Depends()):
        print("async")
        return self.int1, self.int2


client = TestClient(app)
print(client.get("/method_test_sync").content)
"""
Initializing DepClass instance
sync
b'[1,2]'
"""

print(client.get("/method_test_async").content)
"""
Initializing DepClass instance
async
b'[1,2]'
"""

(It also works if you use self = Depends(DepClass) instead).

Here is an example showing it works with inheritance, and that the __del__ method works as a way to trigger cleanup:

class DepSubclass(DepClass):
    def __init__(self, int1: int = Depends(async_dep), int2: int = Depends(sync_dep)) -> None:
        print("Initializing DepSubClass instance")
        super().__init__(int1, 2 * int2)

    def __del__(self):
        print("Cleaning up DepSubclass instance")



class MoreRoutes(DepSubclass):
    @app.get("/subclass_test")
    def route(self: DepSubclass = Depends()):
        return self.int1, self.int2


print(client.get("/subclass_test").content)
"""
Initializing DepSubClass instance
Initializing DepClass instance
Cleaning up DepSubclass instance
b'[1,4]'
"""

Note that __init__ and __del__ both have to be synchronous, but it is still possible to leverage async here:

  • FastAPI handles async dependencies in the usual way, letting you grab the results of any async calls you need to make as inputs to the __init__.
  • You can use __del__ for asynchronous cleanup via asyncio.create_task.
    • (Well, I haven't tried this for anything special but I think it will work. Maybe __del__ has some gotchas that prevent it from covering all clean-up cases, but I think it should cover most if not all.)

The lone awkwardness I see here is that you have to annotate self as a superclass (which is admittedly nonstandard since at evaluation time self will actually only be an instance of the superclass). Otherwise, the annotation won't be a callable when the route decorator is applied, and decoration will fail. If I were to request any modifications to FastAPI to support CBVs, I think it would be a way to have the decorator (lazily?) infer the type of self without needing explicit annotation. But I think even that is more of a nice-to-have than a need-to-have for me to use this capability today.

(Also, this wouldn't be able to replace the use of decorators with pure method names, but you can always use functools.partial or similar to reduce repetition.)

@somada141 @gbgduh @knyghty @bs32g1038 @tiangolo let me know if you see any obvious gaps in this approach, or hate the idea of using Depends to set a default value for self.

@somada141
Copy link
Author

@dmontagu thanks for the approach, I wouldn't have considered this type of thing! Apart from the jarring self annotation my primary beef with this would be that it encourages decentralised routing since the decorators would be all over the place but then again this may be my own pet peeve.

Apart from that I don't see any obvious gaps but it would be preferable if that kinda thing could be 'hidden away' in the FastAPI code (and exposed as some class you inherit in your CBVs) cause I think it would be confusing for peeps.

Curious to see what the others think.

@dmontagu
Copy link
Collaborator

dmontagu commented Aug 5, 2019

@somada141 I played with it some more and got the interface a lot cleaner:

app = FastAPI()
lazy_app: FastAPI = LazyDecoratorGenerator(app)

def get_1():
    return 1

def get_2():
    return 2

def get_3():
    return 3

class RouteBase(BaseDependencies):
    x: int = Depends(get_1)

class MyRoutes(RouteBase):
    y: int = Depends(get_2)

    @lazy_app.get("/sync_method_test")
    def sync_route(self):
        return self.x

    @lazy_app.get("/async_method_test")
    async def async_route(self, z: int = Depends(get_3)):
        return self.y, z

print(TestClient(app).get("/sync_method_test").content)
# b'1'
print(TestClient(app).get("/async_method_test").content)
# b'[2,3]'

The setup is ugly and could use some improvement, but it does work. (I included the setup code below if you want to try it out, just paste it on top of the code above.)

I had to use LazyDecoratorGenerator to delay the evaluation of the decorators, but at least now there is minimal syntactic cruft. With a "proper" implementation, maybe that need could be removed as well.

Setup
import inspect
import sys
from copy import deepcopy
from inspect import Parameter, signature
from typing import TYPE_CHECKING, Any, Dict, List, Tuple

from fastapi import Depends, FastAPI
from fastapi.params import Depends as DependsClass
from pydantic.utils import resolve_annotations
from starlette.testclient import TestClient

LazyDecoratorCall = Any
LazyMethodCall = Tuple[str, Tuple[Any, ...], Dict[str, Any]]


class _LazyDecoratorGenerator:
    def __init__(self, eager_instance: Any):
        self.eager_instance = eager_instance
        self.self_calls: List[LazyDecoratorCall] = []
        self.method_calls: List[Tuple[LazyMethodCall, _LazyDecoratorGenerator]] = []

    def __call__(self, func):
        self.self_calls.append(func)
        return func

    def __getattr__(self, name: str):
        def f(*args, **kwargs) -> _LazyDecoratorGenerator:
            subcaller = _LazyDecoratorGenerator(None)
            self.method_calls.append(((name, args, kwargs), subcaller))
            return subcaller

        return f

    def execute(self):
        for value in self.self_calls:
            self.eager_instance(value)

        for (name, args, kwargs), subcaller in self.method_calls:
            eager_subcaller = getattr(self.eager_instance, name)(*args, **kwargs)
            subcaller.eager_instance = eager_subcaller
            subcaller.execute()
        self.self_calls = []
        self.method_calls = []


def LazyDecoratorGenerator(eager_instance: Any) -> Any:
    return _LazyDecoratorGenerator(eager_instance)


class DependenciesMeta(type):
    def __new__(mcs, name, bases, namespace):
        local_funcs = {}
        dependencies = {}
        for base in reversed(bases):
            if issubclass(base, BaseDependencies) and base != BaseDependencies:
                dependencies.update(deepcopy(base.__dependencies__))

        annotations = namespace.get("__annotations__", {})
        if sys.version_info >= (3, 7):
            annotations = resolve_annotations(annotations, namespace.get("__module__", None))

        for k, v in namespace.items():
            if inspect.isfunction(v):
                local_funcs[k] = v
            elif isinstance(v, DependsClass):
                dependencies[k] = (annotations.get(k), v)

        namespace["__dependencies__"] = dependencies
        namespace["__local_funcs__"] = local_funcs
        return super().__new__(mcs, name, bases, namespace)


class BaseDependencies(metaclass=DependenciesMeta):
    if TYPE_CHECKING:
        __dependencies__: Dict[str, Tuple[Any, Any]]
        __local_funcs__: Dict[str, Any]

    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

    def __init_subclass__(cls, **kwargs):
        for name, member in cls.__local_funcs__.items():
            sig = signature(member)
            old_params: List[Parameter] = list(sig.parameters.values())
            new_first_param = old_params[0].replace(default=Depends(cls))
            new_remaining_params = [old_param.replace(kind=Parameter.KEYWORD_ONLY) for old_param in old_params[1:]]
            new_params = [new_first_param] + new_remaining_params
            member.__signature__ = sig.replace(parameters=new_params)

        sig = signature(cls.__init__)
        annotations = cls.__annotations__
        old_parameters = list(sig.parameters.values())
        new_parameters = [old_parameters[0]]
        for name, (annotation, value) in cls.__dependencies__.items():
            parameter = Parameter(name=name, kind=Parameter.KEYWORD_ONLY, default=value, annotation=annotation)
            new_parameters.append(parameter)
        sig = sig.replace(parameters=new_parameters)
        cls.__init__.__signature__ = sig

        for k, v in globals().items():
            if isinstance(v, _LazyDecoratorGenerator):
                v.execute()

@dmontagu
Copy link
Collaborator

dmontagu commented Sep 29, 2019

I've gotten my implementation of this feature much simpler and more robust (though it depends on the nuances of some behavior of FastAPI that I could imagine changing in the future); see this gist if you are interested.

@jimmy-lt
Copy link

jimmy-lt commented Oct 9, 2019

Hi,

I don't know if you've heard of Cornice already but it deals with the two paths issue by defining two attributes for a given resource: collection_path and path.

from cornice.resource import resource

_USERS = {1: {'name': 'gawel'}, 2: {'name': 'tarek'}}

@resource(collection_path='/users', path='/users/{id}')
class User(object):

    def __init__(self, request, context=None):
        self.request = request

    def __acl__(self):
        return [(Allow, Everyone, 'everything')]

    def collection_get(self):
        return {'users': _USERS.keys()}

    def get(self):
        return _USERS.get(int(self.request.matchdict['id']))

    def collection_post(self):
        print(self.request.json_body)
        _USERS[len(_USERS) + 1] = self.request.json_body
        return True

@dmontagu
Copy link
Collaborator

dmontagu commented Oct 9, 2019

The biggest problem I see with this approach is that you typically at least want to provide a response model when using fastapi, so you still need decorators on all endpoints. Well, it’s not a problem per se, but I don’t think there’s much benefit to this design until the decorators can be removed from most endpoints.

I’ve written a route class that uses the annotated return type as the default choice for response model; integrating that as an option might help. I don’t know what other decorator options are frequently used by other people though; probably not worth creating a new set of decorators just for this purpose.

@dmontagu
Copy link
Collaborator

For anyone following this thread, I recently released fastapi-utils which provides a version of a class-based view.

It doesn't support automatically inferring decorators if you name your method get, post, etc., but given the amount of info that currently goes into the decorators, I'm not sure how useful that would be anyway.

What it does do is allow you to reuse dependencies across multiple routes without repeating them in each signature by just declaring them as class attributes.

If you are interested, you can read the documentation here: https://fastapi-utils.davidmontague.xyz/user-guide/class-based-views/

@tiangolo
Copy link
Owner

Given all the arguments above, I think class-based views do provide extra value and it would make sense to have them in FastAPI itself, but I haven't had the time to investigate much about it.

Right now @dmontagu's CBV implementation in fastapi-utils is the best option (it's quite clever). 🚀

Maybe at some point in the future, we'll have CBV in FastAPI itself, but if you need them now, fastapi-utils should be the best option. And maybe later we can integrate its CBVs (or ideas from it) into FastAPI itself 😬 .

@somada141
Copy link
Author

The work done by @dmontagu seems fantastic and exactly what I was hoping for. I think this will more than enough for our work so I'll close this issue for now. Thanks heaps @dmontagu and @tiangolo!

@kolypto
Copy link

kolypto commented Sep 3, 2020

A relatively simple snippet packed with magic: it lets you use class methods as dependencies or API endpoints.

""" A simple tool to implement class-based views for FastAPI.

It is a decorator that takes a method, and patches it to have the `self` argument
to be declared as a dependency on the class constructor:

* Arguments of the method become dependencies
* Arguments of the __init__() method become dependencies as well
* You can use the method as a dependency, or as an API endpoint.

Example:

    class View:
        def __init__(self, arg: str):  # Arguments are dependencies (?arg)
            self.arg = arg

        @fastapi_compatible_method
        async def get(self, another_arg: str):  # arguments are dependencies (?another_arg)
            return {
                'arg': self.arg,
                'another_arg': another_arg,
            }

    app.get('/test')(View.get)
    # Check: http://localhost:8000/test?arg=value&another_arg=value

It is a little awkward: you can't decorate the method with @app.get() directly: it's only possible
to do it after the class has fully created.

Thanks to: @dmontagu for the inspiration
"""
import inspect
from typing import Callable, TypeVar, Any

from app.src.deps import Depends

ClassmethodT = TypeVar('ClassmethodT', bound=classmethod)
MethodT = TypeVar('MethodT', bound=Callable)


class fastapi_compatible_method:
    """ Decorate a method to make it compatible with FastAPI """

    # It is a descriptor: it wraps a method, and as soon as the method gets associated with a class,
    # it patches the `self` argument with the class dependency and dissolves without leaving a trace.

    def __init__(self, method: Callable):
        self.method = method

    def __set_name__(self, cls: type, method_name: str):
        # Patch the function to become compatible with FastAPI.
        # We only have to declare `self` as a dependency on the class itself: `self = Depends(cls)`.
        patched_method = set_parameter_default(self.method, 'self', Depends(cls))
        # Put the method onto the class. This makes our descriptor go completely away
        return setattr(cls, method_name, patched_method)


def set_parameter_default(func: Callable, param: str, default: Any) -> Callable:
    """ Set a default value for one function parameter; make all other defaults equal to `...`

    This function is normally used to set a default value for `self` or `cls`:
    weird magic that makes FastAPI treat the argument as a dependency.
    All other arguments become keyword-only, because otherwise, Python won't let this function exist.

    Example:
        set_parameter_default(Cls.method, 'self', Depends(Cls))
    """
    # Get the signature
    sig = inspect.signature(func)
    assert param in sig.parameters  # make sure the parameter even exists

    # Make a new parameter list
    new_parameters = []
    for name, parameter in sig.parameters.items():
        # The `self` parameter
        if name == param:
            # Give it the default value
            parameter = parameter.replace(default=default)
        # Positional parameters
        elif parameter.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
            # Make them keyword-only
            # We have to do it because func(one = default, b, c, d) does not make sense in Python
            parameter = parameter.replace(kind=inspect.Parameter.KEYWORD_ONLY)
        # Other arguments, e.g. variadic: leave them as they are
        new_parameters.append(parameter)

    # Replace the signature
    setattr(func, '__signature__', sig.replace(parameters=new_parameters))
    return func

The awkward part is that you cannot decorate a method with @app.get() directly; you'll have to do it after the class is fully constructed.

@loweryjk
Copy link

I'm new to Python and FastAPI but I think I came up with a solution that only requires the addition of a small base controller class.

from fastapi import APIRouter, Request
from starlette.responses import PlainTextResponse

# ---- base controller class ----

class Controller:

    def __init__(self, router: APIRouter) -> None:
        self._router = router

    @property
    def router(self):
        # replace each route endpoint with a method bound to this object
        for route in self._router.routes:
            func = route.endpoint
            if hasattr(func, '__get__'):
                route.endpoint = func.__get__(self, self.__class__)
        return self._router

# ---- derived controller class ----

router: APIRouter = APIRouter()

class MyController(Controller):

    def __init__(self) -> None:
        super().__init__(router)

    @router.get('/api/health-check')
    def health_check(self, request: Request):
        return PlainTextResponse('alive')

# ---- application boot ----

app = FastAPI()
my_controller = MyController()
app.inlude_router(my_controller.router)

@KiraPC
Copy link

KiraPC commented Feb 6, 2021

I also wrote a library with my version of how to implement this.

You can find it here: https://pypi.org/project/fastapi-router-controller/0.1.0/

@delijati
Copy link

delijati commented Mar 29, 2021

Given all the arguments above, I think class-based views do provide extra value and it would make sense to have them in FastAPI itself, but I haven't had the time to investigate much about it.

Right now @dmontagu's CBV implementation in fastapi-utils is the best option (it's quite clever). rocket

Maybe at some point in the future, we'll have CBV in FastAPI itself, but if you need them now, fastapi-utils should be the best option. And maybe later we can integrate its CBVs (or ideas from it) into FastAPI itself grimacing .

@tiangolo

Fast forward 1 year or for me 2 years ;) (#144) CVB are still not there. @dmontagu did some nice work, but the package contains things that are not really needed for everyone and there is no maintenance of it. Is there any chance Fastapi will get CBV's?

I know I could role my own version, but for compatibility I would prefer to have it in Fastapi integrated.

@KiraPC
Copy link

KiraPC commented Mar 29, 2021

Given all the arguments above, I think class-based views do provide extra value and it would make sense to have them in FastAPI itself, but I haven't had the time to investigate much about it.
Right now @dmontagu's CBV implementation in fastapi-utils is the best option (it's quite clever). rocket
Maybe at some point in the future, we'll have CBV in FastAPI itself, but if you need them now, fastapi-utils should be the best option. And maybe later we can integrate its CBVs (or ideas from it) into FastAPI itself grimacing .

@tiangolo

Fast forward 1 year or for me 2 years ;) (#144) CVB are still not there. @dmontagu did some nice work, but the package contains things that are not really needed for everyone and there is no maintenance of it. Is there any chance Fastapi will get CBV's?

I know I could role my own version, but for compatibility I would prefer to have it in Fastapi integrated.

@delijati You could try my lib. It does just that think.

@delijati
Copy link

@KiraPC thanks, but again I would rather see it integrated into Fastapi ... whatever implementation :) ->

I know I could role my own version, but for compatibility I would prefer to have it in Fastapi integrated.

@zon7
Copy link

zon7 commented Apr 9, 2021

I'm also interested in a native implementation in fast-api

@Kludex
Copy link
Sponsor Collaborator

Kludex commented Apr 9, 2021

I don't think we should have CBV on FastAPI itself.

That being said, the most FastAPIonic way that I've thought:

from typing import List

from fastapi import Depends, FastAPI
from fastapi import Resource # It can be `ClassBasedView` or `CBV`

from app.api.deps import current_user


app = FastAPI()

class Users(Resource):
    prefix: str = "/users"
    dependencies: List[Depends] = [Depends(current_user)]
    
    @Resource.get(response_model=List[str]) # Same parameters as `router.get()`, but path being optional
    async def get_users(self):
        return ["A", "B"]

app.include_resource(Users)  # Same as `include_router`

As we need to have the decorator on the method level, I see it as redundant to have it on the class as well. I also think working with blocks the same way as the routers is the best way to handle this idea.

@delijati
Copy link

delijati commented Apr 9, 2021

@Kludex FYI
@KiraPC and me are working on https://github.com/KiraPC/fastapi-router-controller so far it works pretty well :)

@ShipraShalini
Copy link

I would like to create a ModelView somewhat like the one DRF provides so that I can move all basic CRUD APIs to one abstract class and inherit it. So far I have not been able to figure out how to do this. I find myself writing CRUD APIs for different models again and again even though the logic is the same.

@delijati
Copy link

delijati commented Jun 1, 2021

@ShipraShalini do you mean that? https://github.com/KiraPC/fastapi-router-controller/blob/master/tests/test_inherit.py#L45

@archiif
Copy link

archiif commented Jun 5, 2021

@dmontagu
@KiraPC
@delijati
Can any of these libraries callback to a single, previously instantiated instance of a class, so that the same instance is called for each request?

@KiraPC
Copy link

KiraPC commented Jun 5, 2021

@archiif Hi,

my lib use the FastApi depends feature to add the class as route dependency, so I think it create a new instance per request.

@delijati
Copy link

delijati commented Jun 6, 2021

@archiif

@dmontagu
@KiraPC
@delijati
Can any of these libraries callback to a single, previously instantiated instance of a class, so that the same instance is called for each request?

Your question is unrelated to the libs. You can have globals aka singleton aka peviously instantiated instances:

import uvicorn

from fastapi import Depends, FastAPI


class Foo:
    def __init__(self, data):
        self.data = data

    def get_data(self):
        _id = id(self)
        return {"data": self.data, "id": _id}


app = FastAPI()
foo = Foo(42)


def get_foo():
    return foo


@app.get("/")
async def root(foo=Depends(get_foo)):
    return {"message": f"Hello World: {foo.get_data()}"}


if __name__ == "__main__":
    uvicorn.run(app)

As you see the id is always the same:

for (( c=1; c<=5; c++ )); do echo `curl -q http://127.0.0.1:8000`; done;
 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                Dload  Upload   Total   Spent    Left  Speed
100    62  100    62    0     0  62000      0 --:--:-- --:--:-- --:--:-- 62000
{"message":"Hello World: {'data': 42, 'id': 140413337184096}"}
 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                Dload  Upload   Total   Spent    Left  Speed
100    62  100    62    0     0  31000      0 --:--:-- --:--:-- --:--:-- 31000
{"message":"Hello World: {'data': 42, 'id': 140413337184096}"}
 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                Dload  Upload   Total   Spent    Left  Speed
100    62  100    62    0     0  31000      0 --:--:-- --:--:-- --:--:-- 31000
{"message":"Hello World: {'data': 42, 'id': 140413337184096}"}
 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                Dload  Upload   Total   Spent    Left  Speed
100    62  100    62    0     0  62000      0 --:--:-- --:--:-- --:--:-- 62000
{"message":"Hello World: {'data': 42, 'id': 140413337184096}"}
 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                Dload  Upload   Total   Spent    Left  Speed
100    62  100    62    0     0  62000      0 --:--:-- --:--:-- --:--:-- 62000
{"message":"Hello World: {'data': 42, 'id': 140413337184096}"}

@archiif
Copy link

archiif commented Jun 7, 2021

@delijati
I see, this does work, but what I was hoping is a workflow where I somehow can use the decorator right inside of the class so that I don't have to write two functions: one for the class and one for the decorator.
For example if I want to use Class.foo() and Class.bar() as api routes I have to write another function for each one of them so that I can decorate them. For now I'm using the solution in the opening post which lets me avoid writing redundant functions, though it lacks the elegance of decorators.

@oliverdain
Copy link

oliverdain commented Aug 30, 2021

I have a solution that lets you do class-based routing on instances rather than having the class itself have the methods. As a result you can use it with any injection framework you want and you can also instantiate the same class more than once with different dependencies. Here's a working example:

from fastapi import FastAPI
from fastapi.testclient import TestClient

from .decorators import get, post
from .routable import Routable


class ExampleRoutable(Routable):
    def __init__(self, injected: int) -> None:
        super().__init__()
        self.__injected = injected

    @get(path='/add/{x}')
    def add(self, x: int) -> int:
        return x + self.__injected

    @post(path='/sub/{x}')
    def sub(self, x: int) -> int:
        return x - self.__injected


def test_routes_respond() -> None:
    app = FastAPI()
    t = ExampleRoutable(2)
    app.include_router(t.router)

    client = TestClient(app)

    response = client.get('/add/22')
    assert response.status_code == 200
    assert response.text == '24'

    response = client.post('/sub/4')
    assert response.status_code == 200
    assert response.text == '2'

The basic idea is that the decorators simply add some information to the methods. Then Routable has a metaclass that collects all that information. Finally, Routable.__init__ constructs a APIRouter and adds the routes to it after binding each method to self.

If this is of interest I'd be happy to send out a PR to add this to FastAPI or publish it to PyPi.

@archiif I think this does what you were requesting.

Edit: I've now released this: https://pypi.org/project/classy-fastapi/. pip install classy-fastapi should do the trick.

@faresbakhit
Copy link

I made this simple decorator if anyone is interested

from typing import Type, Union

from fastapi import APIRouter, FastAPI

app = FastAPI()


def view(router: Union[FastAPI, APIRouter], path: str = "/"):
    def decorator(cls: Type[object]) -> None:
        obj = cls()
        for method in dir(obj):
            if method in ["get", "put", "post", ...]:
                router.add_api_route(
                    path,
                    getattr(obj, method),
                    methods=[method]
                )
    return decorator


@view(app, "/")
class Index:

    async def get(self):
        return "Hello, Get"

    async def post(self):
        return "Hello, Post"

It lacks any arguments you would commonly add to a decorator like @app.get(response_model=[...]) but can be easily fixed by adding additional logic in the decorator function. It can be also added to the FastAPI/APIRouter classes easily.

from typing import Type

from fastapi import FastAPI


class AddView(FastAPI):
    def view(self, path: str = "/"):
        def decorator(cls: Type[object]) -> None:
            obj = cls()
            for method in dir(obj):
                if method in ["get", "head", "post", ...]:
                    self.router.add_api_route(
                        path,
                        getattr(obj, method),
                        methods=[method],
                    )
        return decorator


app = AddView()


@app.view("/")
class Index:

    async def get(self):
        return "Hello, Get"

    async def post(self):
        return "Hello, Post"

@tarsil
Copy link
Contributor

tarsil commented Nov 19, 2021

@tiangolo thank you for your FastAPI and I've been using it for more than an year now but I cannot disagree with the fact that using Starlette from the same creator of Django Rest Framework and seeing tools like Flask with class based view, I still struggle to understand why FastAPI does not natively support it, at all, this pushes a lot of people off and let me just try to give you a few simple examples why.

  1. Class based views apply the OOP principle, even if some people don't like it, it's the most used paradigm.
  2. You don't need to repeat logic across the rest of the views. DRY principle is good and if we need to repeat our code over and over again, it becomes ugly, hard to maintain and maybe creates a lot of tech debt.
  3. Arguing that "what can you do in CBV that you can't in function based views" I would say to read point 1 and 2.
  4. FastAPI was pointed as a tool that could bring the best of django and the best of flask in one. Here I have mixed feelings because even Flask allows both CBS and function based views as well as django.

It would be amazing to have something CBS native for FastAPI as well. This would allow us to baiscally do the moves from monoliths and flask into full FastAPI and I'm 100% sure that I'm not the only one thinking like this :). Again, fantastic framework.

Starlette already supports HTTPEndpoint (https://www.starlette.io/endpoints/) so I think this could be something that could be added to FastAPI.

I can't convince my company to fully adopt it (neither the companies I worked before) because this simple feature is a killer if cannot be there.

@tiangolo tiangolo changed the title [QUESTION] Are there plans to make class-based-views a first-class feature? Are there plans to make class-based-views a first-class feature? Feb 24, 2023
@tiangolo tiangolo reopened this Feb 28, 2023
Repository owner locked and limited conversation to collaborators Feb 28, 2023
@tiangolo tiangolo converted this issue into discussion #8318 Feb 28, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Question or problem question-migrate
Projects
None yet
Development

No branches or pull requests