diff --git a/docs/middleware/pydantic.md b/docs/middleware/pydantic.md index 774cec4..4ff5c90 100644 --- a/docs/middleware/pydantic.md +++ b/docs/middleware/pydantic.md @@ -9,6 +9,9 @@ app = SpiderwebRouter( ``` When working with form data, you may not want to always have to perform your own validation on the incoming data. Spiderweb gives you a way out of the box to perform this validation using Pydantic. +> [!WARNING] +> Pydantic is not installed by default. Install it with `pip install pydantic` or `pip install spiderweb[pydantic]`. + Let's assume that we have a form view that looks like this: ```python @@ -54,4 +57,4 @@ The Pydantic middleware will automatically detect that the model that you want t If the validation fails, the middleware will call `on_error`, which by default will return a 400 with a list of the broken fields. You may not want this behavior, so the easiest way to address it is to subclass PydanticMiddleware with your own version and override `on_error` to do whatever you'd like. -If validation succeeds, the data from the validator will appear on the request object under `request.validated_data` — to access it, just call `.dict()` on the validated data. \ No newline at end of file +If validation succeeds, the data from the validator will appear on the request object under `request.validated_data` — to access it, just call `.dict()` on the validated data. diff --git a/pyproject.toml b/pyproject.toml index 88ee2ee..33ca90a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,10 @@ peewee = "^3.17.6" jinja2 = "^3.1.4" cryptography = "^43.0.0" email-validator = "^2.2.0" -pydantic = "^2.8.2" + +[tool.poetry.extras] +# Optional dependencies groups +pydantic = ["pydantic>=2.8.2,<3"] [tool.poetry.group.dev.dependencies] ruff = "^0.5.5" @@ -85,4 +88,4 @@ exclude_also = [ "if TYPE_CHECKING:", ] -ignore_errors = true \ No newline at end of file +ignore_errors = true diff --git a/spiderweb/middleware/pydantic.py b/spiderweb/middleware/pydantic.py index 45bbf00..6fca4c7 100644 --- a/spiderweb/middleware/pydantic.py +++ b/spiderweb/middleware/pydantic.py @@ -1,8 +1,26 @@ import inspect from typing import get_type_hints -from pydantic import BaseModel -from pydantic_core._pydantic_core import ValidationError +try: # pragma: no cover - import guard + from pydantic import BaseModel # type: ignore + from pydantic_core._pydantic_core import ValidationError # type: ignore + PYDANTIC_AVAILABLE = True +except Exception: # pragma: no cover - executed only when pydantic isn't installed + PYDANTIC_AVAILABLE = False + + class BaseModel: # minimal stub to allow module import without pydantic + @classmethod + def parse_obj(cls, *args, **kwargs): # noqa: D401 - simple shim + raise RuntimeError( + "Pydantic is not installed. Install with 'pip install" + " spiderweb-framework[pydantic]' or 'pip install pydantic'" + " to use PydanticMiddleware." + ) + + class ValidationError(Exception): # simple stand-in so type hints resolve + def errors(self): # match pydantic's ValidationError API used below + return [] + from spiderweb import SpiderwebMiddleware from spiderweb.request import Request from spiderweb.response import JsonResponse @@ -19,6 +37,12 @@ class PydanticMiddleware(SpiderwebMiddleware): def process_request(self, request): if not request.method == "POST": return + if not PYDANTIC_AVAILABLE: + raise RuntimeError( + "Pydantic is not installed. Install with 'pip install" + " spiderweb-framework[pydantic]' or 'pip install pydantic'" + " to use PydanticMiddleware." + ) types = get_type_hints(request.handler) # we don't know what the user named the request object, but # we know that it's first in the list, and it's always an arg. @@ -34,7 +58,15 @@ class PydanticMiddleware(SpiderwebMiddleware): # Separated out into its own method so that it can be overridden errors = e.errors() error_dict = {"message": "Validation error", "errors": []} - # [{'type': 'missing', 'loc': ('comment',), 'msg': 'Field required', 'input': {'email': 'a@a.com'}, 'url': 'https://errors.pydantic.dev/2.8/v/missing'}] + # [ + # { + # 'type': 'missing', + # 'loc': ('comment',), + # 'msg': 'Field required', + # 'input': {'email': 'a@a.com'}, + # 'url': 'https://errors.pydantic.dev/2.8/v/missing' + # } + # ] for error in errors: field = error["loc"][0] msg = error["msg"]