refactor: change caching methods to accept slots + typing fixes (#1173)

This commit is contained in:
Juro Oravec 2025-05-09 10:19:34 +02:00 committed by GitHub
parent e64cd197c1
commit 661413d4a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 126 additions and 66 deletions

View file

@ -4,38 +4,18 @@
#### 🚨📢 BREAKING CHANGES #### 🚨📢 BREAKING CHANGES
- Component typing no longer uses generics. Instead, the types are now defined as class attributes of the component class. **Middleware**
Before:
```py
Args = Tuple[float, str]
class Button(Component[Args]):
pass
```
After:
```py
class Button(Component):
class Args(NamedTuple):
size: float
text: str
```
See [Migrating from generics to class attributes](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/#migrating-from-generics-to-class-attributes) for more info.
- The middleware `ComponentDependencyMiddleware` was removed as it is no longer needed. - The middleware `ComponentDependencyMiddleware` was removed as it is no longer needed.
The middleware served one purpose - to render the JS and CSS dependencies of components The middleware served one purpose - to render the JS and CSS dependencies of components
when you rendered templates with `Template.render()` or `django.shortcuts.render()` and those templates contained `{% component %}` tags. when you rendered templates with `Template.render()` or `django.shortcuts.render()` and those templates contained `{% component %}` tags.
- NOTE: If you rendered HTML with `Component.render()` or `Component.render_to_response()`, the JS and CSS were already rendered.
Now, the JS and CSS dependencies of components are automatically rendered, Now, the JS and CSS dependencies of components are automatically rendered,
even when you render Templates with `Template.render()` or `django.shortcuts.render()`. even when you render Templates with `Template.render()` or `django.shortcuts.render()`.
- NOTE: If you rendered HTML with `Component.render()` or `Component.render_to_response()`, the JS and CSS were already rendered.
To disable this behavior, set the `DJC_DEPS_STRATEGY` context key to `"ignore"` To disable this behavior, set the `DJC_DEPS_STRATEGY` context key to `"ignore"`
when rendering the template: when rendering the template:
@ -64,6 +44,44 @@
See [Dependencies rendering](https://django-components.github.io/django-components/0.140/concepts/advanced/rendering_js_css/) for more info. See [Dependencies rendering](https://django-components.github.io/django-components/0.140/concepts/advanced/rendering_js_css/) for more info.
**Typing**
- Component typing no longer uses generics. Instead, the types are now defined as class attributes of the component class.
Before:
```py
Args = Tuple[float, str]
class Button(Component[Args]):
pass
```
After:
```py
class Button(Component):
class Args(NamedTuple):
size: float
text: str
```
See [Migrating from generics to class attributes](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/#migrating-from-generics-to-class-attributes) for more info.
- Removed `EmptyTuple` and `EmptyDict` types. Instead, there is now a single `Empty` type.
```py
from django_components import Component, Empty
class Button(Component):
template = "Hello"
Args = Empty
Kwargs = Empty
```
**Component API**
- The interface of the not-yet-released `get_js_data()` and `get_css_data()` methods has changed to - The interface of the not-yet-released `get_js_data()` and `get_css_data()` methods has changed to
match `get_template_data()`. match `get_template_data()`.
@ -81,18 +99,6 @@
def get_css_data(self, args, kwargs, slots, context): def get_css_data(self, args, kwargs, slots, context):
``` ```
- Removed `EmptyTuple` and `EmptyDict` types. Instead, there is now a single `Empty` type.
```py
from django_components import Component, Empty
class Button(Component):
template = "Hello"
Args = Empty
Kwargs = Empty
```
- Arguments in `Component.render_to_response()` have changed - Arguments in `Component.render_to_response()` have changed
to match that of `Component.render()`. to match that of `Component.render()`.
@ -170,6 +176,28 @@
return self.render_to_response() return self.render_to_response()
``` ```
- Caching - The function signatures of `Component.Cache.get_cache_key()` and `Component.Cache.hash()` have changed to enable passing slots.
Args and kwargs are no longer spread, but passed as a list and a dict, respectively.
Before:
```py
def get_cache_key(self, *args: Any, **kwargs: Any) -> str:
def hash(self, *args: Any, **kwargs: Any) -> str:
```
After:
```py
def get_cache_key(self, args: Any, kwargs: Any, slots: Any) -> str:
def hash(self, args: Any, kwargs: Any) -> str:
```
**Template tags**
- Component name in the `{% component %}` tag can no longer be set as a kwarg. - Component name in the `{% component %}` tag can no longer be set as a kwarg.
Instead, the component name MUST be the first POSITIONAL argument only. Instead, the component name MUST be the first POSITIONAL argument only.
@ -193,6 +221,8 @@
{% component "profile" name="John" job="Developer" / %} {% component "profile" name="John" job="Developer" / %}
``` ```
**Miscellaneous**
- The second argument to `render_dependencies()` is now `strategy` instead of `type`. - The second argument to `render_dependencies()` is now `strategy` instead of `type`.
Before: Before:
@ -209,7 +239,9 @@
#### 🚨📢 Deprecation #### 🚨📢 Deprecation
- `get_context_data()` is now deprecated. Use `get_template_data()` instead. **Component API**
- `Component.get_context_data()` is now deprecated. Use `Component.get_template_data()` instead.
`get_template_data()` behaves the same way, but has a different function signature `get_template_data()` behaves the same way, but has a different function signature
to accept also slots and context. to accept also slots and context.
@ -244,7 +276,7 @@
Calendar.render_to_response(deps_strategy="ignore") Calendar.render_to_response(deps_strategy="ignore")
``` ```
- `SlotContent` was renamed to `SlotInput`. The old name is deprecated and will be removed in v1. **Extensions**
- In the `on_component_data()` extension hook, the `context_data` field of the context object was superseded by `template_data`. - In the `on_component_data()` extension hook, the `context_data` field of the context object was superseded by `template_data`.
@ -266,6 +298,10 @@
ctx.template_data["my_template_var"] = "my_value" ctx.template_data["my_template_var"] = "my_value"
``` ```
**Miscellaneous**
- `SlotContent` was renamed to `SlotInput`. The old name is deprecated and will be removed in v1.
#### Feat #### Feat
- New method to render template variables - `get_template_data()` - New method to render template variables - `get_template_data()`
@ -372,7 +408,7 @@
#### Fix #### Fix
- Fix bug: Context processors data was being generated anew for each component. Now the data correctly created once and reused across components with the same request ([#1165](https://github.com/django-components/django-components/issues/1165)). - Fix bug: Context processors data was being generated anew for each component. Now the data is correctly created once and reused across components with the same request ([#1165](https://github.com/django-components/django-components/issues/1165)).
## v0.139.1 ## v0.139.1

View file

@ -54,7 +54,9 @@ python manage.py components ext run <extension> <command>
## `components create` ## `components create`
```txt ```txt
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] name usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
[--dry-run]
name
``` ```
@ -236,7 +238,7 @@ List all extensions.
- `--columns COLUMNS` - `--columns COLUMNS`
- Comma-separated list of columns to show. Available columns: name. Defaults to `--columns name`. - Comma-separated list of columns to show. Available columns: name. Defaults to `--columns name`.
- `-s`, `--simple` - `-s`, `--simple`
- Only show table data, without headers. Use this option for generating machine-readable output. - Only show table data, without headers. Use this option for generating machine- readable output.
@ -399,7 +401,7 @@ List all components created in this project.
- `--columns COLUMNS` - `--columns COLUMNS`
- Comma-separated list of columns to show. Available columns: name, full_name, path. Defaults to `--columns full_name,path`. - Comma-separated list of columns to show. Available columns: name, full_name, path. Defaults to `--columns full_name,path`.
- `-s`, `--simple` - `-s`, `--simple`
- Only show table data, without headers. Use this option for generating machine-readable output. - Only show table data, without headers. Use this option for generating machine- readable output.
@ -461,8 +463,9 @@ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAc
## `upgradecomponent` ## `upgradecomponent`
```txt ```txt
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
[--force-color] [--skip-checks] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
[--skip-checks]
``` ```
@ -506,8 +509,10 @@ Deprecated. Use `components upgrade` instead.
## `startcomponent` ## `startcomponent`
```txt ```txt
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] [--version] [-v {0,1,2,3}] usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force]
[--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks] [--verbose] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
[--skip-checks]
name name
``` ```

View file

@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L2785" target="_blank">See source code</a> <a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L2784" target="_blank">See source code</a>

View file

@ -17,6 +17,7 @@ if TYPE_CHECKING:
TCallable = TypeVar("TCallable", bound=Callable) TCallable = TypeVar("TCallable", bound=Callable)
TClass = TypeVar("TClass", bound=Type[Any])
################################################ ################################################
@ -29,7 +30,7 @@ TCallable = TypeVar("TCallable", bound=Callable)
# Mark a class as an extension hook context so we can place these in # Mark a class as an extension hook context so we can place these in
# a separate documentation section # a separate documentation section
def mark_extension_hook_api(cls: Type[Any]) -> Type[Any]: def mark_extension_hook_api(cls: TClass) -> TClass:
cls._extension_hook_api = True cls._extension_hook_api = True
return cls return cls

View file

@ -1,4 +1,4 @@
from typing import Any, Optional from typing import Any, Dict, List, Optional
from django.core.cache import BaseCache, caches from django.core.cache import BaseCache, caches
@ -67,14 +67,14 @@ class ComponentCache(ComponentExtension.ExtensionClass): # type: ignore
cache = caches[cache_name] cache = caches[cache_name]
return cache return cache
def get_cache_key(self, *args: Any, **kwargs: Any) -> str: def get_cache_key(self, args: List, kwargs: Dict, slots: Dict) -> str:
# Allow user to override how the input is hashed into a cache key with `hash()`, # Allow user to override how the input is hashed into a cache key with `hash()`,
# but then still prefix it wih our own prefix, so it's clear where it comes from. # but then still prefix it wih our own prefix, so it's clear where it comes from.
cache_key = self.hash(*args, **kwargs) cache_key = self.hash(args, kwargs)
cache_key = CACHE_KEY_PREFIX + self.component._class_hash + ":" + cache_key cache_key = CACHE_KEY_PREFIX + self.component._class_hash + ":" + cache_key
return cache_key return cache_key
def hash(self, *args: Any, **kwargs: Any) -> str: def hash(self, args: List, kwargs: Dict) -> str:
""" """
Defines how the input (both args and kwargs) is hashed into a cache key. Defines how the input (both args and kwargs) is hashed into a cache key.
@ -117,11 +117,11 @@ class CacheExtension(ComponentExtension):
self.render_id_to_cache_key: dict[str, str] = {} self.render_id_to_cache_key: dict[str, str] = {}
def on_component_input(self, ctx: OnComponentInputContext) -> Optional[Any]: def on_component_input(self, ctx: OnComponentInputContext) -> Optional[Any]:
cache_instance: ComponentCache = ctx.component.cache cache_instance = ctx.component.cache
if not cache_instance.enabled: if not cache_instance.enabled:
return None return None
cache_key = cache_instance.get_cache_key(*ctx.args, **ctx.kwargs) cache_key = cache_instance.get_cache_key(ctx.args, ctx.kwargs, ctx.slots)
self.render_id_to_cache_key[ctx.component_id] = cache_key self.render_id_to_cache_key[ctx.component_id] = cache_key
# If cache entry exists, return it. This will short-circuit the rendering process. # If cache entry exists, return it. This will short-circuit the rendering process.
@ -132,7 +132,7 @@ class CacheExtension(ComponentExtension):
# Save the rendered component to cache # Save the rendered component to cache
def on_component_rendered(self, ctx: OnComponentRenderedContext) -> None: def on_component_rendered(self, ctx: OnComponentRenderedContext) -> None:
cache_instance: ComponentCache = ctx.component.cache cache_instance = ctx.component.cache
if not cache_instance.enabled: if not cache_instance.enabled:
return None return None

View file

@ -233,7 +233,7 @@ class ViewExtension(ComponentExtension):
# Create URL route on creation # Create URL route on creation
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None: def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
comp_cls: Type["Component"] = ctx.component_cls comp_cls = ctx.component_cls
view_cls: Optional[Type[ComponentView]] = getattr(comp_cls, "View", None) view_cls: Optional[Type[ComponentView]] = getattr(comp_cls, "View", None)
if view_cls is None or not view_cls.public: if view_cls is None or not view_cls.public:
return return
@ -254,7 +254,7 @@ class ViewExtension(ComponentExtension):
# Remove URL route on deletion # Remove URL route on deletion
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None: def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
comp_cls: Type["Component"] = ctx.component_cls comp_cls = ctx.component_cls
route = self.routes_by_component.pop(comp_cls, None) route = self.routes_by_component.pop(comp_cls, None)
if route is None: if route is None:
return return

View file

@ -1,15 +1,31 @@
import sys import sys
from argparse import Action, ArgumentParser from argparse import Action, ArgumentParser
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Sequence, Type, Union from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Literal,
Optional,
Protocol,
Sequence,
Type,
TypeVar,
Union,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from argparse import _ArgumentGroup, _FormatterClass from argparse import _ArgumentGroup, _FormatterClass
TClass = TypeVar("TClass", bound=Type[Any])
# Mark object as related to extension commands so we can place these in # Mark object as related to extension commands so we can place these in
# a separate documentation section # a separate documentation section
def mark_extension_command_api(obj: Any) -> Any: def mark_extension_command_api(obj: TClass) -> TClass:
obj._extension_command_api = True obj._extension_command_api = True
return obj return obj
@ -27,7 +43,7 @@ The basic type of action to be taken when this argument is encountered at the co
This is a subset of the values for `action` in This is a subset of the values for `action` in
[`ArgumentParser.add_argument()`](https://docs.python.org/3/library/argparse.html#the-add-argument-method). [`ArgumentParser.add_argument()`](https://docs.python.org/3/library/argparse.html#the-add-argument-method).
""" """
mark_extension_command_api(CommandLiteralAction) mark_extension_command_api(CommandLiteralAction) # type: ignore
@mark_extension_command_api @mark_extension_command_api

View file

@ -1,10 +1,12 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Dict, Iterable, Optional, Protocol from typing import Any, Dict, Iterable, Optional, Protocol, Type, TypeVar
TClass = TypeVar("TClass", bound=Type[Any])
# Mark object as related to extension URLs so we can place these in # Mark object as related to extension URLs so we can place these in
# a separate documentation section # a separate documentation section
def mark_extension_url_api(obj: Any) -> Any: def mark_extension_url_api(obj: TClass) -> TClass:
obj._extension_url_api = True obj._extension_url_api = True
return obj return obj

View file

@ -45,7 +45,7 @@ class TestComponentCache:
assert result == "Hello" assert result == "Hello"
# Check if the cache entry is set # Check if the cache entry is set
cache_key = component.cache.get_cache_key() cache_key = component.cache.get_cache_key([], {}, {})
assert cache_key == "components:cache:TestComponent_c9770f::" assert cache_key == "components:cache:TestComponent_c9770f::"
assert component.cache.get_entry(cache_key) == "<!-- _RENDERED TestComponent_c9770f,ca1bc3e,, -->Hello" assert component.cache.get_entry(cache_key) == "<!-- _RENDERED TestComponent_c9770f,ca1bc3e,, -->Hello"
assert caches["default"].get(cache_key) == "<!-- _RENDERED TestComponent_c9770f,ca1bc3e,, -->Hello" assert caches["default"].get(cache_key) == "<!-- _RENDERED TestComponent_c9770f,ca1bc3e,, -->Hello"
@ -81,7 +81,7 @@ class TestComponentCache:
# Check if the cache entry is not set # Check if the cache entry is not set
cache_instance = component.cache cache_instance = component.cache
cache_key = cache_instance.get_cache_key() cache_key = cache_instance.get_cache_key([], {}, {})
assert cache_instance.get_entry(cache_key) is None assert cache_instance.get_entry(cache_key) is None
# Second render # Second render
@ -104,7 +104,7 @@ class TestComponentCache:
component.render() component.render()
cache_instance = component.cache cache_instance = component.cache
cache_key = cache_instance.get_cache_key() cache_key = cache_instance.get_cache_key([], {}, {})
assert cache_instance.get_entry(cache_key) == "<!-- _RENDERED TestComponent_42aca9,ca1bc3e,, -->Hello" assert cache_instance.get_entry(cache_key) == "<!-- _RENDERED TestComponent_42aca9,ca1bc3e,, -->Hello"
# Wait for TTL to expire # Wait for TTL to expire
@ -186,7 +186,7 @@ class TestComponentCache:
# The key consists of `component._class_hash`, hashed args, and hashed kwargs # The key consists of `component._class_hash`, hashed args, and hashed kwargs
expected_key = "1,2:key-value" expected_key = "1,2:key-value"
assert component.cache.hash(1, 2, key="value") == expected_key assert component.cache.hash([1, 2], {"key": "value"}) == expected_key
def test_override_hash_methods(self): def test_override_hash_methods(self):
class TestComponent(Component): class TestComponent(Component):
@ -207,7 +207,7 @@ class TestComponentCache:
# The key should use the custom hash methods # The key should use the custom hash methods
expected_key = "components:cache:TestComponent_28880f:custom-args-and-kwargs" expected_key = "components:cache:TestComponent_28880f:custom-args-and-kwargs"
assert component.cache.get_cache_key(1, 2, key="value") == expected_key assert component.cache.get_cache_key([1, 2], {"key": "value"}, {}) == expected_key
def test_cached_component_inside_include(self): def test_cached_component_inside_include(self):