10000 Add support for Pydantic 2 non-destructively by frost-nzcr4 · Pull Request #289 · fastapiutils/fastapi-utils · GitHub
[go: up one dir, main page]

Skip to content

Add support for Pydantic 2 non-destructively #289

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
55 changes: 38 additions & 17 deletions fastapi_utils/api_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
60 changes: 42 additions & 18 deletions fastapi_utils/api_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 5 additions & 1 deletion fastapi_utils/cbv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion tests/test_api_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
0