Skip to content

uuidstr url converter instead of shadowing django uuid #1453

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

Merged
merged 5 commits into from
Apr 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ninja/router.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from typing import (
TYPE_CHECKING,
Any,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ninja/signature/details.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
13 changes: 4 additions & 9 deletions ninja/signature/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
11 changes: 9 additions & 2 deletions tests/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)


Expand Down
9 changes: 7 additions & 2 deletions tests/test_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
Expand Down