diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8f59979718..6cc1ad9ad9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -60,7 +60,10 @@ jobs: run: poetry install --no-interaction - if: ${{ inputs.pydantic-version == '1' }} name: Install pydantic v1 - run: source .venv/bin/activate && pip install "pydantic>=1.10.10" + run: poetry add "pydantic>=1.10.10,<2" && poetry remove pydantic-extra-types + - if: ${{ inputs.pydantic-version == '2' }} + name: Install pydantic v2 + run: poetry add "pydantic>=2" && poetry add pydantic-extra-types - name: Set pythonpath run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV - name: Test diff --git a/docs/conf.py b/docs/conf.py index ac6aa5e7fe..9327834a1d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -87,7 +87,6 @@ (PY_METH, "_types.TypeDecorator.process_bind_param"), (PY_METH, "_types.TypeDecorator.process_result_value"), (PY_METH, "type_engine"), - (PY_METH, "litestar.typing.ParsedType.is_subclass_of"), # type vars and aliases / intentionally undocumented (PY_CLASS, "RouteHandlerType"), (PY_OBJ, "litestar.security.base.AuthType"), @@ -114,6 +113,7 @@ (PY_CLASS, "litestar.response.RedirectResponse"), (PY_CLASS, "anyio.abc.BlockingPortal"), (PY_CLASS, "litestar.typing.ParsedType"), + (PY_CLASS, "pydantic.BaseModel"), ] nitpick_ignore_regex = [ diff --git a/docs/examples/contrib/sqlalchemy/sqlalchemy_async_repository.py b/docs/examples/contrib/sqlalchemy/sqlalchemy_async_repository.py index 5674932f92..f00f132abf 100644 --- a/docs/examples/contrib/sqlalchemy/sqlalchemy_async_repository.py +++ b/docs/examples/contrib/sqlalchemy/sqlalchemy_async_repository.py @@ -25,8 +25,7 @@ class BaseModel(_BaseModel): """Extend Pydantic's BaseModel to enable ORM mode""" - class Config: - orm_mode = True + model_config = {"from_attributes": True} # the SQLAlchemy base includes a declarative model for you to use in your models. diff --git a/docs/examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py b/docs/examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py index fe446b94f3..85fe99a052 100644 --- a/docs/examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py +++ b/docs/examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py @@ -25,8 +25,7 @@ class BaseModel(_BaseModel): """Extend Pydantic's BaseModel to enable ORM mode""" - class Config: - orm_mode = True + model_config = {"from_attributes": True} # we are going to add a simple "slug" to our model that is a URL safe surrogate key to diff --git a/docs/examples/contrib/sqlalchemy/sqlalchemy_sync_repository.py b/docs/examples/contrib/sqlalchemy/sqlalchemy_sync_repository.py index 35c4a565d0..2c3bbb5005 100644 --- a/docs/examples/contrib/sqlalchemy/sqlalchemy_sync_repository.py +++ b/docs/examples/contrib/sqlalchemy/sqlalchemy_sync_repository.py @@ -25,8 +25,7 @@ class BaseModel(_BaseModel): """Extend Pydantic's BaseModel to enable ORM mode""" - class Config: - orm_mode = True + model_config = {"from_attributes": True} # the SQLAlchemy base includes a declarative model for you to use in your models. diff --git a/litestar/dto/factory/_backends/pydantic/utils.py b/litestar/dto/factory/_backends/pydantic/utils.py index bd8f413b3c..690e64256e 100644 --- a/litestar/dto/factory/_backends/pydantic/utils.py +++ b/litestar/dto/factory/_backends/pydantic/utils.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, TypeVar, Union from msgspec import UNSET, UnsetType -from pydantic import BaseModel, create_model +from pydantic import VERSION, BaseModel, create_model from pydantic.fields import FieldInfo from litestar.dto.factory._backends.utils import create_transfer_model_type_annotation @@ -21,9 +21,14 @@ class _OrmModeBase(BaseModel): - class Config: - arbitrary_types_allowed = True - orm_mode = True + if VERSION.startswith("1"): + + class Config: + arbitrary_types_allowed = True + orm_mode = True + + else: + model_config = {"arbitrary_types_allowed": True, "from_attributes": True} def _create_field_info(field_definition: TransferDTOFieldDefinition) -> FieldInfo: diff --git a/litestar/serialization/_pydantic_serialization.py b/litestar/serialization/_pydantic_serialization.py index fe742d78ff..4504cd9f2f 100644 --- a/litestar/serialization/_pydantic_serialization.py +++ b/litestar/serialization/_pydantic_serialization.py @@ -81,7 +81,9 @@ def create_pydantic_encoders() -> dict[Any, Callable[[Any], Any]]: if pydantic.VERSION.startswith("1"): # pragma: no cover encoders.update( { - pydantic.BaseModel: lambda model: model.dict(), + pydantic.BaseModel: lambda model: { + k: v if not isinstance(v, bytes) else v.decode() for k, v in model.dict().items() + }, pydantic.SecretField: str, pydantic.StrictBool: int, pydantic.color.Color: str, # pyright: ignore diff --git a/poetry.lock b/poetry.lock index 5648ab4e4f..af8600cadb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "accessible-pygments" @@ -3720,46 +3720,6 @@ description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ddd6d35c598af872f9a0a5bce7f7c4a1841684a72dab3302e3df7f17d1b5249"}, - {file = "SQLAlchemy-2.0.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:00aa050faf24ce5f2af643e2b86822fa1d7149649995f11bc1e769bbfbf9010b"}, - {file = "SQLAlchemy-2.0.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b52c6741073de5a744d27329f9803938dcad5c9fee7e61690c705f72973f4175"}, - {file = "SQLAlchemy-2.0.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db97eabd440327c35b751d5ebf78a107f505586485159bcc87660da8bb1fdca"}, - {file = "SQLAlchemy-2.0.18-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:589aba9a35869695b319ed76c6f673d896cd01a7ff78054be1596df7ad9b096f"}, - {file = "SQLAlchemy-2.0.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9da4ee8f711e077633730955c8f3cd2485c9abf5ea0f80aac23221a3224b9a8c"}, - {file = "SQLAlchemy-2.0.18-cp310-cp310-win32.whl", hash = "sha256:5dd574a37be388512c72fe0d7318cb8e31743a9b2699847a025e0c08c5bf579d"}, - {file = "SQLAlchemy-2.0.18-cp310-cp310-win_amd64.whl", hash = "sha256:6852cd34d96835e4c9091c1e6087325efb5b607b75fd9f7075616197d1c4688a"}, - {file = "SQLAlchemy-2.0.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:10e001a84f820fea2640e4500e12322b03afc31d8f4f6b813b44813b2a7c7e0d"}, - {file = "SQLAlchemy-2.0.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bffd6cd47c2e68970039c0d3e355c9ed761d3ca727b204e63cd294cad0e3df90"}, - {file = "SQLAlchemy-2.0.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b7b3ebfa9416c8eafaffa65216e229480c495e305a06ba176dcac32710744e6"}, - {file = "SQLAlchemy-2.0.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79228a7b90d95957354f37b9d46f2cc8926262ae17b0d3ed8f36c892f2a37e06"}, - {file = "SQLAlchemy-2.0.18-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ba633b51835036ff0f402c21f3ff567c565a22ff0a5732b060a68f4660e2a38f"}, - {file = "SQLAlchemy-2.0.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8da677135eff43502b7afab5a1e641edfb2dc734ba7fc146e9b1b86817a728e2"}, - {file = "SQLAlchemy-2.0.18-cp311-cp311-win32.whl", hash = "sha256:82edf3a6090554a83942cec79151d6b5eb96e63d143e80e4cf6671e5d772f6be"}, - {file = "SQLAlchemy-2.0.18-cp311-cp311-win_amd64.whl", hash = "sha256:69ae0e9509c43474e33152abe1385b8954922544616426bf793481e1a37e094f"}, - {file = "SQLAlchemy-2.0.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:09397a18733fa2a4c7680b746094f980060666ee549deafdb5e102a99ce4619b"}, - {file = "SQLAlchemy-2.0.18-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45b07470571bda5ee7f5ec471271bbde97267cc8403fce05e280c36ea73f4754"}, - {file = "SQLAlchemy-2.0.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1aac42a21a7fa6c9665392c840b295962992ddf40aecf0a88073bc5c76728117"}, - {file = "SQLAlchemy-2.0.18-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:da46beef0ce882546d92b7b2e8deb9e04dbb8fec72945a8eb28b347ca46bc15a"}, - {file = "SQLAlchemy-2.0.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a6f1d8256d06f58e6ece150fbe05c63c7f9510df99ee8ac37423f5476a2cebb4"}, - {file = "SQLAlchemy-2.0.18-cp37-cp37m-win32.whl", hash = "sha256:67fbb40db3985c0cfb942fe8853ad94a5e9702d2987dec03abadc2f3b6a24afb"}, - {file = "SQLAlchemy-2.0.18-cp37-cp37m-win_amd64.whl", hash = "sha256:afb322ca05e2603deedbcd2e9910f11a3fd2f42bdeafe63018e5641945c7491c"}, - {file = "SQLAlchemy-2.0.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:908c850b98cac1e203ababd4ba76868d19ae0d7172cdc75d3f1b7829b16837d2"}, - {file = "SQLAlchemy-2.0.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10514adc41fc8f5922728fbac13d401a1aefcf037f009e64ca3b92464e33bf0e"}, - {file = "SQLAlchemy-2.0.18-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b791577c546b6bbd7b43953565fcb0a2fec63643ad605353dd48afbc3c48317"}, - {file = "SQLAlchemy-2.0.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:420bc6d06d4ae7fb6921524334689eebcbea7bf2005efef070a8562cc9527a37"}, - {file = "SQLAlchemy-2.0.18-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ebdd2418ab4e2e26d572d9a1c03877f8514a9b7436729525aa571862507b3fea"}, - {file = "SQLAlchemy-2.0.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:556dc18e39b6edb76239acfd1c010e37395a54c7fde8c57481c15819a3ffb13e"}, - {file = "SQLAlchemy-2.0.18-cp38-cp38-win32.whl", hash = "sha256:7b8cba5a25e95041e3413d91f9e50616bcfaec95afa038ce7dc02efefe576745"}, - {file = "SQLAlchemy-2.0.18-cp38-cp38-win_amd64.whl", hash = "sha256:0f7fdcce52cd882b559a57b484efc92e108efeeee89fab6b623aba1ac68aad2e"}, - {file = "SQLAlchemy-2.0.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d7a2c1e711ce59ac9d0bba780318bcd102d2958bb423209f24c6354d8c4da930"}, - {file = "SQLAlchemy-2.0.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c95e3e7cc6285bf7ff263eabb0d3bfe3def9a1ff98124083d45e5ece72f4579"}, - {file = "SQLAlchemy-2.0.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc44e50f9d5e96af1a561faa36863f9191f27364a4df3eb70bca66e9370480b6"}, - {file = "SQLAlchemy-2.0.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfa1a0f83bdf8061db8d17c2029454722043f1e4dd1b3d3d3120d1b54e75825a"}, - {file = "SQLAlchemy-2.0.18-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:194f2d5a7cb3739875c4d25b3fe288ab0b3dc33f7c857ba2845830c8c51170a0"}, - {file = "SQLAlchemy-2.0.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ebc542d2289c0b016d6945fd07a7e2e23f4abc41e731ac8ad18a9e0c2fd0ec2"}, - {file = "SQLAlchemy-2.0.18-cp39-cp39-win32.whl", hash = "sha256:774bd401e7993452ba0596e741c0c4d6d22f882dd2a798993859181dbffadc62"}, - {file = "SQLAlchemy-2.0.18-cp39-cp39-win_amd64.whl", hash = "sha256:2756485f49e7df5c2208bdc64263d19d23eba70666f14ad12d6d8278a2fff65f"}, - {file = "SQLAlchemy-2.0.18-py3-none-any.whl", hash = "sha256:6c5bae4c288bda92a7550fe8de9e068c0a7cd56b1c5d888aae5b40f0e13b40bd"}, {file = "SQLAlchemy-2.0.18.tar.gz", hash = "sha256:1fb792051db66e09c200e7bc3bda3b1eb18a5b8eb153d2cedb2b14b56a68b8cb"}, ] @@ -3769,7 +3729,7 @@ typing-extensions = ">=4.2.0" [package.extras] aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] @@ -3779,7 +3739,7 @@ mssql-pyodbc = ["pyodbc"] mypy = ["mypy (>=0.910)"] mysql = ["mysqlclient (>=1.4.0)"] mysql-connector = ["mysql-connector-python"] -oracle = ["cx-oracle (>=7)"] +oracle = ["cx_oracle (>=7)"] oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] @@ -3789,7 +3749,7 @@ postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3-binary"] +sqlcipher = ["sqlcipher3_binary"] [[package]] name = "sqlalchemy-spanner" @@ -4435,4 +4395,4 @@ tortoise-orm = ["tortoise-orm"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<4.0" -content-hash = "3a803a93020a303daa341b9f32f91d5f9be5c4353d3916c9b5b5987ce80db47f" +content-hash = "f0e156f0fa0734ce9f6e19bfe50d94c87b98bb7bc2b98cd31803a67ff986bc2c" diff --git a/pyproject.toml b/pyproject.toml index 96b264c879..803298701b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,7 +139,7 @@ picologging = "*" pre-commit = "*" prometheus-client = "*" psycopg = "*" -pydantic = ">=2" +pydantic = "*" pydantic-extra-types = "*" pytest = "*" pytest-asyncio = "*" @@ -259,6 +259,7 @@ exclude_lines = [ 'except ImportError:', '\.\.\.', 'raise NotImplementedError', + 'if VERSION.startswith("1"):', ] [tool.pytest.ini_options] diff --git a/tests/unit/test_serialization.py b/tests/unit/test_serialization.py index 7f136e78bd..9908fd07b5 100644 --- a/tests/unit/test_serialization.py +++ b/tests/unit/test_serialization.py @@ -171,7 +171,17 @@ def test_serialization_of_model_instance(model: BaseModel) -> None: def test_pydantic_json_compatibility(model: BaseModel) -> None: raw = model.model_dump_json() if hasattr(model, "model_dump_json") else model.json() encoded_json = encode_json(model) - assert json.loads(raw) == json.loads(encoded_json) + + raw_result = json.loads(raw) + encoded_result = json.loads(encoded_json) + + if VERSION.startswith("1"): + # pydantic v1 dumps decimals into floats as json, we therefore regard this as an error + assert raw_result.get("condecimal") == float(encoded_result.get("condecimal")) + del raw_result["condecimal"] + del encoded_result["condecimal"] + + assert raw_result == encoded_result @pytest.mark.parametrize("encoder", [encode_json, encode_msgpack]) diff --git a/tests/unit/test_signature/test_validation.py b/tests/unit/test_signature/test_validation.py index 5c61da390a..719e5d08f9 100644 --- a/tests/unit/test_signature/test_validation.py +++ b/tests/unit/test_signature/test_validation.py @@ -3,7 +3,7 @@ import pytest from attr import define -from pydantic import VERSION, BaseModel +from pydantic import BaseModel from typing_extensions import TypedDict from litestar import get, post @@ -163,31 +163,20 @@ def test( data = response.json() assert data - if VERSION.startswith("1"): - assert data["extra"] == [ - {"key": "child.val", "message": "value is not a valid integer", "source": "body"}, - {"key": "child.other_val", "message": "value is not a valid integer", "source": "body"}, - {"key": "other_child.val.1", "message": "value is not a valid integer", "source": "body"}, - {"key": "int_param", "message": "value is not a valid integer", "source": "query"}, - {"key": "length_param", "message": "ensure this value has at least 2 characters", "source": "query"}, - {"key": "int_header", "message": "value is not a valid integer", "source": "header"}, - {"key": "int_cookie", "message": "value is not a valid integer", "source": "cookie"}, - ] - else: - assert data["extra"] == [ - { - "message": "Input should be a valid integer, unable to parse string as an integer", - "key": "child.val", - }, - { - "message": "Input should be a valid integer, unable to parse string as an integer", - "key": "child.other_val", - }, - { - "message": "Input should be a valid integer, unable to parse string as an integer", - "key": "other_child.val.1", - }, - ] + assert data["extra"] == [ + { + "message": "Input should be a valid integer, unable to parse string as an integer", + "key": "child.val", + }, + { + "message": "Input should be a valid integer, unable to parse string as an integer", + "key": "child.other_val", + }, + { + "message": "Input should be a valid integer, unable to parse string as an integer", + "key": "other_child.val.1", + }, + ] def test_invalid_input_attrs() -> None: