From 01a333cc5d74a4344fc5a9b42bca02372977fa49 Mon Sep 17 00:00:00 2001 From: Arjan de Pooter Date: Thu, 3 Jul 2025 15:10:01 +0200 Subject: [PATCH] Add _ninja_ignore_args support for dependency injection Enables decorators to inject dependencies as function arguments while excluding them from Django Ninja's parameter processing and OpenAPI schema. See `test_ninja_ignore_args_integration` in `tests/test_decorators.py` for usage example. --- ninja/signature/details.py | 8 +++++++ ninja/utils.py | 7 ++++++ tests/test_decorators.py | 48 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/ninja/signature/details.py b/ninja/signature/details.py index 8f026b067..78bc1e99a 100644 --- a/ninja/signature/details.py +++ b/ninja/signature/details.py @@ -52,6 +52,10 @@ def __init__(self, path: str, view_func: Callable[..., Any]) -> None: self.has_kwargs = False self.params = [] + + # Get list of arguments to ignore if _ninja_ignore_args is set + ignore_args = getattr(view_func, "_ninja_ignore_args", []) + for name, arg in self.signature.parameters.items(): if name == "request": # TODO: maybe better assert that 1st param is request or check by type? @@ -59,6 +63,10 @@ def __init__(self, path: str, view_func: Callable[..., Any]) -> None: # so that users can ignore passing request if not needed continue + if name in ignore_args: + # Skip arguments that should be ignored + continue + if arg.kind == arg.VAR_KEYWORD: # Skipping **kwargs self.has_kwargs = True diff --git a/ninja/utils.py b/ninja/utils.py index 98ff863aa..df7de42b0 100644 --- a/ninja/utils.py +++ b/ninja/utils.py @@ -10,6 +10,7 @@ "is_debug_server", "normalize_path", "contribute_operation_callback", + "ignore_args", ] @@ -71,3 +72,9 @@ def contribute_operation_args( if not hasattr(func, "_ninja_contribute_args"): func._ninja_contribute_args = [] # type: ignore func._ninja_contribute_args.append((arg_name, arg_type, arg_source)) # type: ignore + + +def ignore_args(func: Callable[..., Any], *arg_names: str) -> None: + if not hasattr(func, "_ninja_ignore_args"): + func._ninja_ignore_args = [] # type: ignore + func._ninja_ignore_args.extend(arg_names) # type: ignore diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 387271440..5bcb70c60 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -59,3 +59,51 @@ def dec_multi(request): assert response.status_code == 200 assert response.json() == {"count": 4, "items": [1, 2, 3, 4]} assert response["X-Decorator"] == "some_decorator" + + +class DependencyService: + def get_data(self): + return "injected_data" + + +def inject_dependency(view_func): + @wraps(view_func) + def wrapper(request, *args, **kwargs): + kwargs["injected_service"] = DependencyService() + return view_func(request, *args, **kwargs) + + wrapper._ninja_ignore_args = ["injected_service"] + return wrapper + + +def test_ninja_ignore_args_integration(): + """Integration test for _ninja_ignore_args functionality.""" + api = NinjaAPI() + + @api.get("/test-ignore/{item_id}") + @inject_dependency + def test_view( + request, item_id: int, query_param: str, injected_service: DependencyService + ): + data = injected_service.get_data() + return {"item_id": item_id, "query_param": query_param, "injected_data": data} + + # Test that the endpoint works correctly + client = TestClient(api) + response = client.get("/test-ignore/123?query_param=test") + assert response.status_code == 200 + assert response.json() == { + "item_id": 123, + "query_param": "test", + "injected_data": "injected_data", + } + + # Test that injected_service is not in the OpenAPI schema + schema = api.get_openapi_schema() + parameters = schema["paths"]["/api/test-ignore/{item_id}"]["get"]["parameters"] + + # Should have item_id (path) and query_param (query), but not injected_service + param_names = [p["name"] for p in parameters] + assert "item_id" in param_names + assert "query_param" in param_names + assert "injected_service" not in param_names