fix: implement safe retrieval of component classes from registry (#934)

This commit is contained in:
Oliver Haas 2025-02-01 11:08:35 +01:00 committed by GitHub
parent bafa9f7cc5
commit f01ee1fe8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 24 additions and 30 deletions

View file

@ -1,4 +1,3 @@
import inspect
import types
from collections import deque
from contextlib import contextmanager
@ -52,6 +51,7 @@ from django_components.dependencies import (
cache_component_css_vars,
cache_component_js,
cache_component_js_vars,
comp_hash_mapping,
postprocess_component_html,
set_component_attrs_for_js_and_css,
)
@ -72,7 +72,7 @@ from django_components.slots import (
)
from django_components.template import cached_template
from django_components.util.django_monkeypatch import is_template_cls_patched
from django_components.util.misc import gen_id
from django_components.util.misc import gen_id, hash_comp_cls
from django_components.util.template_tag import TagAttr
from django_components.util.validation import validate_typed_dict, validate_typed_tuple
@ -499,7 +499,7 @@ class Component(
However, there's a few differences from Django's Media class:
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list,
or (CSS-only) a dictonary (See [`ComponentMediaInput`](../api#django_components.ComponentMediaInput)).
or (CSS-only) a dictionary (See [`ComponentMediaInput`](../api#django_components.ComponentMediaInput)).
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
[`SafeString`](https://dev.to/doridoro/django-safestring-afj), or a function
(See [`ComponentMediaInputPath`](../api#django_components.ComponentMediaInputPath)).
@ -556,7 +556,7 @@ class Component(
# MISC
# #####################################
_class_hash: ClassVar[int]
_class_hash: ClassVar[str]
def __init__(
self,
@ -587,7 +587,8 @@ class Component(
self._types: Optional[Union[Tuple[Any, Any, Any, Any, Any, Any], Literal[False]]] = None
def __init_subclass__(cls, **kwargs: Any) -> None:
cls._class_hash = hash(inspect.getfile(cls) + cls.__name__)
cls._class_hash = hash_comp_cls(cls)
comp_hash_mapping[cls._class_hash] = cls
@property
def name(self) -> str:
@ -627,7 +628,7 @@ class Component(
@property
def input(self) -> RenderInput[ArgsType, KwargsType, SlotsType]:
"""
Input holds the data (like arg, kwargs, slots) that were passsed to
Input holds the data (like arg, kwargs, slots) that were passed to
the current execution of the `render` method.
"""
if not len(self._render_stack):

View file

@ -5,7 +5,6 @@ import json
import re
import sys
from abc import ABC, abstractmethod
from functools import lru_cache
from hashlib import md5
from typing import (
TYPE_CHECKING,
@ -36,7 +35,7 @@ from django.utils.safestring import SafeString, mark_safe
from djc_core_html_parser import set_html_attributes
from django_components.node import BaseNode
from django_components.util.misc import get_import_path, is_nonempty_str
from django_components.util.misc import is_nonempty_str
if TYPE_CHECKING:
from django_components.component import Component
@ -103,13 +102,6 @@ else:
# Convert Component class to something like `TableComp_a91d03`
@lru_cache(None)
def _hash_comp_cls(comp_cls: Type["Component"]) -> str:
full_name = get_import_path(comp_cls)
comp_cls_hash = md5(full_name.encode()).hexdigest()[0:6]
return comp_cls.__name__ + "_" + comp_cls_hash
def _gen_cache_key(
comp_cls_hash: str,
script_type: ScriptType,
@ -126,8 +118,7 @@ def _is_script_in_cache(
script_type: ScriptType,
input_hash: Optional[str],
) -> bool:
comp_cls_hash = _hash_comp_cls(comp_cls)
cache_key = _gen_cache_key(comp_cls_hash, script_type, input_hash)
cache_key = _gen_cache_key(comp_cls._class_hash, script_type, input_hash)
return comp_media_cache.has(cache_key)
@ -141,11 +132,10 @@ def _cache_script(
Given a component and it's inlined JS or CSS, store the JS/CSS in a cache,
so it can be retrieved via URL endpoint.
"""
comp_cls_hash = _hash_comp_cls(comp_cls)
# E.g. `__components:MyButton:js:df7c6d10`
if script_type in ("js", "css"):
cache_key = _gen_cache_key(comp_cls_hash, script_type, input_hash)
cache_key = _gen_cache_key(comp_cls._class_hash, script_type, input_hash)
else:
raise ValueError(f"Unexpected script_type '{script_type}'")
@ -345,11 +335,7 @@ def _insert_component_comment(
will be used by the ComponentDependencyMiddleware to collect all
declared JS / CSS scripts.
"""
# Add components to the cache
comp_cls_hash = _hash_comp_cls(component_cls)
comp_hash_mapping[comp_cls_hash] = component_cls
data = f"{comp_cls_hash},{component_id},{js_input_hash or ''},{css_input_hash or ''}"
data = f"{component_cls._class_hash},{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
# use the order of comments to evaluate components' instance JS code in the correct order.
@ -868,8 +854,7 @@ def get_script_content(
comp_cls: Type["Component"],
input_hash: Optional[str],
) -> SafeString:
comp_cls_hash = _hash_comp_cls(comp_cls)
cache_key = _gen_cache_key(comp_cls_hash, script_type, input_hash)
cache_key = _gen_cache_key(comp_cls._class_hash, script_type, input_hash)
script = comp_media_cache.get(cache_key)
return script
@ -897,12 +882,10 @@ def get_script_url(
comp_cls: Type["Component"],
input_hash: Optional[str],
) -> str:
comp_cls_hash = _hash_comp_cls(comp_cls)
return reverse(
CACHE_ENDPOINT_NAME,
kwargs={
"comp_cls_hash": comp_cls_hash,
"comp_cls_hash": comp_cls._class_hash,
"script_type": script_type,
**({"input_hash": input_hash} if input_hash is not None else {}),
},

View file

@ -1,8 +1,12 @@
import re
from typing import Any, Callable, List, Optional, Type, TypeVar
from hashlib import md5
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Type, TypeVar
from django_components.util.nanoid import generate
if TYPE_CHECKING:
from django_components.component import Component
T = TypeVar("T")
@ -71,3 +75,9 @@ def get_last_index(lst: List, key: Callable[[Any], bool]) -> Optional[int]:
def is_nonempty_str(txt: Optional[str]) -> bool:
return txt is not None and bool(txt.strip())
def hash_comp_cls(comp_cls: Type["Component"]) -> str:
full_name = get_import_path(comp_cls)
comp_cls_hash = md5(full_name.encode()).hexdigest()[0:6]
return comp_cls.__name__ + "_" + comp_cls_hash