Skip to content
This repository was archived by the owner on Nov 19, 2024. It is now read-only.

Commit a167f65

Browse files
committed
feat: Now using function parameters
1 parent bfd2b56 commit a167f65

File tree

5 files changed

+239
-190
lines changed

5 files changed

+239
-190
lines changed

docs/source/api.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ Private API
5252
----------------------
5353

5454
.. autofunction:: flask_utils.decorators._is_optional
55-
.. autofunction:: flask_utils.decorators._make_optional
5655
.. autofunction:: flask_utils.decorators._is_allow_empty
5756
.. autofunction:: flask_utils.decorators._check_type
5857

flask_utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Increment versions here according to SemVer
2-
__version__ = "0.7.0"
2+
__version__ = "1.0.0"
33

44
from flask_utils.utils import is_it_true
55
from flask_utils.errors import GoneError

flask_utils/decorators.py

Lines changed: 71 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import inspect
2+
import warnings
13
from typing import Any
2-
from typing import Dict
34
from typing import Type
45
from typing import Union
56
from typing import Callable
@@ -18,6 +19,7 @@
1819

1920
from flask_utils.errors import BadRequestError
2021

22+
# TODO: Allow to set this value from the config/env
2123
VALIDATE_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

Comments
 (0)