Skip to content

Commit 398e1e6

Browse files
authored
use deterministic hash for stateful component names (#5525)
* use deterministic hash for stateful component names * heh * dang it darglint * handle lists and None * hash enum with str
1 parent 898c446 commit 398e1e6

1 file changed

Lines changed: 53 additions & 1 deletion

File tree

reflex/components/component.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import contextlib
66
import copy
77
import dataclasses
8+
import enum
89
import functools
910
import inspect
1011
import typing
@@ -479,6 +480,57 @@ def _components_from(
479480
return ()
480481

481482

483+
def _deterministic_hash(value: object) -> int:
484+
"""Hash a rendered dictionary.
485+
486+
Args:
487+
value: The dictionary to hash.
488+
489+
Returns:
490+
The hash of the dictionary.
491+
492+
Raises:
493+
TypeError: If the value is not hashable.
494+
"""
495+
if isinstance(value, BaseComponent):
496+
# If the value is a component, hash its rendered code.
497+
rendered_code = value.render()
498+
return _deterministic_hash(rendered_code)
499+
if isinstance(value, Var):
500+
return _deterministic_hash((value._js_expr, value._get_all_var_data()))
501+
if isinstance(value, VarData):
502+
return _deterministic_hash(dataclasses.asdict(value))
503+
if isinstance(value, dict):
504+
# Sort the dictionary to ensure consistent hashing.
505+
return _deterministic_hash(
506+
tuple(sorted((k, _deterministic_hash(v)) for k, v in value.items()))
507+
)
508+
if isinstance(value, int):
509+
# Hash numbers and booleans directly.
510+
return int(value)
511+
if isinstance(value, float):
512+
return _deterministic_hash(str(value))
513+
if isinstance(value, str):
514+
return int(md5(f'"{value}"'.encode()).hexdigest(), 16)
515+
if isinstance(value, (tuple, list)):
516+
# Hash tuples by hashing each element.
517+
return _deterministic_hash(
518+
"[" + ",".join(map(str, map(_deterministic_hash, value))) + "]"
519+
)
520+
if isinstance(value, enum.Enum):
521+
# Hash enums by their name.
522+
return _deterministic_hash(str(value))
523+
if value is None:
524+
# Hash None as a special case.
525+
return _deterministic_hash("None")
526+
527+
msg = (
528+
f"Cannot hash value `{value}` of type `{type(value).__name__}`. "
529+
"Only BaseComponent, Var, VarData, dict, str, tuple, and enum.Enum are supported."
530+
)
531+
raise TypeError(msg)
532+
533+
482534
DEFAULT_TRIGGERS: Mapping[str, types.ArgsSpec | Sequence[types.ArgsSpec]] = {
483535
EventTriggers.ON_FOCUS: no_args_event_spec,
484536
EventTriggers.ON_BLUR: no_args_event_spec,
@@ -2430,7 +2482,7 @@ def _get_tag_name(cls, component: Component) -> str | None:
24302482
return None
24312483

24322484
# Compute the hash based on the rendered code.
2433-
code_hash = md5(str(rendered_code).encode("utf-8")).hexdigest()
2485+
code_hash = _deterministic_hash(rendered_code)
24342486

24352487
# Format the tag name including the hash.
24362488
return format.format_state_name(

0 commit comments

Comments
 (0)