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
Comments
I also want this feature. Great, support! |
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 |
@knyghty wouldn't a middleware make more sense there? |
@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. |
I'm glad you're liking it!
Thanks for the request and the explanation @somada141 . Help me understand something, in your example, the same 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 ...and then another class for 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? |
@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: |
Hi @tiangolo, thanks for the great effort! It's really great how it's hooked with 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
That'll allow to cover all actions in the one endpoint for one route, say 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
So, the target model can become:
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
and all child classes will comply. Or there can be a shared business logic, I'm making it up, but say there're also 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
while
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 If you need some assistance I might be able to help to come up with a PR. |
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 |
yeah, I think I see the problem here, it's an interesting dilemma.
and it would also allow to do it as
But indeed complication comes from the response schemas. As inherited from Both But the problem arises as But it's the time where That's where I hit the wall. |
Hm, after all that long thought I think I might have an idea. So at the
that can be later instantiated in the And that sounds to me like the metaclass recipe. |
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 |
I was playing around with this, and it looks you can implement class-based views today by annotating Well, to be clear, you still have to decorate the individual methods, but this enables you to:
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 Here is an example showing it works with inheritance, and that the 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
The lone awkwardness I see here is that you have to annotate (Also, this wouldn't be able to replace the use of decorators with pure method names, but you can always use @somada141 @gbgduh @knyghty @bs32g1038 @tiangolo let me know if you see any obvious gaps in this approach, or hate the idea of using |
@dmontagu thanks for the approach, I wouldn't have considered this type of thing! Apart from the jarring 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. |
@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 Setupimport 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() |
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. |
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: 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 |
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. |
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 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/ |
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 😬 . |
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 |
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.
|
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/ |
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. |
@KiraPC thanks, but again I would rather see it integrated into Fastapi ... whatever implementation :) ->
|
I'm also interested in a native implementation in fast-api |
I don't think we should have CBV on FastAPI itself. That being said, the most 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. |
@Kludex FYI |
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. |
@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. |
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}"}
|
@delijati |
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 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/. |
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 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" |
@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.
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. |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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:
Thus far we've found we can 'hack' CBVs into FastAPI as such:
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?
The text was updated successfully, but these errors were encountered: