Skip to content

Commit cb5cbd4

Browse files
authored
Merge pull request #56 from Ge0rg3/35-conversion-of-empty-string-to-none
Implement blank_none option, satisfying #35
2 parents 5876281 + 3f50227 commit cb5cbd4

File tree

8 files changed

+284
-59
lines changed

8 files changed

+284
-59
lines changed

.github/workflows/python-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ name: Flask-Parameter-Validation Unit Tests
55

66
on:
77
push:
8-
branches: [ "master", "github-ci" ]
8+
branches: [ "master" ]
99
pull_request:
1010
branches: [ "master" ]
1111

README.md

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -142,26 +142,27 @@ These can be used in tandem to describe a parameter to validate: `parameter_name
142142
### Validation with arguments to Parameter
143143
Validation beyond type-checking can be done by passing arguments into the constructor of the `Parameter` subclass. The arguments available for use on each type hint are:
144144

145-
| Parameter Name | Type of Argument | Effective On Types | Description |
146-
|-------------------|--------------------------------------------------|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
147-
| `default` | any | All, except in `Route` | Specifies the default value for the field, makes non-Optional fields not required |
148-
| `min_str_length` | `int` | `str` | Specifies the minimum character length for a string input |
149-
| `max_str_length` | `int` | `str` | Specifies the maximum character length for a string input |
150-
| `min_list_length` | `int` | `list` | Specifies the minimum number of elements in a list |
151-
| `max_list_length` | `int` | `list` | Specifies the maximum number of elements in a list |
152-
| `min_int` | `int` | `int` | Specifies the minimum number for an integer input |
153-
| `max_int` | `int` | `int` | Specifies the maximum number for an integer input |
154-
| `whitelist` | `str` | `str` | A string containing allowed characters for the value |
155-
| `blacklist` | `str` | `str` | A string containing forbidden characters for the value |
156-
| `pattern` | `str` | `str` | A regex pattern to test for string matches |
157-
| `func` | `Callable[Any] -> Union[bool, tuple[bool, str]]` | All | A function containing a fully customized logic to validate the value. See the [custom validation function](#custom-validation-function) below for usage |
158-
| `datetime_format` | `str` | `datetime.datetime` | Python datetime format string datetime format string ([datetime format codes](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes)) |
159-
| `comment` | `str` | All | A string to display as the argument description in any generated documentation |
160-
| `alias` | `str` | All but `FileStorage` | An expected parameter name to receive instead of the function name. |
161-
| `json_schema` | `dict` | `dict` | An expected [JSON Schema](https://json-schema.org) which the dict input must conform to |
162-
| `content_types` | `list[str]` | `FileStorage` | Allowed `Content-Type`s |
163-
| `min_length` | `int` | `FileStorage` | Minimum `Content-Length` for a file |
164-
| `max_length` | `int` | `FileStorage` | Maximum `Content-Length` for a file |
145+
| Parameter Name | Type of Argument | Effective On Types | Description |
146+
|-------------------|--------------------------------------------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
147+
| `default` | any | All, except in `Route` | Specifies the default value for the field, makes non-Optional fields not required |
148+
| `min_str_length` | `int` | `str` | Specifies the minimum character length for a string input |
149+
| `max_str_length` | `int` | `str` | Specifies the maximum character length for a string input |
150+
| `min_list_length` | `int` | `list` | Specifies the minimum number of elements in a list |
151+
| `max_list_length` | `int` | `list` | Specifies the maximum number of elements in a list |
152+
| `min_int` | `int` | `int` | Specifies the minimum number for an integer input |
153+
| `max_int` | `int` | `int` | Specifies the maximum number for an integer input |
154+
| `whitelist` | `str` | `str` | A string containing allowed characters for the value |
155+
| `blacklist` | `str` | `str` | A string containing forbidden characters for the value |
156+
| `pattern` | `str` | `str` | A regex pattern to test for string matches |
157+
| `func` | `Callable[Any] -> Union[bool, tuple[bool, str]]` | All | A function containing a fully customized logic to validate the value. See the [custom validation function](#custom-validation-function) below for usage |
158+
| `datetime_format` | `str` | `datetime.datetime` | Python datetime format string datetime format string ([datetime format codes](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes)) |
159+
| `comment` | `str` | All | A string to display as the argument description in any generated documentation |
160+
| `alias` | `str` | All but `FileStorage` | An expected parameter name to receive instead of the function name. |
161+
| `json_schema` | `dict` | `dict` | An expected [JSON Schema](https://json-schema.org) which the dict input must conform to |
162+
| `content_types` | `list[str]` | `FileStorage` | Allowed `Content-Type`s |
163+
| `min_length` | `int` | `FileStorage` | Minimum `Content-Length` for a file |
164+
| `max_length` | `int` | `FileStorage` | Maximum `Content-Length` for a file |
165+
| `blank_none` | `bool` | `Optional[str]` | If `True`, an empty string will be converted to `None`, defaults to configured `FPV_BLANK_NONE`, see [Validation Behavior Configuration](#validation-behavior-configuration) for more |
165166

166167
These validators are passed into the `Parameter` subclass in the route function, such as:
167168
* `username: str = Json(default="defaultusername", min_length=5)`
@@ -183,18 +184,25 @@ def is_odd(val: int):
183184
return val % 2 != 0, "val must be odd"
184185
```
185186

186-
### API Documentation
187-
Using the data provided through parameters, docstrings, and Flask route registrations, Flask Parameter Validation can generate API Documentation in various formats.
188-
To make this easy to use, it comes with a `Blueprint` and the output and configuration options below:
187+
### Configuration Options
189188

190-
#### Format
189+
#### API Documentation Configuration
191190
* `FPV_DOCS_SITE_NAME: str`: Your site's name, to be displayed in the page title, default: `Site`
192191
* `FPV_DOCS_CUSTOM_BLOCKS: array`: An array of dicts to display as cards at the top of your documentation, with the (optional) keys:
193192
* `title: Optional[str]`: The title of the card
194193
* `body: Optional[str] (HTML allowed)`: The body of the card
195194
* `order: int`: The order in which to display this card (out of the other custom cards)
196195
* `FPV_DOCS_DEFAULT_THEME: str`: The default theme to display in the generated webpage
197196

197+
See the [API Documentation](#api-documentation) below for other information on API Documentation generation
198+
199+
#### Validation Behavior Configuration
200+
* `FPV_BLANK_NONE: bool`: Globally override the default `blank_none` behavior for routes in your application, defaults to `False` if unset
201+
202+
### API Documentation
203+
Using the data provided through parameters, docstrings, and Flask route registrations, Flask Parameter Validation can generate API Documentation in various formats.
204+
To make this easy to use, it comes with a `Blueprint` and the output shown below and configuration options [above](#api-documentation-configuration):
205+
198206
#### Included Blueprint
199207
The documentation blueprint can be added using the following code:
200208
```py

flask_parameter_validation/parameter_types/parameter.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
import dateutil.parser as parser
99
import jsonschema
1010
from jsonschema.exceptions import ValidationError as JSONSchemaValidationError
11-
11+
import flask
12+
from inspect import isclass
1213

1314
class Parameter:
1415

@@ -31,6 +32,7 @@ def __init__(
3132
comment=None, # str: comment for autogenerated documentation
3233
alias=None, # str: alias for parameter name
3334
json_schema=None, # dict: JSON Schema to check received dicts or lists against
35+
blank_none=None, # bool: Whether blank strings should be converted to None when validating a type of Optional[str]
3436
):
3537
self.default = default
3638
self.min_list_length = min_list_length
@@ -47,6 +49,7 @@ def __init__(
4749
self.comment = comment
4850
self.alias = alias
4951
self.json_schema = json_schema
52+
self.blank_none = blank_none
5053

5154
def func_helper(self, v):
5255
func_result = self.func(v)
@@ -156,6 +159,10 @@ def validate(self, value):
156159

157160
def convert(self, value, allowed_types):
158161
"""Some parameter types require manual type conversion (see Query)"""
162+
blank_none = self.blank_none
163+
if blank_none is None: # Default blank_none to False if not provided or set in app config
164+
blank_none = False if "FPV_BLANK_NONE" not in flask.current_app.config else flask.current_app.config["FPV_BLANK_NONE"]
165+
159166
# Datetime conversion
160167
if None in allowed_types and value is None:
161168
return value
@@ -183,9 +190,13 @@ def convert(self, value, allowed_types):
183190
return date.fromisoformat(str(value))
184191
except ValueError:
185192
raise ValueError("date format does not match ISO 8601")
186-
elif len(allowed_types) == 1 and (issubclass(allowed_types[0], str) or issubclass(allowed_types[0], int) and issubclass(allowed_types[0], Enum)):
187-
if issubclass(allowed_types[0], int):
188-
value = int(value)
189-
returning = allowed_types[0](value)
190-
return returning
193+
elif blank_none and type(None) in allowed_types and str in allowed_types and type(value) is str and len(value) == 0:
194+
return None
195+
elif any(isclass(allowed_type) and (issubclass(allowed_type, str) or issubclass(allowed_type, int) and issubclass(allowed_type, Enum)) for allowed_type in allowed_types):
196+
for allowed_type in allowed_types:
197+
if issubclass(allowed_type, Enum):
198+
if issubclass(allowed_types[0], int):
199+
value = int(value)
200+
returning = allowed_types[0](value)
201+
return returning
191202
return value

flask_parameter_validation/parameter_validation.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __call__(self, f):
4545
def nested_func_helper(**kwargs):
4646
"""
4747
Validates the inputs of a Flask route or returns an error. Returns
48-
are wrapped in a dictionary with a flag to let nested_func() know
48+
are wrapped in a dictionary with a flag to let nested_func() know
4949
if it should unpack the resulting dictionary of inputs as kwargs,
5050
or just return the error message.
5151
"""
@@ -199,12 +199,26 @@ def validate(self, expected_input, all_request_inputs):
199199

200200
# In python3.7+, typing.Optional is used instead of typing.Union[..., None]
201201
if expected_input_type_str.startswith("typing.Optional"):
202-
new_type = expected_input_type.__args__[0]
203-
expected_input_type = new_type
204-
expected_input_type_str = str(new_type)
205-
202+
expected_input_types = expected_input_type.__args__
203+
user_inputs = [user_input]
204+
# If typing.List in optional and user supplied valid list, convert remaining check only for list
205+
for exp_type in expected_input_types:
206+
if any(str(exp_type).startswith(list_hint) for list_hint in list_type_hints):
207+
if type(user_input) is list:
208+
if hasattr(exp_type, "__args__"):
209+
if all(type(inp) in exp_type.__args__ for inp in user_input):
210+
expected_input_type = exp_type
211+
expected_input_types = expected_input_type.__args__
212+
expected_input_type_str = str(exp_type)
213+
user_inputs = user_input
214+
elif int in exp_type.__args__: # Ints from list[str] sources haven't been converted yet, so give it a typecast for good measure
215+
if all(type(int(inp)) in exp_type.__args__ for inp in user_input):
216+
expected_input_type = exp_type
217+
expected_input_types = expected_input_type.__args__
218+
expected_input_type_str = str(exp_type)
219+
user_inputs = user_input
206220
# Prepare expected type checks for unions, lists and plain types
207-
if expected_input_type_str.startswith("typing.Union"):
221+
elif expected_input_type_str.startswith("typing.Union"):
208222
expected_input_types = expected_input_type.__args__
209223
user_inputs = [user_input]
210224
# If typing.List in union and user supplied valid list, convert remaining check only for list

0 commit comments

Comments
 (0)