|
| 1 | +import sys |
1 | 2 | from datetime import datetime |
2 | 3 | from enum import IntEnum |
| 4 | +from typing import Optional |
3 | 5 |
|
| 6 | +import pytest |
4 | 7 | from pydantic import BaseModel, Field |
5 | 8 |
|
6 | 9 | from ninja import NinjaAPI, Query, Schema |
7 | 10 | from ninja.testing.client import TestClient |
8 | 11 |
|
| 12 | +PY_310 = sys.version_info >= (3, 10) |
| 13 | + |
9 | 14 |
|
10 | 15 | class Range(IntEnum): |
11 | 16 | TWENTY = 20 |
@@ -163,3 +168,87 @@ def test_schema_all_of_no_ref(): |
163 | 168 | "default": 1, |
164 | 169 | "allOf": [{"title": "Best Type Ever!"}, {"no-ref-here": "xyzzy"}], |
165 | 170 | } |
| 171 | + |
| 172 | + |
| 173 | +@pytest.mark.skipif(not PY_310, reason="requires Python 3.10+ pipe syntax") |
| 174 | +def test_unwrap_union_model_no_model(): |
| 175 | + """_unwrap_union_model returns input as-is when union has no pydantic model.""" |
| 176 | + from ninja.signature.details import _unwrap_union_model |
| 177 | + |
| 178 | + annotation = int | None |
| 179 | + assert _unwrap_union_model(annotation) is annotation |
| 180 | + assert _unwrap_union_model(str) is str |
| 181 | + |
| 182 | + |
| 183 | +def test_optional_query_schema(): |
| 184 | + """Optional[Model] in Query should not crash (issue #1634).""" |
| 185 | + |
| 186 | + class MyFilter(Schema): |
| 187 | + name: str = "default" |
| 188 | + |
| 189 | + temp_api = NinjaAPI() |
| 190 | + |
| 191 | + @temp_api.get("/opt") |
| 192 | + def view(request, f: Optional[MyFilter] = Query(None)): |
| 193 | + if f: |
| 194 | + return f.model_dump() |
| 195 | + return {} |
| 196 | + |
| 197 | + client = TestClient(temp_api) |
| 198 | + |
| 199 | + resp = client.get("/opt?name=hello") |
| 200 | + assert resp.status_code == 200 |
| 201 | + assert resp.json() == {"name": "hello"} |
| 202 | + |
| 203 | + resp = client.get("/opt") |
| 204 | + assert resp.status_code == 200 |
| 205 | + |
| 206 | + |
| 207 | +@pytest.mark.skipif(not PY_310, reason="requires Python 3.10+ pipe syntax") |
| 208 | +def test_union_pipe_syntax_query_schema(): |
| 209 | + """Model | None in Query should not crash (issue #1634, Python 3.10+ pipe syntax).""" |
| 210 | + |
| 211 | + class MyFilter(Schema): |
| 212 | + name: str = "default" |
| 213 | + |
| 214 | + temp_api = NinjaAPI() |
| 215 | + |
| 216 | + @temp_api.get("/pipe") |
| 217 | + def view(request, f: MyFilter | None = Query(None)): |
| 218 | + if f: |
| 219 | + return f.model_dump() |
| 220 | + return {} |
| 221 | + |
| 222 | + client = TestClient(temp_api) |
| 223 | + |
| 224 | + resp = client.get("/pipe?name=world") |
| 225 | + assert resp.status_code == 200 |
| 226 | + assert resp.json() == {"name": "world"} |
| 227 | + |
| 228 | + resp = client.get("/pipe") |
| 229 | + assert resp.status_code == 200 |
| 230 | + |
| 231 | + |
| 232 | +@pytest.mark.skipif(not PY_310, reason="requires Python 3.10+ pipe syntax") |
| 233 | +def test_nested_optional_query_schema(): |
| 234 | + """Nested optional model fields should also work.""" |
| 235 | + |
| 236 | + class Inner(Schema): |
| 237 | + value: Optional[int] = 0 |
| 238 | + items: list[str] = [] |
| 239 | + |
| 240 | + class Outer(Schema): |
| 241 | + inner: Inner | None = None |
| 242 | + label: str = "x" |
| 243 | + |
| 244 | + temp_api = NinjaAPI() |
| 245 | + |
| 246 | + @temp_api.get("/nested") |
| 247 | + def view(request, f: Outer = Query(...)): |
| 248 | + return f.model_dump() |
| 249 | + |
| 250 | + client = TestClient(temp_api) |
| 251 | + |
| 252 | + resp = client.get("/nested?label=test") |
| 253 | + assert resp.status_code == 200 |
| 254 | + assert resp.json()["label"] == "test" |
0 commit comments