Skip to content

Commit

Permalink
feat: skip validation for request & response (#383)
Browse files Browse the repository at this point in the history
* feat: skip validation for request & response

Signed-off-by: Keming <[email protected]>

* fix lint

Signed-off-by: Keming <[email protected]>

* fix mypy

Signed-off-by: Keming <[email protected]>

---------

Signed-off-by: Keming <[email protected]>
  • Loading branch information
kemingy authored Nov 24, 2024
1 parent 175aa2d commit 78868d9
Show file tree
Hide file tree
Showing 21 changed files with 166 additions and 133 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,11 @@ You can change the `validation_error_status` in SpecTree (global) or a specific

> How can I skip the validation?
Add `skip_validation=True` to the decorator. For now, this only skip the response validation.
Add `skip_validation=True` to the decorator.

Before v1.3.0, this only skip the response validation.

Starts from v1.3.0, this will skip all the validations. As an result, you won't be able to access the validated data from `context`.

```py
@api.validate(json=Profile, resp=Response(HTTP_200=Message, HTTP_403=None), skip_validation=True)
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "spectree"
version = "1.2.11"
version = "1.3.0"
dynamic = []
description = "generate OpenAPI document and validate request&response with Python annotations."
readme = "README.md"
Expand Down Expand Up @@ -67,7 +67,7 @@ target-version = "py38"
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "B", "G", "I", "SIM", "TID", "PL", "RUF"]
ignore = ["E501", "PLR2004", "RUF012"]
ignore = ["E501", "PLR2004", "RUF012", "B009"]
[tool.ruff.lint.pylint]
max-args = 12
max-branches = 15
Expand Down
8 changes: 4 additions & 4 deletions spectree/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
from .spec import SpecTree

__all__ = [
"SpecTree",
"Response",
"Tag",
"SecurityScheme",
"BaseFile",
"ExternalDocs",
"Response",
"SecurityScheme",
"SecuritySchemeData",
"SpecTree",
"Tag",
]

# setup library logging
Expand Down
14 changes: 7 additions & 7 deletions spectree/_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@


__all__ = [
"BaseModel",
"ValidationError",
"Field",
"root_validator",
"AnyUrl",
"BaseModel",
"BaseSettings",
"EmailStr",
"validator",
"Field",
"ValidationError",
"is_base_model",
"is_base_model_instance",
"is_root_model",
"is_root_model_instance",
"root_validator",
"serialize_model_instance",
"is_base_model",
"is_base_model_instance",
"validator",
]

if PYDANTIC2:
Expand Down
2 changes: 1 addition & 1 deletion spectree/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
"starlette": Plugin(".starlette_plugin", __name__, "StarlettePlugin"),
}

__all__ = ["BasePlugin", "PLUGINS", "Plugin"]
__all__ = ["PLUGINS", "BasePlugin", "Plugin"]
52 changes: 28 additions & 24 deletions spectree/plugins/falcon_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,18 +208,20 @@ def validate(
# falcon endpoint method arguments: (self, req, resp)
_self, _req, _resp = args[:3]
req_validation_error, resp_validation_error = None, None
try:
self.request_validation(_req, query, json, form, headers, cookies)
if self.config.annotations:
annotations = get_type_hints(func)
for name in ("query", "json", "form", "headers", "cookies"):
if annotations.get(name):
kwargs[name] = getattr(_req.context, name)

except ValidationError as err:
req_validation_error = err
_resp.status = f"{validation_error_status} Validation Error"
_resp.media = err.errors()
if not skip_validation:
try:
self.request_validation(_req, query, json, form, headers, cookies)

except ValidationError as err:
req_validation_error = err
_resp.status = f"{validation_error_status} Validation Error"
_resp.media = err.errors()

if self.config.annotations:
annotations = get_type_hints(func)
for name in ("query", "json", "form", "headers", "cookies"):
if annotations.get(name):
kwargs[name] = getattr(_req.context, name, None)

before(_req, _resp, req_validation_error, _self)
if req_validation_error:
Expand Down Expand Up @@ -312,18 +314,20 @@ async def validate(
# falcon endpoint method arguments: (self, req, resp)
_self, _req, _resp = args[:3]
req_validation_error, resp_validation_error = None, None
try:
await self.request_validation(_req, query, json, form, headers, cookies)
if self.config.annotations:
annotations = get_type_hints(func)
for name in ("query", "json", "form", "headers", "cookies"):
if annotations.get(name):
kwargs[name] = getattr(_req.context, name)

except ValidationError as err:
req_validation_error = err
_resp.status = f"{validation_error_status} Validation Error"
_resp.media = err.errors()
if not skip_validation:
try:
await self.request_validation(_req, query, json, form, headers, cookies)

except ValidationError as err:
req_validation_error = err
_resp.status = f"{validation_error_status} Validation Error"
_resp.media = err.errors()

if self.config.annotations:
annotations = get_type_hints(func)
for name in ("query", "json", "form", "headers", "cookies"):
if annotations.get(name):
kwargs[name] = getattr(_req.context, name, None)

before(_req, _resp, req_validation_error, _self)
if req_validation_error:
Expand Down
24 changes: 14 additions & 10 deletions spectree/plugins/flask_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,16 +182,20 @@ def validate(
**kwargs: Any,
):
response, req_validation_error, resp_validation_error = None, None, None
try:
self.request_validation(request, query, json, form, headers, cookies)
if self.config.annotations:
annotations = get_type_hints(func)
for name in ("query", "json", "form", "headers", "cookies"):
if annotations.get(name):
kwargs[name] = getattr(request.context, name)
except ValidationError as err:
req_validation_error = err
response = make_response(jsonify(err.errors()), validation_error_status)
if not skip_validation:
try:
self.request_validation(request, query, json, form, headers, cookies)
except ValidationError as err:
req_validation_error = err
response = make_response(jsonify(err.errors()), validation_error_status)

if self.config.annotations:
annotations = get_type_hints(func)
for name in ("query", "json", "form", "headers", "cookies"):
if annotations.get(name):
kwargs[name] = getattr(
getattr(request, "context", None), name, None
)

before(request, response, req_validation_error, None)
if req_validation_error:
Expand Down
30 changes: 18 additions & 12 deletions spectree/plugins/quart_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,18 +190,24 @@ async def validate(
**kwargs: Any,
):
response, req_validation_error, resp_validation_error = None, None, None
try:
await self.request_validation(request, query, json, form, headers, cookies)
if self.config.annotations:
annotations = get_type_hints(func)
for name in ("query", "json", "form", "headers", "cookies"):
if annotations.get(name):
kwargs[name] = getattr(request.context, name)
except ValidationError as err:
req_validation_error = err
response = await make_response(
jsonify(err.errors()), validation_error_status
)
if not skip_validation:
try:
await self.request_validation(
request, query, json, form, headers, cookies
)
except ValidationError as err:
req_validation_error = err
response = await make_response(
jsonify(err.errors()), validation_error_status
)

if self.config.annotations:
annotations = get_type_hints(func)
for name in ("query", "json", "form", "headers", "cookies"):
if annotations.get(name):
kwargs[name] = getattr(
getattr(request, "context", None), name, None
)

before(request, response, req_validation_error, None)
if req_validation_error:
Expand Down
44 changes: 26 additions & 18 deletions spectree/plugins/starlette_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,24 +105,32 @@ async def validate(
response = None
req_validation_error = resp_validation_error = json_decode_error = None

try:
await self.request_validation(request, query, json, form, headers, cookies)
if self.config.annotations:
annotations = get_type_hints(func)
for name in ("query", "json", "form", "headers", "cookies"):
if annotations.get(name):
kwargs[name] = getattr(request.context, name)
except ValidationError as err:
req_validation_error = err
response = JSONResponse(err.errors(), validation_error_status)
except JSONDecodeError as err:
json_decode_error = err
self.logger.info(
"%s Validation Error",
validation_error_status,
extra={"spectree_json_decode_error": str(err)},
)
response = JSONResponse({"error_msg": str(err)}, validation_error_status)
if not skip_validation:
try:
await self.request_validation(
request, query, json, form, headers, cookies
)
except ValidationError as err:
req_validation_error = err
response = JSONResponse(err.errors(), validation_error_status)
except JSONDecodeError as err:
json_decode_error = err
self.logger.info(
"%s Validation Error",
validation_error_status,
extra={"spectree_json_decode_error": str(err)},
)
response = JSONResponse(
{"error_msg": str(err)}, validation_error_status
)

if self.config.annotations:
annotations = get_type_hints(func)
for name in ("query", "json", "form", "headers", "cookies"):
if annotations.get(name):
kwargs[name] = getattr(
getattr(request, "context", None), name, None
)

before(request, response, req_validation_error, instance)
if req_validation_error or json_decode_error:
Expand Down
11 changes: 11 additions & 0 deletions spectree/spec.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import warnings
from collections import defaultdict
from copy import deepcopy
from functools import wraps
Expand Down Expand Up @@ -167,13 +168,23 @@ def validate( # noqa: PLR0913 [too-many-arguments]
in :meth:`spectree.spec.SpecTree`.
:param path_parameter_descriptions: A dictionary of path parameter names and
their description.
:param skip_validation: If set to `True`, the endpoint will skip
request / response validations.
:param operation_id: a string override for operationId for the given endpoint
"""
# If the status code for validation errors is not overridden on the level of
# the view function, use the globally set status code for validation errors.
if validation_error_status == 0:
validation_error_status = self.validation_error_status

if self.config.annotations and skip_validation:
warnings.warn(
"`skip_validation` cannot be used with `annotations` enabled. The instances"
" of `json`, `headers`, `cookies`, etc. read from function will be `None`.",
UserWarning,
stacklevel=2,
)

def decorate_validation(func: Callable):
# for sync framework
@wraps(func)
Expand Down
4 changes: 2 additions & 2 deletions spectree/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def parse_params(
attr_to_spec_key = {"query": "query", "headers": "header", "cookies": "cookie"}
route_param_keywords = ("explode", "style", "allowReserved")

for attr in attr_to_spec_key:
for attr, position in attr_to_spec_key.items():
if hasattr(func, attr):
model = models[getattr(func, attr)]
properties = model.get("properties", {model.get("title"): model})
Expand All @@ -125,7 +125,7 @@ def parse_params(
params.append(
{
"name": name,
"in": attr_to_spec_key[attr],
"in": position,
"schema": schema,
"required": name in model.get("required", []),
"description": schema.get("description", ""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -892,11 +892,6 @@
"schema": {
"$ref": "#/components/schemas/JSON.7068f62"
}
},
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Form.7068f62"
}
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import warnings
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from enum import Enum, IntEnum
Expand All @@ -7,6 +8,9 @@
from spectree._pydantic import BaseModel, Field, root_validator
from spectree.utils import hash_module_path

# suppress warnings
warnings.filterwarnings("ignore", category=UserWarning)

api_tag = Tag(
name="API", description="🐱", externalDocs=ExternalDocs(url="https://pypi.org")
)
Expand Down
18 changes: 9 additions & 9 deletions tests/flask_imports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@
)

__all__ = [
"test_flask_return_model",
"test_flask_skip_validation",
"test_flask_validation_error_response_status_code",
"test_flask_doc",
"test_flask_optional_alias_response",
"test_flask_validate_post_data",
"test_flask_no_response",
"test_flask_upload_file",
"test_flask_list_json_request",
"test_flask_return_list_request",
"test_flask_make_response_post",
"test_flask_make_response_get",
"test_flask_make_response_post",
"test_flask_no_response",
"test_flask_optional_alias_response",
"test_flask_return_list_request",
"test_flask_return_model",
"test_flask_skip_validation",
"test_flask_upload_file",
"test_flask_validate_post_data",
"test_flask_validation_error_response_status_code",
]
6 changes: 3 additions & 3 deletions tests/quart_imports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
)

__all__ = [
"test_quart_doc",
"test_quart_no_response",
"test_quart_return_model",
"test_quart_skip_validation",
"test_quart_validation_error_response_status_code",
"test_quart_doc",
"test_quart_validate",
"test_quart_no_response",
"test_quart_validation_error_response_status_code",
]
Loading

0 comments on commit 78868d9

Please sign in to comment.