From ee0f27b169fb227e8786bbf9011659f46b2795ea Mon Sep 17 00:00:00 2001 From: sunfkny <30853461+sunfkny@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:20:55 +0800 Subject: [PATCH] Add pattern support to param (#1336) * Add pattern support to param * Refactor regex to pattern in param functions --- ninja/params/__init__.py | 6 +++--- ninja/params/functions.py | 30 +++++++++++++++--------------- ninja/params/models.py | 15 ++++++++++++++- tests/main.py | 5 +++++ tests/test_path.py | 16 ++++++++++++++++ 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/ninja/params/__init__.py b/ninja/params/__init__.py index fd553e5ba..19863f849 100644 --- a/ninja/params/__init__.py +++ b/ninja/params/__init__.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Pattern, TypeVar, Union from typing_extensions import Annotated @@ -86,7 +86,7 @@ def P( le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Union[str, Pattern[str], None] = None, example: Any = None, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, @@ -104,7 +104,7 @@ def P( le=le, min_length=min_length, max_length=max_length, - regex=regex, + pattern=pattern, example=example, examples=examples, deprecated=deprecated, diff --git a/ninja/params/functions.py b/ninja/params/functions.py index 899f29d90..d48be8430 100644 --- a/ninja/params/functions.py +++ b/ninja/params/functions.py @@ -3,7 +3,7 @@ # what it basically does makes function XXX that create instance of models.XXX # and annotates function with result = Any # idea from https://github.com/tiangolo/fastapi/blob/master/fastapi/param_functions.py -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Pattern, Union from ninja.params import models @@ -20,7 +20,7 @@ def Path( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Union[str, Pattern[str], None] = None, example: Any = None, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, @@ -38,7 +38,7 @@ def Path( # noqa: N802 le=le, min_length=min_length, max_length=max_length, - regex=regex, + pattern=pattern, example=example, examples=examples, deprecated=deprecated, @@ -59,7 +59,7 @@ def Query( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Union[str, Pattern[str], None] = None, example: Any = None, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, @@ -77,7 +77,7 @@ def Query( # noqa: N802 le=le, min_length=min_length, max_length=max_length, - regex=regex, + pattern=pattern, example=example, examples=examples, deprecated=deprecated, @@ -98,7 +98,7 @@ def Header( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Union[str, Pattern[str], None] = None, example: Any = None, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, @@ -116,7 +116,7 @@ def Header( # noqa: N802 le=le, min_length=min_length, max_length=max_length, - regex=regex, + pattern=pattern, example=example, examples=examples, deprecated=deprecated, @@ -137,7 +137,7 @@ def Cookie( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Union[str, Pattern[str], None] = None, example: Any = None, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, @@ -155,7 +155,7 @@ def Cookie( # noqa: N802 le=le, min_length=min_length, max_length=max_length, - regex=regex, + pattern=pattern, example=example, examples=examples, deprecated=deprecated, @@ -176,7 +176,7 @@ def Body( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Union[str, Pattern[str], None] = None, example: Any = None, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, @@ -194,7 +194,7 @@ def Body( # noqa: N802 le=le, min_length=min_length, max_length=max_length, - regex=regex, + pattern=pattern, example=example, examples=examples, deprecated=deprecated, @@ -215,7 +215,7 @@ def Form( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Union[str, Pattern[str], None] = None, example: Any = None, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, @@ -233,7 +233,7 @@ def Form( # noqa: N802 le=le, min_length=min_length, max_length=max_length, - regex=regex, + pattern=pattern, example=example, examples=examples, deprecated=deprecated, @@ -254,7 +254,7 @@ def File( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Union[str, Pattern[str], None] = None, example: Any = None, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, @@ -272,7 +272,7 @@ def File( # noqa: N802 le=le, min_length=min_length, max_length=max_length, - regex=regex, + pattern=pattern, example=example, examples=examples, deprecated=deprecated, diff --git a/ninja/params/models.py b/ninja/params/models.py index 0f36e6e11..96ae8af66 100644 --- a/ninja/params/models.py +++ b/ninja/params/models.py @@ -1,6 +1,17 @@ from abc import ABC, abstractmethod from collections import defaultdict -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, TypeVar +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Optional, + Pattern, + Tuple, + Type, + TypeVar, + Union, +) from django.conf import settings from django.http import HttpRequest @@ -204,6 +215,7 @@ def __init__( examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, include_in_schema: Optional[bool] = True, + pattern: Union[str, Pattern[str], None] = None, # param_name: str = None, # param_type: Any = None, **extra: Any, @@ -237,6 +249,7 @@ def __init__( le=le, min_length=min_length, max_length=max_length, + pattern=pattern, json_schema_extra=json_schema_extra, **extra, ) diff --git a/tests/main.py b/tests/main.py index d313d08ea..6535036a5 100644 --- a/tests/main.py +++ b/tests/main.py @@ -135,6 +135,11 @@ def get_path_param_le_ge_int(request, item_id: int = Path(..., le=3, ge=1)): return item_id +@router.get("/path/param-pattern/{item_id}") +def get_path_param_pattern(request, item_id: str = Path(..., pattern="^foo")): + return item_id + + @router.get("/path/param-django-str/{str:item_id}") def get_path_param_django_str(request, item_id): return item_id diff --git a/tests/test_path.py b/tests/test_path.py index 2b325f585..687d6486d 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -172,6 +172,20 @@ def test_text_get(): } +response_not_valid_pattern = { + "detail": [ + { + "ctx": { + "pattern": "^foo", + }, + "loc": ["path", "item_id"], + "msg": "String should match pattern '^foo'", + "type": "string_pattern_mismatch", + } + ] +} + + @pytest.mark.parametrize( "path,expected_status,expected_response", [ @@ -249,6 +263,8 @@ def test_text_get(): ("/path/param-le-ge-int/3", 200, 3), ("/path/param-le-ge-int/4", 422, response_less_than_equal_3), ("/path/param-le-ge-int/2.7", 422, response_not_valid_int_float), + ("/path/param-pattern/foo", 200, "foo"), + ("/path/param-pattern/fo", 422, response_not_valid_pattern), ], ) def test_get_path(path, expected_status, expected_response):