1+ import inspect
2+ import warnings
13from typing import Any
2- from typing import Dict
34from typing import Type
45from typing import Union
56from typing import Callable
1819
1920from flask_utils .errors import BadRequestError
2021
22+ # TODO: Allow to set this value from the config/env
2123VALIDATE_PARAMS_MAX_DEPTH = 4
2224
2325
@@ -61,41 +63,13 @@ def _is_optional(type_hint: Type) -> bool: # type: ignore
6163 return get_origin (type_hint ) is Union and type (None ) in get_args (type_hint )
6264
6365
64- def _make_optional (type_hint : Type ) -> Type : # type: ignore
65- """Wrap type hint with :data:`~typing.Optional` if it's not already.
66-
67- :param type_hint: Type hint to wrap.
68- :type type_hint: Type
69-
70- :return: Type hint wrapped with :data:`~typing.Optional`.
71- :rtype: Type
72-
73- :Example:
74-
75- .. code-block:: python
76-
77- from typing import Optional
78- from flask_utils.decorators import _make_optional
79-
80- _make_optional(str) # Optional[str]
81- _make_optional(Optional[str]) # Optional[str]
82-
83- .. versionadded:: 0.2.0
84- """
85- if not _is_optional (type_hint ):
86- return Optional [type_hint ] # type: ignore
87- return type_hint
88-
89-
90- def _is_allow_empty (value : Any , type_hint : Type , allow_empty : bool ) -> bool : # type: ignore
66+ def _is_allow_empty (value : Any , type_hint : Type ) -> bool : # type: ignore
9167 """Determine if the value is considered empty and whether it's allowed.
9268
9369 :param value: Value to check.
9470 :type value: Any
9571 :param type_hint: Type hint to check against.
9672 :type type_hint: Type
97- :param allow_empty: Whether to allow empty values.
98- :type allow_empty: bool
9973
10074 :return: True if the value is empty and allowed, False otherwise.
10175 :rtype: bool
@@ -117,22 +91,21 @@ def _is_allow_empty(value: Any, type_hint: Type, allow_empty: bool) -> bool: #
11791
11892 .. versionadded:: 0.2.0
11993 """
120- if value in [None , "" , [], {}]:
94+ if not value :
95+ # TODO: Find a test for this
12196 # Check if type is explicitly Optional or allow_empty is True
122- if _is_optional (type_hint ) or allow_empty :
97+ if _is_optional (type_hint ):
12398 return True
12499 return False
125100
126101
127- def _check_type (value : Any , expected_type : Type , allow_empty : bool = False , curr_depth : int = 0 ) -> bool : # type: ignore
102+ def _check_type (value : Any , expected_type : Type , curr_depth : int = 0 ) -> bool : # type: ignore
128103 """Check if the value matches the expected type, recursively if necessary.
129104
130105 :param value: Value to check.
131106 :type value: Any
132107 :param expected_type: Expected type.
133108 :type expected_type: Type
134- :param allow_empty: Whether to allow empty values.
135- :type allow_empty: bool
136109 :param curr_depth: Current depth of the recursive check.
137110 :type curr_depth: int
138111
@@ -171,8 +144,9 @@ def _check_type(value: Any, expected_type: Type, allow_empty: bool = False, curr
171144 """
172145
173146 if curr_depth >= VALIDATE_PARAMS_MAX_DEPTH :
147+ warnings .warn (f"Maximum depth of { VALIDATE_PARAMS_MAX_DEPTH } reached." , SyntaxWarning , stacklevel = 2 )
174148 return True
175- if expected_type is Any or _is_allow_empty (value , expected_type , allow_empty ): # type: ignore
149+ if expected_type is Any or _is_allow_empty (value , expected_type ): # type: ignore
176150 return True
177151
178152 if isinstance (value , bool ):
@@ -186,41 +160,30 @@ def _check_type(value: Any, expected_type: Type, allow_empty: bool = False, curr
186160 args = get_args (expected_type )
187161
188162 if origin is Union :
189- return any (_check_type (value , arg , allow_empty , (curr_depth + 1 )) for arg in args )
163+ return any (_check_type (value , arg , (curr_depth + 1 )) for arg in args )
190164 elif origin is list :
191- return isinstance (value , list ) and all (
192- _check_type (item , args [0 ], allow_empty , (curr_depth + 1 )) for item in value
193- )
165+ return isinstance (value , list ) and all (_check_type (item , args [0 ], (curr_depth + 1 )) for item in value )
194166 elif origin is dict :
195167 key_type , val_type = args
196168 if not isinstance (value , dict ):
197169 return False
198170 for k , v in value .items ():
199171 if not isinstance (k , key_type ):
200172 return False
201- if not _check_type (v , val_type , allow_empty , (curr_depth + 1 )):
173+ if not _check_type (v , val_type , (curr_depth + 1 )):
202174 return False
203175 return True
204176 else :
205177 return isinstance (value , expected_type )
206178
207179
208- def validate_params (
209- parameters : Dict [Any , Any ],
210- allow_empty : bool = False ,
211- ) -> Callable : # type: ignore
180+ def validate_params () -> Callable : # type: ignore
212181 """
213182 Decorator to validate request JSON body parameters.
214183
215184 This decorator ensures that the JSON body of a request matches the specified
216185 parameter types and includes all required parameters.
217186
218- :param parameters: Dictionary of parameters to validate. The keys are parameter names
219- and the values are the expected types.
220- :type parameters: Dict[Any, Any]
221- :param allow_empty: Allow empty values for parameters. Defaults to False.
222- :type allow_empty: bool
223-
224187 :raises BadRequestError: If the JSON body is malformed,
225188 the Content-Type header is missing or incorrect, required parameters are missing,
226189 or parameters are of the wrong type.
@@ -232,21 +195,13 @@ def validate_params(
232195 from flask import Flask, request
233196 from typing import List, Dict
234197 from flask_utils.decorators import validate_params
235- from flask_utils.errors.badrequest import BadRequestError
198+ from flask_utils.errors import BadRequestError
236199
237200 app = Flask(__name__)
238201
239202 @app.route("/example", methods=["POST"])
240- @validate_params(
241- {
242- "name": str,
243- "age": int,
244- "is_student": bool,
245- "courses": List[str],
246- "grades": Dict[str, int],
247- }
248- )
249- def example():
203+ @validate_params()
204+ def example(name: str, age: int, is_student: bool, courses: List[str], grades: Dict[str, int]):
250205 \" ""
251206 This route expects a JSON body with the following:
252207 - name: str
@@ -255,8 +210,8 @@ def example():
255210 - courses: list of str
256211 - grades: dict with str keys and int values
257212 \" ""
258- data = request.get_json()
259- return data
213+ # Use the data in your route
214+ ...
260215
261216 .. tip::
262217 You can use any of the following types:
@@ -270,6 +225,10 @@ def example():
270225 * Optional
271226 * Union
272227
228+ .. versionchanged:: 1.0.0
229+ The decorator doesn't take any parameters anymore,
230+ it loads the types and parameters from the function signature as well as the Flask route's slug parameters.
231+
273232 .. versionchanged:: 0.7.0
274233 The decorator will now use the custom error handlers if ``register_error_handlers`` has been set to ``True``
275234 when initializing the :class:`~flask_utils.extension.FlaskUtils` extension.
@@ -296,33 +255,67 @@ def wrapper(*args, **kwargs): # type: ignore
296255 "or the JSON body is missing." ,
297256 original_exception = e ,
298257 )
299-
300- if not data :
301- return _handle_bad_request (use_error_handlers , "Missing json body." )
302-
303258 if not isinstance (data , dict ):
304- return _handle_bad_request (use_error_handlers , "JSON body must be a dict" )
259+ return _handle_bad_request (
260+ use_error_handlers ,
261+ "JSON body must be a dict" ,
262+ original_exception = BadRequestError ("JSON body must be a dict" ),
263+ )
305264
306- for key , type_hint in parameters .items ():
307- if not _is_optional (type_hint ) and key not in data :
265+ signature = inspect .signature (fn )
266+ parameters = signature .parameters
267+ # Extract the parameter names and annotations
268+ expected_params = {}
269+ for name , param in parameters .items ():
270+ if param .annotation != inspect .Parameter .empty :
271+ expected_params [name ] = param .annotation
272+ else :
273+ warnings .warn (f"Parameter { name } has no type annotation." , SyntaxWarning , stacklevel = 2 )
274+ expected_params [name ] = Any
275+
276+ request_data = request .view_args # Flask route parameters
277+ for key in data :
278+ if key in request_data :
279+ warnings .warn (
280+ f"Parameter { key } is defined in both the route and the JSON body. "
281+ f"The JSON body will override the route parameter." ,
282+ SyntaxWarning ,
283+ stacklevel = 2 ,
284+ )
285+ request_data .update (data or {})
286+
287+ for key , type_hint in expected_params .items ():
288+ # TODO: Handle deeply nested types
289+ if key not in request_data and not _is_optional (type_hint ):
308290 return _handle_bad_request (
309- use_error_handlers , f"Missing key: { key } " , f"Expected keys are: { list (parameters .keys ())} "
291+ use_error_handlers , f"Missing key: { key } " , f"Expected keys are: { list (expected_params .keys ())} "
310292 )
311293
312- for key in data :
313- if key not in parameters :
294+ for key in request_data :
295+ if key not in expected_params :
314296 return _handle_bad_request (
315- use_error_handlers , f"Unexpected key: { key } ." , f"Expected keys are: { list (parameters .keys ())} "
297+ use_error_handlers ,
298+ f"Unexpected key: { key } ." ,
299+ f"Expected keys are: { list (expected_params .keys ())} " ,
316300 )
317301
318- for key in data :
319- if key in parameters and not _check_type (data [ key ], parameters [key ], allow_empty ):
302+ for key , value in request_data . items () :
303+ if key in expected_params and not _check_type (value , expected_params [key ]):
320304 return _handle_bad_request (
321305 use_error_handlers ,
322306 f"Wrong type for key { key } ." ,
323- f"It should be { getattr (parameters [key ], '__name__' , str (parameters [key ]))} " ,
307+ f"It should be { getattr (expected_params [key ], '__name__' , str (expected_params [key ]))} " ,
324308 )
325309
310+ provided_values = {}
311+ for key in expected_params :
312+ if not _is_optional (expected_params [key ]):
313+ provided_values [key ] = request_data [key ]
314+ else :
315+ provided_values [key ] = request_data .get (key , None )
316+
317+ kwargs .update (provided_values )
318+
326319 return fn (* args , ** kwargs )
327320
328321 return wrapper
0 commit comments