Skip to content

Commit 5198c15

Browse files
authored
Merge pull request #243 from eadwinCode/pydantic_v1_drop
fix: Dropped Pydantic V1
2 parents dd7c293 + 9f6caa3 commit 5198c15

38 files changed

+937
-886
lines changed

docs/dependency-injection/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Let's create a more practical example with a Todo application that demonstrates
3535
from typing import List, Optional
3636
from datetime import datetime
3737
from pydantic import BaseModel
38-
from injector import inject, singleton
38+
from injector import inject
3939

4040
# Data Models
4141
class TodoItem(BaseModel):

docs/route_context.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ You can access the `RouteContext` during schema validation using the `service_re
7777

7878
```python
7979
from ninja_extra import service_resolver
80-
from ninja_extra.controllers import RouteContext
80+
from ninja_extra.context import RouteContext
8181
from ninja import ModelSchema
82-
from pydantic import validator
82+
from pydantic import field_validator
8383
from django.urls import reverse
8484

8585
class UserProfileSchema(ModelSchema):
@@ -89,7 +89,7 @@ class UserProfileSchema(ModelSchema):
8989
model = UserProfile
9090
model_fields = ["avatar_url", "bio"]
9191

92-
@validator("avatar_url")
92+
@field_validator("avatar_url", mode="before")
9393
def make_absolute_url(cls, value):
9494
# Get RouteContext to access request
9595
context: RouteContext = service_resolver(RouteContext)

docs/settings.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ NINJA_EXTRA = {
2121
'NUM_PROXIES': None,
2222
'ORDERING_CLASS':"ninja_extra.ordering.Ordering",
2323
'SEARCHING_CLASS':"ninja_extra.searching.Searching",
24-
'ROUTE_CONTEXT_CLASS':"ninja_extra.controllers.RouteContext",
24+
'ROUTE_CONTEXT_CLASS':"ninja_extra.context.RouteContext",
2525
}
2626
```
2727

@@ -74,6 +74,6 @@ default: `ninja_extra.searching.Searching`
7474
# `ROUTE_CONTEXT_CLASS`
7575

7676
It defines the default `RouteContext` class that will be instantiated during request processing.
77-
default: `ninja_extra.controllers.RouteContext`
77+
default: `ninja_extra.context.RouteContext`
7878

7979
See [Route Context](route_context.md) for more details.

ninja_extra/apps.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from typing import Any, cast
1+
import typing as t
22

33
from django.apps import AppConfig, apps
44
from django.utils.translation import gettext_lazy as _
55
from injector import Injector, Module
66

7-
from ninja_extra.conf import settings
7+
from ninja_extra.lazy import settings_lazy
88
from ninja_extra.modules import NinjaExtraModule
99
from ninja_extra.shortcuts import fail_silently
1010

@@ -23,15 +23,15 @@ def ready(self) -> None:
2323
django_injector_app = fail_silently(
2424
apps.get_app_config, app_label="django_injector"
2525
)
26-
app = cast(Any, django_injector_app)
26+
app = t.cast(t.Any, django_injector_app)
2727
if app: # pragma: no cover
2828
app.ready()
2929
self.injector = app.injector
3030
self.injector.binder.install(self.ninja_extra_module)
3131
self.register_injector_modules()
3232

3333
def register_injector_modules(self) -> None: # pragma: no cover
34-
for module in settings.INJECTOR_MODULES:
34+
for module in settings_lazy().INJECTOR_MODULES:
3535
if isinstance(module, type) and issubclass(module, Module):
3636
module = module()
3737
self.injector.binder.install(module)

ninja_extra/conf/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from .settings import settings
1+
from .package_settings import settings
22

33
__all__ = ["settings"]

ninja_extra/conf/decorator.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import typing as t
2+
3+
from django.utils.module_loading import import_string
4+
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
5+
from pydantic.json_schema import JsonSchemaValue
6+
from pydantic_core.core_schema import CoreSchema, with_info_plain_validator_function
7+
8+
from ninja_extra.shortcuts import fail_silently
9+
10+
11+
class AllowTypeOfSource:
12+
def __init__(
13+
self,
14+
schema: t.Optional[t.Dict[str, t.Any]] = None,
15+
validator: t.Optional[t.Callable[..., bool]] = None,
16+
error_message: t.Optional[t.Callable[..., str]] = None,
17+
) -> None:
18+
self._schema = schema
19+
self.validator = validator
20+
self.error_message = error_message
21+
22+
def __get_pydantic_core_schema__(
23+
self,
24+
source: t.Type[t.Any],
25+
handler: GetCoreSchemaHandler,
26+
) -> CoreSchema:
27+
def validate(value: t.Any, *args: t.Any) -> t.Any:
28+
if isinstance(value, str):
29+
try_import_value = fail_silently(import_string, value)
30+
if try_import_value is not None:
31+
value = try_import_value
32+
33+
if (self.validator and not self.validator(source, value)) or (
34+
not self.validator and not isinstance(value, source)
35+
):
36+
self._handle_error(source, value)
37+
return value
38+
39+
return with_info_plain_validator_function(validate)
40+
41+
def _handle_error(self, source: t.Any, value: t.Any) -> None:
42+
error_message = (
43+
f"Expected an instance of {source}, got an instance of {type(value)}"
44+
if self.error_message is None
45+
else self.error_message(source, value)
46+
)
47+
raise ValueError(error_message)
48+
49+
def __get_pydantic_json_schema__(
50+
self, core_schema: CoreSchema, handler: GetJsonSchemaHandler
51+
) -> JsonSchemaValue: # pragma: no cover
52+
return self._schema # type:ignore[return-value]

ninja_extra/conf/package_settings.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from typing import Any, Dict, List, Optional
2+
3+
from django.conf import settings as django_settings
4+
from django.core.signals import setting_changed
5+
from ninja.pagination import PaginationBase
6+
from ninja.throttling import BaseThrottle
7+
from pydantic import BaseModel, Field
8+
from typing_extensions import Annotated
9+
10+
from ninja_extra.conf.decorator import AllowTypeOfSource
11+
from ninja_extra.interfaces.ordering import OrderingBase
12+
from ninja_extra.interfaces.route_context import RouteContextBase
13+
from ninja_extra.interfaces.searching import SearchingBase
14+
15+
_GenericModelValidator = AllowTypeOfSource(
16+
validator=lambda source, value: isinstance(value, type)
17+
and issubclass(value, source),
18+
error_message=lambda source,
19+
value: f"Expected type of {source.__name__}, received: {type(value)}",
20+
)
21+
PaginationClassHandlerType = Annotated[PaginationBase, _GenericModelValidator]
22+
ThrottlingClassHandlerType = Annotated[BaseThrottle, _GenericModelValidator]
23+
SearchingClassHandlerType = Annotated[SearchingBase, _GenericModelValidator]
24+
OrderingClassHandlerType = Annotated[OrderingBase, _GenericModelValidator]
25+
RouteContextHandlerType = Annotated[RouteContextBase, _GenericModelValidator]
26+
27+
_InjectorModuleValidator = AllowTypeOfSource(
28+
validator=lambda source, value: value is not None,
29+
error_message=lambda source,
30+
value: f"Expected PaginationBase object, received: {type(value)}",
31+
)
32+
InjectorModuleHandlerType = Annotated[Any, _InjectorModuleValidator]
33+
34+
35+
class UserDefinedSettingsMapper:
36+
def __init__(self, data: dict) -> None:
37+
self.__dict__ = data
38+
39+
40+
NinjaEXTRA_SETTINGS_DEFAULTS = {
41+
"INJECTOR_MODULES": [],
42+
"PAGINATION_CLASS": "ninja.pagination.LimitOffsetPagination",
43+
"THROTTLE_CLASSES": [
44+
"ninja_extra.throttling.AnonRateThrottle",
45+
"ninja_extra.throttling.UserRateThrottle",
46+
],
47+
"THROTTLE_RATES": {"user": None, "anon": None},
48+
"ORDERING_CLASS": "ninja_extra.ordering.Ordering",
49+
"SEARCHING_CLASS": "ninja_extra.searching.Searching",
50+
"ROUTE_CONTEXT_CLASS": "ninja_extra.context.RouteContext",
51+
}
52+
53+
USER_SETTINGS = UserDefinedSettingsMapper(
54+
getattr(django_settings, "NINJA_EXTRA", NinjaEXTRA_SETTINGS_DEFAULTS)
55+
)
56+
57+
58+
class NinjaExtraSettings(BaseModel):
59+
class Config:
60+
from_attributes = True
61+
validate_assignment = True
62+
63+
PAGINATION_CLASS: PaginationClassHandlerType = Field( # type: ignore[assignment]
64+
"ninja.pagination.LimitOffsetPagination",
65+
)
66+
PAGINATION_PER_PAGE: int = Field(100)
67+
THROTTLE_RATES: Dict[str, Optional[str]] = Field(
68+
{"user": "1000/day", "anon": "100/day"}
69+
)
70+
THROTTLE_CLASSES: List[ThrottlingClassHandlerType] = Field(
71+
[
72+
"ninja_extra.throttling.AnonRateThrottle", # type: ignore[list-item]
73+
"ninja_extra.throttling.UserRateThrottle", # type: ignore[list-item]
74+
]
75+
)
76+
NUM_PROXIES: Optional[int] = None
77+
INJECTOR_MODULES: List[InjectorModuleHandlerType] = []
78+
ORDERING_CLASS: OrderingClassHandlerType = Field( # type: ignore[assignment]
79+
"ninja_extra.ordering.Ordering",
80+
)
81+
SEARCHING_CLASS: SearchingClassHandlerType = Field( # type: ignore[assignment]
82+
"ninja_extra.searching.Searching",
83+
)
84+
ROUTE_CONTEXT_CLASS: RouteContextHandlerType = Field( # type: ignore[assignment]
85+
"ninja_extra.context.RouteContext",
86+
)
87+
88+
89+
settings = NinjaExtraSettings.model_validate(USER_SETTINGS)
90+
91+
92+
def reload_settings(*args: Any, **kwargs: Any) -> None: # pragma: no cover
93+
global settings
94+
95+
setting, value = kwargs["setting"], kwargs["value"]
96+
97+
if setting == "NINJA_EXTRA":
98+
settings = NinjaExtraSettings.model_validate(UserDefinedSettingsMapper(value))
99+
100+
101+
setting_changed.connect(reload_settings) # pragma: no cover

ninja_extra/conf/settings.py

Lines changed: 0 additions & 125 deletions
This file was deleted.

ninja_extra/constants.py

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

44
if t.TYPE_CHECKING: # pragma: no cover
5-
from ninja_extra.controllers.route.context import RouteContext
5+
from ninja_extra.context import RouteContext
66

77

88
POST = "POST"

0 commit comments

Comments
 (0)