Skip to content

Commit 4f04887

Browse files
authored
Merge pull request #148 from python-ellar/pydanticV2_support
Pydantic v2 migration
2 parents abb19f4 + d46821d commit 4f04887

File tree

111 files changed

+3903
-1772
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+3903
-1772
lines changed

.github/workflows/test_full.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ jobs:
3737
run: pip install flit
3838
- name: Install Dependencies
3939
run: flit install --symlink
40-
- name: Black
41-
run: black --check ellar tests examples
4240
- name: Ruff Linting Check
4341
run: ruff check ellar tests examples
4442
- name: mypy

Makefile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ install-full: ## Install dependencies
1919
pre-commit install -f
2020

2121
lint:fmt ## Run code linters
22-
black --check ellar tests examples
2322
ruff check ellar tests examples
2423
mypy ellar
2524

2625
fmt format:clean ## Run code formatters
27-
black ellar tests examples
26+
ruff format ellar tests examples
2827
ruff check --fix ellar tests examples
2928

3029
test:clean ## Run tests

ellar/app/context.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from types import TracebackType
55

66
from ellar.common.constants import ELLAR_CONFIG_MODULE
7+
from ellar.common.logger import logger
78
from ellar.common.utils.functional import SimpleLazyObject, empty
89
from ellar.core import Config
910
from ellar.di import EllarInjector
@@ -95,10 +96,10 @@ def _get_application_config() -> Config:
9596
if app_context is empty:
9697
config_module = os.environ.get(ELLAR_CONFIG_MODULE)
9798
if not config_module:
98-
raise RuntimeError(
99+
logger.warning(
99100
"You are trying to access app config outside app context "
100101
"and %s is not specified. This may cause differences in config "
101-
"values when the app." % (ELLAR_CONFIG_MODULE,)
102+
"values with the app" % (ELLAR_CONFIG_MODULE,)
102103
)
103104
return Config(config_module=config_module)
104105

ellar/auth/handlers/schemes/base.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,7 @@ def openapi_security_scheme(
103103
) -> t.Dict:
104104
assert cls.openapi_in, "openapi_in is required"
105105
return {
106-
cls.openapi_name
107-
or cls.__name__: {
106+
cls.openapi_name or cls.__name__: {
108107
"type": "apiKey",
109108
"description": cls.openapi_description,
110109
"in": cls.openapi_in,
@@ -153,8 +152,7 @@ def openapi_security_scheme(
153152
) -> t.Dict:
154153
assert cls.scheme, "openapi_scheme is required"
155154
return {
156-
cls.openapi_name
157-
or cls.__name__: {
155+
cls.openapi_name or cls.__name__: {
158156
"type": "http",
159157
"description": cls.openapi_description,
160158
"scheme": cls.scheme,

ellar/auth/session/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import sys
22
import typing as t
33

4-
from pydantic import BaseModel
4+
from ellar.pydantic import BaseModel
55

66
if sys.version_info >= (3, 8): # pragma: no cover
77
from typing import Literal

ellar/cache/schema.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
11
import typing as t
22

33
from ellar.cache import BaseCacheBackend
4-
from pydantic import BaseModel, validator
4+
from ellar.pydantic import BaseModel, as_pydantic_validator, field_validator
55

66

7+
@as_pydantic_validator("__validate_input__", schema={"type": "object"})
78
class TBaseCacheBackend:
89
@classmethod
9-
def __get_validators__(
10-
cls: t.Type["TBaseCacheBackend"],
11-
) -> t.Iterable[t.Callable[..., t.Any]]:
12-
yield cls.validate
10+
def __validate_input__(
11+
cls: t.Type["TBaseCacheBackend"], __input: t.Any, _: t.Any
12+
) -> t.Any:
13+
if isinstance(__input, BaseCacheBackend):
14+
return __input
1315

14-
@classmethod
15-
def validate(cls: t.Type["TBaseCacheBackend"], v: t.Any) -> t.Any:
16-
if isinstance(v, BaseCacheBackend):
17-
return v
18-
19-
raise ValueError(f"Expected BaseCacheBackend, received: {type(v)}")
16+
raise ValueError(f"Expected BaseCacheBackend, received: {type(__input)}")
2017

2118

2219
class CacheModuleSchemaSetup(BaseModel):
2320
CACHES: t.Dict[str, TBaseCacheBackend] = {}
2421

25-
@validator("CACHES", pre=True)
22+
@field_validator("CACHES", mode="before")
2623
def pre_cache_validate(cls, value: t.Dict) -> t.Any:
2724
if value and not value.get("default"):
2825
raise ValueError("CACHES configuration must have a 'default' key")

ellar/common/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
trace,
9797
ws_route,
9898
)
99-
from .serializer import DataclassSerializer, Serializer, serialize_object
99+
from .serializer import Serializer, serialize_object
100100
from .templating import TemplateResponse, render_template, render_template_string
101101

102102
__all__ = [
@@ -108,7 +108,6 @@
108108
"EllarTyper",
109109
"GlobalGuard",
110110
"Serializer",
111-
"DataclassSerializer",
112111
"WebSocketException",
113112
"APIException",
114113
"AuthenticationFailed",

ellar/common/compatible/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
from .cache_properties import cached_property
22
from .dict import AttributeDict, AttributeDictAccessMixin
3-
from .emails import EmailStr
43

54
__all__ = [
65
"cached_property",
7-
"EmailStr",
86
"AttributeDictAccessMixin",
97
"AttributeDict",
108
]

ellar/common/constants.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@
55

66
from ellar.common.types import TMessage
77
from ellar.di import AnnotationToValue
8-
from pydantic.fields import (
9-
SHAPE_LIST,
10-
SHAPE_SEQUENCE,
11-
SHAPE_SET,
12-
SHAPE_TUPLE,
13-
SHAPE_TUPLE_ELLIPSIS,
14-
)
158

169
POST = "POST"
1710
PUT = "PUT"
@@ -81,21 +74,8 @@ class CONTROLLER_METADATA(metaclass=AnnotationToValue):
8174
INCLUDE_IN_SCHEMA: str
8275

8376

84-
sequence_shapes = {
85-
SHAPE_LIST,
86-
SHAPE_SET,
87-
SHAPE_TUPLE,
88-
SHAPE_SEQUENCE,
89-
SHAPE_TUPLE_ELLIPSIS,
90-
}
9177
sequence_types = (list, set, tuple)
92-
sequence_shape_to_type = {
93-
SHAPE_LIST: list,
94-
SHAPE_SET: set,
95-
SHAPE_TUPLE: tuple,
96-
SHAPE_SEQUENCE: list,
97-
SHAPE_TUPLE_ELLIPSIS: list,
98-
}
78+
9979
primitive_types = (int, float, bool, str)
10080
METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
10181
STATUS_CODES_WITH_NO_BODY = {100, 101, 102, 103, 204, 304}

ellar/common/converters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import typing as t
22
from abc import ABC, abstractmethod
33

4-
from pydantic.typing import get_args, get_origin
4+
from typing_extensions import get_args, get_origin
55

66
_origin_maps = {
77
list: t.List,

ellar/common/datastructures.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import typing as t
22

3+
from ellar.pydantic import as_pydantic_validator
34
from starlette.datastructures import (
45
URL as URL,
56
)
@@ -38,6 +39,9 @@
3839
]
3940

4041

42+
@as_pydantic_validator(
43+
"__validate_input__", schema={"type": "string", "format": "binary"}
44+
)
4145
class UploadFile(StarletteUploadFile):
4246
"""
4347
A file uploaded in a request.
@@ -76,17 +80,12 @@ async def create_upload_file(file: UploadFile):
7680
]
7781

7882
@classmethod
79-
def __modify_schema__(cls, field_schema: t.Dict[str, t.Any]) -> None:
80-
field_schema.update({"type": "string", "format": "binary"})
81-
82-
@classmethod
83-
def __get_validators__(
84-
cls: t.Type["UploadFile"],
85-
) -> t.Iterable[t.Callable[..., t.Any]]:
86-
yield cls.validate
87-
88-
@classmethod
89-
def validate(cls: t.Type["UploadFile"], v: t.Any) -> t.Any:
90-
if not isinstance(v, StarletteUploadFile):
91-
raise ValueError(f"Expected UploadFile, received: {type(v)}")
92-
return cls(v.file, size=v.size, filename=v.filename, headers=v.headers)
83+
def __validate_input__(cls, __input_value: t.Any, _: t.Any) -> "UploadFile":
84+
if not isinstance(__input_value, StarletteUploadFile):
85+
raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
86+
return cls(
87+
__input_value.file,
88+
size=__input_value.size,
89+
filename=__input_value.filename,
90+
headers=__input_value.headers,
91+
)

ellar/common/decorators/exception.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import typing as t
22

33
from ellar.common.constants import EXCEPTION_HANDLERS_KEY
4-
from pydantic import BaseModel
4+
from ellar.pydantic import BaseModel
55

66

77
class ValidateExceptionHandler(BaseModel):

ellar/common/decorators/serializer.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ def serializer_filter(
1414
t.Union[t.Set[t.Union[int, str]], t.Mapping[t.Union[int, str], t.Any]]
1515
] = None,
1616
by_alias: bool = True,
17-
skip_defaults: t.Optional[bool] = None,
1817
exclude_unset: bool = False,
1918
exclude_defaults: bool = False,
2019
exclude_none: bool = False,
@@ -26,7 +25,6 @@ def serializer_filter(
2625
:param include:
2726
:param exclude:
2827
:param by_alias:
29-
:param skip_defaults:
3028
:param exclude_unset:
3129
:param exclude_defaults:
3230
:param exclude_none:
@@ -39,7 +37,6 @@ def serializer_filter(
3937
include=include,
4038
exclude=exclude,
4139
by_alias=by_alias,
42-
skip_defaults=skip_defaults,
4340
exclude_unset=exclude_unset,
4441
exclude_defaults=exclude_defaults,
4542
exclude_none=exclude_none,

ellar/common/exceptions/callable_exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ async def catch(
6666
) -> t.Union[Response, t.Any]:
6767
args = tuple(list(self.func_args) + [ctx, exc])
6868
if self.is_async:
69-
return await self.callable_exception_handler(*args) # type:ignore
69+
return await self.callable_exception_handler(*args) # type:ignore[misc]
7070
return await run_in_threadpool(self.callable_exception_handler, *args)
7171

7272
def __eq__(self, other: t.Any) -> bool:

ellar/common/exceptions/handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,5 @@ async def catch(
7676
config = ctx.get_app().config
7777
return config.DEFAULT_JSON_CLASS(
7878
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
79-
content={"detail": serialize_object(exc.errors())},
79+
content={"detail": exc.errors()},
8080
)

ellar/common/exceptions/validation.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import typing as t
22

3-
from pydantic import BaseModel, ValidationError, create_model
4-
from pydantic.error_wrappers import ErrorList
3+
from ellar.common.serializer import serialize_object
4+
from ellar.pydantic import BaseModel, create_model
55

66
RequestErrorModel: t.Type[BaseModel] = create_model("Request")
77
WebSocketErrorModel: t.Type[BaseModel] = create_model("WebSocket")
88

99

10-
class RequestValidationError(ValidationError):
11-
def __init__(self, errors: t.Sequence[ErrorList]) -> None:
12-
super().__init__(errors, RequestErrorModel)
10+
class ValidationException(Exception):
11+
def __init__(self, errors: t.Sequence[t.Any]) -> None:
12+
self._errors = errors
1313

14+
def errors(self) -> t.Sequence[t.Any]:
15+
return serialize_object(self._errors) # type:ignore[no-any-return]
1416

15-
class WebSocketRequestValidationError(ValidationError):
16-
def __init__(self, errors: t.Sequence[ErrorList]) -> None:
17-
super().__init__(errors, WebSocketErrorModel)
17+
18+
class RequestValidationError(ValidationException):
19+
def __init__(self, errors: t.Sequence[t.Any], *, body: t.Any = None) -> None:
20+
super().__init__(errors)
21+
self.body = body
22+
23+
24+
class WebSocketRequestValidationError(ValidationException):
25+
pass

ellar/common/interfaces/exceptions.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
import typing as t
22
from abc import ABC, abstractmethod
33

4+
from ellar.pydantic import as_pydantic_validator
45
from starlette.responses import Response
56

67
from .context import IHostContext
78

89

10+
@as_pydantic_validator("__validate_input__", schema={"type": "object"})
911
class IExceptionHandler(ABC, t.Iterable):
1012
@classmethod
11-
def __get_validators__(
12-
cls: t.Type["IExceptionHandler"],
13-
) -> t.Iterable[t.Callable[..., t.Any]]:
14-
# for Pydantic Model Validation
15-
yield cls.__validate
16-
17-
@classmethod
18-
def __validate(cls, v: t.Any) -> t.Any:
13+
def __validate_input__(cls, v: t.Any, _: t.Any) -> t.Any:
1914
if not isinstance(v, cls):
2015
raise ValueError(f"Expected IExceptionHandler object, received: {type(v)}")
2116
return v

ellar/common/interfaces/middleware.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import typing as t
22

3+
from ellar.pydantic import as_pydantic_validator
34

4-
class IEllarMiddleware:
5-
@classmethod
6-
def __get_validators__(
7-
cls: t.Type["IEllarMiddleware"],
8-
) -> t.Iterable[t.Callable[..., t.Any]]:
9-
# for Pydantic Model Validation
10-
yield cls.__validate
115

6+
@as_pydantic_validator("__validate_input__", schema={"type": "object"})
7+
class IEllarMiddleware:
128
@classmethod
13-
def __validate(cls, v: t.Any) -> t.Any:
9+
def __validate_input__(cls, v: t.Any, _: t.Any) -> t.Any:
1410
if not isinstance(v, cls):
1511
raise ValueError(f"Expected EllarMiddleware object, received: {type(v)}")
1612
return v # pragma: no cover

ellar/common/interfaces/response_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import typing as t
22
from abc import ABC, abstractmethod
33

4-
from pydantic.fields import ModelField
4+
from ellar.pydantic import ModelField
55
from starlette.responses import Response
66

77
from .context import IExecutionContext

ellar/common/interfaces/versioning.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from abc import ABC, abstractmethod
33

44
from ellar.common.types import TScope
5+
from ellar.pydantic import as_pydantic_validator
56

67

78
class IAPIVersioningResolver(ABC):
@@ -10,20 +11,14 @@ def can_activate(self, route_versions: t.Set[t.Union[int, float, str]]) -> bool:
1011
"""Validate Version routes"""
1112

1213

14+
@as_pydantic_validator("__validate_input__", schema={"type": "object"})
1315
class IAPIVersioning(ABC):
1416
@abstractmethod
1517
def get_version_resolver(self, scope: TScope) -> IAPIVersioningResolver:
1618
"""Retrieve Version Resolver"""
1719

1820
@classmethod
19-
def __get_validators__(
20-
cls: t.Type["IAPIVersioning"],
21-
) -> t.Iterable[t.Callable[..., t.Any]]:
22-
# for Pydantic Model Validation
23-
yield cls.__validate
24-
25-
@classmethod
26-
def __validate(cls, v: t.Any) -> t.Any:
21+
def __validate_input__(cls, v: t.Any, _: t.Any) -> t.Any:
2722
if not isinstance(v, cls):
2823
raise ValueError(f"Expected BaseAPIVersioning, received: {type(v)}")
2924
return v

0 commit comments

Comments
 (0)