Repository: sanic-org/sanic-ext
Pull request #251 (2024-06-09 20:33:51)

Fix coerce in Hint
A `None` of a `Optional[...]` type should not be coerced to be the non-None value.

Resolves https://github.com/sanic-org/sanic/issues/2794


Patch:
diff --git a/pyproject.toml b/pyproject.toml
index ced5c23..58cf7dc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,5 @@
 [build-system]
-requires = ["setuptools<60.0", "wheel"]
+requires = ["setuptools", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [tool.ruff]
diff --git a/sanic_ext/exceptions.py b/sanic_ext/exceptions.py
index f88b61c..c04230c 100644
--- a/sanic_ext/exceptions.py
+++ b/sanic_ext/exceptions.py
@@ -5,9 +5,7 @@ class ValidationError(SanicException):
     status_code = 400
 
 
-class InitError(SanicException):
-    ...
+class InitError(SanicException): ...
 
 
-class ExtensionNotFound(SanicException):
-    ...
+class ExtensionNotFound(SanicException): ...
diff --git a/sanic_ext/extensions/base.py b/sanic_ext/extensions/base.py
index ad5fa67..e5bf922 100644
--- a/sanic_ext/extensions/base.py
+++ b/sanic_ext/extensions/base.py
@@ -46,8 +46,7 @@ class Extension(ABC):
         self._started = True
 
     @abstractmethod
-    def startup(self, bootstrap) -> None:
-        ...
+    def startup(self, bootstrap) -> None: ...
 
     def label(self):
         return ""
diff --git a/sanic_ext/extensions/health/monitor.py b/sanic_ext/extensions/health/monitor.py
index d43d8d7..ba48669 100644
--- a/sanic_ext/extensions/health/monitor.py
+++ b/sanic_ext/extensions/health/monitor.py
@@ -17,8 +17,7 @@ if TYPE_CHECKING:
     from sanic import Sanic
 
 
-class Stale(ValueError):
-    ...
+class Stale(ValueError): ...
 
 
 @dataclass
diff --git a/sanic_ext/extensions/openapi/blueprint.py b/sanic_ext/extensions/openapi/blueprint.py
index c7cdbc6..89f8609 100644
--- a/sanic_ext/extensions/openapi/blueprint.py
+++ b/sanic_ext/extensions/openapi/blueprint.py
@@ -173,9 +173,9 @@ def blueprint_factory(config: Config):
                 ):
                     operation.autodoc(docstring)
 
-                operation._default[
-                    "operationId"
-                ] = f"{method.lower()}~{route_name}"
+                operation._default["operationId"] = (
+                    f"{method.lower()}~{route_name}"
+                )
                 operation._default["summary"] = clean_route_name(route_name)
 
                 if host:
diff --git a/sanic_ext/extensions/openapi/definitions.py b/sanic_ext/extensions/openapi/definitions.py
index e29c66f..0b93fa4 100644
--- a/sanic_ext/extensions/openapi/definitions.py
+++ b/sanic_ext/extensions/openapi/definitions.py
@@ -4,6 +4,7 @@ Classes defined from the OpenAPI 3.0 specifications.
 I.e., the objects described https://swagger.io/docs/specification
 
 """
+
 from __future__ import annotations
 
 from inspect import isclass
diff --git a/sanic_ext/extensions/openapi/openapi.py b/sanic_ext/extensions/openapi/openapi.py
index 6f0330b..8d26cec 100644
--- a/sanic_ext/extensions/openapi/openapi.py
+++ b/sanic_ext/extensions/openapi/openapi.py
@@ -3,6 +3,7 @@ This module provides decorators which append
 documentation to OperationStore() and components created in the blueprints.
 
 """
+
 from functools import wraps
 from inspect import isawaitable, isclass
 from typing import (
@@ -94,13 +95,11 @@ def _content_or_component(content):
 
 
 @overload
-def exclude(flag: bool = True, *, bp: Blueprint) -> None:
-    ...
+def exclude(flag: bool = True, *, bp: Blueprint) -> None: ...
 
 
 @overload
-def exclude(flag: bool = True) -> Callable:
-    ...
+def exclude(flag: bool = True) -> Callable: ...
 
 
 def exclude(flag: bool = True, *, bp: Optional[Blueprint] = None):
@@ -247,8 +246,7 @@ def parameter(
     *,
     parameter: definitions.Parameter,
     **kwargs,
-) -> Callable[[T], T]:
-    ...
+) -> Callable[[T], T]: ...
 
 
 @overload
@@ -258,8 +256,7 @@ def parameter(
     location: None,
     parameter: definitions.Parameter,
     **kwargs,
-) -> Callable[[T], T]:
-    ...
+) -> Callable[[T], T]: ...
 
 
 @overload
@@ -269,8 +266,7 @@ def parameter(
     location: Optional[str] = None,
     parameter: None = None,
     **kwargs,
-) -> Callable[[T], T]:
-    ...
+) -> Callable[[T], T]: ...
 
 
 def parameter(
diff --git a/sanic_ext/extensions/templating/render.py b/sanic_ext/extensions/templating/render.py
index 0650f9d..ce67066 100644
--- a/sanic_ext/extensions/templating/render.py
+++ b/sanic_ext/extensions/templating/render.py
@@ -16,8 +16,7 @@ if TYPE_CHECKING:
     from jinja2 import Environment
 
 
-class TemplateResponse(HTTPResponse):
-    ...
+class TemplateResponse(HTTPResponse): ...
 
 
 class LazyResponse(TemplateResponse):
diff --git a/sanic_ext/extras/validation/check.py b/sanic_ext/extras/validation/check.py
index b7c6150..fb3f9c8 100644
--- a/sanic_ext/extras/validation/check.py
+++ b/sanic_ext/extras/validation/check.py
@@ -141,6 +141,8 @@ class Hint(NamedTuple):
             try:
                 if isinstance(value, list):
                     value = [coerce_type(item) for item in value]
+                elif value is None and self.nullable:
+                    value = None
                 else:
                     value = coerce_type(value)
             except (ValueError, TypeError):


Test patch:
diff --git a/tests/extra/test_validation_dataclass.py b/tests/extra/test_validation_dataclass.py
index bf4ce8c..2a85ab3 100644
--- a/tests/extra/test_validation_dataclass.py
+++ b/tests/extra/test_validation_dataclass.py
@@ -340,6 +340,7 @@ def test_validate_form(app):
     class Pet:
         name: str
         alter_ego: List[str]
+        description: Optional[str] = None
 
     @app.post("/function")
     @validate(form=Pet)
@@ -348,6 +349,7 @@ def test_validate_form(app):
             {
                 "is_pet": isinstance(body, Pet),
                 "pet": {"name": body.name, "alter_ego": body.alter_ego},
+                "description": body.description if body.description else "",
             }
         )
 
@@ -359,6 +361,7 @@ def test_validate_form(app):
                 {
                     "is_pet": isinstance(body, Pet),
                     "pet": {"name": body.name, "alter_ego": body.alter_ego},
+                    "description": body.description if body.description else "",
                 }
             )
 
@@ -366,11 +369,13 @@ def test_validate_form(app):
     assert response.status == 200
     assert response.json["is_pet"]
     assert response.json["pet"] == SNOOPY_DATA
+    assert response.json["description"] == ""
 
     _, response = app.test_client.post("/method", data=SNOOPY_DATA)
     assert response.status == 200
     assert response.json["is_pet"]
     assert response.json["pet"] == SNOOPY_DATA
+    assert response.json["description"] == ""
 
 
 def test_validate_query(app):