From a1e3853d7bf7478e3d0e18bbd9fe47edf1bb98a2 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 9 Jan 2025 09:31:37 +0100 Subject: [PATCH 1/8] Add ruff linting and formatter Closes #62 --- .editorconfig | 14 ++++++++++++++ .github/workflows/ruff.yml | 18 ++++++++++++++++++ .gitignore | 5 ++++- pyproject.toml | 13 +++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/ruff.yml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2933ecf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..442fc0e --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,18 @@ +name: Lint and format + +on: + push: + branches-ignore: + - main + pull_request: + paths-ignore: + - '**.md' + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3 + - run: ruff check + - run: ruff format --check diff --git a/.gitignore b/.gitignore index e93fb9d..14842f4 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,9 @@ coverage.xml .pytest_cache/ cover/ +# Linter / formatter +.ruff_cache + # Translations *.mo *.pot @@ -163,4 +166,4 @@ cython_debug/ poetry.lock .DS_Store -.tool-versions \ No newline at end of file +.tool-versions diff --git a/pyproject.toml b/pyproject.toml index 0c5ed5a..e7a4c10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,20 @@ requests = "^2" [tool.poetry.dev-dependencies] pytest = "^7.1.2" pytest-django = "^4.5.2" +ruff = "^0.8.6" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle + "W", # pycodestyle + "F", # Pyflakes + "B", # flake8-bugbear + "SIM", # flake8-simplify + "I", # isort +] +ignore = ["E501"] # line too long, formatter will handle this +unfixable = ["B", "SIM"] From 586bbe0c307abcaa2e48b9ad7559054edb86a498 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 9 Jan 2025 09:37:58 +0100 Subject: [PATCH 2/8] Initial ruff check --fix --- inertia/__init__.py | 4 +-- inertia/helpers.py | 4 +-- inertia/http.py | 48 +++++++++++++++++--------------- inertia/middleware.py | 7 +++-- inertia/prop_classes.py | 6 ++-- inertia/settings.py | 3 +- inertia/test.py | 25 +++++++++-------- inertia/tests/test_encoder.py | 11 +++++--- inertia/tests/test_history.py | 7 ++--- inertia/tests/test_middleware.py | 2 +- inertia/tests/test_rendering.py | 20 +++++++------ inertia/tests/test_settings.py | 4 ++- inertia/tests/test_ssr.py | 15 ++++++---- inertia/tests/test_tests.py | 1 + inertia/tests/testapp/apps.py | 1 + inertia/tests/testapp/models.py | 1 + inertia/tests/testapp/urls.py | 1 + inertia/tests/testapp/views.py | 6 ++-- inertia/utils.py | 11 +++++--- 19 files changed, 101 insertions(+), 76 deletions(-) diff --git a/inertia/__init__.py b/inertia/__init__.py index 8f0d506..c91b109 100644 --- a/inertia/__init__.py +++ b/inertia/__init__.py @@ -1,3 +1,3 @@ -from .http import inertia, render, location, InertiaResponse -from .utils import lazy, optional, defer, merge +from .http import InertiaResponse, inertia, location, render from .share import share +from .utils import defer, lazy, merge, optional diff --git a/inertia/helpers.py b/inertia/helpers.py index 0fa1a89..5457516 100644 --- a/inertia/helpers.py +++ b/inertia/helpers.py @@ -1,7 +1,7 @@ def deep_transform_callables(prop): 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]) @@ -10,5 +10,5 @@ def deep_transform_callables(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 diff --git a/inertia/http.py b/inertia/http.py index f496979..1692012 100644 --- a/inertia/http.py +++ b/inertia/http.py @@ -1,12 +1,14 @@ +from functools import wraps from http import HTTPStatus -from django.template.loader import render_to_string -from django.http import HttpResponse -from .settings import settings from json import dumps as json_encode -from functools import wraps + import requests -from .prop_classes import IgnoreOnFirstLoadProp, DeferredProp, MergeableProp +from django.http import HttpResponse +from django.template.loader import render_to_string + from .helpers import deep_transform_callables, validate_type +from .prop_classes import DeferredProp, IgnoreOnFirstLoadProp, MergeableProp +from .settings import settings INERTIA_REQUEST_ENCRYPT_HISTORY = "_inertia_encrypt_history" INERTIA_SESSION_CLEAR_HISTORY = "_inertia_clear_history" @@ -17,27 +19,27 @@ class InertiaRequest: def __init__(self, request): self.request = request - + def __getattr__(self, name): return getattr(self.request, name) - + @property def headers(self): return self.request.headers - + @property def inertia(self): return self.request.inertia.all() if hasattr(self.request, 'inertia') else {} - + def is_a_partial_render(self, component): return 'X-Inertia-Partial-Data' in self.headers and self.headers.get('X-Inertia-Partial-Component', '') == component def partial_keys(self): return self.headers.get('X-Inertia-Partial-Data', '').split(',') - + def reset_keys(self): return self.headers.get('X-Inertia-Reset', '').split(',') - + def is_inertia(self): return 'X-Inertia' in self.headers @@ -55,7 +57,7 @@ def page_data(self): expected_type=bool, name="clear_history" ) - + _page = { 'component': self.component, 'props': self.build_props(), @@ -68,11 +70,11 @@ def page_data(self): _deferred_props = self.build_deferred_props() if _deferred_props: _page['deferredProps'] = _deferred_props - + _merge_props = self.build_merge_props() if _merge_props: _page['mergeProps'] = _merge_props - + return _page def build_props(self): @@ -104,7 +106,7 @@ def build_deferred_props(self): def build_merge_props(self): return [ - key + key for key, prop in self.props.items() if ( isinstance(prop, MergeableProp) @@ -112,7 +114,7 @@ def build_merge_props(self): and key not in self.request.reset_keys() ) ] - + def build_first_load(self, data): context, template = self.build_first_load_context_and_template(data) @@ -126,10 +128,10 @@ def build_first_load(self, data): using=None, ) - + def build_first_load_context_and_template(self, data): if settings.INERTIA_SSR_ENABLED: - try: + try: response = requests.post( f"{settings.INERTIA_SSR_URL}/render", data=data, @@ -142,7 +144,7 @@ def build_first_load_context_and_template(self, data): }, INERTIA_SSR_TEMPLATE except Exception: pass - + return { 'page': data, **(self.template_data), @@ -169,9 +171,9 @@ def __init__(self, request, component, props=None, template_data=None, headers=N 'Content-Type': 'application/json', } content = data - else: + else: content = self.build_first_load(data) - + super().__init__( content=content, headers=_headers, @@ -209,7 +211,7 @@ def inner(request, *args, **kwargs): return props return render(request, component, props) - + return inner - + return decorator diff --git a/inertia/middleware.py b/inertia/middleware.py index 71f859c..559382a 100644 --- a/inertia/middleware.py +++ b/inertia/middleware.py @@ -1,13 +1,14 @@ -from .settings import settings from django.contrib import messages -from django.http import HttpResponse from django.middleware.csrf import get_token + from .http import location +from .settings import settings + class InertiaMiddleware: def __init__(self, get_response): self.get_response = get_response - + def __call__(self, request): response = self.get_response(request) diff --git a/inertia/prop_classes.py b/inertia/prop_classes.py index 9b37495..528a8e5 100644 --- a/inertia/prop_classes.py +++ b/inertia/prop_classes.py @@ -1,10 +1,10 @@ from abc import ABC, abstractmethod -import warnings + class CallableProp(ABC): def __init__(self, prop): self.prop = prop - + def __call__(self): return self.prop() if callable(self.prop) else self.prop @@ -24,7 +24,7 @@ def __init__(self, prop, group, merge=False): super().__init__(prop) self.group = group self.merge = merge - + def should_merge(self): return self.merge diff --git a/inertia/settings.py b/inertia/settings.py index ab9c1a3..a49377d 100644 --- a/inertia/settings.py +++ b/inertia/settings.py @@ -1,4 +1,5 @@ from django.conf import settings as django_settings + from .utils import InertiaJsonEncoder __all__ = ['settings'] @@ -9,7 +10,7 @@ class InertiaSettings: INERTIA_SSR_URL = 'http://localhost:13714' INERTIA_SSR_ENABLED = False INERTIA_ENCRYPT_HISTORY = False - + def __getattribute__(self, name): try: return getattr(django_settings, name) diff --git a/inertia/test.py b/inertia/test.py index 0b88f15..d25d6f1 100644 --- a/inertia/test.py +++ b/inertia/test.py @@ -1,19 +1,22 @@ -from django.test import TestCase, Client +from json import dumps, loads from unittest.mock import patch + from django.template.loader import render_to_string as base_render_to_string -from inertia.settings import settings -from json import dumps, loads +from django.test import Client, TestCase from django.utils.html import escape +from inertia.settings import settings + + class ClientWithLastResponse: def __init__(self, client): self.client = client self.last_response = None - + def get(self, *args, **kwargs): self.last_response = self.client.get(*args, **kwargs) return self.last_response - + def __getattr__(self, name): return getattr(self.client, name) @@ -21,7 +24,7 @@ class BaseInertiaTestCase: def setUp(self): self.inertia = ClientWithLastResponse(Client(HTTP_X_INERTIA=True)) self.client = ClientWithLastResponse(Client()) - + def last_response(self): return self.inertia.last_response or self.client.last_response @@ -41,15 +44,15 @@ def tearDown(self): def page(self): page_data = self.mock_render.call_args[0][1]['page'] if self.mock_render.call_args else self.last_response().content - + return loads(page_data) def props(self): return self.page()['props'] - + def merge_props(self): return self.page()['mergeProps'] - + def deferred_props(self): return self.page()['deferredProps'] @@ -87,10 +90,10 @@ def inertia_page(url, component='TestComponent', props={}, template_data={}, def if deferred_props: _page['deferredProps'] = deferred_props - + if merge_props: _page['mergeProps'] = merge_props - + return _page def inertia_div(*args, **kwargs): diff --git a/inertia/tests/test_encoder.py b/inertia/tests/test_encoder.py index 773e2e3..b842366 100644 --- a/inertia/tests/test_encoder.py +++ b/inertia/tests/test_encoder.py @@ -1,8 +1,11 @@ -from inertia.tests.testapp.models import User +from datetime import date, datetime +from json import dumps + from django.test import TestCase + +from inertia.tests.testapp.models import User from inertia.utils import InertiaJsonEncoder -from json import dumps -from datetime import date, datetime + class InertiaJsonEncoderTestCase(TestCase): def setUp(self): @@ -32,4 +35,4 @@ def test_it_handles_querysets(self): self.assertEqual( dumps([{'id': 1, 'name': 'Brandon', 'birthdate': '1987-02-15', 'registered_at': '2022-10-31T10:13:01'}]), self.encode(User.objects.all()) - ) \ No newline at end of file + ) diff --git a/inertia/tests/test_history.py b/inertia/tests/test_history.py index 07ba81d..a6388d9 100644 --- a/inertia/tests/test_history.py +++ b/inertia/tests/test_history.py @@ -1,8 +1,7 @@ -from inertia.http import encrypt_history -from inertia.test import InertiaTestCase from django.test import override_settings -from django.test.client import RequestFactory -from inertia.tests.testapp.views import empty_test + +from inertia.test import InertiaTestCase + class HistoryTestCase(InertiaTestCase): def test_encrypt_history_setting(self): diff --git a/inertia/tests/test_middleware.py b/inertia/tests/test_middleware.py index 04c3494..e4161fe 100644 --- a/inertia/tests/test_middleware.py +++ b/inertia/tests/test_middleware.py @@ -1,6 +1,6 @@ -from django.test import TestCase, Client, override_settings from inertia.test import InertiaTestCase + class MiddlewareTestCase(InertiaTestCase): def test_anything(self): response = self.client.get('/test/') diff --git a/inertia/tests/test_rendering.py b/inertia/tests/test_rendering.py index 6f9d608..2e41f89 100644 --- a/inertia/tests/test_rendering.py +++ b/inertia/tests/test_rendering.py @@ -1,6 +1,8 @@ -from inertia.test import InertiaTestCase, inertia_div, inertia_page from pytest import warns +from inertia.test import InertiaTestCase, inertia_div, inertia_page + + class FirstLoadTestCase(InertiaTestCase): def test_with_props(self): self.assertContains( @@ -140,8 +142,8 @@ def test_deferred_props_are_set(self): self.assertJSONResponse( self.inertia.get('/defer/'), inertia_page( - 'defer', - props={'name': 'Brian'}, + 'defer', + props={'name': 'Brian'}, deferred_props={'default': ['sport']}) ) @@ -149,9 +151,9 @@ def test_deferred_props_are_grouped(self): self.assertJSONResponse( self.inertia.get('/defer-group/'), inertia_page( - 'defer-group', - props={'name': 'Brian'}, - deferred_props={'group': ['sport', 'team'], 'default': ['grit']}) + 'defer-group', + props={'name': 'Brian'}, + deferred_props={'group': ['sport', 'team'], 'default': ['grit']}) ) def test_deferred_props_are_included_when_requested(self): @@ -159,7 +161,7 @@ def test_deferred_props_are_included_when_requested(self): self.inertia.get('/defer/', HTTP_X_INERTIA_PARTIAL_DATA='sport', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'), inertia_page('defer', props={'sport': 'Basketball'}) ) - + def test_only_deferred_props_in_group_are_included_when_requested(self): self.assertJSONResponse( @@ -190,7 +192,7 @@ def test_deferred_merge_props_are_included_on_subsequent_load(self): 'team': 'Penguins', }, merge_props=['sport', 'team']) ) - + def test_merge_props_are_not_included_when_reset(self): self.assertJSONResponse( self.inertia.get( @@ -203,4 +205,4 @@ def test_merge_props_are_not_included_when_reset(self): 'sport': 'Hockey', 'team': 'Penguins', }) - ) \ No newline at end of file + ) diff --git a/inertia/tests/test_settings.py b/inertia/tests/test_settings.py index 452f27c..6c588e8 100644 --- a/inertia/tests/test_settings.py +++ b/inertia/tests/test_settings.py @@ -1,6 +1,8 @@ -from inertia.test import InertiaTestCase from django.test import override_settings +from inertia.test import InertiaTestCase + + class SettingsTestCase(InertiaTestCase): @override_settings( INERTIA_VERSION='2.0' diff --git a/inertia/tests/test_ssr.py b/inertia/tests/test_ssr.py index 93624d5..5921c87 100644 --- a/inertia/tests/test_ssr.py +++ b/inertia/tests/test_ssr.py @@ -1,9 +1,12 @@ import json -from inertia.test import InertiaTestCase, inertia_page, inertia_div +from unittest.mock import Mock, patch + from django.test import override_settings -from unittest.mock import patch, Mock from requests.exceptions import RequestException +from inertia.test import InertiaTestCase, inertia_div, inertia_page + + @override_settings( INERTIA_SSR_ENABLED=True, INERTIA_SSR_URL='ssr-url', @@ -20,9 +23,9 @@ def test_it_returns_ssr_calls(self, mock_request): } mock_request.post.return_value = mock_response - + response = self.client.get('/props/') - + mock_request.post.assert_called_once_with( 'ssr-url/render', data=json.dumps(inertia_page('props', props={'name': 'Brandon', 'sport': 'Hockey'})), @@ -31,7 +34,7 @@ def test_it_returns_ssr_calls(self, mock_request): self.assertTemplateUsed('inertia_ssr.html') self.assertContains(response, '
Body Works
') self.assertContains(response, 'head--Head works--head') - + @patch('inertia.http.requests') def test_it_returns_ssr_calls_with_template_data(self, mock_request): mock_response = Mock() @@ -41,7 +44,7 @@ def test_it_returns_ssr_calls_with_template_data(self, mock_request): } mock_request.post.return_value = mock_response - + response = self.client.get('/template_data/') self.assertTemplateUsed('inertia_ssr.html') diff --git a/inertia/tests/test_tests.py b/inertia/tests/test_tests.py index 38e4882..715ef7b 100644 --- a/inertia/tests/test_tests.py +++ b/inertia/tests/test_tests.py @@ -1,5 +1,6 @@ from inertia.test import InertiaTestCase + class TestTestCase(InertiaTestCase): def test_include_props(self): diff --git a/inertia/tests/testapp/apps.py b/inertia/tests/testapp/apps.py index 1303e30..b37eb9e 100644 --- a/inertia/tests/testapp/apps.py +++ b/inertia/tests/testapp/apps.py @@ -1,5 +1,6 @@ from django.apps import AppConfig + class TestAppConfig(AppConfig): name = 'inertia.tests.testapp' verbose_name = 'TestApp' diff --git a/inertia/tests/testapp/models.py b/inertia/tests/testapp/models.py index 766770c..eb24f49 100644 --- a/inertia/tests/testapp/models.py +++ b/inertia/tests/testapp/models.py @@ -1,5 +1,6 @@ from django.db import models + class User(models.Model): name = models.CharField(max_length=255) password = models.CharField(max_length=255) diff --git a/inertia/tests/testapp/urls.py b/inertia/tests/testapp/urls.py index 93ed1c1..f9357e6 100644 --- a/inertia/tests/testapp/urls.py +++ b/inertia/tests/testapp/urls.py @@ -1,4 +1,5 @@ from django.urls import path + from . import views urlpatterns = [ diff --git a/inertia/tests/testapp/views.py b/inertia/tests/testapp/views.py index 995d9fe..f315ea1 100644 --- a/inertia/tests/testapp/views.py +++ b/inertia/tests/testapp/views.py @@ -1,15 +1,17 @@ from django.http.response import HttpResponse from django.shortcuts import redirect from django.utils.decorators import decorator_from_middleware -from inertia import inertia, render, lazy, merge, optional, defer, share, location + +from inertia import defer, inertia, lazy, location, merge, optional, render, share from inertia.http import INERTIA_SESSION_CLEAR_HISTORY, clear_history, encrypt_history + class ShareMiddleware: def __init__(self, get_response): self.get_response = get_response def process_request(self, request): - share(request, + share(request, position=lambda: 'goalie', number=29, ) diff --git a/inertia/utils.py b/inertia/utils.py index ae77b41..49011dc 100644 --- a/inertia/utils.py +++ b/inertia/utils.py @@ -1,9 +1,12 @@ +import warnings + from django.core.serializers.json import DjangoJSONEncoder from django.db import models from django.db.models.query import QuerySet from django.forms.models import model_to_dict as base_model_to_dict -from .prop_classes import OptionalProp, DeferredProp, MergeProp -import warnings + +from .prop_classes import DeferredProp, MergeProp, OptionalProp + def model_to_dict(model): return base_model_to_dict(model, exclude=('password',)) @@ -12,10 +15,10 @@ class InertiaJsonEncoder(DjangoJSONEncoder): def default(self, value): if isinstance(value, models.Model): return model_to_dict(value) - + if isinstance(value, QuerySet): return [model_to_dict(model) for model in value] - + return super().default(value) def lazy(prop): From 306879514a78b53d44a308984bd56d793420c044 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 9 Jan 2025 09:40:47 +0100 Subject: [PATCH 3/8] Initial ruff format --- inertia/helpers.py | 19 +- inertia/http.py | 359 ++++++++++++++------------- inertia/middleware.py | 61 ++--- inertia/prop_classes.py | 39 +-- inertia/settings.py | 24 +- inertia/share.py | 27 ++- inertia/test.py | 147 ++++++----- inertia/tests/settings.py | 71 +++--- inertia/tests/test_encoder.py | 72 +++--- inertia/tests/test_history.py | 66 ++--- inertia/tests/test_middleware.py | 56 ++--- inertia/tests/test_rendering.py | 401 +++++++++++++++++-------------- inertia/tests/test_settings.py | 22 +- inertia/tests/test_ssr.py | 127 +++++----- inertia/tests/test_tests.py | 32 ++- inertia/tests/testapp/apps.py | 4 +- inertia/tests/testapp/models.py | 8 +- inertia/tests/testapp/urls.py | 40 +-- inertia/tests/testapp/views.py | 183 +++++++------- inertia/utils.py | 37 +-- 20 files changed, 970 insertions(+), 825 deletions(-) diff --git a/inertia/helpers.py b/inertia/helpers.py index 5457516..6bbde8d 100644 --- a/inertia/helpers.py +++ b/inertia/helpers.py @@ -1,14 +1,17 @@ def deep_transform_callables(prop): - if not isinstance(prop, dict): - return prop() if callable(prop) else prop + 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]) + for key in list(prop.keys()): + prop[key] = deep_transform_callables(prop[key]) + + return prop - 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__}") + if not isinstance(value, expected_type): + raise TypeError( + f"Expected {expected_type.__name__} for {name}, got {type(value).__name__}" + ) - return value + return value diff --git a/inertia/http.py b/inertia/http.py index 1692012..4090ba0 100644 --- a/inertia/http.py +++ b/inertia/http.py @@ -13,205 +13,226 @@ INERTIA_REQUEST_ENCRYPT_HISTORY = "_inertia_encrypt_history" INERTIA_SESSION_CLEAR_HISTORY = "_inertia_clear_history" -INERTIA_TEMPLATE = 'inertia.html' -INERTIA_SSR_TEMPLATE = 'inertia_ssr.html' +INERTIA_TEMPLATE = "inertia.html" +INERTIA_SSR_TEMPLATE = "inertia_ssr.html" + class InertiaRequest: - def __init__(self, request): - self.request = request + def __init__(self, request): + self.request = request - def __getattr__(self, name): - return getattr(self.request, name) + def __getattr__(self, name): + return getattr(self.request, name) - @property - def headers(self): - return self.request.headers + @property + def headers(self): + return self.request.headers - @property - def inertia(self): - return self.request.inertia.all() if hasattr(self.request, 'inertia') else {} + @property + def inertia(self): + return self.request.inertia.all() if hasattr(self.request, "inertia") else {} - def is_a_partial_render(self, component): - return 'X-Inertia-Partial-Data' in self.headers and self.headers.get('X-Inertia-Partial-Component', '') == component + def is_a_partial_render(self, component): + return ( + "X-Inertia-Partial-Data" in self.headers + and self.headers.get("X-Inertia-Partial-Component", "") == component + ) - def partial_keys(self): - return self.headers.get('X-Inertia-Partial-Data', '').split(',') + def partial_keys(self): + return self.headers.get("X-Inertia-Partial-Data", "").split(",") - def reset_keys(self): - return self.headers.get('X-Inertia-Reset', '').split(',') + def reset_keys(self): + return self.headers.get("X-Inertia-Reset", "").split(",") - def is_inertia(self): - return 'X-Inertia' in self.headers + def is_inertia(self): + return "X-Inertia" in self.headers + + def should_encrypt_history(self): + return validate_type( + getattr( + self.request, + INERTIA_REQUEST_ENCRYPT_HISTORY, + settings.INERTIA_ENCRYPT_HISTORY, + ), + expected_type=bool, + name="encrypt_history", + ) - def should_encrypt_history(self): - return validate_type( - getattr(self.request, INERTIA_REQUEST_ENCRYPT_HISTORY, settings.INERTIA_ENCRYPT_HISTORY), - expected_type=bool, - name="encrypt_history" - ) 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" - ) + def page_data(self): + clear_history = validate_type( + self.request.session.pop(INERTIA_SESSION_CLEAR_HISTORY, False), + expected_type=bool, + name="clear_history", + ) - _page = { - 'component': self.component, - 'props': self.build_props(), - 'url': self.request.get_full_path(), - 'version': settings.INERTIA_VERSION, - 'encryptHistory': self.request.should_encrypt_history(), - 'clearHistory': clear_history, - } - - _deferred_props = self.build_deferred_props() - if _deferred_props: - _page['deferredProps'] = _deferred_props - - _merge_props = self.build_merge_props() - if _merge_props: - _page['mergeProps'] = _merge_props - - return _page - - def build_props(self): - _props = { - **(self.request.inertia), - **self.props, - } - - for key in list(_props.keys()): - if self.request.is_a_partial_render(self.component): - if key not in self.request.partial_keys(): - del _props[key] - else: - if isinstance(_props[key], IgnoreOnFirstLoadProp): - del _props[key] - - return deep_transform_callables(_props) - - def build_deferred_props(self): - if self.request.is_a_partial_render(self.component): - return None - - _deferred_props = {} - 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): - return [ - key - for key, prop in self.props.items() - if ( - isinstance(prop, MergeableProp) - and prop.should_merge() - and key not in self.request.reset_keys() - ) - ] - - def build_first_load(self, data): - context, template = self.build_first_load_context_and_template(data) - - return render_to_string( - template, - { - 'inertia_layout': settings.INERTIA_LAYOUT, - **context, - }, - self.request, - using=None, - ) + _page = { + "component": self.component, + "props": self.build_props(), + "url": self.request.get_full_path(), + "version": settings.INERTIA_VERSION, + "encryptHistory": self.request.should_encrypt_history(), + "clearHistory": clear_history, + } + + _deferred_props = self.build_deferred_props() + if _deferred_props: + _page["deferredProps"] = _deferred_props + + _merge_props = self.build_merge_props() + if _merge_props: + _page["mergeProps"] = _merge_props + + return _page + + def build_props(self): + _props = { + **(self.request.inertia), + **self.props, + } + + for key in list(_props.keys()): + if self.request.is_a_partial_render(self.component): + if key not in self.request.partial_keys(): + del _props[key] + else: + if isinstance(_props[key], IgnoreOnFirstLoadProp): + del _props[key] + + return deep_transform_callables(_props) + + def build_deferred_props(self): + if self.request.is_a_partial_render(self.component): + return None + + _deferred_props = {} + 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): + return [ + key + for key, prop in self.props.items() + if ( + isinstance(prop, MergeableProp) + and prop.should_merge() + and key not in self.request.reset_keys() + ) + ] + + def build_first_load(self, data): + context, template = self.build_first_load_context_and_template(data) + + return render_to_string( + template, + { + "inertia_layout": settings.INERTIA_LAYOUT, + **context, + }, + self.request, + using=None, + ) + def build_first_load_context_and_template(self, data): + if settings.INERTIA_SSR_ENABLED: + try: + response = requests.post( + f"{settings.INERTIA_SSR_URL}/render", + data=data, + headers={"Content-Type": "application/json"}, + ) + response.raise_for_status() + return { + **response.json(), + **self.template_data, + }, INERTIA_SSR_TEMPLATE + except Exception: + pass - def build_first_load_context_and_template(self, data): - if settings.INERTIA_SSR_ENABLED: - try: - response = requests.post( - f"{settings.INERTIA_SSR_URL}/render", - data=data, - headers={"Content-Type": "application/json"}, - ) - response.raise_for_status() return { - **response.json(), - **self.template_data, - }, INERTIA_SSR_TEMPLATE - except Exception: - pass - - return { - 'page': data, - **(self.template_data), - }, INERTIA_TEMPLATE + "page": data, + **(self.template_data), + }, INERTIA_TEMPLATE class InertiaResponse(BaseInertiaResponseMixin, HttpResponse): - json_encoder = settings.INERTIA_JSON_ENCODER - - def __init__(self, request, component, props=None, template_data=None, headers=None, *args, **kwargs): - self.request = InertiaRequest(request) - self.component = component - self.props = props or {} - self.template_data = template_data or {} - _headers = headers or {} - - data = json_encode(self.page_data(), cls=self.json_encoder) - - if self.request.is_inertia(): - _headers = { - **_headers, - 'Vary': 'X-Inertia', - 'X-Inertia': 'true', - 'Content-Type': 'application/json', - } - content = data - else: - content = self.build_first_load(data) - - super().__init__( - content=content, - headers=_headers, - *args, - **kwargs, - ) + json_encoder = settings.INERTIA_JSON_ENCODER + + def __init__( + self, + request, + component, + props=None, + template_data=None, + headers=None, + *args, + **kwargs, + ): + self.request = InertiaRequest(request) + self.component = component + self.props = props or {} + self.template_data = template_data or {} + _headers = headers or {} + + data = json_encode(self.page_data(), cls=self.json_encoder) + + if self.request.is_inertia(): + _headers = { + **_headers, + "Vary": "X-Inertia", + "X-Inertia": "true", + "Content-Type": "application/json", + } + content = data + else: + content = self.build_first_load(data) + + super().__init__( + content=content, + headers=_headers, + *args, + **kwargs, + ) + def render(request, component, props=None, template_data=None): - return InertiaResponse( - request, - component, - props or {}, - template_data or {} - ) + return InertiaResponse(request, component, props or {}, template_data or {}) + def location(location): - return HttpResponse('', status=HTTPStatus.CONFLICT, headers={ - 'X-Inertia-Location': location, - }) + return HttpResponse( + "", + status=HTTPStatus.CONFLICT, + headers={ + "X-Inertia-Location": location, + }, + ) + def encrypt_history(request, value=True): - setattr(request, INERTIA_REQUEST_ENCRYPT_HISTORY, value) + setattr(request, INERTIA_REQUEST_ENCRYPT_HISTORY, value) + def clear_history(request): - request.session[INERTIA_SESSION_CLEAR_HISTORY] = True + request.session[INERTIA_SESSION_CLEAR_HISTORY] = True + def inertia(component): - def decorator(func): - @wraps(func) - def inner(request, *args, **kwargs): - props = func(request, *args, **kwargs) + def decorator(func): + @wraps(func) + def inner(request, *args, **kwargs): + props = func(request, *args, **kwargs) - # if something other than a dict is returned, the user probably wants to return a specific response - if not isinstance(props, dict): - return props + # if something other than a dict is returned, the user probably wants to return a specific response + if not isinstance(props, dict): + return props - return render(request, component, props) + return render(request, component, props) - return inner + return inner - return decorator + return decorator diff --git a/inertia/middleware.py b/inertia/middleware.py index 559382a..874c56e 100644 --- a/inertia/middleware.py +++ b/inertia/middleware.py @@ -6,42 +6,49 @@ class InertiaMiddleware: - def __init__(self, get_response): - self.get_response = get_response + def __init__(self, get_response): + self.get_response = get_response - def __call__(self, request): - response = self.get_response(request) + def __call__(self, request): + response = self.get_response(request) - # Inertia requests don't ever render templates, so they skip the typical Django - # CSRF path. We'll manually add a CSRF token for every request here. - get_token(request) + # Inertia requests don't ever render templates, so they skip the typical Django + # CSRF path. We'll manually add a CSRF token for every request here. + get_token(request) - if not self.is_inertia_request(request): - return response + if not self.is_inertia_request(request): + return response - if self.is_non_post_redirect(request, response): - response.status_code = 303 + if self.is_non_post_redirect(request, response): + response.status_code = 303 - if self.is_stale(request): - return self.force_refresh(request) + if self.is_stale(request): + return self.force_refresh(request) - return response + return response - def is_non_post_redirect(self, request, response): - return self.is_redirect_request(response) and request.method in ['PUT', 'PATCH', 'DELETE'] + def is_non_post_redirect(self, request, response): + return self.is_redirect_request(response) and request.method in [ + "PUT", + "PATCH", + "DELETE", + ] - def is_inertia_request(self, request): - return 'X-Inertia' in request.headers + def is_inertia_request(self, request): + return "X-Inertia" in request.headers - def is_redirect_request(self, response): - return response.status_code in [301, 302] + def is_redirect_request(self, response): + return response.status_code in [301, 302] - def is_stale(self, request): - return request.headers.get('X-Inertia-Version', settings.INERTIA_VERSION) != settings.INERTIA_VERSION + def is_stale(self, request): + return ( + request.headers.get("X-Inertia-Version", settings.INERTIA_VERSION) + != settings.INERTIA_VERSION + ) - def is_stale_inertia_get(self, request): - return request.method == 'GET' and self.is_stale(request) + def is_stale_inertia_get(self, request): + return request.method == "GET" and self.is_stale(request) - def force_refresh(self, request): - messages.get_messages(request).used = False - return location(request.build_absolute_uri()) + def force_refresh(self, request): + messages.get_messages(request).used = False + return location(request.build_absolute_uri()) diff --git a/inertia/prop_classes.py b/inertia/prop_classes.py index 528a8e5..02fdcfa 100644 --- a/inertia/prop_classes.py +++ b/inertia/prop_classes.py @@ -2,32 +2,37 @@ class CallableProp(ABC): - def __init__(self, prop): - self.prop = prop + def __init__(self, prop): + self.prop = prop + + def __call__(self): + return self.prop() if callable(self.prop) else self.prop - def __call__(self): - return self.prop() if callable(self.prop) else self.prop class MergeableProp(ABC): - @abstractmethod - def should_merge(self): - pass + @abstractmethod + def should_merge(self): + pass + class IgnoreOnFirstLoadProp(ABC): - pass + pass + class OptionalProp(CallableProp, IgnoreOnFirstLoadProp): - pass + pass + class DeferredProp(CallableProp, MergeableProp, IgnoreOnFirstLoadProp): - def __init__(self, prop, group, merge=False): - super().__init__(prop) - self.group = group - self.merge = merge + def __init__(self, prop, group, merge=False): + super().__init__(prop) + self.group = group + self.merge = merge + + def should_merge(self): + return self.merge - def should_merge(self): - return self.merge class MergeProp(CallableProp, MergeableProp): - def should_merge(self): - return True + def should_merge(self): + return True diff --git a/inertia/settings.py b/inertia/settings.py index a49377d..69dbab1 100644 --- a/inertia/settings.py +++ b/inertia/settings.py @@ -2,19 +2,21 @@ from .utils import InertiaJsonEncoder -__all__ = ['settings'] +__all__ = ["settings"] + class InertiaSettings: - INERTIA_VERSION = '1.0' - INERTIA_JSON_ENCODER = InertiaJsonEncoder - INERTIA_SSR_URL = 'http://localhost:13714' - INERTIA_SSR_ENABLED = False - INERTIA_ENCRYPT_HISTORY = False + INERTIA_VERSION = "1.0" + INERTIA_JSON_ENCODER = InertiaJsonEncoder + INERTIA_SSR_URL = "http://localhost:13714" + INERTIA_SSR_ENABLED = False + INERTIA_ENCRYPT_HISTORY = False + + def __getattribute__(self, name): + try: + return getattr(django_settings, name) + except AttributeError: + return super().__getattribute__(name) - def __getattribute__(self, name): - try: - return getattr(django_settings, name) - except AttributeError: - return super().__getattribute__(name) settings = InertiaSettings() diff --git a/inertia/share.py b/inertia/share.py index eb4283a..4204ad5 100644 --- a/inertia/share.py +++ b/inertia/share.py @@ -1,21 +1,22 @@ -__all__ = ['share'] +__all__ = ["share"] + class InertiaShare: - def __init__(self): - self.props = {} + def __init__(self): + self.props = {} - def set(self, **kwargs): - self.props = { - **self.props, - **kwargs, - } + def set(self, **kwargs): + self.props = { + **self.props, + **kwargs, + } - def all(self): - return self.props + def all(self): + return self.props def share(request, **kwargs): - if not hasattr(request, 'inertia'): - request.inertia = InertiaShare() + if not hasattr(request, "inertia"): + request.inertia = InertiaShare() - request.inertia.set(**kwargs) + request.inertia.set(**kwargs) diff --git a/inertia/test.py b/inertia/test.py index d25d6f1..eb35669 100644 --- a/inertia/test.py +++ b/inertia/test.py @@ -9,93 +9,116 @@ class ClientWithLastResponse: - def __init__(self, client): - self.client = client - self.last_response = None + def __init__(self, client): + self.client = client + self.last_response = None - def get(self, *args, **kwargs): - self.last_response = self.client.get(*args, **kwargs) - return self.last_response + def get(self, *args, **kwargs): + self.last_response = self.client.get(*args, **kwargs) + return self.last_response + + def __getattr__(self, name): + return getattr(self.client, name) - def __getattr__(self, name): - return getattr(self.client, name) class BaseInertiaTestCase: - def setUp(self): - self.inertia = ClientWithLastResponse(Client(HTTP_X_INERTIA=True)) - self.client = ClientWithLastResponse(Client()) + def setUp(self): + self.inertia = ClientWithLastResponse(Client(HTTP_X_INERTIA=True)) + self.client = ClientWithLastResponse(Client()) + + def last_response(self): + return self.inertia.last_response or self.client.last_response - def last_response(self): - return self.inertia.last_response or self.client.last_response + def assertJSONResponse(self, response, json_obj): + self.assertEqual(response.headers["Content-Type"], "application/json") + self.assertEqual(response.json(), json_obj) - def assertJSONResponse(self, response, json_obj): - self.assertEqual(response.headers['Content-Type'], 'application/json') - self.assertEqual(response.json(), json_obj) class InertiaTestCase(BaseInertiaTestCase, TestCase): - def setUp(self): - super().setUp() + def setUp(self): + super().setUp() + + self.mock_inertia = patch( + "inertia.http.render_to_string", wraps=base_render_to_string + ) + self.mock_render = self.mock_inertia.start() + + def tearDown(self): + self.mock_inertia.stop() - self.mock_inertia = patch('inertia.http.render_to_string', wraps=base_render_to_string) - self.mock_render = self.mock_inertia.start() + def page(self): + page_data = ( + self.mock_render.call_args[0][1]["page"] + if self.mock_render.call_args + else self.last_response().content + ) - def tearDown(self): - self.mock_inertia.stop() + return loads(page_data) - def page(self): - page_data = self.mock_render.call_args[0][1]['page'] if self.mock_render.call_args else self.last_response().content + def props(self): + return self.page()["props"] - return loads(page_data) + def merge_props(self): + return self.page()["mergeProps"] - def props(self): - return self.page()['props'] + def deferred_props(self): + return self.page()["deferredProps"] - def merge_props(self): - return self.page()['mergeProps'] + def template_data(self): + context = self.mock_render.call_args[0][1] + return { + key: context[key] + for key in context + if key not in ["page", "inertia_layout"] + } - def deferred_props(self): - return self.page()['deferredProps'] + def component(self): + return self.page()["component"] - def template_data(self): - context = self.mock_render.call_args[0][1] - return {key: context[key] for key in context if key not in ['page', 'inertia_layout']} + def assertIncludesProps(self, props): + self.assertDictEqual(self.props(), {**self.props(), **props}) - def component(self): - return self.page()['component'] + def assertHasExactProps(self, props): + self.assertDictEqual(self.props(), props) - def assertIncludesProps(self, props): - self.assertDictEqual(self.props(), {**self.props(), **props}) + def assertIncludesTemplateData(self, template_data): + self.assertDictEqual( + self.template_data(), {**self.template_data(), **template_data} + ) - def assertHasExactProps(self, props): - self.assertDictEqual(self.props(), props) + def assertHasExactTemplateData(self, template_data): + self.assertDictEqual(self.template_data(), template_data) - def assertIncludesTemplateData(self, template_data): - self.assertDictEqual(self.template_data(), {**self.template_data(), **template_data}) + def assertComponentUsed(self, component_name): + self.assertEqual(component_name, self.component()) - def assertHasExactTemplateData(self, template_data): - self.assertDictEqual(self.template_data(), template_data) - def assertComponentUsed(self, component_name): - self.assertEqual(component_name, self.component()) +def inertia_page( + url, + component="TestComponent", + props={}, + template_data={}, + deferred_props=None, + merge_props=None, +): + _page = { + "component": component, + "props": props, + "url": f"/{url}/", + "version": settings.INERTIA_VERSION, + "encryptHistory": False, + "clearHistory": False, + } -def inertia_page(url, component='TestComponent', props={}, template_data={}, deferred_props=None, merge_props=None): - _page = { - 'component': component, - 'props': props, - 'url': f'/{url}/', - 'version': settings.INERTIA_VERSION, - 'encryptHistory': False, - 'clearHistory': False, - } + if deferred_props: + _page["deferredProps"] = deferred_props - if deferred_props: - _page['deferredProps'] = deferred_props + if merge_props: + _page["mergeProps"] = merge_props - if merge_props: - _page['mergeProps'] = merge_props + return _page - return _page def inertia_div(*args, **kwargs): - page = inertia_page(*args, **kwargs) - return f'
' + page = inertia_page(*args, **kwargs) + return f'
' diff --git a/inertia/tests/settings.py b/inertia/tests/settings.py index c16ba56..d92d035 100644 --- a/inertia/tests/settings.py +++ b/inertia/tests/settings.py @@ -2,57 +2,52 @@ BASE_DIR = Path(__file__).resolve().parent.parent -INERTIA_LAYOUT = 'layout.html' +INERTIA_LAYOUT = "layout.html" MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'inertia.middleware.InertiaMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "inertia.middleware.InertiaMiddleware", ] INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.sites", - "inertia", - "inertia.tests.testapp.apps.TestAppConfig" + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "inertia", + "inertia.tests.testapp.apps.TestAppConfig", ] -ROOT_URLCONF = 'inertia.tests.testapp.urls' +ROOT_URLCONF = "inertia.tests.testapp.urls" TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - BASE_DIR / 'tests/testapp', - ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + BASE_DIR / "tests/testapp", + ], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, }, - }, ] # only in place to silence an error -SECRET_KEY = 'django-insecure-3p_!uve+em7f45+74jh16)y)h00ve@9d2edh=cuebdsrbco%vb' -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": "unused" - } -} +SECRET_KEY = "django-insecure-3p_!uve+em7f45+74jh16)y)h00ve@9d2edh=cuebdsrbco%vb" +DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "unused"}} # silence a warning USE_TZ = False diff --git a/inertia/tests/test_encoder.py b/inertia/tests/test_encoder.py index b842366..4972126 100644 --- a/inertia/tests/test_encoder.py +++ b/inertia/tests/test_encoder.py @@ -8,31 +8,47 @@ class InertiaJsonEncoderTestCase(TestCase): - def setUp(self): - self.encode = lambda obj: dumps(obj, cls=InertiaJsonEncoder) - - def test_it_handles_models_with_dates_and_removes_passwords(self): - user = User( - name='Brandon', - password='something-top-secret', - birthdate=date(1987, 2, 15), - registered_at=datetime(2022, 10, 31, 10, 13, 1), - ) - - self.assertEqual( - dumps({'id': None, 'name': 'Brandon', 'birthdate': '1987-02-15', 'registered_at': '2022-10-31T10:13:01'}), - self.encode(user) - ) - - def test_it_handles_querysets(self): - User( - name='Brandon', - password='something-top-secret', - birthdate=date(1987, 2, 15), - registered_at=datetime(2022, 10, 31, 10, 13, 1), - ).save() - - self.assertEqual( - dumps([{'id': 1, 'name': 'Brandon', 'birthdate': '1987-02-15', 'registered_at': '2022-10-31T10:13:01'}]), - self.encode(User.objects.all()) - ) + def setUp(self): + self.encode = lambda obj: dumps(obj, cls=InertiaJsonEncoder) + + def test_it_handles_models_with_dates_and_removes_passwords(self): + user = User( + name="Brandon", + password="something-top-secret", + birthdate=date(1987, 2, 15), + registered_at=datetime(2022, 10, 31, 10, 13, 1), + ) + + self.assertEqual( + dumps( + { + "id": None, + "name": "Brandon", + "birthdate": "1987-02-15", + "registered_at": "2022-10-31T10:13:01", + } + ), + self.encode(user), + ) + + def test_it_handles_querysets(self): + User( + name="Brandon", + password="something-top-secret", + birthdate=date(1987, 2, 15), + registered_at=datetime(2022, 10, 31, 10, 13, 1), + ).save() + + self.assertEqual( + dumps( + [ + { + "id": 1, + "name": "Brandon", + "birthdate": "1987-02-15", + "registered_at": "2022-10-31T10:13:01", + } + ] + ), + self.encode(User.objects.all()), + ) diff --git a/inertia/tests/test_history.py b/inertia/tests/test_history.py index a6388d9..4b0a32d 100644 --- a/inertia/tests/test_history.py +++ b/inertia/tests/test_history.py @@ -4,34 +4,38 @@ class HistoryTestCase(InertiaTestCase): - def test_encrypt_history_setting(self): - self.client.get('/empty/') - assert self.page()['encryptHistory'] is False - - with override_settings(INERTIA_ENCRYPT_HISTORY=True): - self.client.get('/empty/') - assert self.page()['encryptHistory'] is True - - def test_encrypt_history(self): - self.client.get('/encrypt-history/') - assert self.page()['encryptHistory'] is True - - with override_settings(INERTIA_ENCRYPT_HISTORY=True): - self.client.get('/no-encrypt-history/') - assert self.page()['encryptHistory'] is False - - def test_clear_history(self): - self.client.get('/clear-history/') - assert self.page()['clearHistory'] is True - - def test_clear_history_redirect(self): - response = self.client.get('/clear-history-redirect/', follow=True) - self.assertRedirects(response, '/empty/') - assert self.page()['clearHistory'] is True - - def test_raises_type_error(self): - with self.assertRaisesMessage(TypeError, 'Expected bool for encrypt_history, got str'): - self.client.get('/encrypt-history-type-error/') - - with self.assertRaisesMessage(TypeError, 'Expected bool for clear_history, got str'): - self.client.get('/clear-history-type-error/') + def test_encrypt_history_setting(self): + self.client.get("/empty/") + assert self.page()["encryptHistory"] is False + + with override_settings(INERTIA_ENCRYPT_HISTORY=True): + self.client.get("/empty/") + assert self.page()["encryptHistory"] is True + + def test_encrypt_history(self): + self.client.get("/encrypt-history/") + assert self.page()["encryptHistory"] is True + + with override_settings(INERTIA_ENCRYPT_HISTORY=True): + self.client.get("/no-encrypt-history/") + assert self.page()["encryptHistory"] is False + + def test_clear_history(self): + self.client.get("/clear-history/") + assert self.page()["clearHistory"] is True + + def test_clear_history_redirect(self): + response = self.client.get("/clear-history-redirect/", follow=True) + self.assertRedirects(response, "/empty/") + assert self.page()["clearHistory"] is True + + def test_raises_type_error(self): + with self.assertRaisesMessage( + TypeError, "Expected bool for encrypt_history, got str" + ): + self.client.get("/encrypt-history-type-error/") + + with self.assertRaisesMessage( + TypeError, "Expected bool for clear_history, got str" + ): + self.client.get("/clear-history-type-error/") diff --git a/inertia/tests/test_middleware.py b/inertia/tests/test_middleware.py index e4161fe..4792440 100644 --- a/inertia/tests/test_middleware.py +++ b/inertia/tests/test_middleware.py @@ -2,39 +2,41 @@ class MiddlewareTestCase(InertiaTestCase): - def test_anything(self): - response = self.client.get('/test/') + def test_anything(self): + response = self.client.get("/test/") - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) - def test_stale_versions_are_refreshed(self): - response = self.inertia.get('/empty/', - HTTP_X_INERTIA_VERSION='some-nonsense', - ) + def test_stale_versions_are_refreshed(self): + response = self.inertia.get( + "/empty/", + HTTP_X_INERTIA_VERSION="some-nonsense", + ) - self.assertEqual(response.status_code, 409) - self.assertEqual(response.headers['X-Inertia-Location'], 'http://testserver/empty/') + self.assertEqual(response.status_code, 409) + self.assertEqual( + response.headers["X-Inertia-Location"], "http://testserver/empty/" + ) - def test_redirect_status(self): - response = self.inertia.post('/redirect/') - self.assertEqual(response.status_code, 302) + def test_redirect_status(self): + response = self.inertia.post("/redirect/") + self.assertEqual(response.status_code, 302) - for http_method in ['put', 'patch', 'delete']: - response = getattr(self.inertia, http_method)('/redirect/') + for http_method in ["put", "patch", "delete"]: + response = getattr(self.inertia, http_method)("/redirect/") - self.assertEqual(response.status_code, 303) + self.assertEqual(response.status_code, 303) - def test_a_request_not_from_inertia_is_ignored(self): - response = self.client.get('/empty/', - HTTP_X_INERTIA_VERSION='some-nonsense', - ) + def test_a_request_not_from_inertia_is_ignored(self): + response = self.client.get( + "/empty/", + HTTP_X_INERTIA_VERSION="some-nonsense", + ) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) - def test_external_redirect_status(self): - response = self.inertia.post('/external-redirect/') - self.assertEqual(response.status_code, 409) - self.assertIn("X-Inertia-Location", response.headers) - self.assertEqual( - "http://foobar.com/", response.headers["X-Inertia-Location"] - ) + def test_external_redirect_status(self): + response = self.inertia.post("/external-redirect/") + self.assertEqual(response.status_code, 409) + self.assertIn("X-Inertia-Location", response.headers) + self.assertEqual("http://foobar.com/", response.headers["X-Inertia-Location"]) diff --git a/inertia/tests/test_rendering.py b/inertia/tests/test_rendering.py index 2e41f89..e790e27 100644 --- a/inertia/tests/test_rendering.py +++ b/inertia/tests/test_rendering.py @@ -4,205 +4,244 @@ class FirstLoadTestCase(InertiaTestCase): - def test_with_props(self): - self.assertContains( - self.client.get('/props/'), - inertia_div('props', props={ - 'name': 'Brandon', - 'sport': 'Hockey', - }) - ) - - def test_with_template_data(self): - response = self.client.get('/template_data/') - - self.assertContains( - response, - inertia_div('template_data', template_data={ - 'name': 'Brian', - 'sport': 'Basketball', - }) - ) - - self.assertContains( - response, - 'template data:Brian, Basketball' - ) - - def test_with_no_data(self): - self.assertContains( - self.client.get('/empty/'), - inertia_div('empty') - ) - - def test_proper_status_code(self): - self.assertEqual( - self.client.get('/empty/').status_code, - 200 - ) - - def test_template_rendered(self): - self.assertTemplateUsed(self.client.get('/empty/'), 'inertia.html') + def test_with_props(self): + self.assertContains( + self.client.get("/props/"), + inertia_div( + "props", + props={ + "name": "Brandon", + "sport": "Hockey", + }, + ), + ) + + def test_with_template_data(self): + response = self.client.get("/template_data/") + + self.assertContains( + response, + inertia_div( + "template_data", + template_data={ + "name": "Brian", + "sport": "Basketball", + }, + ), + ) + + self.assertContains(response, "template data:Brian, Basketball") + + def test_with_no_data(self): + self.assertContains(self.client.get("/empty/"), inertia_div("empty")) + + def test_proper_status_code(self): + self.assertEqual(self.client.get("/empty/").status_code, 200) + + def test_template_rendered(self): + self.assertTemplateUsed(self.client.get("/empty/"), "inertia.html") class SubsequentLoadTestCase(InertiaTestCase): - def test_with_props(self): - self.assertJSONResponse( - self.inertia.get('/props/'), - inertia_page('props', props={ - 'name': 'Brandon', - 'sport': 'Hockey', - }) - ) - - def test_with_template_data(self): - self.assertJSONResponse( - self.inertia.get('/template_data/'), - inertia_page('template_data', template_data={ - 'name': 'Brian', - 'sport': 'Basketball', - }) - ) - - def test_with_no_data(self): - self.assertJSONResponse( - self.inertia.get('/empty/'), - inertia_page('empty') - ) - - def test_proper_status_code(self): - self.assertEqual( - self.inertia.get('/empty/').status_code, - 200 - ) - - def test_redirects_from_inertia_views(self): - self.assertEqual( - self.inertia.get('/inertia-redirect/').status_code, - 302 - ) + def test_with_props(self): + self.assertJSONResponse( + self.inertia.get("/props/"), + inertia_page( + "props", + props={ + "name": "Brandon", + "sport": "Hockey", + }, + ), + ) + + def test_with_template_data(self): + self.assertJSONResponse( + self.inertia.get("/template_data/"), + inertia_page( + "template_data", + template_data={ + "name": "Brian", + "sport": "Basketball", + }, + ), + ) + + def test_with_no_data(self): + self.assertJSONResponse(self.inertia.get("/empty/"), inertia_page("empty")) + + def test_proper_status_code(self): + self.assertEqual(self.inertia.get("/empty/").status_code, 200) + + def test_redirects_from_inertia_views(self): + self.assertEqual(self.inertia.get("/inertia-redirect/").status_code, 302) + class LazyPropsTestCase(InertiaTestCase): - def test_lazy_props_are_not_included(self): - with warns(DeprecationWarning): - self.assertJSONResponse( - self.inertia.get('/lazy/'), - inertia_page('lazy', props={'name': 'Brian'}) - ) - - def test_lazy_props_are_included_when_requested(self): - with warns(DeprecationWarning): - self.assertJSONResponse( - self.inertia.get('/lazy/', HTTP_X_INERTIA_PARTIAL_DATA='sport,grit', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'), - inertia_page('lazy', props={'sport': 'Basketball', 'grit': 'intense'}) - ) + def test_lazy_props_are_not_included(self): + with warns(DeprecationWarning): + self.assertJSONResponse( + self.inertia.get("/lazy/"), + inertia_page("lazy", props={"name": "Brian"}), + ) + + def test_lazy_props_are_included_when_requested(self): + with warns(DeprecationWarning): + self.assertJSONResponse( + self.inertia.get( + "/lazy/", + HTTP_X_INERTIA_PARTIAL_DATA="sport,grit", + HTTP_X_INERTIA_PARTIAL_COMPONENT="TestComponent", + ), + inertia_page("lazy", props={"sport": "Basketball", "grit": "intense"}), + ) + class OptionalPropsTestCase(InertiaTestCase): - def test_optional_props_are_not_included(self): - self.assertJSONResponse( - self.inertia.get('/optional/'), - inertia_page('optional', props={'name': 'Brian'}) - ) - - def test_optional_props_are_included_when_requested(self): - self.assertJSONResponse( - self.inertia.get('/optional/', HTTP_X_INERTIA_PARTIAL_DATA='sport,grit', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'), - inertia_page('optional', props={'sport': 'Basketball', 'grit': 'intense'}) - ) + def test_optional_props_are_not_included(self): + self.assertJSONResponse( + self.inertia.get("/optional/"), + inertia_page("optional", props={"name": "Brian"}), + ) + + def test_optional_props_are_included_when_requested(self): + self.assertJSONResponse( + self.inertia.get( + "/optional/", + HTTP_X_INERTIA_PARTIAL_DATA="sport,grit", + HTTP_X_INERTIA_PARTIAL_COMPONENT="TestComponent", + ), + inertia_page("optional", props={"sport": "Basketball", "grit": "intense"}), + ) + class ComplexPropsTestCase(InertiaTestCase): - def test_nested_callable_props_work(self): - self.assertJSONResponse( - self.inertia.get('/complex-props/'), - inertia_page('complex-props', props={'person': {'name': 'Brandon'}}) - ) + def test_nested_callable_props_work(self): + self.assertJSONResponse( + self.inertia.get("/complex-props/"), + inertia_page("complex-props", props={"person": {"name": "Brandon"}}), + ) + class ShareTestCase(InertiaTestCase): - def test_that_shared_props_are_merged(self): - self.assertJSONResponse( - self.inertia.get('/share/'), - inertia_page('share', props={'name': 'Brandon', 'position': 'goalie', 'number': 29}) - ) + def test_that_shared_props_are_merged(self): + self.assertJSONResponse( + self.inertia.get("/share/"), + inertia_page( + "share", props={"name": "Brandon", "position": "goalie", "number": 29} + ), + ) + + self.assertHasExactProps( + {"name": "Brandon", "position": "goalie", "number": 29} + ) - self.assertHasExactProps({'name': 'Brandon', 'position': 'goalie', 'number': 29}) class CSRFTestCase(InertiaTestCase): - def test_that_csrf_inclusion_is_automatic(self): - response = self.inertia.get('/props/') + def test_that_csrf_inclusion_is_automatic(self): + response = self.inertia.get("/props/") + + self.assertIsNotNone(response.cookies.get("csrftoken")) - self.assertIsNotNone(response.cookies.get('csrftoken')) + def test_that_csrf_is_included_even_on_initial_page_load(self): + response = self.client.get("/props/") - def test_that_csrf_is_included_even_on_initial_page_load(self): - response = self.client.get('/props/') + self.assertIsNotNone(response.cookies.get("csrftoken")) - self.assertIsNotNone(response.cookies.get('csrftoken')) class DeferredPropsTestCase(InertiaTestCase): - def test_deferred_props_are_set(self): - self.assertJSONResponse( - self.inertia.get('/defer/'), - inertia_page( - 'defer', - props={'name': 'Brian'}, - deferred_props={'default': ['sport']}) - ) - - def test_deferred_props_are_grouped(self): - self.assertJSONResponse( - self.inertia.get('/defer-group/'), - inertia_page( - 'defer-group', - props={'name': 'Brian'}, - deferred_props={'group': ['sport', 'team'], 'default': ['grit']}) - ) - - def test_deferred_props_are_included_when_requested(self): - self.assertJSONResponse( - self.inertia.get('/defer/', HTTP_X_INERTIA_PARTIAL_DATA='sport', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'), - inertia_page('defer', props={'sport': 'Basketball'}) - ) - - - def test_only_deferred_props_in_group_are_included_when_requested(self): - self.assertJSONResponse( - self.inertia.get('/defer-group/', HTTP_X_INERTIA_PARTIAL_DATA='sport,team', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'), - inertia_page('defer-group', props={'sport': 'Basketball', 'team': 'Bulls'}) - ) - - self.assertJSONResponse( - self.inertia.get('/defer-group/', HTTP_X_INERTIA_PARTIAL_DATA='grit', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'), - inertia_page('defer-group', props={'grit': 'intense'}) - ) + def test_deferred_props_are_set(self): + self.assertJSONResponse( + self.inertia.get("/defer/"), + inertia_page( + "defer", props={"name": "Brian"}, deferred_props={"default": ["sport"]} + ), + ) + + def test_deferred_props_are_grouped(self): + self.assertJSONResponse( + self.inertia.get("/defer-group/"), + inertia_page( + "defer-group", + props={"name": "Brian"}, + deferred_props={"group": ["sport", "team"], "default": ["grit"]}, + ), + ) + + def test_deferred_props_are_included_when_requested(self): + self.assertJSONResponse( + self.inertia.get( + "/defer/", + HTTP_X_INERTIA_PARTIAL_DATA="sport", + HTTP_X_INERTIA_PARTIAL_COMPONENT="TestComponent", + ), + inertia_page("defer", props={"sport": "Basketball"}), + ) + + def test_only_deferred_props_in_group_are_included_when_requested(self): + self.assertJSONResponse( + self.inertia.get( + "/defer-group/", + HTTP_X_INERTIA_PARTIAL_DATA="sport,team", + HTTP_X_INERTIA_PARTIAL_COMPONENT="TestComponent", + ), + inertia_page("defer-group", props={"sport": "Basketball", "team": "Bulls"}), + ) + + self.assertJSONResponse( + self.inertia.get( + "/defer-group/", + HTTP_X_INERTIA_PARTIAL_DATA="grit", + HTTP_X_INERTIA_PARTIAL_COMPONENT="TestComponent", + ), + inertia_page("defer-group", props={"grit": "intense"}), + ) + class MergePropsTestCase(InertiaTestCase): - def test_merge_props_are_included_on_initial_load(self): - self.assertJSONResponse( - self.inertia.get('/merge/'), - inertia_page('merge', props={ - 'name': 'Brandon', - 'sport': 'Hockey', - }, merge_props=['sport', 'team'], deferred_props={'default': ['team']}) - ) - - - def test_deferred_merge_props_are_included_on_subsequent_load(self): - self.assertJSONResponse( - self.inertia.get('/merge/', HTTP_X_INERTIA_PARTIAL_DATA='team', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'), - inertia_page('merge', props={ - 'team': 'Penguins', - }, merge_props=['sport', 'team']) - ) - - def test_merge_props_are_not_included_when_reset(self): - self.assertJSONResponse( - self.inertia.get( - '/merge/', - HTTP_X_INERTIA_PARTIAL_DATA='sport,team', - HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent', - HTTP_X_INERTIA_RESET='sport,team' - ), - inertia_page('merge', props={ - 'sport': 'Hockey', - 'team': 'Penguins', - }) - ) + def test_merge_props_are_included_on_initial_load(self): + self.assertJSONResponse( + self.inertia.get("/merge/"), + inertia_page( + "merge", + props={ + "name": "Brandon", + "sport": "Hockey", + }, + merge_props=["sport", "team"], + deferred_props={"default": ["team"]}, + ), + ) + + def test_deferred_merge_props_are_included_on_subsequent_load(self): + self.assertJSONResponse( + self.inertia.get( + "/merge/", + HTTP_X_INERTIA_PARTIAL_DATA="team", + HTTP_X_INERTIA_PARTIAL_COMPONENT="TestComponent", + ), + inertia_page( + "merge", + props={ + "team": "Penguins", + }, + merge_props=["sport", "team"], + ), + ) + + def test_merge_props_are_not_included_when_reset(self): + self.assertJSONResponse( + self.inertia.get( + "/merge/", + HTTP_X_INERTIA_PARTIAL_DATA="sport,team", + HTTP_X_INERTIA_PARTIAL_COMPONENT="TestComponent", + HTTP_X_INERTIA_RESET="sport,team", + ), + inertia_page( + "merge", + props={ + "sport": "Hockey", + "team": "Penguins", + }, + ), + ) diff --git a/inertia/tests/test_settings.py b/inertia/tests/test_settings.py index 6c588e8..5dc6ded 100644 --- a/inertia/tests/test_settings.py +++ b/inertia/tests/test_settings.py @@ -4,19 +4,17 @@ class SettingsTestCase(InertiaTestCase): - @override_settings( - INERTIA_VERSION='2.0' - ) - def test_version_works(self): - response = self.inertia.get('/empty/', HTTP_X_INERTIA_VERSION='2.0') + @override_settings(INERTIA_VERSION="2.0") + def test_version_works(self): + response = self.inertia.get("/empty/", HTTP_X_INERTIA_VERSION="2.0") - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) - def test_version_fallsback(self): - response = self.inertia.get('/empty/', HTTP_X_INERTIA_VERSION='1.0') + def test_version_fallsback(self): + response = self.inertia.get("/empty/", HTTP_X_INERTIA_VERSION="1.0") - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) - def test_layout(self): - response = self.client.get('/empty/') - self.assertTemplateUsed(response, 'layout.html') + def test_layout(self): + response = self.client.get("/empty/") + self.assertTemplateUsed(response, "layout.html") diff --git a/inertia/tests/test_ssr.py b/inertia/tests/test_ssr.py index 5921c87..b9927e9 100644 --- a/inertia/tests/test_ssr.py +++ b/inertia/tests/test_ssr.py @@ -8,66 +8,71 @@ @override_settings( - INERTIA_SSR_ENABLED=True, - INERTIA_SSR_URL='ssr-url', - INERTIA_VERSION='1.0', + INERTIA_SSR_ENABLED=True, + INERTIA_SSR_URL="ssr-url", + INERTIA_VERSION="1.0", ) class SSRTestCase(InertiaTestCase): - - @patch('inertia.http.requests') - def test_it_returns_ssr_calls(self, mock_request): - mock_response = Mock() - mock_response.json.return_value = { - 'body': '
Body Works
', - 'head': 'Head works', - } - - mock_request.post.return_value = mock_response - - response = self.client.get('/props/') - - mock_request.post.assert_called_once_with( - 'ssr-url/render', - data=json.dumps(inertia_page('props', props={'name': 'Brandon', 'sport': 'Hockey'})), - headers={'Content-Type': 'application/json'}, - ) - self.assertTemplateUsed('inertia_ssr.html') - self.assertContains(response, '
Body Works
') - self.assertContains(response, 'head--Head works--head') - - @patch('inertia.http.requests') - def test_it_returns_ssr_calls_with_template_data(self, mock_request): - mock_response = Mock() - mock_response.json.return_value = { - 'body': '
Body Works
', - 'head': 'Head works', - } - - mock_request.post.return_value = mock_response - - response = self.client.get('/template_data/') - - self.assertTemplateUsed('inertia_ssr.html') - self.assertContains(response, '
Body Works
') - self.assertContains(response, 'head--Head works--head') - self.assertContains(response, 'Brian, Basketball') - - - @patch('inertia.http.requests') - def test_it_uses_inertia_if_inertia_requests_are_made(self, mock_requests): - response = self.inertia.get('/props/') - - mock_requests.post.assert_not_called() - self.assertJSONResponse(response, inertia_page('props', props={'name': 'Brandon', 'sport': 'Hockey'})) - - @patch('inertia.http.requests') - def test_it_fallsback_on_failure(self, mock_requests): - def uh_oh(*args, **kwargs): - raise RequestException() - - mock_response = Mock() - mock_response.raise_for_status.side_effect = uh_oh - mock_requests.post.return_value = mock_response - - response = self.client.get('/props/') - self.assertContains(response, inertia_div('props', props={'name': 'Brandon', 'sport': 'Hockey'})) + @patch("inertia.http.requests") + def test_it_returns_ssr_calls(self, mock_request): + mock_response = Mock() + mock_response.json.return_value = { + "body": "
Body Works
", + "head": "Head works", + } + + mock_request.post.return_value = mock_response + + response = self.client.get("/props/") + + mock_request.post.assert_called_once_with( + "ssr-url/render", + data=json.dumps( + inertia_page("props", props={"name": "Brandon", "sport": "Hockey"}) + ), + headers={"Content-Type": "application/json"}, + ) + self.assertTemplateUsed("inertia_ssr.html") + self.assertContains(response, "
Body Works
") + self.assertContains(response, "head--Head works--head") + + @patch("inertia.http.requests") + def test_it_returns_ssr_calls_with_template_data(self, mock_request): + mock_response = Mock() + mock_response.json.return_value = { + "body": "
Body Works
", + "head": "Head works", + } + + mock_request.post.return_value = mock_response + + response = self.client.get("/template_data/") + + self.assertTemplateUsed("inertia_ssr.html") + self.assertContains(response, "
Body Works
") + self.assertContains(response, "head--Head works--head") + self.assertContains(response, "Brian, Basketball") + + @patch("inertia.http.requests") + def test_it_uses_inertia_if_inertia_requests_are_made(self, mock_requests): + response = self.inertia.get("/props/") + + mock_requests.post.assert_not_called() + self.assertJSONResponse( + response, + inertia_page("props", props={"name": "Brandon", "sport": "Hockey"}), + ) + + @patch("inertia.http.requests") + def test_it_fallsback_on_failure(self, mock_requests): + def uh_oh(*args, **kwargs): + raise RequestException() + + mock_response = Mock() + mock_response.raise_for_status.side_effect = uh_oh + mock_requests.post.return_value = mock_response + + response = self.client.get("/props/") + self.assertContains( + response, inertia_div("props", props={"name": "Brandon", "sport": "Hockey"}) + ) diff --git a/inertia/tests/test_tests.py b/inertia/tests/test_tests.py index 715ef7b..8330a5e 100644 --- a/inertia/tests/test_tests.py +++ b/inertia/tests/test_tests.py @@ -2,29 +2,27 @@ class TestTestCase(InertiaTestCase): + def test_include_props(self): + response = self.client.get("/props/") - def test_include_props(self): - response = self.client.get('/props/') + self.assertIncludesProps({"name": "Brandon"}) - self.assertIncludesProps({'name': 'Brandon'}) + def test_has_exact_props(self): + response = self.client.get("/props/") - def test_has_exact_props(self): - response = self.client.get('/props/') + self.assertHasExactProps({"name": "Brandon", "sport": "Hockey"}) - self.assertHasExactProps({'name': 'Brandon', 'sport': 'Hockey'}) + def test_has_template_data(self): + response = self.client.get("/template_data/") - def test_has_template_data(self): - response = self.client.get('/template_data/') + self.assertIncludesTemplateData({"name": "Brian"}) - self.assertIncludesTemplateData({'name': 'Brian'}) + def test_has_exact_template_data(self): + response = self.client.get("/template_data/") - def test_has_exact_template_data(self): - response = self.client.get('/template_data/') + self.assertHasExactTemplateData({"name": "Brian", "sport": "Basketball"}) - self.assertHasExactTemplateData({'name': 'Brian', 'sport': 'Basketball'}) - - def test_component_name(self): - response = self.client.get('/props/') - - self.assertComponentUsed('TestComponent') + def test_component_name(self): + response = self.client.get("/props/") + self.assertComponentUsed("TestComponent") diff --git a/inertia/tests/testapp/apps.py b/inertia/tests/testapp/apps.py index b37eb9e..726053d 100644 --- a/inertia/tests/testapp/apps.py +++ b/inertia/tests/testapp/apps.py @@ -2,5 +2,5 @@ class TestAppConfig(AppConfig): - name = 'inertia.tests.testapp' - verbose_name = 'TestApp' + name = "inertia.tests.testapp" + verbose_name = "TestApp" diff --git a/inertia/tests/testapp/models.py b/inertia/tests/testapp/models.py index eb24f49..545e87b 100644 --- a/inertia/tests/testapp/models.py +++ b/inertia/tests/testapp/models.py @@ -2,7 +2,7 @@ class User(models.Model): - name = models.CharField(max_length=255) - password = models.CharField(max_length=255) - birthdate = models.DateField() - registered_at = models.DateTimeField() + name = models.CharField(max_length=255) + password = models.CharField(max_length=255) + birthdate = models.DateField() + registered_at = models.DateTimeField() diff --git a/inertia/tests/testapp/urls.py b/inertia/tests/testapp/urls.py index f9357e6..dd88e0f 100644 --- a/inertia/tests/testapp/urls.py +++ b/inertia/tests/testapp/urls.py @@ -3,24 +3,24 @@ from . import views urlpatterns = [ - path('test/', views.test), - path('empty/', views.empty_test), - path('redirect/', views.redirect_test), - path('props/', views.props_test), - path('template_data/', views.template_data_test), - path('lazy/', views.lazy_test), - path('optional/', views.optional_test), - path('defer/', views.defer_test), - path('defer-group/', views.defer_group_test), - path('merge/', views.merge_test), - path('complex-props/', views.complex_props_test), - path('share/', views.share_test), - path('inertia-redirect/', views.inertia_redirect_test), - path('external-redirect/', views.external_redirect_test), - path('encrypt-history/', views.encrypt_history_test), - path('no-encrypt-history/', views.encrypt_history_false_test), - path('encrypt-history-type-error/', views.encrypt_history_type_error_test), - path('clear-history/', views.clear_history_test), - path('clear-history-redirect/', views.clear_history_redirect_test), - path('clear-history-type-error/', views.clear_history_type_error_test), + path("test/", views.test), + path("empty/", views.empty_test), + path("redirect/", views.redirect_test), + path("props/", views.props_test), + path("template_data/", views.template_data_test), + path("lazy/", views.lazy_test), + path("optional/", views.optional_test), + path("defer/", views.defer_test), + path("defer-group/", views.defer_group_test), + path("merge/", views.merge_test), + path("complex-props/", views.complex_props_test), + path("share/", views.share_test), + path("inertia-redirect/", views.inertia_redirect_test), + path("external-redirect/", views.external_redirect_test), + path("encrypt-history/", views.encrypt_history_test), + path("no-encrypt-history/", views.encrypt_history_false_test), + path("encrypt-history-type-error/", views.encrypt_history_type_error_test), + path("clear-history/", views.clear_history_test), + path("clear-history-redirect/", views.clear_history_redirect_test), + path("clear-history-type-error/", views.clear_history_type_error_test), ] diff --git a/inertia/tests/testapp/views.py b/inertia/tests/testapp/views.py index f315ea1..26c9f1f 100644 --- a/inertia/tests/testapp/views.py +++ b/inertia/tests/testapp/views.py @@ -7,127 +7,148 @@ class ShareMiddleware: - def __init__(self, get_response): - self.get_response = get_response + def __init__(self, get_response): + self.get_response = get_response + + def process_request(self, request): + share( + request, + position=lambda: "goalie", + number=29, + ) - def process_request(self, request): - share(request, - position=lambda: 'goalie', - number=29, - ) def test(request): - return HttpResponse('Hey good stuff') + return HttpResponse("Hey good stuff") -@inertia('TestComponent') + +@inertia("TestComponent") def empty_test(request): - return {} + return {} + def redirect_test(request): - return redirect(empty_test) + return redirect(empty_test) -@inertia('TestComponent') + +@inertia("TestComponent") def inertia_redirect_test(request): - return redirect(empty_test) + return redirect(empty_test) + def external_redirect_test(request): - return location("http://foobar.com/") + return location("http://foobar.com/") + -@inertia('TestComponent') +@inertia("TestComponent") def props_test(request): - return { - 'name': 'Brandon', - 'sport': 'Hockey', - } + return { + "name": "Brandon", + "sport": "Hockey", + } + def template_data_test(request): - return render(request, 'TestComponent', template_data={ - 'name': 'Brian', - 'sport': 'Basketball', - }) + return render( + request, + "TestComponent", + template_data={ + "name": "Brian", + "sport": "Basketball", + }, + ) + -@inertia('TestComponent') +@inertia("TestComponent") def lazy_test(request): - return { - 'name': 'Brian', - 'sport': lazy(lambda: 'Basketball'), - 'grit': lazy(lambda: 'intense'), - } + return { + "name": "Brian", + "sport": lazy(lambda: "Basketball"), + "grit": lazy(lambda: "intense"), + } + -@inertia('TestComponent') +@inertia("TestComponent") def optional_test(request): - return { - 'name': 'Brian', - 'sport': optional(lambda: 'Basketball'), - 'grit': optional(lambda: 'intense'), - } + return { + "name": "Brian", + "sport": optional(lambda: "Basketball"), + "grit": optional(lambda: "intense"), + } -@inertia('TestComponent') + +@inertia("TestComponent") def defer_test(request): - return { - 'name': 'Brian', - 'sport': defer(lambda: 'Basketball') - } + return {"name": "Brian", "sport": defer(lambda: "Basketball")} -@inertia('TestComponent') +@inertia("TestComponent") def defer_group_test(request): - return { - 'name': 'Brian', - 'sport': defer(lambda: 'Basketball', 'group'), - 'team': defer(lambda: 'Bulls', 'group'), - 'grit': defer(lambda: 'intense') - } - -@inertia('TestComponent') + return { + "name": "Brian", + "sport": defer(lambda: "Basketball", "group"), + "team": defer(lambda: "Bulls", "group"), + "grit": defer(lambda: "intense"), + } + + +@inertia("TestComponent") def merge_test(request): - return { - 'name': 'Brandon', - 'sport': merge(lambda: 'Hockey'), - 'team': defer(lambda: 'Penguins', merge=True), - } + return { + "name": "Brandon", + "sport": merge(lambda: "Hockey"), + "team": defer(lambda: "Penguins", merge=True), + } + -@inertia('TestComponent') +@inertia("TestComponent") def complex_props_test(request): - return { - 'person': { - 'name': lambda: 'Brandon', + return { + "person": { + "name": lambda: "Brandon", + } } - } + @decorator_from_middleware(ShareMiddleware) -@inertia('TestComponent') +@inertia("TestComponent") def share_test(request): - return { - 'name': 'Brandon', - } + return { + "name": "Brandon", + } + -@inertia('TestComponent') +@inertia("TestComponent") def encrypt_history_test(request): - encrypt_history(request) - return {} + encrypt_history(request) + return {} -@inertia('TestComponent') + +@inertia("TestComponent") def encrypt_history_false_test(request): - encrypt_history(request, False) - return {} + encrypt_history(request, False) + return {} + -@inertia('TestComponent') +@inertia("TestComponent") def encrypt_history_type_error_test(request): - encrypt_history(request, "foo") - return {} + encrypt_history(request, "foo") + return {} -@inertia('TestComponent') + +@inertia("TestComponent") def clear_history_test(request): - clear_history(request) - return {} + clear_history(request) + return {} + -@inertia('TestComponent') +@inertia("TestComponent") def clear_history_redirect_test(request): - clear_history(request) - return redirect(empty_test) + clear_history(request) + return redirect(empty_test) + -@inertia('TestComponent') +@inertia("TestComponent") def clear_history_type_error_test(request): - request.session[INERTIA_SESSION_CLEAR_HISTORY] = "foo" - return {} + request.session[INERTIA_SESSION_CLEAR_HISTORY] = "foo" + return {} diff --git a/inertia/utils.py b/inertia/utils.py index 49011dc..9efa61a 100644 --- a/inertia/utils.py +++ b/inertia/utils.py @@ -9,31 +9,36 @@ def model_to_dict(model): - return base_model_to_dict(model, exclude=('password',)) + return base_model_to_dict(model, exclude=("password",)) + class InertiaJsonEncoder(DjangoJSONEncoder): - def default(self, value): - if isinstance(value, models.Model): - return model_to_dict(value) + def default(self, value): + if isinstance(value, models.Model): + return model_to_dict(value) + + if isinstance(value, QuerySet): + return [model_to_dict(model) for model in value] - if isinstance(value, QuerySet): - return [model_to_dict(model) for model in value] + return super().default(value) - return super().default(value) def lazy(prop): - warnings.warn( - "lazy is deprecated and will be removed in a future version. Please use optional instead.", - DeprecationWarning, - stacklevel=2 - ) - return optional(prop) + warnings.warn( + "lazy is deprecated and will be removed in a future version. Please use optional instead.", + DeprecationWarning, + stacklevel=2, + ) + return optional(prop) + def optional(prop): - return OptionalProp(prop) + return OptionalProp(prop) + def defer(prop, group="default", merge=False): - return DeferredProp(prop, group=group, merge=merge) + return DeferredProp(prop, group=group, merge=merge) + def merge(prop): - return MergeProp(prop) + return MergeProp(prop) From 5d00c12db728156be35e822e2885f9c8cf9dcbaf Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 9 Jan 2025 09:44:25 +0100 Subject: [PATCH 4/8] Fix unused variables F841 --- inertia/tests/test_tests.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/inertia/tests/test_tests.py b/inertia/tests/test_tests.py index 8330a5e..e47d04c 100644 --- a/inertia/tests/test_tests.py +++ b/inertia/tests/test_tests.py @@ -3,26 +3,26 @@ class TestTestCase(InertiaTestCase): def test_include_props(self): - response = self.client.get("/props/") + self.client.get("/props/") self.assertIncludesProps({"name": "Brandon"}) def test_has_exact_props(self): - response = self.client.get("/props/") + self.client.get("/props/") self.assertHasExactProps({"name": "Brandon", "sport": "Hockey"}) def test_has_template_data(self): - response = self.client.get("/template_data/") + self.client.get("/template_data/") self.assertIncludesTemplateData({"name": "Brian"}) def test_has_exact_template_data(self): - response = self.client.get("/template_data/") + self.client.get("/template_data/") self.assertHasExactTemplateData({"name": "Brian", "sport": "Basketball"}) def test_component_name(self): - response = self.client.get("/props/") + self.client.get("/props/") self.assertComponentUsed("TestComponent") From dacab442d89eff81871c26927be05286cfd8bbe4 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 9 Jan 2025 09:45:57 +0100 Subject: [PATCH 5/8] Fix unused import F401 --- inertia/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/inertia/__init__.py b/inertia/__init__.py index c91b109..c242923 100644 --- a/inertia/__init__.py +++ b/inertia/__init__.py @@ -1,3 +1,15 @@ from .http import InertiaResponse, inertia, location, render from .share import share from .utils import defer, lazy, merge, optional + +__all__ = [ + "InertiaResponse", + "inertia", + "location", + "render", + "share", + "defer", + "lazy", + "merge", + "optional", +] From 2b7ac49141b7c69dc919c049cd0ab3081f82780e Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 9 Jan 2025 09:48:12 +0100 Subject: [PATCH 6/8] Fix star-arg unpacking after a keyword argument is strongly discouraged B026 --- inertia/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inertia/http.py b/inertia/http.py index 4090ba0..55d09d0 100644 --- a/inertia/http.py +++ b/inertia/http.py @@ -192,9 +192,9 @@ def __init__( content = self.build_first_load(data) super().__init__( + *args, content=content, headers=_headers, - *args, **kwargs, ) From e7ed01f100ee157388fb0e2f3906742ceb7176ae Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 9 Jan 2025 09:57:07 +0100 Subject: [PATCH 7/8] Fix abstract base class without abstract method B024 --- inertia/prop_classes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inertia/prop_classes.py b/inertia/prop_classes.py index 02fdcfa..2563796 100644 --- a/inertia/prop_classes.py +++ b/inertia/prop_classes.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod -class CallableProp(ABC): +class CallableProp: def __init__(self, prop): self.prop = prop @@ -15,7 +15,7 @@ def should_merge(self): pass -class IgnoreOnFirstLoadProp(ABC): +class IgnoreOnFirstLoadProp: pass From bca66d451d3e5d4b63ac0ca34f382a88e429394d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 9 Jan 2025 10:00:39 +0100 Subject: [PATCH 8/8] Fix do not use mutable data structures for argument defaults B006 --- inertia/test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/inertia/test.py b/inertia/test.py index eb35669..000bd99 100644 --- a/inertia/test.py +++ b/inertia/test.py @@ -96,11 +96,13 @@ def assertComponentUsed(self, component_name): def inertia_page( url, component="TestComponent", - props={}, - template_data={}, + props=None, + template_data=None, deferred_props=None, merge_props=None, ): + props = props or {} + template_data = template_data or {} _page = { "component": component, "props": props,