Skip to content

Commit 85d7032

Browse files
stainless-botRobertCraigie
authored andcommitted
feat: add support for Pydantic v2 (#66)
1 parent b325bb7 commit 85d7032

36 files changed

+829
-307
lines changed

noxfile.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import nox
2+
import nox_poetry
3+
4+
5+
@nox_poetry.session(reuse_venv=True, name="test-pydantic-v1")
6+
def test_pydantic_v1(session: nox.Session) -> None:
7+
session.run_always("poetry", "install", external=True)
8+
9+
# https://github.com/cjolowicz/nox-poetry/issues/1116
10+
session._session.run("python", "-m", "pip", "install", "pydantic<2", external=True) # type: ignore
11+
12+
session.run("pytest", "--showlocals", "--ignore=tests/functional")

poetry.lock

+276-42
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ packages = [
1313
[tool.poetry.dependencies]
1414
python = "^3.7"
1515
httpx = ">= 0.23.0, < 1"
16-
pydantic = "^1.9.0"
16+
pydantic = ">= 1.9.0, < 3"
1717
typing-extensions = ">= 4.5, < 5"
1818
anyio = ">= 3.5.0, < 4"
1919
distro = ">= 1.7.0, < 2"
@@ -30,6 +30,8 @@ pytest-asyncio = "0.21.1"
3030
ruff = "0.0.282"
3131
isort = "5.10.1"
3232
time-machine = "^2.9.0"
33+
nox = "^2023.4.22"
34+
nox-poetry = "^1.0.3"
3335

3436

3537
[build-system]
@@ -60,7 +62,8 @@ pythonVersion = "3.7"
6062

6163
exclude = [
6264
"_dev",
63-
".venv"
65+
".venv",
66+
".nox",
6467
]
6568
reportImportCycles = false
6669
reportPrivateUsage = false

src/finch/_base_client.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
ModelBuilderProtocol,
5656
)
5757
from ._utils import is_dict, is_mapping
58+
from ._compat import model_copy
5859
from ._models import (
5960
BaseModel,
6061
GenericModel,
@@ -151,7 +152,7 @@ def _params_from_url(self, url: URL) -> httpx.QueryParams:
151152
return httpx.QueryParams(cast(Any, self._options.params)).merge(url.params)
152153

153154
def _info_to_options(self, info: PageInfo) -> FinalRequestOptions:
154-
options = self._options.copy()
155+
options = model_copy(self._options)
155156

156157
if not isinstance(info.params, NotGiven):
157158
options.params = {**options.params, **info.params}

src/finch/_compat.py

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any, Union, TypeVar, cast
4+
from datetime import date, datetime
5+
6+
import pydantic
7+
from pydantic.fields import FieldInfo
8+
9+
from ._types import StrBytesIntFloat
10+
11+
_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel)
12+
13+
# --------------- Pydantic v2 compatibility ---------------
14+
15+
# Pyright incorrectly reports some of our functions as overriding a method when they don't
16+
# pyright: reportIncompatibleMethodOverride=false
17+
18+
PYDANTIC_V2 = pydantic.VERSION.startswith("2.")
19+
20+
# v1 re-exports
21+
if TYPE_CHECKING:
22+
23+
def parse_date(value: date | StrBytesIntFloat) -> date:
24+
...
25+
26+
def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
27+
...
28+
29+
def get_args(t: type[Any]) -> tuple[Any, ...]:
30+
...
31+
32+
def is_union(tp: type[Any] | None) -> bool:
33+
...
34+
35+
def get_origin(t: type[Any]) -> type[Any] | None:
36+
...
37+
38+
def is_literal_type(type_: type[Any]) -> bool:
39+
...
40+
41+
def is_typeddict(type_: type[Any]) -> bool:
42+
...
43+
44+
else:
45+
if PYDANTIC_V2:
46+
from pydantic.v1.typing import get_args as get_args
47+
from pydantic.v1.typing import is_union as is_union
48+
from pydantic.v1.typing import get_origin as get_origin
49+
from pydantic.v1.typing import is_typeddict as is_typeddict
50+
from pydantic.v1.typing import is_literal_type as is_literal_type
51+
from pydantic.v1.datetime_parse import parse_date as parse_date
52+
from pydantic.v1.datetime_parse import parse_datetime as parse_datetime
53+
else:
54+
from pydantic.typing import get_args as get_args
55+
from pydantic.typing import is_union as is_union
56+
from pydantic.typing import get_origin as get_origin
57+
from pydantic.typing import is_typeddict as is_typeddict
58+
from pydantic.typing import is_literal_type as is_literal_type
59+
from pydantic.datetime_parse import parse_date as parse_date
60+
from pydantic.datetime_parse import parse_datetime as parse_datetime
61+
62+
63+
# refactored config
64+
if TYPE_CHECKING:
65+
from pydantic import ConfigDict as ConfigDict
66+
else:
67+
if PYDANTIC_V2:
68+
from pydantic import ConfigDict
69+
else:
70+
# TODO: provide an error message here?
71+
ConfigDict = None
72+
73+
74+
# renamed methods / properties
75+
def parse_obj(model: type[_ModelT], value: object) -> _ModelT:
76+
if PYDANTIC_V2:
77+
return model.model_validate(value)
78+
else:
79+
return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
80+
81+
82+
def field_is_required(field: FieldInfo) -> bool:
83+
if PYDANTIC_V2:
84+
return field.is_required()
85+
return field.required # type: ignore
86+
87+
88+
def field_get_default(field: FieldInfo) -> Any:
89+
value = field.get_default()
90+
if PYDANTIC_V2:
91+
from pydantic_core import PydanticUndefined
92+
93+
if value == PydanticUndefined:
94+
return None
95+
return value
96+
return value
97+
98+
99+
def field_outer_type(field: FieldInfo) -> Any:
100+
if PYDANTIC_V2:
101+
return field.annotation
102+
return field.outer_type_ # type: ignore
103+
104+
105+
def get_model_config(model: type[pydantic.BaseModel]) -> Any:
106+
if PYDANTIC_V2:
107+
return model.model_config
108+
return model.__config__ # type: ignore
109+
110+
111+
def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]:
112+
if PYDANTIC_V2:
113+
return model.model_fields
114+
return model.__fields__ # type: ignore
115+
116+
117+
def model_copy(model: _ModelT) -> _ModelT:
118+
if PYDANTIC_V2:
119+
return model.model_copy()
120+
return model.copy() # type: ignore
121+
122+
123+
def model_json(model: pydantic.BaseModel) -> str:
124+
if PYDANTIC_V2:
125+
return model.model_dump_json()
126+
return model.json() # type: ignore
127+
128+
129+
def model_dump(model: pydantic.BaseModel) -> dict[str, Any]:
130+
if PYDANTIC_V2:
131+
return model.model_dump()
132+
return cast("dict[str, Any]", model.dict()) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
133+
134+
135+
# generic models
136+
if TYPE_CHECKING:
137+
138+
class GenericModel(pydantic.BaseModel):
139+
...
140+
141+
else:
142+
if PYDANTIC_V2:
143+
# there no longer needs to be a distinction in v2 but
144+
# we still have to create our own subclass to avoid
145+
# inconsistent MRO ordering errors
146+
class GenericModel(pydantic.BaseModel):
147+
...
148+
149+
else:
150+
151+
class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel):
152+
...

0 commit comments

Comments
 (0)