feat: expose _class_hash as class_id (#1094)

* feat: expose _class_hash as class_id

* refactor: fix linting
This commit is contained in:
Juro Oravec 2025-04-07 11:08:02 +02:00 committed by GitHub
parent a49f5e51dd
commit bb5de86b69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 141 additions and 82 deletions

View file

@ -4,6 +4,12 @@
#### Feat #### Feat
- Each Component class now has a `class_id` attribute, which is unique to the component subclass.
NOTE: This is different from `Component.id`, which is unique to each rendered instance.
To look up a component class by its `class_id`, use `get_component_by_class_id()`.
- It's now easier to create URLs for component views. - It's now easier to create URLs for component views.
Before, you had to call `Component.as_view()` and pass that to `urlpatterns`. Before, you had to call `Component.as_view()` and pass that to `urlpatterns`.

View file

@ -207,11 +207,11 @@ This is how we achieve that:
5. To be able to fetch component's inlined JS and CSS, django-components adds a URL path under: 5. To be able to fetch component's inlined JS and CSS, django-components adds a URL path under:
`/components/cache/<str:comp_cls_hash>.<str:script_type>/` `/components/cache/<str:comp_cls_id>.<str:script_type>/`
E.g. `/components/cache/my_table_10bc2c.js/` E.g. `/components/cache/MyTable_10bc2c.js/`
This endpoint takes the component's unique hash, e.g. `my_table_10bc2c`, and looks up the component's inlined JS or CSS. This endpoint takes the component's unique ID, e.g. `MyTable_10bc2c`, and looks up the component's inlined JS or CSS.
--- ---

View file

@ -119,6 +119,10 @@
options: options:
show_if_no_docstring: true show_if_no_docstring: true
::: django_components.get_component_by_class_id
options:
show_if_no_docstring: true
::: django_components.get_component_dirs ::: django_components.get_component_dirs
options: options:
show_if_no_docstring: true show_if_no_docstring: true

View file

@ -22,6 +22,6 @@ urlpatterns = [
## List of URLs ## List of URLs
- `components/cache/<str:comp_cls_hash>.<str:input_hash>.<str:script_type>` - `components/cache/<str:comp_cls_id>.<str:input_hash>.<str:script_type>`
- `components/cache/<str:comp_cls_hash>.<str:script_type>` - `components/cache/<str:comp_cls_id>.<str:script_type>`

View file

@ -201,6 +201,12 @@ def gen_reference_components():
# If the component classes define any extra methods, we want to show them. # If the component classes define any extra methods, we want to show them.
# BUT, we don't to show the methods that belong to the base Component class. # BUT, we don't to show the methods that belong to the base Component class.
unique_methods = _get_unique_methods(Component, obj) unique_methods = _get_unique_methods(Component, obj)
# NOTE: `class_id` is declared on the `Component` class, only as a type,
# so it's not picked up by `_get_unique_methods`.
if "class_id" in unique_methods:
unique_methods.remove("class_id")
if unique_methods: if unique_methods:
members = ", ".join(unique_methods) members = ", ".join(unique_methods)
members = f"[{unique_methods}]" members = f"[{unique_methods}]"
@ -456,7 +462,7 @@ def gen_reference_urls():
f.write(preface + "\n\n") f.write(preface + "\n\n")
# Simply list all URLs, e.g. # Simply list all URLs, e.g.
# `- components/cache/<str:comp_cls_hash>.<str:script_type>/` # `- components/cache/<str:comp_cls_id>.<str:script_type>/`
f.write("\n".join([f"- `{url_path}`\n" for url_path in all_urls])) f.write("\n".join([f"- `{url_path}`\n" for url_path in all_urls]))

View file

@ -16,7 +16,7 @@ from django_components.util.command import (
CommandSubcommand, CommandSubcommand,
ComponentCommand, ComponentCommand,
) )
from django_components.component import Component, ComponentVars, all_components from django_components.component import Component, ComponentVars, all_components, get_component_by_class_id
from django_components.component_media import ComponentMediaInput, ComponentMediaInputPath from django_components.component_media import ComponentMediaInput, ComponentMediaInputPath
from django_components.component_registry import ( from django_components.component_registry import (
AlreadyRegistered, AlreadyRegistered,
@ -96,6 +96,7 @@ __all__ = [
"EmptyTuple", "EmptyTuple",
"EmptyDict", "EmptyDict",
"format_attributes", "format_attributes",
"get_component_by_class_id",
"get_component_dirs", "get_component_dirs",
"get_component_files", "get_component_files",
"get_component_url", "get_component_url",

View file

@ -22,7 +22,7 @@ from typing import (
Union, Union,
cast, cast,
) )
from weakref import ReferenceType, finalize from weakref import ReferenceType, WeakValueDictionary, finalize
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.forms.widgets import Media as MediaCls from django.forms.widgets import Media as MediaCls
@ -46,7 +46,6 @@ from django_components.dependencies import (
cache_component_css_vars, cache_component_css_vars,
cache_component_js, cache_component_js,
cache_component_js_vars, cache_component_js_vars,
comp_hash_mapping,
insert_component_dependencies_comment, insert_component_dependencies_comment,
) )
from django_components.dependencies import render_dependencies as _render_dependencies from django_components.dependencies import render_dependencies as _render_dependencies
@ -113,8 +112,10 @@ CssDataType = TypeVar("CssDataType", bound=Mapping[str, Any])
# NOTE: `ReferenceType` is NOT a generic pre-3.9 # NOTE: `ReferenceType` is NOT a generic pre-3.9
if sys.version_info >= (3, 9): if sys.version_info >= (3, 9):
AllComponents = List[ReferenceType[Type["Component"]]] AllComponents = List[ReferenceType[Type["Component"]]]
CompHashMapping = WeakValueDictionary[str, Type["Component"]]
else: else:
AllComponents = List[ReferenceType] AllComponents = List[ReferenceType]
CompHashMapping = WeakValueDictionary
# Keep track of all the Component classes created, so we can clean up after tests # Keep track of all the Component classes created, so we can clean up after tests
@ -131,6 +132,47 @@ def all_components() -> List[Type["Component"]]:
return components return components
# NOTE: Initially, we fetched components by their registered name, but that didn't work
# for multiple registries and unregistered components.
#
# To have unique identifiers that works across registries, we rely
# on component class' module import path (e.g. `path.to.my.MyComponent`).
#
# But we also don't want to expose the module import paths to the outside world, as
# that information could be potentially exploited. So, instead, each component is
# associated with a hash that's derived from its module import path, ensuring uniqueness,
# consistency and privacy.
#
# E.g. `path.to.my.secret.MyComponent` -> `ab01f32`
#
# For easier debugging, we then prepend the hash with the component class name, so that
# we can easily identify the component class by its hash.
#
# E.g. `path.to.my.secret.MyComponent` -> `MyComponent_ab01f32`
#
# The associations are defined as WeakValue map, so deleted components can be garbage
# collected and automatically deleted from the dict.
comp_cls_id_mapping: CompHashMapping = WeakValueDictionary()
def get_component_by_class_id(comp_cls_id: str) -> Type["Component"]:
"""
Get a component class by its unique ID.
Each component class is associated with a unique hash that's derived from its module import path.
E.g. `path.to.my.secret.MyComponent` -> `MyComponent_ab01f32`
This hash is available under [`class_id`](../api#django_components.Component.class_id)
on the component class.
Raises `KeyError` if the component class is not found.
NOTE: This is mainly intended for extensions.
"""
return comp_cls_id_mapping[comp_cls_id]
@dataclass(frozen=True) @dataclass(frozen=True)
class RenderInput(Generic[ArgsType, KwargsType, SlotsType]): class RenderInput(Generic[ArgsType, KwargsType, SlotsType]):
context: Context context: Context
@ -589,7 +631,18 @@ class Component(
# MISC # MISC
# ##################################### # #####################################
_class_hash: ClassVar[str] class_id: ClassVar[str]
"""
Unique ID of the component class, e.g. `MyComponent_ab01f2`.
This is derived from the component class' module import path, e.g. `path.to.my.MyComponent`.
"""
# TODO_V1 - Remove this in v1
@property
def _class_hash(self) -> str:
"""Deprecated. Use `Component.class_id` instead."""
return self.class_id
def __init__( def __init__(
self, self,
@ -622,8 +675,8 @@ class Component(
extensions._init_component_instance(self) extensions._init_component_instance(self)
def __init_subclass__(cls, **kwargs: Any) -> None: def __init_subclass__(cls, **kwargs: Any) -> None:
cls._class_hash = hash_comp_cls(cls) cls.class_id = hash_comp_cls(cls)
comp_hash_mapping[cls._class_hash] = cls comp_cls_id_mapping[cls.class_id] = cls
ALL_COMPONENTS.append(cached_ref(cls)) # type: ignore[arg-type] ALL_COMPONENTS.append(cached_ref(cls)) # type: ignore[arg-type]
extensions._init_component_class(cls) extensions._init_component_class(cls)

View file

@ -351,7 +351,7 @@ class ComponentRegistry:
``` ```
""" """
existing_component = self._registry.get(name) existing_component = self._registry.get(name)
if existing_component and existing_component.cls._class_hash != component._class_hash: if existing_component and existing_component.cls.class_id != component.class_id:
raise AlreadyRegistered('The component "%s" has already been registered' % name) raise AlreadyRegistered('The component "%s" has already been registered' % name)
entry = self._register_to_library(name, component) entry = self._register_to_library(name, component)

View file

@ -3,7 +3,6 @@
import base64 import base64
import json import json
import re import re
import sys
from hashlib import md5 from hashlib import md5
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
@ -20,7 +19,6 @@ from typing import (
Union, Union,
cast, cast,
) )
from weakref import WeakValueDictionary
from asgiref.sync import iscoroutinefunction, markcoroutinefunction from asgiref.sync import iscoroutinefunction, markcoroutinefunction
from django.forms import Media from django.forms import Media
@ -57,39 +55,18 @@ RenderType = Literal["document", "fragment"]
######################################################### #########################################################
# NOTE: Initially, we fetched components by their registered name, but that didn't work
# for multiple registries and unregistered components.
#
# To have unique identifiers that works across registries, we rely
# on component class' module import path (e.g. `path.to.my.MyComponent`).
#
# But we also don't want to expose the module import paths to the outside world, as
# that information could be potentially exploited. So, instead, each component is
# associated with a hash that's derived from its module import path, ensuring uniqueness,
# consistency and privacy.
#
# E.g. `path.to.my.secret.MyComponent` -> `MyComponent_ab01f32`
#
# The associations are defined as WeakValue map, so deleted components can be garbage
# collected and automatically deleted from the dict.
if sys.version_info < (3, 9):
comp_hash_mapping: WeakValueDictionary = WeakValueDictionary()
else:
comp_hash_mapping: WeakValueDictionary[str, Type["Component"]] = WeakValueDictionary()
# Generate keys like # Generate keys like
# `__components:MyButton_a78y37:js:df7c6d10` # `__components:MyButton_a78y37:js:df7c6d10`
# `__components:MyButton_a78y37:css` # `__components:MyButton_a78y37:css`
def _gen_cache_key( def _gen_cache_key(
comp_cls_hash: str, comp_cls_id: str,
script_type: ScriptType, script_type: ScriptType,
input_hash: Optional[str], input_hash: Optional[str],
) -> str: ) -> str:
if input_hash: if input_hash:
return f"__components:{comp_cls_hash}:{script_type}:{input_hash}" return f"__components:{comp_cls_id}:{script_type}:{input_hash}"
else: else:
return f"__components:{comp_cls_hash}:{script_type}" return f"__components:{comp_cls_id}:{script_type}"
def _is_script_in_cache( def _is_script_in_cache(
@ -97,7 +74,7 @@ def _is_script_in_cache(
script_type: ScriptType, script_type: ScriptType,
input_hash: Optional[str], input_hash: Optional[str],
) -> bool: ) -> bool:
cache_key = _gen_cache_key(comp_cls._class_hash, script_type, input_hash) cache_key = _gen_cache_key(comp_cls.class_id, script_type, input_hash)
cache = get_component_media_cache() cache = get_component_media_cache()
return cache.has_key(cache_key) return cache.has_key(cache_key)
@ -115,7 +92,7 @@ def _cache_script(
# E.g. `__components:MyButton:js:df7c6d10` # E.g. `__components:MyButton:js:df7c6d10`
if script_type in ("js", "css"): if script_type in ("js", "css"):
cache_key = _gen_cache_key(comp_cls._class_hash, script_type, input_hash) cache_key = _gen_cache_key(comp_cls.class_id, script_type, input_hash)
else: else:
raise ValueError(f"Unexpected script_type '{script_type}'") raise ValueError(f"Unexpected script_type '{script_type}'")
@ -333,7 +310,7 @@ def insert_component_dependencies_comment(
will be used by the ComponentDependencyMiddleware to collect all will be used by the ComponentDependencyMiddleware to collect all
declared JS / CSS scripts. declared JS / CSS scripts.
""" """
data = f"{component_cls._class_hash},{component_id},{js_input_hash or ''},{css_input_hash or ''}" data = f"{component_cls.class_id},{component_id},{js_input_hash or ''},{css_input_hash or ''}"
# NOTE: It's important that we put the comment BEFORE the content, so we can # NOTE: It's important that we put the comment BEFORE the content, so we can
# use the order of comments to evaluate components' instance JS code in the correct order. # use the order of comments to evaluate components' instance JS code in the correct order.
@ -366,12 +343,12 @@ COMPONENT_DEPS_COMMENT = "<!-- _RENDERED {data} -->"
# E.g. `<!-- _RENDERED table,123,a92ef298,bd002c3 -->` # E.g. `<!-- _RENDERED table,123,a92ef298,bd002c3 -->`
COMPONENT_COMMENT_REGEX = re.compile(rb"<!--\s+_RENDERED\s+(?P<data>[\w\-,/]+?)\s+-->") COMPONENT_COMMENT_REGEX = re.compile(rb"<!--\s+_RENDERED\s+(?P<data>[\w\-,/]+?)\s+-->")
# E.g. `table,123,a92ef298,bd002c3` # E.g. `table,123,a92ef298,bd002c3`
# - comp_cls_hash - Cache key of the component class that was rendered # - comp_cls_id - Cache key of the component class that was rendered
# - id - Component render ID # - id - Component render ID
# - js - Cache key for the JS data from `get_js_data()` # - js - Cache key for the JS data from `get_js_data()`
# - css - Cache key for the CSS data from `get_css_data()` # - css - Cache key for the CSS data from `get_css_data()`
SCRIPT_NAME_REGEX = re.compile( SCRIPT_NAME_REGEX = re.compile(
rb"^(?P<comp_cls_hash>[\w\-\./]+?),(?P<id>[\w]+?),(?P<js>[0-9a-f]*?),(?P<css>[0-9a-f]*?)$" rb"^(?P<comp_cls_id>[\w\-\./]+?),(?P<id>[\w]+?),(?P<js>[0-9a-f]*?),(?P<css>[0-9a-f]*?)$"
) )
# E.g. `data-djc-id-a1b2c3` # E.g. `data-djc-id-a1b2c3`
MAYBE_COMP_ID = r'(?: data-djc-id-\w{6}="")?' MAYBE_COMP_ID = r'(?: data-djc-id-\w{6}="")?'
@ -550,26 +527,26 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
if not part_match: if not part_match:
raise RuntimeError("Malformed dependencies data") raise RuntimeError("Malformed dependencies data")
comp_cls_hash: str = part_match.group("comp_cls_hash").decode("utf-8") comp_cls_id: str = part_match.group("comp_cls_id").decode("utf-8")
js_input_hash: Optional[str] = part_match.group("js").decode("utf-8") or None js_input_hash: Optional[str] = part_match.group("js").decode("utf-8") or None
css_input_hash: Optional[str] = part_match.group("css").decode("utf-8") or None css_input_hash: Optional[str] = part_match.group("css").decode("utf-8") or None
if comp_cls_hash in seen_comp_hashes: if comp_cls_id in seen_comp_hashes:
continue continue
comp_hashes.append(comp_cls_hash) comp_hashes.append(comp_cls_id)
seen_comp_hashes.add(comp_cls_hash) seen_comp_hashes.add(comp_cls_id)
# Schedule to load the `<script>` / `<link>` tags for the JS / CSS from `Component.js/css`. # Schedule to load the `<script>` / `<link>` tags for the JS / CSS from `Component.js/css`.
comp_data.append((comp_cls_hash, "js", None)) comp_data.append((comp_cls_id, "js", None))
comp_data.append((comp_cls_hash, "css", None)) comp_data.append((comp_cls_id, "css", None))
# Schedule to load the `<script>` / `<link>` tags for the JS / CSS variables. # Schedule to load the `<script>` / `<link>` tags for the JS / CSS variables.
# Skip if no variables are defined. # Skip if no variables are defined.
if js_input_hash is not None: if js_input_hash is not None:
inputs_data.append((comp_cls_hash, "js", js_input_hash)) inputs_data.append((comp_cls_id, "js", js_input_hash))
if css_input_hash is not None: if css_input_hash is not None:
inputs_data.append((comp_cls_hash, "css", css_input_hash)) inputs_data.append((comp_cls_id, "css", css_input_hash))
( (
to_load_input_js_urls, to_load_input_js_urls,
@ -589,15 +566,17 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
loaded_component_css_urls, loaded_component_css_urls,
) = _prepare_tags_and_urls(comp_data, type) ) = _prepare_tags_and_urls(comp_data, type)
def get_component_media(comp_cls_hash: str) -> Media: def get_component_media(comp_cls_id: str) -> Media:
comp_cls = comp_hash_mapping[comp_cls_hash] from django_components.component import get_component_by_class_id
comp_cls = get_component_by_class_id(comp_cls_id)
# NOTE: We instantiate the component classes so the `Media` are processed into `media` # NOTE: We instantiate the component classes so the `Media` are processed into `media`
comp = comp_cls() comp = comp_cls()
return comp.media return comp.media
all_medias = [ all_medias = [
# JS / CSS files from Component.Media.js/css. # JS / CSS files from Component.Media.js/css.
*[get_component_media(comp_cls_hash) for comp_cls_hash in comp_hashes], *[get_component_media(comp_cls_id) for comp_cls_id in comp_hashes],
# All the inlined scripts that we plan to fetch / load # All the inlined scripts that we plan to fetch / load
Media( Media(
js=[*to_load_component_js_urls, *to_load_input_js_urls], js=[*to_load_component_js_urls, *to_load_input_js_urls],
@ -747,6 +726,8 @@ def _prepare_tags_and_urls(
data: List[Tuple[str, ScriptType, Optional[str]]], data: List[Tuple[str, ScriptType, Optional[str]]],
type: RenderType, type: RenderType,
) -> Tuple[List[str], List[str], List[str], List[str], List[str], List[str]]: ) -> Tuple[List[str], List[str], List[str], List[str], List[str], List[str]]:
from django_components.component import get_component_by_class_id
to_load_js_urls: List[str] = [] to_load_js_urls: List[str] = []
to_load_css_urls: List[str] = [] to_load_css_urls: List[str] = []
inlined_js_tags: List[str] = [] inlined_js_tags: List[str] = []
@ -758,12 +739,12 @@ def _prepare_tags_and_urls(
# But even in that case we still need to call `Components.manager.markScriptLoaded`, # But even in that case we still need to call `Components.manager.markScriptLoaded`,
# so the client knows NOT to fetch them again. # so the client knows NOT to fetch them again.
# So in that case we populate both `inlined` and `loaded` lists # So in that case we populate both `inlined` and `loaded` lists
for comp_cls_hash, script_type, input_hash in data: for comp_cls_id, script_type, input_hash in data:
# NOTE: When CSS is scoped, then EVERY component instance will have different # NOTE: When CSS is scoped, then EVERY component instance will have different
# copy of the style, because each copy will have component's ID embedded. # copy of the style, because each copy will have component's ID embedded.
# So, in that case we inline the style into the HTML (See `_link_dependencies_with_component_html`), # So, in that case we inline the style into the HTML (See `_link_dependencies_with_component_html`),
# which means that we are NOT going to load / inline it again. # which means that we are NOT going to load / inline it again.
comp_cls = comp_hash_mapping[comp_cls_hash] comp_cls = get_component_by_class_id(comp_cls_id)
if type == "document": if type == "document":
# NOTE: Skip fetching of inlined JS/CSS if it's not defined or empty for given component # NOTE: Skip fetching of inlined JS/CSS if it's not defined or empty for given component
@ -805,7 +786,7 @@ def get_script_content(
input_hash: Optional[str], input_hash: Optional[str],
) -> Optional[str]: ) -> Optional[str]:
cache = get_component_media_cache() cache = get_component_media_cache()
cache_key = _gen_cache_key(comp_cls._class_hash, script_type, input_hash) cache_key = _gen_cache_key(comp_cls.class_id, script_type, input_hash)
script = cache.get(cache_key) script = cache.get(cache_key)
return script return script
@ -819,8 +800,7 @@ def get_script_tag(
content = get_script_content(script_type, comp_cls, input_hash) content = get_script_content(script_type, comp_cls, input_hash)
if content is None: if content is None:
raise RuntimeError( raise RuntimeError(
f"Could not find {script_type.upper()} for component '{comp_cls.__name__}' " f"Could not find {script_type.upper()} for component '{comp_cls.__name__}' (id: {comp_cls.class_id})"
f"(hash: {comp_cls._class_hash})"
) )
if script_type == "js": if script_type == "js":
@ -841,7 +821,7 @@ def get_script_url(
return reverse( return reverse(
CACHE_ENDPOINT_NAME, CACHE_ENDPOINT_NAME,
kwargs={ kwargs={
"comp_cls_hash": comp_cls._class_hash, "comp_cls_id": comp_cls.class_id,
"script_type": script_type, "script_type": script_type,
**({"input_hash": input_hash} if input_hash is not None else {}), **({"input_hash": input_hash} if input_hash is not None else {}),
}, },
@ -963,15 +943,18 @@ def _get_content_types(script_type: ScriptType) -> str:
def cached_script_view( def cached_script_view(
req: HttpRequest, req: HttpRequest,
comp_cls_hash: str, comp_cls_id: str,
script_type: ScriptType, script_type: ScriptType,
input_hash: Optional[str] = None, input_hash: Optional[str] = None,
) -> HttpResponse: ) -> HttpResponse:
from django_components.component import get_component_by_class_id
if req.method != "GET": if req.method != "GET":
return HttpResponseNotAllowed(["GET"]) return HttpResponseNotAllowed(["GET"])
comp_cls = comp_hash_mapping.get(comp_cls_hash) try:
if comp_cls is None: comp_cls = get_component_by_class_id(comp_cls_id)
except KeyError:
return HttpResponseNotFound() return HttpResponseNotFound()
script = get_script_content(script_type, comp_cls, input_hash) script = get_script_content(script_type, comp_cls, input_hash)
@ -983,9 +966,9 @@ def cached_script_view(
urlpatterns = [ urlpatterns = [
# E.g. `/components/cache/table.js` or `/components/cache/table.0ab2c3.js` # E.g. `/components/cache/MyTable_a1b2c3.js` or `/components/cache/MyTable_a1b2c3.0ab2c3.js`
path("cache/<str:comp_cls_hash>.<str:input_hash>.<str:script_type>", cached_script_view, name=CACHE_ENDPOINT_NAME), path("cache/<str:comp_cls_id>.<str:input_hash>.<str:script_type>", cached_script_view, name=CACHE_ENDPOINT_NAME),
path("cache/<str:comp_cls_hash>.<str:script_type>", cached_script_view, name=CACHE_ENDPOINT_NAME), path("cache/<str:comp_cls_id>.<str:script_type>", cached_script_view, name=CACHE_ENDPOINT_NAME),
] ]

View file

@ -24,7 +24,7 @@ else:
def _get_component_route_name(component: Union[Type["Component"], "Component"]) -> str: def _get_component_route_name(component: Union[Type["Component"], "Component"]) -> str:
return f"__component_url__{component._class_hash}" return f"__component_url__{component.class_id}"
def get_component_url(component: Union[Type["Component"], "Component"]) -> str: def get_component_url(component: Union[Type["Component"], "Component"]) -> str:
@ -162,7 +162,7 @@ class UrlExtension(ComponentExtension):
# Create a URL route like `components/MyTable_a1b2c3/` # Create a URL route like `components/MyTable_a1b2c3/`
# And since this is within the `url` extension, the full URL path will then be: # And since this is within the `url` extension, the full URL path will then be:
# `/components/ext/url/components/MyTable_a1b2c3/` # `/components/ext/url/components/MyTable_a1b2c3/`
route_path = f"components/{ctx.component_cls._class_hash}/" route_path = f"components/{ctx.component_cls.class_id}/"
route_name = _get_component_route_name(ctx.component_cls) route_name = _get_component_route_name(ctx.component_cls)
route = URLRoute( route = URLRoute(
path=route_path, path=route_path,

View file

@ -129,16 +129,16 @@ class TestComponentMediaCache:
TestMediaAndVarsComponent.render() TestMediaAndVarsComponent.render()
# Check that JS/CSS is cached for components that have them # Check that JS/CSS is cached for components that have them
assert test_cache.has_key(f"__components:{TestMediaAndVarsComponent._class_hash}:js") assert test_cache.has_key(f"__components:{TestMediaAndVarsComponent.class_id}:js")
assert test_cache.has_key(f"__components:{TestMediaAndVarsComponent._class_hash}:css") assert test_cache.has_key(f"__components:{TestMediaAndVarsComponent.class_id}:css")
assert test_cache.has_key(f"__components:{TestMediaNoVarsComponent._class_hash}:js") assert test_cache.has_key(f"__components:{TestMediaNoVarsComponent.class_id}:js")
assert test_cache.has_key(f"__components:{TestMediaNoVarsComponent._class_hash}:css") assert test_cache.has_key(f"__components:{TestMediaNoVarsComponent.class_id}:css")
assert not test_cache.has_key(f"__components:{TestSimpleComponent._class_hash}:js") assert not test_cache.has_key(f"__components:{TestSimpleComponent.class_id}:js")
assert not test_cache.has_key(f"__components:{TestSimpleComponent._class_hash}:css") assert not test_cache.has_key(f"__components:{TestSimpleComponent.class_id}:css")
# Check that we cache `Component.js` / `Component.css` # Check that we cache `Component.js` / `Component.css`
assert test_cache.get(f"__components:{TestMediaNoVarsComponent._class_hash}:js").strip() == "console.log('Hello from JS');" # noqa: E501 assert test_cache.get(f"__components:{TestMediaNoVarsComponent.class_id}:js").strip() == "console.log('Hello from JS');" # noqa: E501
assert test_cache.get(f"__components:{TestMediaNoVarsComponent._class_hash}:css").strip() == ".novars-component { color: blue; }" # noqa: E501 assert test_cache.get(f"__components:{TestMediaNoVarsComponent.class_id}:css").strip() == ".novars-component { color: blue; }" # noqa: E501
# Check that we cache JS / CSS scripts generated from `get_js_data` / `get_css_data` # Check that we cache JS / CSS scripts generated from `get_js_data` / `get_css_data`
# NOTE: The hashes is generated from the data. # NOTE: The hashes is generated from the data.
@ -146,5 +146,5 @@ class TestComponentMediaCache:
css_vars_hash = "d039a3" css_vars_hash = "d039a3"
# TODO - Update once JS and CSS vars are enabled # TODO - Update once JS and CSS vars are enabled
assert test_cache.get(f"__components:{TestMediaAndVarsComponent._class_hash}:js:{js_vars_hash}").strip() == "" assert test_cache.get(f"__components:{TestMediaAndVarsComponent.class_id}:js:{js_vars_hash}").strip() == ""
assert test_cache.get(f"__components:{TestMediaAndVarsComponent._class_hash}:css:{css_vars_hash}").strip() == "" # noqa: E501 assert test_cache.get(f"__components:{TestMediaAndVarsComponent.class_id}:css:{css_vars_hash}").strip() == "" # noqa: E501

View file

@ -16,7 +16,7 @@ from django.test import Client
from django.urls import path from django.urls import path
from pytest_django.asserts import assertHTMLEqual, assertInHTML from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import Component, ComponentView, all_components, register, types from django_components import Component, ComponentView, all_components, get_component_by_class_id, register, types
from django_components.slots import SlotRef from django_components.slots import SlotRef
from django_components.urls import urlpatterns as dc_urlpatterns from django_components.urls import urlpatterns as dc_urlpatterns
@ -356,6 +356,12 @@ class TestComponent:
): ):
Root.render() Root.render()
def test_get_component_by_id(self):
class SimpleComponent(Component):
pass
assert get_component_by_class_id(SimpleComponent.class_id) == SimpleComponent
@djc_test @djc_test
class TestComponentRender: class TestComponentRender:

View file

@ -41,7 +41,7 @@ class TestComponentUrl:
# Check if the URL is correctly generated # Check if the URL is correctly generated
component_url = get_component_url(TestComponent) component_url = get_component_url(TestComponent)
assert component_url == f"/components/ext/url/components/{TestComponent._class_hash}/" assert component_url == f"/components/ext/url/components/{TestComponent.class_id}/"
client = Client() client = Client()
response = client.get(component_url) response = client.get(component_url)
@ -79,7 +79,7 @@ class TestComponentUrl:
get_component_url(TestComponent) get_component_url(TestComponent)
# Even calling the URL directly should raise an error # Even calling the URL directly should raise an error
component_url = f"/components/ext/url/components/{TestComponent._class_hash}/" component_url = f"/components/ext/url/components/{TestComponent.class_id}/"
client = Client() client = Client()
response = client.get(component_url) response = client.get(component_url)