|
5 | 5 | import contextlib |
6 | 6 | import copy |
7 | 7 | import dataclasses |
| 8 | +import enum |
8 | 9 | import functools |
9 | 10 | import inspect |
10 | 11 | import typing |
@@ -479,6 +480,57 @@ def _components_from( |
479 | 480 | return () |
480 | 481 |
|
481 | 482 |
|
| 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 | + |
482 | 534 | DEFAULT_TRIGGERS: Mapping[str, types.ArgsSpec | Sequence[types.ArgsSpec]] = { |
483 | 535 | EventTriggers.ON_FOCUS: no_args_event_spec, |
484 | 536 | EventTriggers.ON_BLUR: no_args_event_spec, |
@@ -2430,7 +2482,7 @@ def _get_tag_name(cls, component: Component) -> str | None: |
2430 | 2482 | return None |
2431 | 2483 |
|
2432 | 2484 | # 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) |
2434 | 2486 |
|
2435 | 2487 | # Format the tag name including the hash. |
2436 | 2488 | return format.format_state_name( |
|
0 commit comments