Skip to content

Commit

Permalink
feat: maintain pydantic.v1 model compatibility while using pydantic v2 (
Browse files Browse the repository at this point in the history
#393)

* We have pydantic V2 installed in our environemnt, but have been using
`from pydantic.v1 import ...`.  With the new changes to support pydantic
V2 we are wanting to upgrade Spectree to latest and begin writing new
serializers, however, we are not ready to update all of our current
models to pydantic be pydantic v2 BaseModels.

Currently if a ValidationError is raised, the falcon plug in does not
catch it, since ValidationError in the plugin is coming from pydantic,
not pydantic.v1.

Since you are aliasing the pydantic.v1.ValidationError as
InternalValidationError, catching it as well allows us to continue
to support our v1 models while we migrate to v2 models.

Looking at the codebase, this looks like a similar issue in the other
plugins.  Let me know if you would like me to submit this update for
them as well.  I'm not sure where you would like a test for this in the
project.

* update other plugins

* test to verify functionality

* bump version

* update test

* make test routes local to test

* ruff formatting
  • Loading branch information
voidnologo authored Dec 13, 2024
1 parent 95aff9d commit 7160ae5
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 14 deletions.
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:
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

0 comments on commit 7160ae5

Please sign in to comment.