Skip to content

Add type hints using basedpyright #85

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,12 @@ jobs:
- uses: astral-sh/ruff-action@v3
- run: ruff check
- run: ruff format --check
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/[email protected]
with:
python-version: '3.12'
- run: uvx poetry install --extras=ssr
- run: uvx poetry run mypy .
14 changes: 4 additions & 10 deletions inertia/helpers.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
def deep_transform_callables(prop):
from typing import Any


def deep_transform_callables(prop: Any) -> Any:
if not isinstance(prop, dict):
return prop() if callable(prop) else prop

for key in list(prop.keys()):
prop[key] = deep_transform_callables(prop[key])

return prop


def validate_type(value, name, expected_type):
if not isinstance(value, expected_type):
raise TypeError(
f"Expected {expected_type.__name__} for {name}, got {type(value).__name__}"
)

return value
127 changes: 77 additions & 50 deletions inertia/http.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from functools import wraps
from http import HTTPStatus
from json import dumps as json_encode
from typing import Any, Callable
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lazy annotations and if TYPE_CHECKING might be a nice way to avoid unessecary imports.


from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse
from django.template.loader import render_to_string

from .helpers import deep_transform_callables, validate_type
from .helpers import deep_transform_callables
from .prop_classes import DeferredProp, IgnoreOnFirstLoadProp, MergeableProp
from .settings import settings

Expand All @@ -15,7 +16,7 @@
# a mock module
import requests
except ImportError:
requests = None
requests = None # type: ignore[assignment]


INERTIA_REQUEST_ENCRYPT_HISTORY = "_inertia_encrypt_history"
Expand All @@ -26,51 +27,55 @@


class InertiaRequest(HttpRequest):
def __init__(self, request):
def __init__(self, request: HttpRequest):
super().__init__()
self.__dict__.update(request.__dict__)

@property
def inertia(self):
def inertia(self) -> dict[str, Any]:
inertia_attr = self.__dict__.get("inertia")
return (
inertia_attr.all() if inertia_attr and hasattr(inertia_attr, "all") else {}
)

def is_a_partial_render(self, component):
def is_a_partial_render(self, component: str) -> bool:
return (
"X-Inertia-Partial-Data" in self.headers
and self.headers.get("X-Inertia-Partial-Component", "") == component
)

def partial_keys(self):
def partial_keys(self) -> list[str]:
return self.headers.get("X-Inertia-Partial-Data", "").split(",")

def reset_keys(self):
def reset_keys(self) -> list[str]:
return self.headers.get("X-Inertia-Reset", "").split(",")

def is_inertia(self):
def is_inertia(self) -> bool:
return "X-Inertia" in self.headers

def should_encrypt_history(self):
return validate_type(
getattr(
self,
INERTIA_REQUEST_ENCRYPT_HISTORY,
settings.INERTIA_ENCRYPT_HISTORY,
),
expected_type=bool,
name="encrypt_history",
def should_encrypt_history(self) -> bool:
should_encrypt = getattr(
self, INERTIA_REQUEST_ENCRYPT_HISTORY, settings.INERTIA_ENCRYPT_HISTORY
)
if not isinstance(should_encrypt, bool):
raise TypeError(
f"Expected bool for encrypt_history, got {type(should_encrypt).__name__}"
)
return should_encrypt


class BaseInertiaResponseMixin:
def page_data(self):
clear_history = validate_type(
self.request.session.pop(INERTIA_SESSION_CLEAR_HISTORY, False),
expected_type=bool,
name="clear_history",
)
request: InertiaRequest
component: str
props: dict[str, Any]
template_data: dict[str, Any]

def page_data(self) -> dict[str, Any]:
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 = {
"component": self.component,
Expand All @@ -91,7 +96,7 @@ def page_data(self):

return _page

def build_props(self):
def build_props(self) -> Any:
_props = {
**(self.request.inertia),
**self.props,
Expand All @@ -107,18 +112,18 @@ def build_props(self):

return deep_transform_callables(_props)

def build_deferred_props(self):
def build_deferred_props(self) -> dict[str, Any] | None:
if self.request.is_a_partial_render(self.component):
return None

_deferred_props = {}
_deferred_props: dict[str, Any] = {}
for key, prop in self.props.items():
if isinstance(prop, DeferredProp):
_deferred_props.setdefault(prop.group, []).append(key)

return _deferred_props

def build_merge_props(self):
def build_merge_props(self) -> list[str]:
return [
key
for key, prop in self.props.items()
Expand All @@ -129,7 +134,7 @@ def build_merge_props(self):
)
]

def build_first_load(self, data):
def build_first_load(self, data: Any) -> str:
context, template = self.build_first_load_context_and_template(data)

try:
Expand All @@ -151,7 +156,9 @@ def build_first_load(self, data):
using=None,
)

def build_first_load_context_and_template(self, data):
def build_first_load_context_and_template(
self, data: Any
) -> tuple[dict[str, Any], str]:
if settings.INERTIA_SSR_ENABLED:
try:
response = requests.post(
Expand All @@ -178,14 +185,14 @@ class InertiaResponse(BaseInertiaResponseMixin, HttpResponse):

def __init__(
self,
request,
component,
props=None,
template_data=None,
headers=None,
*args,
**kwargs,
):
request: HttpRequest,
component: str,
props: dict[str, Any] | None = None,
template_data: dict[str, Any] | None = None,
headers: dict[str, Any] | None = None,
*args: Any,
**kwargs: Any,
) -> None:
self.request = InertiaRequest(request)
self.component = component
self.props = props or {}
Expand All @@ -208,19 +215,30 @@ def __init__(
else:
content = self.build_first_load(data)

super().__init__(
*args,
content=content,
headers=_headers,
**kwargs,
)
if args:
super().__init__(
*args,
Copy link
Preview

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional logic for handling args creates inconsistent behavior. When args is provided, the content parameter is not passed to the parent constructor, which could result in empty response content. The content should be passed in both branches.

Suggested change
*args,
*args,
content=content,

Copilot uses AI. Check for mistakes.

headers=_headers,
**kwargs,
)
else:
super().__init__(
content=content,
headers=_headers,
**kwargs,
)


def render(request, component, props=None, template_data=None):
def render(
request: HttpRequest,
component: str,
props: dict[str, Any] | None = None,
template_data: dict[str, Any] | None = None,
) -> InertiaResponse:
return InertiaResponse(request, component, props or {}, template_data or {})


def location(location):
def location(location: str) -> HttpResponse:
return HttpResponse(
"",
status=HTTPStatus.CONFLICT,
Expand All @@ -230,18 +248,27 @@ def location(location):
)


def encrypt_history(request, value=True):
def encrypt_history(request: HttpRequest, value: bool = True) -> None:
setattr(request, INERTIA_REQUEST_ENCRYPT_HISTORY, value)


def clear_history(request):
def clear_history(request: HttpRequest) -> None:
request.session[INERTIA_SESSION_CLEAR_HISTORY] = True


def inertia(component):
def decorator(func):
def inertia(
component: str,
) -> Callable[
[Callable[..., HttpResponse | InertiaResponse | dict[str, Any]]],
Callable[..., HttpResponse],
]:
def decorator(
func: Callable[..., HttpResponse | InertiaResponse | dict[str, Any]],
) -> Callable[..., HttpResponse]:
@wraps(func)
def process_inertia_response(request, *args, **kwargs):
def process_inertia_response(
request: HttpRequest, *args: Any, **kwargs: Any
) -> HttpResponse:
props = func(request, *args, **kwargs)

# if a response is returned, return it
Expand Down
26 changes: 17 additions & 9 deletions inertia/middleware.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from typing import Callable

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


class InertiaMiddleware:
def __init__(self, get_response):
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
self.get_response = get_response

def __call__(self, request):
def __call__(self, request: HttpRequest) -> HttpResponse:
response = self.get_response(request)

# Inertia requests don't ever render templates, so they skip the typical Django
Expand All @@ -27,28 +30,33 @@ def __call__(self, request):

return response

def is_non_post_redirect(self, request, response):
def is_non_post_redirect(
self, request: HttpRequest, response: HttpResponse
) -> bool:
return self.is_redirect_request(response) and request.method in [
"PUT",
"PATCH",
"DELETE",
]

def is_inertia_request(self, request):
def is_inertia_request(self, request: HttpRequest) -> bool:
return "X-Inertia" in request.headers

def is_redirect_request(self, response):
def is_redirect_request(self, response: HttpResponse) -> bool:
return response.status_code in [301, 302]

def is_stale(self, request):
def is_stale(self, request: HttpRequest) -> bool:
return (
request.headers.get("X-Inertia-Version", settings.INERTIA_VERSION)
!= settings.INERTIA_VERSION
)

def is_stale_inertia_get(self, request):
def is_stale_inertia_get(self, request: HttpRequest) -> bool:
return request.method == "GET" and self.is_stale(request)

def force_refresh(self, request):
messages.get_messages(request).used = False
def force_refresh(self, request: HttpRequest) -> HttpResponse:
# If the storage middleware is not defined, get_messages returns an empty list
storage = messages.get_messages(request)
if not isinstance(storage, list):
storage.used = False
return location(request.build_absolute_uri())
13 changes: 7 additions & 6 deletions inertia/prop_classes.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from abc import ABC, abstractmethod
from typing import Any


class CallableProp:
def __init__(self, prop):
def __init__(self, prop: Any) -> None:
self.prop = prop

def __call__(self):
def __call__(self) -> Any:
return self.prop() if callable(self.prop) else self.prop


class MergeableProp(ABC):
@abstractmethod
def should_merge(self):
def should_merge(self) -> bool:
pass


Expand All @@ -24,15 +25,15 @@ class OptionalProp(CallableProp, IgnoreOnFirstLoadProp):


class DeferredProp(CallableProp, MergeableProp, IgnoreOnFirstLoadProp):
def __init__(self, prop, group, merge=False):
def __init__(self, prop: Any, group: str, merge: bool = False) -> None:
super().__init__(prop)
self.group = group
self.merge = merge

def should_merge(self):
def should_merge(self) -> bool:
return self.merge


class MergeProp(CallableProp, MergeableProp):
def should_merge(self):
def should_merge(self) -> bool:
return True
4 changes: 3 additions & 1 deletion inertia/settings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

from django.conf import settings as django_settings

from .utils import InertiaJsonEncoder
Expand All @@ -12,7 +14,7 @@ class InertiaSettings:
INERTIA_SSR_ENABLED = False
INERTIA_ENCRYPT_HISTORY = False

def __getattribute__(self, name):
def __getattribute__(self, name: str) -> Any:
try:
return getattr(django_settings, name)
except AttributeError:
Expand Down
Loading
Loading