diff --git a/src/django_components/component.py b/src/django_components/component.py index c02b1e7c..6cd60d73 100644 --- a/src/django_components/component.py +++ b/src/django_components/component.py @@ -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): diff --git a/src/django_components/dependencies.py b/src/django_components/dependencies.py index d711192f..bd930935 100644 --- a/src/django_components/dependencies.py +++ b/src/django_components/dependencies.py @@ -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 {}), }, diff --git a/src/django_components/util/misc.py b/src/django_components/util/misc.py index 63faa771..5a12b135 100644 --- a/src/django_components/util/misc.py +++ b/src/django_components/util/misc.py @@ -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