mirror of
https://github.com/django-components/django-components.git
synced 2025-09-19 04:09:44 +00:00
refactor: change caching methods to accept slots + typing fixes (#1173)
This commit is contained in:
parent
e64cd197c1
commit
661413d4a9
9 changed files with 126 additions and 66 deletions
112
CHANGELOG.md
112
CHANGELOG.md
|
@ -4,38 +4,18 @@
|
|||
|
||||
#### 🚨📢 BREAKING CHANGES
|
||||
|
||||
- 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.
|
||||
**Middleware**
|
||||
|
||||
- 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
|
||||
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,
|
||||
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"`
|
||||
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.
|
||||
|
||||
**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
|
||||
match `get_template_data()`.
|
||||
|
||||
|
@ -81,18 +99,6 @@
|
|||
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
|
||||
to match that of `Component.render()`.
|
||||
|
||||
|
@ -170,6 +176,28 @@
|
|||
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.
|
||||
|
||||
Instead, the component name MUST be the first POSITIONAL argument only.
|
||||
|
@ -193,6 +221,8 @@
|
|||
{% component "profile" name="John" job="Developer" / %}
|
||||
```
|
||||
|
||||
**Miscellaneous**
|
||||
|
||||
- The second argument to `render_dependencies()` is now `strategy` instead of `type`.
|
||||
|
||||
Before:
|
||||
|
@ -209,7 +239,9 @@
|
|||
|
||||
#### 🚨📢 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
|
||||
to accept also slots and context.
|
||||
|
@ -244,7 +276,7 @@
|
|||
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`.
|
||||
|
||||
|
@ -266,6 +298,10 @@
|
|||
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
|
||||
|
||||
- New method to render template variables - `get_template_data()`
|
||||
|
@ -372,7 +408,7 @@
|
|||
|
||||
#### 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
|
||||
|
||||
|
|
|
@ -54,7 +54,9 @@ python manage.py components ext run <extension> <command>
|
|||
## `components create`
|
||||
|
||||
```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`
|
||||
- Comma-separated list of columns to show. Available columns: name. Defaults to `--columns name`.
|
||||
- `-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`
|
||||
- Comma-separated list of columns to show. Available columns: name, full_name, path. Defaults to `--columns full_name,path`.
|
||||
- `-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`
|
||||
|
||||
```txt
|
||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color]
|
||||
[--force-color] [--skip-checks]
|
||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||
[--skip-checks]
|
||||
|
||||
```
|
||||
|
||||
|
@ -506,8 +509,10 @@ Deprecated. Use `components upgrade` instead.
|
|||
## `startcomponent`
|
||||
|
||||
```txt
|
||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] [--version] [-v {0,1,2,3}]
|
||||
[--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force]
|
||||
[--verbose] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||
[--skip-checks]
|
||||
name
|
||||
|
||||
```
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
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
|
||||
# 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
|
||||
return cls
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from django.core.cache import BaseCache, caches
|
||||
|
||||
|
@ -67,14 +67,14 @@ class ComponentCache(ComponentExtension.ExtensionClass): # type: ignore
|
|||
cache = caches[cache_name]
|
||||
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()`,
|
||||
# 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
|
||||
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.
|
||||
|
||||
|
@ -117,11 +117,11 @@ class CacheExtension(ComponentExtension):
|
|||
self.render_id_to_cache_key: dict[str, str] = {}
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
# 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
|
||||
def on_component_rendered(self, ctx: OnComponentRenderedContext) -> None:
|
||||
cache_instance: ComponentCache = ctx.component.cache
|
||||
cache_instance = ctx.component.cache
|
||||
if not cache_instance.enabled:
|
||||
return None
|
||||
|
||||
|
|
|
@ -233,7 +233,7 @@ class ViewExtension(ComponentExtension):
|
|||
|
||||
# Create URL route on creation
|
||||
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)
|
||||
if view_cls is None or not view_cls.public:
|
||||
return
|
||||
|
@ -254,7 +254,7 @@ class ViewExtension(ComponentExtension):
|
|||
|
||||
# Remove URL route on deletion
|
||||
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)
|
||||
if route is None:
|
||||
return
|
||||
|
|
|
@ -1,15 +1,31 @@
|
|||
import sys
|
||||
from argparse import Action, ArgumentParser
|
||||
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:
|
||||
from argparse import _ArgumentGroup, _FormatterClass
|
||||
|
||||
|
||||
TClass = TypeVar("TClass", bound=Type[Any])
|
||||
|
||||
|
||||
# Mark object as related to extension commands so we can place these in
|
||||
# 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
|
||||
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
|
||||
[`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
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
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
|
||||
# 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
|
||||
return obj
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ class TestComponentCache:
|
|||
assert result == "Hello"
|
||||
|
||||
# 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 component.cache.get_entry(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
|
||||
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
|
||||
|
||||
# Second render
|
||||
|
@ -104,7 +104,7 @@ class TestComponentCache:
|
|||
component.render()
|
||||
|
||||
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"
|
||||
|
||||
# Wait for TTL to expire
|
||||
|
@ -186,7 +186,7 @@ class TestComponentCache:
|
|||
|
||||
# The key consists of `component._class_hash`, hashed args, and hashed kwargs
|
||||
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):
|
||||
class TestComponent(Component):
|
||||
|
@ -207,7 +207,7 @@ class TestComponentCache:
|
|||
|
||||
# The key should use the custom hash methods
|
||||
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):
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue