1

I'm developing a simple application with FastAPI.

I need a function to be called as endpoint for a certain route. Everything works just fine with the function's default parameters, but wheels come off the bus as soon as I try to override one of them.

Example. This works just fine:

async def my_function(request=Request, clientname='my_client'):
    print(request.method)
    print(clientname)
    ## DO OTHER STUFF...
    return SOMETHING

private_router.add_route('/api/my/test/route', my_function, ['GET'])

This returns an error instead:

async def my_function(request=Request, clientname='my_client'):
    print(request.method)
    print(clientname)
    ## DO OTHER STUFF...
    return SOMETHING

private_router.add_route('/api/my/test/route', my_function(clientname='my_other_client'), ['GET'])

The Error:

INFO:     127.0.0.1:60005 - "GET /api/my/test/route HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
...
...
TypeError: 'coroutine' object is not callable

The only difference is I'm trying to override the clientname value in my_function.

It is apparent that this isn't the right syntax but I looked everywhere and I'm just appalled that the documentation about the add_route method is nowhere to be found.

Is anyone able to point me to the right way to do this supposedly simple thing?

Thanks!

Chris
  • 18,724
  • 6
  • 46
  • 80
MariusPontmercy
  • 398
  • 1
  • 5
  • 20
  • 1
    You're _calling_ the (endpoint) function, not registering it with a predetermined parameter - in that case you'd probaly want a helper function that returns an inner function - which is the function that should be registered to the endpoint (i.e. something like `def client(client_name): async def wrapped(): print(client_name) ... return wrapped`); this will bind the given `client_name` to the function that gets returned (which has the given values in the scope). This also matches that the API requirement for the function is (no parameters), since client_name is given when creating the api. – MatsLindh Jun 21 '23 at 11:31
  • Thanks @MatsLindh, it's still not clear to me how to override that parameter in the route definition, once I wrapped the function. What the add_route definition should look like in my example? My need is to be able to pass different `clientname` values to different routes. – MariusPontmercy Jun 21 '23 at 14:40
  • Hi @Chris, I tried both `:` and `=` the function works both ways. It returns the expected payload when called as in my first example. – MariusPontmercy Jun 21 '23 at 14:42

1 Answers1

1

Option 1

One way is to make a partial application of the function using functools.partial. As per functools.partial's documentation:

functools.partial(func, /, *args, **keywords)

Return a new partial object which when called will behave like func called with the positional arguments args and keyword arguments keywords. If more arguments are supplied to the call, they are appended to args. If additional keyword arguments are supplied, they extend and override keywords. Roughly equivalent to:

def partial(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = {**keywords, **fkeywords}
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

The partial() is used for partial function application which "freezes" some portion of a function's arguments and/or keywords resulting in a new object with a simplified signature.

Working Example

Here is the source for the add_route() method, as well as the part in Route class where Starlette checks if the endpoint_handler that is passed to add_route() is an instance of functools.partial.

Note that the endpoint has to return an instance of Response/JSONResponse/etc., as returning a str or dict object (e.g., return client_name), for instance, would throw TypeError: 'str' object is not callable or TypeError: 'dict' object is not callable, respectively. Please have a look at this answer for more details and examples on how to return JSON data using a custom Response.

from fastapi import FastAPI, Request, APIRouter, Response
from functools import partial


async def my_endpoint(request: Request, client_name: str ='my_client'):
    print(request.method)
    return Response(client_name)


app = FastAPI()
router = APIRouter()
router.add_route('/', partial(my_endpoint, client_name='my_other_client'), ['GET'])  
app.include_router(router)

Option 2

As noted by @MatsLindh in the comments section, you could use a helper function that returns an inner function, which is essentially the same as using functools.partial in Option 1, as that is exactly how that function works under the hood (as shown in the quote block earlier).

Working Example

from fastapi import FastAPI, Request, APIRouter, Response

def my_endpoint(client_name: str ='my_client'): 
    async def newfunc(request: Request): 
        print(request.method)
        return Response(client_name)
    return newfunc

app = FastAPI()
router = APIRouter()
router.add_route('/', my_endpoint(client_name='my_other_client'), ['GET'])  
app.include_router(router)
Chris
  • 18,724
  • 6
  • 46
  • 80
  • 1
    Used Option 2 ant it works perfectly! Thank you so much @Chris! I still cannot wrap my head around the fact that it's impossible to find any documentation about this, I assume, quite common need... – MariusPontmercy Jun 22 '23 at 11:00