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

Commit 2a66a9f

Browse files
committed
Docs: Added documentation for decorators.py
1 parent a21d69a commit 2a66a9f

File tree

3 files changed

+203
-22
lines changed

3 files changed

+203
-22
lines changed

docs/source/api.rst

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
11
API
22
===
33

4-
.. autosummary::
5-
:toctree: generated
4+
.. module:: flask_utils
5+
:synopsis: Flask utilities
66

7-
flask_utils
7+
This part of the documentation covers all the interfaces of Flask-Utils
8+
9+
Custom exceptions
10+
-----------------
11+
12+
.. automodule:: flask_utils.errors
13+
:members:
14+
15+
16+
Decorators
17+
----------
18+
19+
.. automodule:: flask_utils.decorators
20+
:members:
21+
22+
Private API
23+
----------------------
24+
25+
.. autofunction:: flask_utils.decorators._is_optional
26+
.. autofunction:: flask_utils.decorators._make_optional
27+
.. autofunction:: flask_utils.decorators._is_allow_empty
28+
.. autofunction:: flask_utils.decorators._check_type
29+
30+
.. autofunction:: flask_utils.errors._error_template._generate_error_json

docs/source/conf.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,15 @@
2323
"sphinx.ext.autodoc",
2424
"sphinx.ext.autosummary",
2525
"sphinx.ext.intersphinx",
26+
"sphinx.ext.viewcode",
2627
]
2728

29+
autodoc_default_options = {
30+
"members": True,
31+
"undoc-members": True,
32+
"private-members": False,
33+
}
34+
2835
intersphinx_mapping = {
2936
"python": ("https://docs.python.org/3/", None),
3037
"sphinx": ("https://www.sphinx-doc.org/en/master/", None),

flask_utils/decorators.py

Lines changed: 170 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,127 @@
1919
VALIDATE_PARAMS_MAX_DEPTH = 4
2020

2121

22-
def is_optional(type_hint: Type) -> bool:
23-
"""Check if the type hint is Optional[SomeType]."""
22+
def _is_optional(type_hint: Type) -> bool:
23+
"""Check if the type hint is :data:`~typing.Optional`.
24+
25+
:param type_hint: Type hint to check.
26+
:return: True if the type hint is :data:`~typing.Optional`, False otherwise.
27+
28+
:Example:
29+
30+
.. code-block:: python
31+
32+
from typing import Optional
33+
from flask_utils.decorators import _is_optional
34+
35+
_is_optional(Optional[str]) # True
36+
_is_optional(str) # False
37+
38+
.. versionadded:: 0.2.0
39+
"""
2440
return get_origin(type_hint) is Union and type(None) in get_args(type_hint)
2541

2642

27-
def make_optional(type_hint: Type) -> Type:
28-
"""Wrap type hint with Optional if it's not already."""
29-
if not is_optional(type_hint):
43+
def _make_optional(type_hint: Type) -> Type:
44+
"""Wrap type hint with :data:`~typing.Optional` if it's not already.
45+
46+
:param type_hint: Type hint to wrap.
47+
:return: Type hint wrapped with :data:`~typing.Optional`.
48+
49+
:Example:
50+
51+
.. code-block:: python
52+
53+
from typing import Optional
54+
from flask_utils.decorators import _make_optional
55+
56+
_make_optional(str) # Optional[str]
57+
_make_optional(Optional[str]) # Optional[str]
58+
59+
.. versionadded:: 0.2.0
60+
"""
61+
if not _is_optional(type_hint):
3062
return Optional[type_hint] # type: ignore
3163
return type_hint
3264

3365

34-
def is_allow_empty(value: Any, type_hint: Type, allow_empty: bool) -> bool:
35-
"""Determine if the value is considered empty and whether it's allowed."""
66+
def _is_allow_empty(value: Any, type_hint: Type, allow_empty: bool) -> bool:
67+
"""Determine if the value is considered empty and whether it's allowed.
68+
69+
:param value: Value to check.
70+
:param type_hint: Type hint to check against.
71+
:param allow_empty: Whether to allow empty values.
72+
73+
:return: True if the value is empty and allowed, False otherwise.
74+
75+
:Example:
76+
77+
.. code-block:: python
78+
79+
from typing import Optional
80+
from flask_utils.decorators import _is_allow_empty
81+
82+
_is_allow_empty(None, str, False) # False
83+
_is_allow_empty("", str, False) # False
84+
_is_allow_empty(None, Optional[str], False) # True
85+
_is_allow_empty("", Optional[str], False) # True
86+
_is_allow_empty("", Optional[str], True) # True
87+
_is_allow_empty("", str, True) # True
88+
_is_allow_empty([], Optional[list], False) # True
89+
90+
.. versionadded:: 0.2.0
91+
"""
3692
if value in [None, "", [], {}]:
3793
# Check if type is explicitly Optional or allow_empty is True
38-
if is_optional(type_hint) or allow_empty:
94+
if _is_optional(type_hint) or allow_empty:
3995
return True
4096
return False
4197

4298

43-
def check_type(value: Any, expected_type: Type, allow_empty: bool = False, curr_depth: int = 0) -> bool:
99+
def _check_type(value: Any, expected_type: Type, allow_empty: bool = False, curr_depth: int = 0) -> bool:
100+
"""Check if the value matches the expected type, recursively if necessary.
101+
102+
:param value: Value to check.
103+
:param expected_type: Expected type.
104+
:param allow_empty: Whether to allow empty values.
105+
:param curr_depth: Current depth of the recursive check.
106+
107+
:return: True if the value matches the expected type, False otherwise.
108+
109+
:Example:
110+
111+
.. code-block:: python
112+
113+
from typing import List, Dict
114+
from flask_utils.decorators import _check_type
115+
116+
_check_type("hello", str) # True
117+
_check_type(42, int) # True
118+
_check_type(42.0, float) # True
119+
_check_type(True, bool) # True
120+
_check_type(["hello", "world"], List[str]) # True
121+
_check_type({"name": "Jules", "city": "Rouen"}, Dict[str, str]) # True
122+
123+
It also works recursively:
124+
125+
.. code-block:: python
126+
127+
from typing import List, Dict
128+
from flask_utils.decorators import _check_type
129+
130+
_check_type(["hello", "world"], List[str]) # True
131+
_check_type(["hello", 42], List[str]) # False
132+
_check_type([{"name": "Jules", "city": "Rouen"},
133+
{"name": "John", "city": "Paris"}], List[Dict[str, str]]) # True
134+
_check_type([{"name": "Jules", "city": "Rouen"},
135+
{"name": "John", "city": 42}], List[Dict[str, str]]) # False
136+
137+
.. versionadded:: 0.2.0
138+
"""
139+
44140
if curr_depth >= VALIDATE_PARAMS_MAX_DEPTH:
45141
return True
46-
if expected_type is Any or is_allow_empty(value, expected_type, allow_empty):
142+
if expected_type is Any or _is_allow_empty(value, expected_type, allow_empty):
47143
return True
48144

49145
if isinstance(value, bool):
@@ -57,10 +153,10 @@ def check_type(value: Any, expected_type: Type, allow_empty: bool = False, curr_
57153
args = get_args(expected_type)
58154

59155
if origin is Union:
60-
return any(check_type(value, arg, allow_empty, (curr_depth + 1)) for arg in args)
156+
return any(_check_type(value, arg, allow_empty, (curr_depth + 1)) for arg in args)
61157
elif origin is list:
62158
return isinstance(value, list) and all(
63-
check_type(item, args[0], allow_empty, (curr_depth + 1)) for item in value
159+
_check_type(item, args[0], allow_empty, (curr_depth + 1)) for item in value
64160
)
65161
elif origin is dict:
66162
key_type, val_type = args
@@ -69,7 +165,7 @@ def check_type(value: Any, expected_type: Type, allow_empty: bool = False, curr_
69165
for k, v in value.items():
70166
if not isinstance(k, key_type):
71167
return False
72-
if not check_type(v, val_type, allow_empty, (curr_depth + 1)):
168+
if not _check_type(v, val_type, allow_empty, (curr_depth + 1)):
73169
return False
74170
return True
75171
else:
@@ -80,11 +176,66 @@ def validate_params(
80176
parameters: Dict[Any, Any],
81177
allow_empty: bool = False,
82178
):
83-
"""Decorator to validate request JSON body parameters.
179+
"""
180+
Decorator to validate request JSON body parameters.
181+
182+
This decorator ensures that the JSON body of a request matches the specified
183+
parameter types and includes all required parameters.
184+
185+
:param parameters: Dictionary of parameters to validate. The keys are parameter names
186+
and the values are the expected types.
187+
:param allow_empty: Allow empty values for parameters. Defaults to False.
188+
189+
:raises flask_utils.errors.badrequest.BadRequestError: If the JSON body is malformed,
190+
the Content-Type header is missing or incorrect, required parameters are missing,
191+
or parameters are of the wrong type.
192+
193+
:Example:
194+
195+
.. code-block:: python
196+
197+
from flask import Flask, request
198+
from typing import List, Dict
199+
from flask_utils.decorators import validate_params
200+
from flask_utils.errors.badrequest import BadRequestError
201+
202+
app = Flask(__name__)
203+
204+
@app.route("/example", methods=["POST"])
205+
@validate_params(
206+
{
207+
"name": str,
208+
"age": int,
209+
"is_student": bool,
210+
"courses": List[str],
211+
"grades": Dict[str, int],
212+
}
213+
)
214+
def example():
215+
\"""
216+
This route expects a JSON body with the following:
217+
- name: str
218+
- age: int (optional)
219+
- is_student: bool
220+
- courses: list of str
221+
- grades: dict with str keys and int values
222+
\"""
223+
data = request.get_json()
224+
return data
225+
226+
.. tip::
227+
You can use any of the following types:
228+
* str
229+
* int
230+
* float
231+
* bool
232+
* List
233+
* Dict
234+
* Any
235+
* Optional
236+
* Union
84237
85-
Args:
86-
parameters (Dict[Any, Any]): Dictionary of parameters to validate.
87-
allow_empty (bool, optional): Allow empty values for parameters.
238+
.. versionadded:: 0.2.0
88239
"""
89240

90241
def decorator(fn):
@@ -106,7 +257,7 @@ def wrapper(*args, **kwargs):
106257
raise BadRequestError("JSON body must be a dict")
107258

108259
for key, type_hint in parameters.items():
109-
if not is_optional(type_hint) and key not in data:
260+
if not _is_optional(type_hint) and key not in data:
110261
raise BadRequestError(f"Missing key: {key}", f"Expected keys are: {parameters.keys()}")
111262

112263
for key in data:
@@ -117,7 +268,7 @@ def wrapper(*args, **kwargs):
117268
)
118269

119270
for key in data:
120-
if key in parameters and not check_type(data[key], parameters[key], allow_empty):
271+
if key in parameters and not _check_type(data[key], parameters[key], allow_empty):
121272
raise BadRequestError(f"Wrong type for key {key}.", f"It should be {parameters[key]}")
122273

123274
return fn(*args, **kwargs)

0 commit comments

Comments
 (0)