Skip to content
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

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

Open
LeeJB-48 opened this issue Nov 27, 2024 · 3 comments

Comments

@LeeJB-48
Copy link

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):

  • Python version: 3.11
  • Django version: 4.2.5
  • Django-Ninja version: 1.2.2
  • Pydantic version: 2.8.2
@vitalik
Copy link
Owner

vitalik commented Nov 28, 2024

Hi @LeeJB-48

You should use async test client for async views:

from ninja.testing import TestAsyncClient

client = TestAsyncClient(router)
response = await client.post("/test/")

@LeeJB-48
Copy link
Author

@vitalik

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

then even if /test/ with POST method , then the view for that endpoint is considered "async" view even if that the exact POST method's function is sync? (reason is that the same endpoint for GET method is async?)

@svenvanlierde
Copy link

@vitalik same issue here.

async GET endpoint: /sales/{sale.id}
sync PATCH endpoint /sales/{sale.id}

Here's the test case for the sync PATCH endpoint that gives the error: AttributeError: 'coroutine' object has no attribute

@pytest.fixture(scope="session")
def test_client_with_mock_auth():
    with patch("sales.api.api_sales.JWTAuth.authenticate"):
        yield TestClient(router_or_app=api)


@pytest.mark.django_db(databases=["default"])
class TestPatchSaleCase:

    @pytest.fixture(autouse=True)
    def setup(self, test_client_with_mock_auth):
        self.client = test_client_with_mock_auth
        self.headers = {"Authorization": "Bearer valid_token"}

    def test_update_price_b2c(self):
        sale = baker.make("sales.Sale", price_b2c=1000)

        response = self.client.patch(
            f"/sales/{sale.id}",
            json={"priceB2C": 1200},
            headers=self.headers,
        )
        sale.refresh_from_db()

        assert response.status_code == 200
        assert sale.price_b2c == 1200

But if i convert my 'test_update_price_b2c' test to async, with sync_to_async for django stuff it works:

@pytest.fixture(scope="session")
def test_client_with_mock_auth_async():
    with patch("sales.api.api_sales.JWTAuth.authenticate"):
        with patch(
            "sales.api.api_sales.api.urls_namespace", return_value="sales_api_async"
        ):
            yield TestAsyncClient(router_or_app=api)

@sync_to_async
def create_sale_instance():
    return baker.make("sales.Sale", price_b2c=1000)


@sync_to_async
def refresh_sale_from_db(sale):
    sale.refresh_from_db()
    return sale


@pytest.mark.django_db(databases=["default"])
class TestPatchSaleCase:

    @pytest.fixture(autouse=True)
    def setup(self, test_client_with_mock_auth_async):
        self.client = test_client_with_mock_auth_async
        self.headers = {"Authorization": "Bearer valid_token"}

    @pytest.mark.asyncio
    async def test_update_price_b2c(self):
        sale = await create_sale_instance()

        response = await self.client.patch(
            f"/sales/{sale.id}",
            json={"priceB2C": 1200},
            headers=self.headers,
        )
        sale = await refresh_sale_from_db(sale)

        assert response.status_code == 200
        assert sale.price_b2c == 1200

But this isn't expected behaviour. I would like to test my sync patch endpoint in a sync way?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants