diff --git a/ninja_extra/__init__.py b/ninja_extra/__init__.py index c161995a..b800c6f1 100644 --- a/ninja_extra/__init__.py +++ b/ninja_extra/__init__.py @@ -1,6 +1,6 @@ """Django Ninja Extra - Class Based Utility and more for Django Ninja(Fast Django REST framework)""" -__version__ = "0.21.7" +__version__ = "0.21.8" import django diff --git a/ninja_extra/controllers/base.py b/ninja_extra/controllers/base.py index 065fb376..00853323 100644 --- a/ninja_extra/controllers/base.py +++ b/ninja_extra/controllers/base.py @@ -1,6 +1,7 @@ import inspect import re import uuid +from abc import ABC from typing import ( TYPE_CHECKING, Any, @@ -62,9 +63,24 @@ class MissingAPIControllerDecoratorException(Exception): def get_route_functions(cls: Type) -> Iterable[RouteFunction]: - for _, method in inspect.getmembers(cls, predicate=inspect.isfunction): - if hasattr(method, ROUTE_FUNCTION): - yield getattr(method, ROUTE_FUNCTION) + """ + Get all route functions from a controller class. + This function will recursively search for route functions in the base classes of the controller class + in order that they are defined. + + Args: + cls (Type): The controller class. + + Returns: + Iterable[RouteFunction]: An iterable of route functions. + """ + + bases = inspect.getmro(cls) + for base_cls in reversed(bases): + if base_cls not in [ControllerBase, ABC, object]: + for method in base_cls.__dict__.values(): + if hasattr(method, ROUTE_FUNCTION): + yield getattr(method, ROUTE_FUNCTION) def get_all_controller_route_function( diff --git a/tests/test_operation.py b/tests/test_operation.py index ba3100c7..8d6511db 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -1,7 +1,9 @@ +import uuid + import django import pytest -from ninja_extra import api_controller, route +from ninja_extra import api_controller, http_delete, http_get, route, status from ninja_extra.controllers import AsyncRouteFunction, RouteFunction from ninja_extra.helper import get_route_function from ninja_extra.operation import AsyncOperation, Operation @@ -113,3 +115,29 @@ async def test_async_route_operation_execution_should_log_execution(self): client = TestAsyncClient(self.SomeTestController) with pytest.raises(CustomException): await client.get("/example_exception") + + +def test_controller_operation_order(): + @api_controller("/my/api/users", tags=["User"]) + class UserAPIController: + @http_get("/me") + def get_current_user(self, request): + return {"debug": "ok", "message": "Current user"} + + @http_get("/{user_id}") + def get_user(self, request, user_id: uuid.UUID): + return {"debug": "ok", "message": "User"} + + @http_delete("/{user_id}", response={status.HTTP_204_NO_CONTENT: None}) + def delete_user_from_clinic(self, request, user_id: uuid.UUID): + return {"debug": "ok", "message": "User deleted"} + + client = TestClient(UserAPIController) + response = client.get("/me") + assert response.json() == {"debug": "ok", "message": "Current user"} + + response = client.get(f"/{uuid.uuid4()}") + assert response.json() == {"debug": "ok", "message": "User"} + + response = client.delete(f"/{uuid.uuid4()}") + assert response.content == b""