diff --git a/ninja/router.py b/ninja/router.py index 6e29b0da8..d6f96d063 100644 --- a/ninja/router.py +++ b/ninja/router.py @@ -1,3 +1,4 @@ +import re from typing import ( TYPE_CHECKING, Any, @@ -320,6 +321,12 @@ def add_api_operation( include_in_schema: bool = True, openapi_extra: Optional[Dict[str, Any]] = None, ) -> None: + path = re.sub(r"\{uuid:(\w+)\}", r"{uuidstr:\1}", path, flags=re.IGNORECASE) + # django by default convert strings to UUIDs + # but we want to keep them as strings to let pydantic handle conversion/validation + # if user whants UUID object + # uuidstr is custom registered converter + if path not in self.path_operations: path_view = PathView() self.path_operations[path] = path_view diff --git a/ninja/signature/details.py b/ninja/signature/details.py index cb79cf1eb..607b750f3 100644 --- a/ninja/signature/details.py +++ b/ninja/signature/details.py @@ -289,7 +289,7 @@ def is_pydantic_model(cls: Any) -> bool: if get_origin(cls) in UNION_TYPES: return any(issubclass(arg, pydantic.BaseModel) for arg in get_args(cls)) return issubclass(cls, pydantic.BaseModel) - except TypeError: + except TypeError: # pragma: no cover return False diff --git a/ninja/signature/utils.py b/ninja/signature/utils.py index bc04eacbb..c3993d83e 100644 --- a/ninja/signature/utils.py +++ b/ninja/signature/utils.py @@ -72,16 +72,11 @@ def get_args_names(func: Callable[..., Any]) -> List[str]: return list(inspect.signature(func).parameters.keys()) -class NinjaUUIDConverter: +class UUIDStrConverter(UUIDConverter): """Return a path converted UUID as a str instead of the standard UUID""" - regex = UUIDConverter.regex + def to_python(self, value: str) -> str: # type: ignore + return value # return string value instead of UUID - def to_python(self, value: str) -> str: - return value - def to_url(self, value: Any) -> str: - return str(value) - - -register_converter(NinjaUUIDConverter, "uuid") +register_converter(UUIDStrConverter, "uuidstr") diff --git a/tests/main.py b/tests/main.py index 6535036a5..38ff7a150 100644 --- a/tests/main.py +++ b/tests/main.py @@ -174,8 +174,15 @@ def get_path_param_django_uuid(request, item_id: UUID): return item_id -@router.get("/path/param-django-uuid-str/{uuid:item_id}") -def get_path_param_django_uuid_str(request, item_id): +@router.get("/path/param-django-uuid-notype/{uuid:item_id}") +def get_path_param_django_uuid_notype(request, item_id): + # no type annotation defaults to str..............^ + assert isinstance(item_id, str) + return item_id + + +@router.get("/path/param-django-uuid-typestr/{uuid:item_id}") +def get_path_param_django_uuid_typestr(request, item_id: str): assert isinstance(item_id, str) return item_id diff --git a/tests/test_misc.py b/tests/test_misc.py index 3060a5eae..355dc8883 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -7,7 +7,7 @@ from ninja import NinjaAPI from ninja.constants import NOT_SET from ninja.signature.details import is_pydantic_model -from ninja.signature.utils import NinjaUUIDConverter +from ninja.signature.utils import UUIDStrConverter from ninja.testing import TestClient @@ -48,7 +48,7 @@ def operation(request, a: str, *args, **kwargs): def test_uuid_converter(): - conv = NinjaUUIDConverter() + conv = UUIDStrConverter() assert isinstance(conv.to_url(uuid.uuid4()), str) diff --git a/tests/test_path.py b/tests/test_path.py index 687d6486d..e9fea9dd2 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -301,12 +301,17 @@ def test_get_path(path, expected_status, expected_response): "31ea378c-c052-4b4c-bf0b-679ce5cfcc2a", ), ( - "/path/param-django-uuid/31ea378c-c052-4b4c-bf0b-679ce5cfcc2", + "/path/param-django-uuid/31ea378c-c052-4b4c-bf0b-679ce5cfcc2", # invalid UUID (missing last digit) "Cannot resolve", Exception, ), ( - "/path/param-django-uuid-str/31ea378c-c052-4b4c-bf0b-679ce5cfcc2a", + "/path/param-django-uuid-notype/31ea378c-c052-4b4c-bf0b-679ce5cfcc2a", + 200, + "31ea378c-c052-4b4c-bf0b-679ce5cfcc2a", + ), + ( + "/path/param-django-uuid-typestr/31ea378c-c052-4b4c-bf0b-679ce5cfcc2a", 200, "31ea378c-c052-4b4c-bf0b-679ce5cfcc2a", ),