Skip to content

Commit 4ea4d3f

Browse files
authored
Merge pull request #54 from team23/dev/fix-current-bugs
fix: Fix outstanding bugs, see #49 and #52
2 parents df830d6 + d53a008 commit 4ea4d3f

File tree

5 files changed

+88
-16
lines changed

5 files changed

+88
-16
lines changed

pydantic_partial/partial.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ class Something(PartialModelMixin, pydantic.BaseModel):
3737

3838
import pydantic
3939

40-
NULLABLE_KWARGS = {"json_schema_extra": {"nullable": True, "required": False}}
41-
4240
SelfT = TypeVar("SelfT", bound=pydantic.BaseModel)
4341
ModelSelfT = TypeVar("ModelSelfT", bound="PartialModelMixin")
4442

@@ -97,6 +95,8 @@ def _partial_annotation_arg(field_name_: str, field_annotation: type) -> type:
9795
if recursive or sub_fields_requested:
9896
field_annotation_origin = get_origin(field_annotation)
9997
if field_annotation_origin in (Union, UnionType, tuple, list, set, dict):
98+
if field_annotation_origin is UnionType:
99+
field_annotation_origin = Union
100100
field_annotation = field_annotation_origin[ # type: ignore
101101
tuple( # type: ignore
102102
_partial_annotation_arg(field_name, field_annotation_arg)
@@ -123,7 +123,6 @@ def _partial_annotation_arg(field_name_: str, field_annotation: type) -> type:
123123
field_info,
124124
default=None, # Set default to None
125125
default_factory=None, # Remove default_factory if set
126-
**NULLABLE_KWARGS, # For API usage: set field as nullable and not required
127126
),
128127
)
129128
elif recursive or sub_fields_requested:

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ pytest-cov = ">=3,<7"
1717
tox = ">=3.26,<5.0"
1818
ruff = ">=0.5.0,<0.13.0"
1919
pyright = ">=1.1.350,<1.2"
20+
fastapi = ">=0.116.0,<1" # just for testing the compatibilty
21+
anyio = ">=4.9.0,<5.0.0"
22+
httpx = ">=0.28.1,<0.29.0"
23+
trio = ">=0.30.0,<0.31.0"
2024

2125
[tool.ruff]
2226
line-length = 115

tests/test_fastapi_compatibility.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from typing import Annotated
2+
3+
import fastapi
4+
import pydantic
5+
import pytest
6+
from httpx import ASGITransport, AsyncClient
7+
8+
from pydantic_partial import PartialModelMixin
9+
10+
11+
class Something(PartialModelMixin, pydantic.BaseModel):
12+
name: str
13+
14+
15+
class SomethingPartial(Something.model_as_partial()):
16+
pass
17+
18+
19+
@pytest.fixture
20+
def app():
21+
app = fastapi.FastAPI()
22+
23+
@app.post("/something")
24+
async def something(data: Annotated[SomethingPartial, fastapi.Body()]) -> Something:
25+
return Something(name=data.name if data.name else "Undefined")
26+
27+
return app
28+
29+
30+
@pytest.fixture
31+
async def client(app):
32+
async with AsyncClient(
33+
transport=ASGITransport(app=app),
34+
base_url="http://test",
35+
) as client:
36+
yield client
37+
38+
39+
def test_openapi_spec_can_be_generated(app):
40+
assert app.openapi()
41+
42+
43+
@pytest.mark.anyio
44+
async def test_endpoint_accepts_partial_data(client):
45+
response = await client.post(
46+
"/something",
47+
json={},
48+
)
49+
50+
assert response.status_code == 200
51+
assert response.json()["name"] == "Undefined"
52+
53+
54+
55+
@pytest.mark.anyio
56+
async def test_endpoint_still_accepts_data(client):
57+
response = await client.post(
58+
"/something",
59+
json={"name": "Some name"},
60+
)
61+
62+
assert response.status_code == 200
63+
assert response.json()["name"] == "Some name"

tests/test_partial_without_mixin.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import json
1+
import sys
22
from typing import Union
33

44
import pydantic
@@ -9,24 +9,28 @@
99

1010
def _field_is_required(model: Union[type[pydantic.BaseModel], pydantic.BaseModel], field_name: str) -> bool:
1111
"""Check if a field is required on a pydantic V2 model."""
12-
json_required = (
13-
model.model_fields[field_name].json_schema_extra is not None
14-
and model.model_fields[field_name].json_schema_extra.get("required", False)
15-
)
16-
return model.model_fields[field_name].is_required() or json_required
12+
13+
return model.model_fields[field_name].is_required()
1714

1815

1916
class Something(pydantic.BaseModel):
2017
name: str
2118
age: int
2219
already_optional: None = None
23-
already_required: int = pydantic.Field(default=1, json_schema_extra={"required": True})
20+
already_required: int = pydantic.Field(default=1)
2421

2522

2623
class SomethingWithMixin(PartialModelMixin, pydantic.BaseModel):
2724
name: str
2825

2926

27+
class SomethingWithUnionTypes(PartialModelMixin, pydantic.BaseModel):
28+
name_as_union: Union[str, int]
29+
30+
if sys.version_info >= (3, 10):
31+
name_as_uniontype: str | int
32+
33+
3034
def test_setup_is_sane():
3135
assert _field_is_required(Something, "name") is True
3236
assert _field_is_required(Something, "age") is True
@@ -62,12 +66,6 @@ def test_partial_model_will_be_the_same_on_mixin():
6266

6367
assert SomethingWithMixinPartial1 is SomethingWithMixinPartial2
6468

65-
def test_pydantic_v2_partial_model_will_override_json_required():
66-
SomethingPartial = create_partial_model(Something)
67-
assert _field_is_required(SomethingPartial, "already_required") is False
68-
schema = SomethingPartial.model_json_schema()
69-
assert schema["properties"]["already_required"]["nullable"] is True
70-
assert schema["properties"]["already_required"]["required"] is False
7169

7270
def test_partial_class_name_can_be_overridden():
7371
SomethingPartial = create_partial_model(Something, "name")
@@ -76,3 +74,7 @@ def test_partial_class_name_can_be_overridden():
7674
partial_cls_name = "SomethingWithOptionalName"
7775
SomethingWithOptionalName = create_partial_model(Something, "name", partial_cls_name=partial_cls_name)
7876
assert SomethingWithOptionalName.__name__ == partial_cls_name
77+
78+
79+
def test_recursive_on_unions_work(): # see https://github.com/team23/pydantic-partial/issues/52
80+
create_partial_model(SomethingWithUnionTypes, recursive=True)

tox.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ envlist =
1010
[testenv]
1111
deps =
1212
pytest
13+
fastapi
14+
anyio
15+
httpx
16+
trio
1317
commands = pytest

0 commit comments

Comments
 (0)