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

Commit 853c7a4

Browse files
committed
feat: Now using function parameters
1 parent 6ff7ae5 commit 853c7a4

File tree

5 files changed

+239
-197
lines changed

5 files changed

+239
-197
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.1"
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 & 78 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

@@ -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

Comments
 (0)