Skip to content

Commit

Permalink
fix merge conflict
Browse files Browse the repository at this point in the history
  • Loading branch information
waketzheng committed May 10, 2024
2 parents 8665efb + b71eea3 commit b11b19f
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 12 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Fixed
^^^^^
- Fix `DatetimeField` use '__year' report `'int' object has no attribute 'utcoffset'`. (#1575)
- Fix `bulk_update` when using custom fields. (#1564)
- Fix `optional` parameter in `pydantic_model_creator` does not work for pydantic v2. (#1551)
- Fix `get_annotations` now evaluates annotations in the default scope instead of the app namespace. (#1552)

0.20.1
------
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Contributors
* Waket Zheng ``@waketzheng``
* Yuval Ben-Arie ``@yuvalbenarie``
* Stephan Klein ``@privatwolke``
* ``@WizzyGeek``

Special Thanks
==============
Expand Down
3 changes: 1 addition & 2 deletions examples/fastapi/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, AsyncGenerator, List

from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from models import Users
from pydantic import BaseModel
from starlette.exceptions import HTTPException

from tortoise.contrib.fastapi import RegisterTortoise
from tortoise.contrib.pydantic import PydanticModel
Expand Down
84 changes: 80 additions & 4 deletions tests/contrib/test_pydantic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import copy

from pydantic import ConfigDict
import pytest
from pydantic import ConfigDict, ValidationError

from tests.testmodels import (
Address,
Expand Down Expand Up @@ -1559,24 +1560,99 @@ def test_update_schema(self):
This demonstrates an example PATCH endpoint in an API, where a client may want
to update a single field of a model without modifying the rest.
"""

self.assertEqual(
self.UserUpdate_Pydantic.model_json_schema(),
{
"additionalProperties": False,
"properties": {
"bio": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Bio"},
"bio": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"default": None,
"title": "Bio",
},
"mail": {
"anyOf": [{"maxLength": 64, "type": "string"}, {"type": "null"}],
"default": None,
"title": "Mail",
},
"username": {
"anyOf": [{"maxLength": 32, "type": "string"}, {"type": "null"}],
"default": None,
"title": "Username",
},
},
"required": ["username", "mail", "bio"],
"title": "UserUpdate",
"type": "object",
},
)


class TestPydanticOptionalUpdate(test.TestCase):
def setUp(self) -> None:
self.UserUpdateAllOptional_Pydantic = pydantic_model_creator(
User,
name="UserUpdateAllOptional",
exclude_readonly=True,
optional=("username", "mail", "bio"),
)
self.UserUpdatePartialOptional_Pydantic = pydantic_model_creator(
User,
name="UserUpdatePartialOptional",
exclude_readonly=True,
optional=("username", "mail"),
)
self.UserUpdateWithoutOptional_Pydantic = pydantic_model_creator(
User,
name="UserUpdateWithoutOptional",
exclude_readonly=True,
)

def test_optional_update(self):
# All fields are optional
self.assertEqual(self.UserUpdateAllOptional_Pydantic().model_dump(exclude_unset=True), {})
self.assertEqual(
self.UserUpdateAllOptional_Pydantic(bio="foo").model_dump(exclude_unset=True),
{"bio": "foo"},
)
self.assertEqual(
self.UserUpdateAllOptional_Pydantic(username="name", mail="a@example.com").model_dump(
exclude_unset=True
),
{"username": "name", "mail": "a@example.com"},
)
self.assertEqual(
self.UserUpdateAllOptional_Pydantic(username="name", mail="a@example.com").model_dump(),
{"username": "name", "mail": "a@example.com", "bio": None},
)
# Some fields are optional
with pytest.raises(ValidationError):
self.UserUpdatePartialOptional_Pydantic()
with pytest.raises(ValidationError):
self.UserUpdatePartialOptional_Pydantic(username="name")
self.assertEqual(
self.UserUpdatePartialOptional_Pydantic(bio="foo").model_dump(exclude_unset=True),
{"bio": "foo"},
)
self.assertEqual(
self.UserUpdatePartialOptional_Pydantic(
username="name", mail="a@example.com", bio=""
).model_dump(exclude_unset=True),
{"username": "name", "mail": "a@example.com", "bio": ""},
)
self.assertEqual(
self.UserUpdatePartialOptional_Pydantic(mail="a@example.com", bio="").model_dump(),
{"username": None, "mail": "a@example.com", "bio": ""},
)
# None of the fields is optional
with pytest.raises(ValidationError):
self.UserUpdateWithoutOptional_Pydantic()
with pytest.raises(ValidationError):
self.UserUpdateWithoutOptional_Pydantic(username="name")
with pytest.raises(ValidationError):
self.UserUpdateWithoutOptional_Pydantic(username="name", email="")
self.assertEqual(
self.UserUpdateWithoutOptional_Pydantic(
username="name", mail="a@example.com", bio=""
).model_dump(),
{"username": "name", "mail": "a@example.com", "bio": ""},
)
5 changes: 3 additions & 2 deletions tortoise/contrib/pydantic/creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ def field_map_update(keys: tuple, is_relation=True) -> None:
}
field_type = fdesc["field_type"]
field_default = fdesc.get("default")
is_optional_field = fname in optional

def get_submodel(_model: "Type[Model]") -> Optional[Type[PydanticModel]]:
"""Get Pydantic model for the submodel"""
Expand Down Expand Up @@ -404,7 +405,7 @@ def get_submodel(_model: "Type[Model]") -> Optional[Type[PydanticModel]]:
ptype = fdesc["python_type"]
if fdesc.get("nullable"):
json_schema_extra["nullable"] = True
if fdesc.get("nullable") or field_default is not None or fname in optional:
if is_optional_field or field_default is not None or fdesc.get("nullable"):
ptype = Optional[ptype]
if not (exclude_readonly and json_schema_extra.get("readOnly") is True):
properties[fname] = annotation or ptype
Expand All @@ -417,7 +418,7 @@ def get_submodel(_model: "Type[Model]") -> Optional[Type[PydanticModel]]:
ftype = properties[fname]
if isinstance(ftype, PydanticDescriptorProxy):
continue
if field_default is not None and not callable(field_default):
if is_optional_field or (field_default is not None and not callable(field_default)):
properties[fname] = (ftype, Field(default=field_default, **fconfig))
else:
if (j := fconfig.get("json_schema_extra")) and (
Expand Down
5 changes: 1 addition & 4 deletions tortoise/contrib/pydantic/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import typing
from typing import Any, Callable, Dict, Optional, Type

import tortoise

if typing.TYPE_CHECKING: # pragma: nocoverage
from tortoise.models import Model

Expand All @@ -14,5 +12,4 @@ def get_annotations(cls: "Type[Model]", method: Optional[Callable] = None) -> Di
:param method: If specified, we try to get the annotations for the callable
:return: The list of annotations
"""
globalns = tortoise.Tortoise.apps.get(cls._meta.app, None) if cls._meta.app else None
return typing.get_type_hints(method or cls, globalns=globalns)
return typing.get_type_hints(method or cls)

0 comments on commit b11b19f

Please sign in to comment.