diff --git a/inertia/helpers.py b/inertia/helpers.py index 2b5b6c2..919cc59 100644 --- a/inertia/helpers.py +++ b/inertia/helpers.py @@ -1,9 +1,17 @@ -from typing import Any +from __future__ import annotations +from typing import TYPE_CHECKING, cast -def deep_transform_callables(prop: Any) -> Any: +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, TypeVar + + T = TypeVar("T", bound=Any) + + +def deep_transform_callables(prop: T | Callable[[], T]) -> T: if not isinstance(prop, dict): - return prop() if callable(prop) else prop + return cast("T", prop() if callable(prop) else prop) for key in list(prop.keys()): prop[key] = deep_transform_callables(prop[key]) diff --git a/inertia/http.py b/inertia/http.py index 433c80c..5f0cfe0 100644 --- a/inertia/http.py +++ b/inertia/http.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import logging from functools import wraps from http import HTTPStatus from json import dumps as json_encode -from typing import Any, Callable +from typing import TYPE_CHECKING from django.core.exceptions import ImproperlyConfigured from django.http import HttpRequest, HttpResponse @@ -19,6 +21,25 @@ except ImportError: requests = None # type: ignore[assignment] + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Concatenate, NotRequired, ParamSpec, TypedDict, TypeVar + + P = ParamSpec("P") + R = TypeVar("R", HttpResponse, "InertiaResponse", dict[str, Any]) + + class PageDataDict(TypedDict): + component: str + props: dict[str, Any] + url: str + version: str + encryptHistory: bool + clearHistory: bool + deferredProps: NotRequired[dict[str, Any] | None] + mergeProps: NotRequired[list[str]] + + logger = logging.getLogger(__name__) INERTIA_REQUEST_ENCRYPT_HISTORY = "_inertia_encrypt_history" @@ -29,7 +50,7 @@ class InertiaRequest(HttpRequest): - def __init__(self, request: HttpRequest): + def __init__(self, request: HttpRequest) -> None: super().__init__() self.__dict__.update(request.__dict__) @@ -72,14 +93,14 @@ class BaseInertiaResponseMixin: props: dict[str, Any] template_data: dict[str, Any] - def page_data(self) -> dict[str, Any]: + def page_data(self) -> PageDataDict: clear_history = self.request.session.pop(INERTIA_SESSION_CLEAR_HISTORY, False) if not isinstance(clear_history, bool): raise TypeError( f"Expected bool for clear_history, got {type(clear_history).__name__}" ) - _page = { + _page: PageDataDict = { "component": self.component, "props": self.build_props(), "url": self.request.get_full_path(), @@ -98,9 +119,9 @@ def page_data(self) -> dict[str, Any]: return _page - def build_props(self) -> Any: + def build_props(self) -> dict[str, Any]: _props = { - **(self.request.inertia), + **self.request.inertia, **self.props, } @@ -178,7 +199,7 @@ def build_first_load_context_and_template( return { "page": data, - **(self.template_data), + **self.template_data, }, INERTIA_TEMPLATE @@ -261,15 +282,15 @@ def clear_history(request: HttpRequest) -> None: def inertia( component: str, ) -> Callable[ - [Callable[..., HttpResponse | InertiaResponse | dict[str, Any]]], - Callable[..., HttpResponse], + [Callable[Concatenate[HttpRequest, P], R]], + Callable[Concatenate[HttpRequest, P], HttpResponse], ]: def decorator( - func: Callable[..., HttpResponse | InertiaResponse | dict[str, Any]], - ) -> Callable[..., HttpResponse]: + func: Callable[Concatenate[HttpRequest, P], R], + ) -> Callable[Concatenate[HttpRequest, P], HttpResponse]: @wraps(func) def process_inertia_response( - request: HttpRequest, *args: Any, **kwargs: Any + request: HttpRequest, /, *args: P.args, **kwargs: P.kwargs ) -> HttpResponse: props = func(request, *args, **kwargs) diff --git a/inertia/middleware.py b/inertia/middleware.py index 37170bf..ce3176b 100644 --- a/inertia/middleware.py +++ b/inertia/middleware.py @@ -1,12 +1,18 @@ -from typing import Callable +from __future__ import annotations + +from typing import TYPE_CHECKING from django.contrib import messages -from django.http import HttpRequest, HttpResponse from django.middleware.csrf import get_token from .http import location from .settings import settings +if TYPE_CHECKING: + from collections.abc import Callable + + from django.http import HttpRequest, HttpResponse + class InertiaMiddleware: def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None: diff --git a/inertia/prop_classes.py b/inertia/prop_classes.py index 0ae8d07..b9f6058 100644 --- a/inertia/prop_classes.py +++ b/inertia/prop_classes.py @@ -1,13 +1,21 @@ +from __future__ import annotations + from abc import ABC, abstractmethod -from typing import Any +from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast + +if TYPE_CHECKING: + from collections.abc import Callable + + +T = TypeVar("T", bound=Any) -class CallableProp: - def __init__(self, prop: Any) -> None: +class CallableProp(Generic[T]): + def __init__(self, prop: T | Callable[[], T]) -> None: self.prop = prop - def __call__(self) -> Any: - return self.prop() if callable(self.prop) else self.prop + def __call__(self) -> T: + return cast(T, self.prop() if callable(self.prop) else self.prop) class MergeableProp(ABC): @@ -20,12 +28,12 @@ class IgnoreOnFirstLoadProp: pass -class OptionalProp(CallableProp, IgnoreOnFirstLoadProp): +class OptionalProp(CallableProp[T], IgnoreOnFirstLoadProp): pass -class DeferredProp(CallableProp, MergeableProp, IgnoreOnFirstLoadProp): - def __init__(self, prop: Any, group: str, merge: bool = False) -> None: +class DeferredProp(CallableProp[T], MergeableProp, IgnoreOnFirstLoadProp): + def __init__(self, prop: T | Callable[[], T], group: str, merge: bool = False) -> None: super().__init__(prop) self.group = group self.merge = merge @@ -34,6 +42,6 @@ def should_merge(self) -> bool: return self.merge -class MergeProp(CallableProp, MergeableProp): +class MergeProp(CallableProp[T], MergeableProp): def should_merge(self) -> bool: return True diff --git a/inertia/share.py b/inertia/share.py index ff67b9e..85c15c2 100644 --- a/inertia/share.py +++ b/inertia/share.py @@ -1,6 +1,12 @@ -from typing import Any +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + from django.http import HttpRequest -from django.http import HttpRequest __all__ = ["share"] diff --git a/inertia/utils.py b/inertia/utils.py index fc79896..d64be5c 100644 --- a/inertia/utils.py +++ b/inertia/utils.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import warnings -from typing import Any +from typing import TYPE_CHECKING, Any, TypeVar from django.core.serializers.json import DjangoJSONEncoder from django.db import models @@ -8,6 +10,12 @@ from .prop_classes import DeferredProp, MergeProp, OptionalProp +if TYPE_CHECKING: + from collections.abc import Callable + + +T = TypeVar("T", bound=Any) + def model_to_dict(model: models.Model) -> dict[str, Any]: return base_model_to_dict(model, exclude=("password",)) @@ -32,7 +40,7 @@ def default(self, o: Any) -> Any: return super().default(o) -def lazy(prop: Any) -> OptionalProp: +def lazy(prop: T | Callable[[], T]) -> OptionalProp[T]: warnings.warn( "lazy is deprecated and will be removed in a future version. Please use optional instead.", DeprecationWarning, @@ -41,13 +49,13 @@ def lazy(prop: Any) -> OptionalProp: return optional(prop) -def optional(prop: Any) -> OptionalProp: +def optional(prop: T | Callable[[], T]) -> OptionalProp[T]: return OptionalProp(prop) -def defer(prop: Any, group: str = "default", merge: bool = False) -> DeferredProp: +def defer(prop: T | Callable[[], T], group: str = "default", merge: bool = False) -> DeferredProp[T]: return DeferredProp(prop, group=group, merge=merge) -def merge(prop: Any) -> MergeProp: +def merge(prop: T | Callable[[], T]) -> MergeProp[T]: return MergeProp(prop)