Skip to content

Commit 050727a

Browse files
dry it up
1 parent 6cdf383 commit 050727a

4 files changed

Lines changed: 207 additions & 201 deletions

File tree

packages/reflex-base/src/reflex_base/registry.py

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from __future__ import annotations
1010

1111
import dataclasses
12-
from typing import TYPE_CHECKING, Protocol, runtime_checkable
12+
from collections.abc import Callable, Iterable
13+
from typing import TYPE_CHECKING, Protocol, TypeVar, runtime_checkable
1314

1415
from typing_extensions import Self
1516

@@ -21,6 +22,37 @@
2122
from reflex_base.components.component import StatefulComponent
2223
from reflex_base.event import EventHandler
2324

25+
_T = TypeVar("_T")
26+
27+
28+
def _rekey(
29+
items: Iterable[_T], key_fn: Callable[[_T], str], kind: str
30+
) -> dict[str, _T]:
31+
"""Build a name-keyed dict, warning on collisions.
32+
33+
Args:
34+
items: Source items to re-key.
35+
key_fn: Computes the new key for each item.
36+
kind: Human-readable noun for the collision warning (e.g. ``"state class"``).
37+
38+
Returns:
39+
A new dict mapping resolved key to item; later items overwrite earlier.
40+
"""
41+
from reflex.utils import console
42+
43+
out: dict[str, _T] = {}
44+
for item in items:
45+
key = key_fn(item)
46+
existing = out.get(key)
47+
if existing is not None and existing is not item:
48+
console.warn(
49+
f"Two {kind}s resolve to the same full name {key!r}: "
50+
f"{existing!r} and {item!r}. The first one will be unreachable "
51+
"in the registry. Check minify.json for duplicate ids."
52+
)
53+
out[key] = item
54+
return out
55+
2456

2557
@runtime_checkable
2658
class NameResolver(Protocol):
@@ -287,39 +319,21 @@ def refresh_keys(self) -> None:
287319
intact. A console warning is emitted on full-name collisions —
288320
usually a sign of duplicate ids in ``minify.json``.
289321
"""
290-
from reflex.utils import console
291322
from reflex.utils.format import format_event_handler
292323

293-
all_classes = list(self.base_states.values())
294-
new_base_states: dict[str, type[BaseState]] = {}
324+
new_base_states = _rekey(
325+
self.base_states.values(), lambda c: c.get_full_name(), "state class"
326+
)
295327
new_substates: dict[str, set[type[BaseState]]] = {}
296-
for cls in all_classes:
297-
full_name = cls.get_full_name()
298-
existing = new_base_states.get(full_name)
299-
if existing is not None and existing is not cls:
300-
console.warn(
301-
f"Two state classes resolve to the same full name "
302-
f"{full_name!r}: {existing!r} and {cls!r}. The first one "
303-
"will be unreachable in the registry. Check minify.json "
304-
"for duplicate ids."
305-
)
306-
new_base_states[full_name] = cls
328+
for cls in new_base_states.values():
307329
parent = cls.get_parent_state()
308330
if parent is not None:
309331
new_substates.setdefault(parent.get_full_name(), set()).add(cls)
310-
311-
all_handlers = list(self.event_handlers.values())
312-
new_handlers: dict[str, RegisteredEventHandler] = {}
313-
for reg in all_handlers:
314-
full_name = format_event_handler(reg.handler)
315-
existing_handler = new_handlers.get(full_name)
316-
if existing_handler is not None and existing_handler is not reg:
317-
console.warn(
318-
f"Two event handlers resolve to the same full name "
319-
f"{full_name!r}. The first one will be unreachable in "
320-
"the registry. Check minify.json for duplicate ids."
321-
)
322-
new_handlers[full_name] = reg
332+
new_handlers = _rekey(
333+
self.event_handlers.values(),
334+
lambda r: format_event_handler(r.handler),
335+
"event handler",
336+
)
323337

324338
self.base_states.clear()
325339
self.base_states.update(new_base_states)

reflex/minify.py

Lines changed: 104 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import dataclasses
1313
import functools
1414
import json
15+
from collections.abc import Iterable
1516
from pathlib import Path
1617
from typing import TYPE_CHECKING, TypedDict
1718

@@ -122,67 +123,38 @@ def is_minify_enabled() -> bool:
122123

123124

124125
@functools.cache
125-
def is_state_minify_enabled() -> bool:
126-
"""Whether state-id minification is enabled.
127-
128-
Returns:
129-
``True`` if ``REFLEX_MINIFY_STATES=enabled`` and ``minify.json`` exists.
130-
"""
131-
from reflex.environment import MinifyMode, environment
132-
133-
return (
134-
environment.REFLEX_MINIFY_STATES.get() == MinifyMode.ENABLED
135-
and get_minify_config() is not None
136-
)
137-
126+
def _is_mode_enabled(env_var_name: str) -> bool:
127+
"""Whether the given ``REFLEX_MINIFY_*`` env var is on and a config exists.
138128
139-
@functools.cache
140-
def is_event_minify_enabled() -> bool:
141-
"""Whether event-id minification is enabled.
129+
Args:
130+
env_var_name: The env-var attribute name on
131+
:class:`~reflex.environment.EnvironmentVariables`.
142132
143133
Returns:
144-
``True`` if ``REFLEX_MINIFY_EVENTS=enabled`` and ``minify.json`` exists.
134+
``True`` if the env var is ``ENABLED`` and ``minify.json`` exists.
145135
"""
146136
from reflex.environment import MinifyMode, environment
147137

148-
return (
149-
environment.REFLEX_MINIFY_EVENTS.get() == MinifyMode.ENABLED
150-
and get_minify_config() is not None
151-
)
138+
env_var = getattr(environment, env_var_name)
139+
return env_var.get() == MinifyMode.ENABLED and get_minify_config() is not None
152140

153141

154-
def get_state_id(state_full_path: str) -> str | None:
155-
"""Look up the minified id for a state path.
156-
157-
Args:
158-
state_full_path: e.g. ``"myapp.state.AppState.UserState"``.
142+
def is_state_minify_enabled() -> bool:
143+
"""Whether state-id minification is enabled.
159144
160145
Returns:
161-
The minified id, or ``None`` if not configured.
146+
``True`` if ``REFLEX_MINIFY_STATES=enabled`` and ``minify.json`` exists.
162147
"""
163-
config = get_minify_config()
164-
if config is None:
165-
return None
166-
return config["states"].get(state_full_path)
148+
return _is_mode_enabled("REFLEX_MINIFY_STATES")
167149

168150

169-
def get_event_id(state_full_path: str, handler_name: str) -> str | None:
170-
"""Look up the minified id for an event handler.
171-
172-
Args:
173-
state_full_path: The full path to the state.
174-
handler_name: The handler's original name.
151+
def is_event_minify_enabled() -> bool:
152+
"""Whether event-id minification is enabled.
175153
176154
Returns:
177-
The minified id, or ``None`` if not configured.
155+
``True`` if ``REFLEX_MINIFY_EVENTS=enabled`` and ``minify.json`` exists.
178156
"""
179-
config = get_minify_config()
180-
if config is None:
181-
return None
182-
state_events = config["events"].get(state_full_path)
183-
if state_events is None:
184-
return None
185-
return state_events.get(handler_name)
157+
return _is_mode_enabled("REFLEX_MINIFY_EVENTS")
186158

187159

188160
def save_minify_config(config: MinifyConfig) -> None:
@@ -245,8 +217,22 @@ def from_disk(cls) -> MinifyNameResolver:
245217
events_enabled=environment.REFLEX_MINIFY_EVENTS.get() == MinifyMode.ENABLED,
246218
)
247219

220+
def _is_minify_allowed(self, state_cls: type[BaseState], enabled: bool) -> bool:
221+
"""Whether minification applies to ``state_cls`` for the given mode.
222+
223+
Args:
224+
state_cls: The state class being resolved.
225+
enabled: Whether the relevant ``REFLEX_MINIFY_*`` env var is on.
226+
227+
Returns:
228+
``True`` when the env var is on and ``state_cls`` is user-defined.
229+
"""
230+
return enabled and not _is_framework_state(state_cls)
231+
248232
def resolve_state_name(self, state_cls: type[BaseState]) -> str | None: # noqa: D102
249-
if not (self.states_enabled and self.config) or _is_framework_state(state_cls):
233+
if self.config is None or not self._is_minify_allowed(
234+
state_cls, self.states_enabled
235+
):
250236
return None
251237
cached = self._state_cache.get(state_cls)
252238
if cached is not None:
@@ -259,7 +245,9 @@ def resolve_state_name(self, state_cls: type[BaseState]) -> str | None: # noqa:
259245
def resolve_handler_name( # noqa: D102
260246
self, state_cls: type[BaseState], handler_name: str
261247
) -> str | None:
262-
if not (self.events_enabled and self.config) or _is_framework_state(state_cls):
248+
if self.config is None or not self._is_minify_allowed(
249+
state_cls, self.events_enabled
250+
):
263251
return None
264252
per_state = self._event_cache.get(state_cls)
265253
if per_state is None:
@@ -329,8 +317,7 @@ def clear_config_cache() -> None:
329317
``REFLEX_MINIFY_*`` env vars at runtime.
330318
"""
331319
get_minify_config.cache_clear()
332-
is_state_minify_enabled.cache_clear()
333-
is_event_minify_enabled.cache_clear()
320+
_is_mode_enabled.cache_clear()
334321
install_minify_resolver()
335322

336323

@@ -484,6 +471,51 @@ def generate_minify_config(
484471
)
485472

486473

474+
def _find_duplicate_ids(items: Iterable[tuple[str, str]]) -> dict[str, list[str]]:
475+
"""Group ``(label, minified_id)`` pairs by id, keeping only collisions.
476+
477+
Args:
478+
items: Pairs of ``(label, minified_id)``.
479+
480+
Returns:
481+
Mapping from minified id to the labels sharing it (always ``len >= 2``).
482+
"""
483+
by_id: dict[str, list[str]] = {}
484+
for label, mid in items:
485+
by_id.setdefault(mid, []).append(label)
486+
return {mid: labels for mid, labels in by_id.items() if len(labels) > 1}
487+
488+
489+
def _assign_next_ids(
490+
new_keys: Iterable[str],
491+
existing_ids: set[int],
492+
reassign_deleted: bool,
493+
) -> dict[str, str]:
494+
"""Assign minified ids to ``new_keys`` while skipping ``existing_ids``.
495+
496+
Mutates ``existing_ids`` so callers can chain assignments against the same
497+
pool. Keys are sorted for deterministic output.
498+
499+
Args:
500+
new_keys: Keys needing new ids.
501+
existing_ids: Already-used integer ids in the same scope.
502+
reassign_deleted: When ``True``, scan from 0 (filling gaps);
503+
otherwise start past the current max.
504+
505+
Returns:
506+
Mapping from key to its newly-assigned minified id.
507+
"""
508+
next_id = 0 if reassign_deleted else max(existing_ids, default=-1) + 1
509+
out: dict[str, str] = {}
510+
for key in sorted(new_keys):
511+
while next_id in existing_ids:
512+
next_id += 1
513+
out[key] = int_to_minified_name(next_id)
514+
existing_ids.add(next_id)
515+
next_id += 1
516+
return out
517+
518+
487519
def validate_minify_config(
488520
config: MinifyConfig,
489521
root_state: type[BaseState] | None = None,
@@ -506,40 +538,27 @@ def validate_minify_config(
506538

507539
all_states = collect_all_states(root_state)
508540

509-
# Check for duplicate state IDs among siblings.
510-
# Group by actual parent class (not string-split path) since children of
511-
# the same parent can be defined in different modules.
541+
# Group sibling states by their actual parent class (not by string-split
542+
# path) since children of the same parent can live in different modules.
512543
path_to_cls = {get_state_full_path(s): s for s in all_states}
513-
parent_cls_to_state_ids: dict[type[BaseState] | None, dict[str, list[str]]] = {}
544+
parent_to_pairs: dict[type[BaseState] | None, list[tuple[str, str]]] = {}
514545
for state_path, minified_name in config["states"].items():
515546
state_cls = path_to_cls.get(state_path)
516-
parent_cls = state_cls.get_parent_state() if state_cls else None
517-
parent_cls_to_state_ids.setdefault(parent_cls, {}).setdefault(
518-
minified_name, []
519-
).append(state_path)
520-
521-
for parent_cls, id_to_states in parent_cls_to_state_ids.items():
522-
for minified_name, state_paths in id_to_states.items():
523-
if len(state_paths) > 1:
524-
parent_name = parent_cls.__name__ if parent_cls else "root"
525-
errors.append(
526-
f"Duplicate state_id='{minified_name}' under '{parent_name}': "
527-
f"{state_paths}"
528-
)
529-
530-
# Check for duplicate event IDs within same state
547+
parent = state_cls.get_parent_state() if state_cls else None
548+
parent_to_pairs.setdefault(parent, []).append((state_path, minified_name))
549+
550+
for parent, pairs in parent_to_pairs.items():
551+
parent_name = parent.__name__ if parent else "root"
552+
errors.extend(
553+
f"Duplicate state_id='{mid}' under '{parent_name}': {paths}"
554+
for mid, paths in _find_duplicate_ids(pairs).items()
555+
)
556+
531557
for state_path, state_events in config["events"].items():
532-
id_to_handlers: dict[str, list[str]] = {}
533-
for handler_name, minified_name in state_events.items():
534-
if minified_name not in id_to_handlers:
535-
id_to_handlers[minified_name] = []
536-
id_to_handlers[minified_name].append(handler_name)
537-
538-
for minified_name, handler_names in id_to_handlers.items():
539-
if len(handler_names) > 1:
540-
errors.append(
541-
f"Duplicate event_id='{minified_name}' in '{state_path}': {handler_names}"
542-
)
558+
errors.extend(
559+
f"Duplicate event_id='{mid}' in '{state_path}': {handlers}"
560+
for mid, handlers in _find_duplicate_ids(state_events.items()).items()
561+
)
543562

544563
# Check for missing states (in code but not in config)
545564
code_state_paths = {get_state_full_path(s) for s in all_states}
@@ -652,39 +671,21 @@ def sync_minify_config(
652671
parent = state_cls.get_parent_state()
653672
parent_cls_to_new_children.setdefault(parent, []).append(state_path)
654673

655-
# Assign new state IDs (unique among siblings of the same parent class)
674+
# Assign new state IDs (unique among siblings of the same parent class).
656675
for parent_cls, children in parent_cls_to_new_children.items():
657676
existing_ids = parent_cls_to_existing_ids.get(parent_cls, set()).copy()
677+
new_states.update(_assign_next_ids(children, existing_ids, reassign_deleted))
658678

659-
# Assign IDs starting from max + 1 (or 0 if reassign_deleted and gaps exist)
660-
next_id = 0 if reassign_deleted else (max(existing_ids, default=-1) + 1)
661-
662-
for state_path in sorted(children):
663-
while next_id in existing_ids:
664-
next_id += 1
665-
new_states[state_path] = int_to_minified_name(next_id)
666-
existing_ids.add(next_id)
667-
next_id += 1
668-
669-
# Find events that need IDs assigned
679+
# Assign new event IDs (unique within each state).
670680
for state_cls in all_states:
671681
state_path = get_state_full_path(state_cls)
672682
state_events = new_events.get(state_path, {})
673683
new_handlers = [h for h in state_cls.event_handlers if h not in state_events]
674-
675684
if new_handlers:
676-
# Get existing IDs for this state's events
677685
existing_ids = {minified_name_to_int(eid) for eid in state_events.values()}
678-
679-
next_id = 0 if reassign_deleted else (max(existing_ids, default=-1) + 1)
680-
681-
for handler_name in sorted(new_handlers):
682-
while next_id in existing_ids:
683-
next_id += 1
684-
state_events[handler_name] = int_to_minified_name(next_id)
685-
existing_ids.add(next_id)
686-
next_id += 1
687-
686+
state_events.update(
687+
_assign_next_ids(new_handlers, existing_ids, reassign_deleted)
688+
)
688689
new_events[state_path] = state_events
689690

690691
return MinifyConfig(

0 commit comments

Comments
 (0)