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
@@ -58,35 +60,11 @@ def _is_optional(type_hint: Type) -> bool: # type: ignore
5860 return get_origin (type_hint ) is Union and type (None ) in get_args (type_hint )
5961
6062
61- def _make_optional (type_hint : Type ) -> Type : # type: ignore
62- """Wrap type hint with :data:`~typing.Optional` if it's not already.
63-
64- :param type_hint: Type hint to wrap.
65- :return: Type hint wrapped with :data:`~typing.Optional`.
66-
67- :Example:
68-
69- .. code-block:: python
70-
71- from typing import Optional
72- from flask_utils.decorators import _make_optional
73-
74- _make_optional(str) # Optional[str]
75- _make_optional(Optional[str]) # Optional[str]
76-
77- .. versionadded:: 0.2.0
78- """
79- if not _is_optional (type_hint ):
80- return Optional [type_hint ] # type: ignore
81- return type_hint
82-
83-
84- def _is_allow_empty (value : Any , type_hint : Type , allow_empty : bool ) -> bool : # type: ignore
63+ def _is_allow_empty (value : Any , type_hint : Type ) -> bool : # type: ignore
8564 """Determine if the value is considered empty and whether it's allowed.
8665
8766 :param value: Value to check.
8867 :param type_hint: Type hint to check against.
89- :param allow_empty: Whether to allow empty values.
9068
9169 :return: True if the value is empty and allowed, False otherwise.
9270
@@ -107,19 +85,19 @@ def _is_allow_empty(value: Any, type_hint: Type, allow_empty: bool) -> bool: #
10785
10886 .. versionadded:: 0.2.0
10987 """
110- if value in [None , "" , [], {}]:
88+ if not value :
89+ # TODO: Find a test for this
11190 # Check if type is explicitly Optional or allow_empty is True
112- if _is_optional (type_hint ) or allow_empty :
91+ if _is_optional (type_hint ):
11392 return True
11493 return False
11594
11695
117- def _check_type (value : Any , expected_type : Type , allow_empty : bool = False , curr_depth : int = 0 ) -> bool : # type: ignore
96+ def _check_type (value : Any , expected_type : Type , curr_depth : int = 0 ) -> bool : # type: ignore
11897 """Check if the value matches the expected type, recursively if necessary.
11998
12099 :param value: Value to check.
121100 :param expected_type: Expected type.
122- :param allow_empty: Whether to allow empty values.
123101 :param curr_depth: Current depth of the recursive check.
124102
125103 :return: True if the value matches the expected type, False otherwise.
@@ -156,8 +134,9 @@ def _check_type(value: Any, expected_type: Type, allow_empty: bool = False, curr
156134 """
157135
158136 if curr_depth >= VALIDATE_PARAMS_MAX_DEPTH :
137+ warnings .warn (f"Maximum depth of { VALIDATE_PARAMS_MAX_DEPTH } reached." , SyntaxWarning , stacklevel = 2 )
159138 return True
160- if expected_type is Any or _is_allow_empty (value , expected_type , allow_empty ): # type: ignore
139+ if expected_type is Any or _is_allow_empty (value , expected_type ): # type: ignore
161140 return True
162141
163142 if isinstance (value , bool ):
@@ -171,39 +150,30 @@ def _check_type(value: Any, expected_type: Type, allow_empty: bool = False, curr
171150 args = get_args (expected_type )
172151
173152 if origin is Union :
174- return any (_check_type (value , arg , allow_empty , (curr_depth + 1 )) for arg in args )
153+ return any (_check_type (value , arg , (curr_depth + 1 )) for arg in args )
175154 elif origin is list :
176- return isinstance (value , list ) and all (
177- _check_type (item , args [0 ], allow_empty , (curr_depth + 1 )) for item in value
178- )
155+ return isinstance (value , list ) and all (_check_type (item , args [0 ], (curr_depth + 1 )) for item in value )
179156 elif origin is dict :
180157 key_type , val_type = args
181158 if not isinstance (value , dict ):
182159 return False
183160 for k , v in value .items ():
184161 if not isinstance (k , key_type ):
185162 return False
186- if not _check_type (v , val_type , allow_empty , (curr_depth + 1 )):
163+ if not _check_type (v , val_type , (curr_depth + 1 )):
187164 return False
188165 return True
189166 else :
190167 return isinstance (value , expected_type )
191168
192169
193- def validate_params (
194- parameters : Dict [Any , Any ],
195- allow_empty : bool = False ,
196- ) -> Callable : # type: ignore
170+ def validate_params () -> Callable : # type: ignore
197171 """
198172 Decorator to validate request JSON body parameters.
199173
200174 This decorator ensures that the JSON body of a request matches the specified
201175 parameter types and includes all required parameters.
202176
203- :param parameters: Dictionary of parameters to validate. The keys are parameter names
204- and the values are the expected types.
205- :param allow_empty: Allow empty values for parameters. Defaults to False.
206-
207177 :raises BadRequestError: If the JSON body is malformed,
208178 the Content-Type header is missing or incorrect, required parameters are missing,
209179 or parameters are of the wrong type.
@@ -215,21 +185,13 @@ def validate_params(
215185 from flask import Flask, request
216186 from typing import List, Dict
217187 from flask_utils.decorators import validate_params
218- from flask_utils.errors.badrequest import BadRequestError
188+ from flask_utils.errors import BadRequestError
219189
220190 app = Flask(__name__)
221191
222192 @app.route("/example", methods=["POST"])
223- @validate_params(
224- {
225- "name": str,
226- "age": int,
227- "is_student": bool,
228- "courses": List[str],
229- "grades": Dict[str, int],
230- }
231- )
232- def example():
193+ @validate_params()
194+ def example(name: str, age: int, is_student: bool, courses: List[str], grades: Dict[str, int]):
233195 \" ""
234196 This route expects a JSON body with the following:
235197 - name: str
@@ -238,8 +200,8 @@ def example():
238200 - courses: list of str
239201 - grades: dict with str keys and int values
240202 \" ""
241- data = request.get_json()
242- return data
203+ # Use the data in your route
204+ ...
243205
244206 .. tip::
245207 You can use any of the following types:
@@ -253,6 +215,10 @@ def example():
253215 * Optional
254216 * Union
255217
218+ .. versionchanged:: 1.0.0
219+ The decorator doesn't take any parameters anymore,
220+ it loads the types and parameters from the function signature as well as the Flask route's slug parameters.
221+
256222 .. versionchanged:: 0.7.0
257223 The decorator will now use the custom error handlers if ``register_error_handlers`` has been set to ``True``
258224 when initializing the :class:`~flask_utils.extension.FlaskUtils` extension.
@@ -279,33 +245,67 @@ def wrapper(*args, **kwargs): # type: ignore
279245 "or the JSON body is missing." ,
280246 original_exception = e ,
281247 )
282-
283- if not data :
284- return _handle_bad_request (use_error_handlers , "Missing json body." )
285-
286248 if not isinstance (data , dict ):
287- return _handle_bad_request (use_error_handlers , "JSON body must be a dict" )
249+ return _handle_bad_request (
250+ use_error_handlers ,
251+ "JSON body must be a dict" ,
252+ original_exception = BadRequestError ("JSON body must be a dict" ),
253+ )
254+
255+ signature = inspect .signature (fn )
256+ parameters = signature .parameters
257+ # Extract the parameter names and annotations
258+ expected_params = {}
259+ for name , param in parameters .items ():
260+ if param .annotation != inspect .Parameter .empty :
261+ expected_params [name ] = param .annotation
262+ else :
263+ warnings .warn (f"Parameter { name } has no type annotation." , SyntaxWarning , stacklevel = 2 )
264+ expected_params [name ] = Any
265+
266+ request_data = request .view_args # Flask route parameters
267+ for key in data :
268+ if key in request_data :
269+ warnings .warn (
270+ f"Parameter { key } is defined in both the route and the JSON body. "
271+ f"The JSON body will override the route parameter." ,
272+ SyntaxWarning ,
273+ stacklevel = 2 ,
274+ )
275+ request_data .update (data or {})
288276
289- for key , type_hint in parameters .items ():
290- if not _is_optional (type_hint ) and key not in data :
277+ for key , type_hint in expected_params .items ():
278+ # TODO: Handle deeply nested types
279+ if key not in request_data and not _is_optional (type_hint ):
291280 return _handle_bad_request (
292- use_error_handlers , f"Missing key: { key } " , f"Expected keys are: { list (parameters .keys ())} "
281+ use_error_handlers , f"Missing key: { key } " , f"Expected keys are: { list (expected_params .keys ())} "
293282 )
294283
295- for key in data :
296- if key not in parameters :
284+ for key in request_data :
285+ if key not in expected_params :
297286 return _handle_bad_request (
298- use_error_handlers , f"Unexpected key: { key } ." , f"Expected keys are: { list (parameters .keys ())} "
287+ use_error_handlers ,
288+ f"Unexpected key: { key } ." ,
289+ f"Expected keys are: { list (expected_params .keys ())} " ,
299290 )
300291
301- for key in data :
302- if key in parameters and not _check_type (data [ key ], parameters [key ], allow_empty ):
292+ for key , value in request_data . items () :
293+ if key in expected_params and not _check_type (value , expected_params [key ]):
303294 return _handle_bad_request (
304295 use_error_handlers ,
305296 f"Wrong type for key { key } ." ,
306- f"It should be { getattr (parameters [key ], '__name__' , str (parameters [key ]))} " ,
297+ f"It should be { getattr (expected_params [key ], '__name__' , str (expected_params [key ]))} " ,
307298 )
308299
300+ provided_values = {}
301+ for key in expected_params :
302+ if not _is_optional (expected_params [key ]):
303+ provided_values [key ] = request_data [key ]
304+ else :
305+ provided_values [key ] = request_data .get (key , None )
306+
307+ kwargs .update (provided_values )
308+
309309 return fn (* args , ** kwargs )
310310
311311 return wrapper
0 commit comments