diff --git a/docs/release-notes.md b/docs/release-notes.md index 0ff63fe1..2284a703 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,9 @@ * Stop supporting Python 3.6 * Deprecate InferringRouter (as its functionality is now built into `fastapi.APIRouter`) * Resolve various deprecationwarnings introduced by sqlalchemy 1.4. +* Added support for Pydantic 2, you have to select the dependency in your project: + - for v1 use `fastapi-utils = "^0.3"`, or `pydantic = "^1.10"`, or both, + - for v2 use `fastapi-utils = { version = "^0.3", extras = ["pydantic_settings"] }`, or `pydantic = "^2.0"`, or both. ## 0.2.1 diff --git a/fastapi_utils/api_model.py b/fastapi_utils/api_model.py index 02edf265..1dc81dbe 100644 --- a/fastapi_utils/api_model.py +++ b/fastapi_utils/api_model.py @@ -2,26 +2,47 @@ from functools import partial -from pydantic import BaseConfig, BaseModel +from pydantic import BaseModel from fastapi_utils.camelcase import snake2camel - -class APIModel(BaseModel): - """ - Intended for use as a base class for externally-facing models. - - Any models that inherit from this class will: - * accept fields using snake_case or camelCase keys - * use camelCase keys in the generated OpenAPI spec - * have orm_mode on by default - * Because of this, FastAPI will automatically attempt to parse returned orm instances into the model - """ - - class Config(BaseConfig): - orm_mode = True - allow_population_by_field_name = True - alias_generator = partial(snake2camel, start_lower=True) +try: + from pydantic import ConfigDict + + class APIModel(BaseModel): + """ + Intended for use as a base class for externally-facing models. + + Any models that inherit from this class will: + * accept fields using snake_case or camelCase keys + * use camelCase keys in the generated OpenAPI spec + * have orm_mode on by default + * Because of this, FastAPI will automatically attempt to parse returned orm instances into the model + """ + + model_config = ConfigDict( + alias_generator=partial(snake2camel, start_lower=True), + populate_by_name=True, + from_attributes=True, + ) +except ImportError: + from pydantic import BaseConfig + + class APIModel(BaseModel): + """ + Intended for use as a base class for externally-facing models. + + Any models that inherit from this class will: + * accept fields using snake_case or camelCase keys + * use camelCase keys in the generated OpenAPI spec + * have orm_mode on by default + * Because of this, FastAPI will automatically attempt to parse returned orm instances into the model + """ + + class Config(BaseConfig): + orm_mode = True + allow_population_by_field_name = True + alias_generator = partial(snake2camel, start_lower=True) class APIMessage(APIModel): diff --git a/fastapi_utils/api_settings.py b/fastapi_utils/api_settings.py index 814b08db..bfba7e59 100644 --- a/fastapi_utils/api_settings.py +++ b/fastapi_utils/api_settings.py @@ -3,23 +3,13 @@ from functools import lru_cache from typing import Any -from pydantic import BaseSettings +try: + from pydantic_settings import BaseSettings +except ImportError: + from pydantic import BaseSettings -class APISettings(BaseSettings): - """ - This class enables the configuration of your FastAPI instance through the use of environment variables. - - Any of the instance attributes can be overridden upon instantiation by either passing the desired value to the - initializer, or by setting the corresponding environment variable. - - Attribute `xxx_yyy` corresponds to environment variable `API_XXX_YYY`. So, for example, to override - `openapi_prefix`, you would set the environment variable `API_OPENAPI_PREFIX`. - - Note that assignments to variables are also validated, ensuring that even if you make runtime-modifications - to the config, they should have the correct types. - """ - +class APISettingsBase(BaseSettings): # fastapi.applications.FastAPI initializer kwargs debug: bool = False docs_url: str = "/docs" @@ -53,9 +43,43 @@ def fastapi_kwargs(self) -> dict[str, Any]: fastapi_kwargs.update({"docs_url": None, "openapi_url": None, "redoc_url": None}) return fastapi_kwargs - class Config: - env_prefix = "api_" - validate_assignment = True + +try: + from pydantic_settings import SettingsConfigDict + + class APISettings(APISettingsBase): + """ + This class enables the configuration of your FastAPI instance through the use of environment variables. + + Any of the instance attributes can be overridden upon instantiation by either passing the desired value to the + initializer, or by setting the corresponding environment variable. + + Attribute `xxx_yyy` corresponds to environment variable `API_XXX_YYY`. So, for example, to override + `openapi_prefix`, you would set the environment variable `API_OPENAPI_PREFIX`. + + Note that assignments to variables are also validated, ensuring that even if you make runtime-modifications + to the config, they should have the correct types. + """ + + model_config = SettingsConfigDict(env_prefix="api_", validate_assignment=True) +except ImportError: + class APISettings(APISettingsBase): + """ + This class enables the configuration of your FastAPI instance through the use of environment variables. + + Any of the instance attributes can be overridden upon instantiation by either passing the desired value to the + initializer, or by setting the corresponding environment variable. + + Attribute `xxx_yyy` corresponds to environment variable `API_XXX_YYY`. So, for example, to override + `openapi_prefix`, you would set the environment variable `API_OPENAPI_PREFIX`. + + Note that assignments to variables are also validated, ensuring that even if you make runtime-modifications + to the config, they should have the correct types. + """ + + class Config: + env_prefix = "api_" + validate_assignment = True @lru_cache() diff --git a/fastapi_utils/cbv.py b/fastapi_utils/cbv.py index 9b2d832c..d4c6ccf1 100644 --- a/fastapi_utils/cbv.py +++ b/fastapi_utils/cbv.py @@ -5,7 +5,11 @@ from typing import Any, TypeVar, get_type_hints from fastapi import APIRouter, Depends -from pydantic.typing import is_classvar + +try: + from pydantic.v1.typing import is_classvar # noqa:F401,RUF100 +except ImportError: + from pydantic.typing import is_classvar # noqa:F401,RUF100 from starlette.routing import Route, WebSocketRoute T = TypeVar("T") diff --git a/poetry.lock b/poetry.lock index b2797345..a4be74ef 100644 --- a/poetry.lock +++ b/poetry.lock @@ -93,7 +93,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "codecov" -version = "2.1.12" +version = "2.1.13" description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" category = "dev" optional = false @@ -869,8 +869,8 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] codecov = [ - {file = "codecov-2.1.12-py2.py3-none-any.whl", hash = "sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47"}, - {file = "codecov-2.1.12.tar.gz", hash = "sha256:a0da46bb5025426da895af90938def8ee12d37fcbcbbbc15b6dc64cf7ebc51c1"}, + {file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"}, + {file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"}, ] colorama = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, diff --git a/pyproject.toml b/pyproject.toml index e6d8a88c..a7b18222 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,9 +40,13 @@ classifiers = [ python = "^3.7" fastapi = "*" -pydantic = "^1.10,<2.0" +pydantic = "^1.10 || ^2.0" +pydantic_settings = { version = "^2.0", optional = true } sqlalchemy = "^1.4,<2.0" +[tool.poetry.extras] +pydantic_settings = ["pydantic_settings"] + [tool.poetry.dev-dependencies] # Starlette features aiofiles = "*" # Serving static files diff --git a/requirements.txt b/requirements.txt index e43574eb..6c3c43ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -115,9 +115,9 @@ charset-normalizer==3.1.0 ; python_version >= "3.7" and python_version < "4" \ click==8.1.3 ; python_version >= "3.7" and python_version < "4.0" \ --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 -codecov==2.1.12 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47 \ - --hash=sha256:a0da46bb5025426da895af90938def8ee12d37fcbcbbbc15b6dc64cf7ebc51c1 +codecov==2.1.13 ; python_version >= "3.7" and python_version < "4.0" \ + --hash=sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5 \ + --hash=sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c colorama==0.4.6 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 diff --git a/tests/test_api_model.py b/tests/test_api_model.py index 32c00155..20b1f5d4 100644 --- a/tests/test_api_model.py +++ b/tests/test_api_model.py @@ -13,7 +13,8 @@ class Data: class Model(APIModel): x: int - assert Model.from_orm(Data(x=1)).x == 1 + model_validate = getattr(Model, "model_validate", "from_orm") + assert model_validate(Data(x=1)).x == 1 def test_aliases() -> None: