Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maintain pydantic.v1 model compatibility while using pydantic v2 #393

Merged
merged 7 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "spectree"
version = "1.4.1"
version = "1.4.2"
dynamic = []
description = "generate OpenAPI document and validate request&response with Python annotations."
readme = "README.md"
Expand Down
1 change: 1 addition & 0 deletions spectree/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class License(InternalBaseModel):

class Configuration(InternalBaseModel):
"""Global configuration."""

# OpenAPI configurations
#: title of the service
title: str = "Service API Document"
Expand Down
10 changes: 5 additions & 5 deletions spectree/plugins/falcon_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from falcon import HTTP_400, HTTP_415, HTTPError
from falcon.routing.compiled import _FIELD_PATTERN as FALCON_FIELD_PATTERN

from spectree._pydantic import ValidationError
from spectree._pydantic import InternalValidationError, ValidationError
from spectree._types import ModelType
from spectree.plugins.base import BasePlugin, validate_response
from spectree.response import Response
Expand Down Expand Up @@ -211,7 +211,7 @@ def validate(
try:
self.request_validation(_req, query, json, form, headers, cookies)

except ValidationError as err:
except (InternalValidationError, ValidationError) as err:
req_validation_error = err
_resp.status = f"{validation_error_status} Validation Error"
_resp.media = err.errors()
Expand All @@ -235,7 +235,7 @@ def validate(
validation_model=resp.find_model(status),
response_payload=_resp.media,
)
except ValidationError as err:
except (InternalValidationError, ValidationError) as err:
resp_validation_error = err
_resp.status = HTTP_500
_resp.media = err.errors()
Expand Down Expand Up @@ -317,7 +317,7 @@ async def validate(
try:
await self.request_validation(_req, query, json, form, headers, cookies)

except ValidationError as err:
except (InternalValidationError, ValidationError) as err:
req_validation_error = err
_resp.status = f"{validation_error_status} Validation Error"
_resp.media = err.errors()
Expand Down Expand Up @@ -345,7 +345,7 @@ async def validate(
validation_model=resp.find_model(status) if resp else None,
response_payload=_resp.media,
)
except ValidationError as err:
except (InternalValidationError, ValidationError) as err:
resp_validation_error = err
_resp.status = HTTP_500
_resp.media = err.errors()
Expand Down
6 changes: 3 additions & 3 deletions spectree/plugins/flask_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from flask import Blueprint, abort, current_app, jsonify, make_response, request
from werkzeug.routing import parse_converter_args

from spectree._pydantic import ValidationError
from spectree._pydantic import InternalValidationError, ValidationError
from spectree._types import ModelType
from spectree.plugins.base import BasePlugin, Context, validate_response
from spectree.response import Response
Expand Down Expand Up @@ -185,7 +185,7 @@ def validate(
if not skip_validation:
try:
self.request_validation(request, query, json, form, headers, cookies)
except ValidationError as err:
except (InternalValidationError, ValidationError) as err:
req_validation_error = err
response = make_response(jsonify(err.errors()), validation_error_status)

Expand Down Expand Up @@ -223,7 +223,7 @@ def validate(
validation_model=resp.find_model(status),
response_payload=payload,
)
except ValidationError as err:
except (InternalValidationError, ValidationError) as err:
response = make_response(err.errors(), 500)
resp_validation_error = err
else:
Expand Down
6 changes: 3 additions & 3 deletions spectree/plugins/quart_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from quart import Blueprint, abort, current_app, jsonify, make_response, request
from werkzeug.routing import parse_converter_args

from spectree._pydantic import ValidationError
from spectree._pydantic import InternalValidationError, ValidationError
from spectree._types import ModelType
from spectree.plugins.base import BasePlugin, Context, validate_response
from spectree.response import Response
Expand Down Expand Up @@ -195,7 +195,7 @@ async def validate(
await self.request_validation(
request, query, json, form, headers, cookies
)
except ValidationError as err:
except (InternalValidationError, ValidationError) as err:
req_validation_error = err
response = await make_response(
jsonify(err.errors()), validation_error_status
Expand Down Expand Up @@ -240,7 +240,7 @@ async def validate(
validation_model=resp.find_model(status),
response_payload=payload,
)
except ValidationError as err:
except (InternalValidationError, ValidationError) as err:
response = await make_response(err.errors(), 500)
resp_validation_error = err
else:
Expand Down
5 changes: 3 additions & 2 deletions spectree/plugins/starlette_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from starlette.routing import compile_path

from spectree._pydantic import (
InternalValidationError,
ValidationError,
generate_root_model,
serialize_model_instance,
Expand Down Expand Up @@ -114,7 +115,7 @@ async def validate(
await self.request_validation(
request, query, json, form, headers, cookies
)
except ValidationError as err:
except (InternalValidationError, ValidationError) as err:
req_validation_error = err
response = JSONResponse(err.errors(), validation_error_status)
except JSONDecodeError as err:
Expand Down Expand Up @@ -160,7 +161,7 @@ async def validate(
validation_model=resp.find_model(response.status_code),
response_payload=RawResponsePayload(payload=response.body),
)
except ValidationError as err:
except (InternalValidationError, ValidationError) as err:
response = JSONResponse(err.errors(), 500)
resp_validation_error = err

Expand Down
42 changes: 42 additions & 0 deletions tests/test_plugin_falcon.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
from falcon import HTTP_202, App, testing

from spectree import Response, SpecTree
from spectree._pydantic import (
PYDANTIC2,
BaseModel,
InternalBaseModel,
)

from .common import (
JSON,
Expand Down Expand Up @@ -545,3 +550,40 @@ def test_falcon_optional_alias_response(client):
)
assert resp.status_code == 200, resp.json
assert resp.json == {"schema": "test"}, resp.json


@pytest.mark.skipif(not PYDANTIC2, reason="only matters if using both model types")
def test_falcon_validate_both_v1_and_v2_validation_errors(client):
class CompatibilityView:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inlined this view because adding it to the test app then causes issue with the generated spec tests.

name = "validation works for both pydantic v1 and v2 models simultaneously"

class V1(InternalBaseModel):
value: int

class V2(BaseModel):
value: int

@api.validate(
resp=Response(HTTP_200=Resp),
)
def on_post_v1(self, req, resp, json: V1):
resp.media = Resp(name="falcon v1", score=[1, 2, 3])

@api.validate(
resp=Response(HTTP_200=Resp),
)
def on_post_v2(self, req, resp, json: V2):
resp.media = Resp(name="falcon v2", score=[1, 2, 3])

app.add_route("/api/compatibility/v1", CompatibilityView(), suffix="v1")
app.add_route("/api/compatibility/v2", CompatibilityView(), suffix="v2")

resp = client.simulate_request(
"POST", "/api/compatibility/v1", json={"value": "invalid"}
)
assert resp.status_code == 422

resp = client.simulate_request(
"POST", "/api/compatibility/v2", json={"value": "invalid"}
)
assert resp.status_code == 422
Loading