|
2 | 2 | # pylint: disable=unused-argument |
3 | 3 | # pylint: disable=unused-variable |
4 | 4 | # pylint: disable=too-many-arguments |
5 | | -from typing import Annotated |
6 | 5 |
|
| 6 | +from typing import Annotated, Any |
| 7 | + |
| 8 | +import fastapi |
| 9 | +import pydantic |
| 10 | +import pytest |
7 | 11 | from fastapi import FastAPI |
8 | | -from pydantic import BaseModel, HttpUrl |
| 12 | +from pydantic import ( |
| 13 | + AnyHttpUrl, |
| 14 | + AnyUrl, |
| 15 | + BaseModel, |
| 16 | + HttpUrl, |
| 17 | + TypeAdapter, |
| 18 | + ValidationError, |
| 19 | +) |
9 | 20 | from simcore_service_api_server.models._utils_pydantic import UriSchema |
10 | 21 |
|
11 | 22 |
|
12 | | -def test_annotated_url_in_pydantic_changes_in_fastapi(): |
13 | | - class TestModel(BaseModel): |
14 | | - urls0: list[HttpUrl] |
15 | | - urls1: list[Annotated[HttpUrl, UriSchema()]] |
| 23 | +class _FakeModel(BaseModel): |
| 24 | + urls0: list[HttpUrl] |
| 25 | + urls1: list[Annotated[HttpUrl, UriSchema()]] |
| 26 | + |
| 27 | + # with and w/o |
| 28 | + url0: HttpUrl |
| 29 | + url1: Annotated[HttpUrl, UriSchema()] |
| 30 | + |
| 31 | + # # including None inside/outside annotated |
| 32 | + url2: Annotated[HttpUrl, UriSchema()] | None |
| 33 | + url3: Annotated[HttpUrl | None, UriSchema()] |
16 | 34 |
|
17 | | - # with and w/o |
18 | | - url0: HttpUrl |
19 | | - url1: Annotated[HttpUrl, UriSchema()] |
| 35 | + # # mistake |
| 36 | + int0: Annotated[int, UriSchema()] |
20 | 37 |
|
21 | | - # # including None inside/outside annotated |
22 | | - url2: Annotated[HttpUrl, UriSchema()] | None |
23 | | - url3: Annotated[HttpUrl | None, UriSchema()] |
24 | 38 |
|
25 | | - # # mistake |
26 | | - int0: Annotated[int, UriSchema()] |
| 39 | +@pytest.fixture |
| 40 | +def pydantic_schema() -> dict[str, Any]: |
| 41 | + return _FakeModel.model_json_schema() |
27 | 42 |
|
28 | | - pydantic_schema = TestModel.model_json_schema() |
29 | 43 |
|
| 44 | +def test_pydantic_json_schema(pydantic_schema: dict[str, Any]): |
30 | 45 | assert pydantic_schema["properties"] == { |
31 | 46 | "int0": {"title": "Int0", "type": "integer"}, |
32 | 47 | "url0": { |
@@ -79,14 +94,20 @@ class TestModel(BaseModel): |
79 | 94 | }, |
80 | 95 | } |
81 | 96 |
|
| 97 | + |
| 98 | +@pytest.fixture |
| 99 | +def fastapi_schema() -> dict[str, Any]: |
82 | 100 | app = FastAPI() |
83 | 101 |
|
84 | | - @app.get("/", response_model=TestModel) |
| 102 | + @app.get("/", response_model=_FakeModel) |
85 | 103 | def _h(): |
86 | 104 | ... |
87 | 105 |
|
88 | 106 | openapi = app.openapi() |
89 | | - fastapi_schema = openapi["components"]["schemas"]["TestModel"] |
| 107 | + return openapi["components"]["schemas"][_FakeModel.__name__] |
| 108 | + |
| 109 | + |
| 110 | +def test_fastapi_openapi_component_schemas(fastapi_schema: dict[str, Any]): |
90 | 111 |
|
91 | 112 | assert fastapi_schema["properties"] == { |
92 | 113 | "int0": {"title": "Int0", "type": "integer"}, |
@@ -119,5 +140,66 @@ def _h(): |
119 | 140 | }, |
120 | 141 | } |
121 | 142 |
|
| 143 | + |
| 144 | +@pytest.mark.xfail( |
| 145 | + reason=f"{pydantic.__version__=} and {fastapi.__version__=} produce different json-schemas for the same model" |
| 146 | +) |
| 147 | +def test_compare_pydantic_vs_fastapi_schemas( |
| 148 | + fastapi_schema: dict[str, Any], pydantic_schema: dict[str, Any] |
| 149 | +): |
| 150 | + |
122 | 151 | # NOTE @all: I cannot understand this?! |
123 | 152 | assert fastapi_schema["properties"] != pydantic_schema["properties"] |
| 153 | + |
| 154 | + |
| 155 | +def test_differences_between_new_pydantic_url_types(): |
| 156 | + # SEE https://docs.pydantic.dev/2.10/api/networks/ |
| 157 | + |
| 158 | + # | **URL** | **AnyUrl** | **HttpUrl** | **AnyHttpUrl** | |
| 159 | + # |-------------------------------|------------|-------------|----------------| |
| 160 | + # | `http://example.com` | ✅ | ✅ | ✅ | |
| 161 | + # | `https://example.com/resource`| ✅ | ✅ | ✅ | |
| 162 | + # | `ftp://example.com` | ✅ | ❌ | ❌ | |
| 163 | + # | `http://localhost` | ✅ | ✅ | ✅ | |
| 164 | + # | `http://127.0.0.1` | ✅ | ✅ | ✅ | |
| 165 | + # | `http://127.0.0.1:8080` | ✅ | ✅ | ✅ | |
| 166 | + # | `customscheme://example.com` | ✅ | ❌ | ❌ | |
| 167 | + |
| 168 | + url = "http://example.com" |
| 169 | + TypeAdapter(AnyUrl).validate_python(url) |
| 170 | + TypeAdapter(HttpUrl).validate_python(url) |
| 171 | + TypeAdapter(AnyHttpUrl).validate_python(url) |
| 172 | + |
| 173 | + url = "https://example.com/resource" |
| 174 | + TypeAdapter(AnyUrl).validate_python(url) |
| 175 | + TypeAdapter(HttpUrl).validate_python(url) |
| 176 | + TypeAdapter(AnyHttpUrl).validate_python(url) |
| 177 | + |
| 178 | + url = "ftp://example.com" |
| 179 | + TypeAdapter(AnyUrl).validate_python(url) |
| 180 | + with pytest.raises(ValidationError): |
| 181 | + TypeAdapter(HttpUrl).validate_python(url) |
| 182 | + with pytest.raises(ValidationError): |
| 183 | + TypeAdapter(AnyHttpUrl).validate_python(url) |
| 184 | + |
| 185 | + url = "http://localhost" |
| 186 | + TypeAdapter(AnyUrl).validate_python(url) |
| 187 | + TypeAdapter(HttpUrl).validate_python(url) |
| 188 | + TypeAdapter(AnyHttpUrl).validate_python(url) |
| 189 | + |
| 190 | + url = "http://127.0.0.1" |
| 191 | + TypeAdapter(AnyUrl).validate_python(url) |
| 192 | + TypeAdapter(HttpUrl).validate_python(url) |
| 193 | + TypeAdapter(AnyHttpUrl).validate_python(url) |
| 194 | + |
| 195 | + url = "http://127.0.0.1:8080" |
| 196 | + TypeAdapter(AnyUrl).validate_python(url) |
| 197 | + TypeAdapter(HttpUrl).validate_python(url) |
| 198 | + TypeAdapter(AnyHttpUrl).validate_python(url) |
| 199 | + |
| 200 | + url = "customscheme://example.com" |
| 201 | + TypeAdapter(AnyUrl).validate_python(url) |
| 202 | + with pytest.raises(ValidationError): |
| 203 | + TypeAdapter(HttpUrl).validate_python(url) |
| 204 | + with pytest.raises(ValidationError): |
| 205 | + TypeAdapter(AnyHttpUrl).validate_python(url) |
0 commit comments