vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
7.34k stars 437 forks source link

[BUG] Same path apis with different method and async sync are mixed then all considered as async when testing #1347

Open LeeJB-48 opened 4 hours ago

LeeJB-48 commented 4 hours ago

Describe the bug if same path with different method and async sync are mixed then they are all considered as async when testing

i use async for (GET) operation and use sync for (POST,DELETE,PUT,PATCH) operations but i got error when testing

example code is below

def test_bug():
    router = Router()
    @router.get("/test/")
    async def test_get(request):
        return {"test": "test"}
    @router.post("/test/")
    def test_post(request):
        return {"test": "test"}
    client = TestClient(router)
    response = client.post("/test/")

and it throws an error says

AttributeError sys:1: RuntimeWarning: coroutine 'PathView._async_view' was never awaited

so i found PathView._async_view from

client.urls[0].callback # also for client.urls[1].callback

but i found that both callbacks are all PathView._async_view , even for the sync view (POST method)

and the reason is that when operations are added to Router() for same path , then even if one operation is async , then all considered async

class PathView:
    def __init__(self) -> None:
        self.operations: List[Operation] = []
        self.is_async = False  # if at least one operation is async - will become True    <---------- Here
        self.url_name: Optional[str] = None

    def add_operation(
        self,
        path: str,
        methods: List[str],
        view_func: Callable,
        *,
        auth: Optional[Union[Sequence[Callable], Callable, NOT_SET_TYPE]] = NOT_SET,
        throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
        response: Any = NOT_SET,
        operation_id: Optional[str] = None,
        summary: Optional[str] = None,
        description: Optional[str] = None,
        tags: Optional[List[str]] = None,
        deprecated: Optional[bool] = None,
        by_alias: bool = False,
        exclude_unset: bool = False,
        exclude_defaults: bool = False,
        exclude_none: bool = False,
        url_name: Optional[str] = None,
        include_in_schema: bool = True,
        openapi_extra: Optional[Dict[str, Any]] = None,
    ) -> Operation:
        if url_name:
            self.url_name = url_name

        OperationClass = Operation
        if is_async(view_func):
            self.is_async = True   # <----------------------- Here
            OperationClass = AsyncOperation

        operation = OperationClass(
            path,
            methods,
            view_func,
            auth=auth,
            throttle=throttle,
            response=response,
            operation_id=operation_id,
            summary=summary,
            description=description,
            tags=tags,
            deprecated=deprecated,
            by_alias=by_alias,
            exclude_unset=exclude_unset,
            exclude_defaults=exclude_defaults,
            exclude_none=exclude_none,
            include_in_schema=include_in_schema,
            url_name=url_name,
            openapi_extra=openapi_extra,
        )

        self.operations.append(operation)
        view_func._ninja_operation = operation  # type: ignore
        return operation

i'm having a trouble because of that

is this a bug? or is there any purpose for that?

Versions (please complete the following information):