Skip to content

Commit e13073b

Browse files
fix: prevent parent-child minified name collision in substate resolution
When a parent and child state have the same minified name, substate resolution can fail because the leading segment is stripped incorrectly. This change adds a flag to skip stripping only on the initial recursive call, ensuring correct resolution even in name collision scenarios.
1 parent ae477ae commit e13073b

2 files changed

Lines changed: 117 additions & 6 deletions

File tree

reflex/state.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,11 +1068,17 @@ def get_full_name(cls) -> str:
10681068

10691069
@classmethod
10701070
@functools.lru_cache
1071-
def get_class_substate(cls, path: Sequence[str] | str) -> type[BaseState]:
1071+
def get_class_substate(
1072+
cls, path: Sequence[str] | str, _skip_self: bool = True
1073+
) -> type[BaseState]:
10721074
"""Get the class substate.
10731075
10741076
Args:
10751077
path: The path to the substate.
1078+
_skip_self: If True, strip the leading segment when it matches this
1079+
state's name. Only the initial (root) call should use True;
1080+
recursive calls pass False so that a child whose minified name
1081+
collides with its parent is resolved correctly.
10761082
10771083
Returns:
10781084
The class substate.
@@ -1085,13 +1091,13 @@ def get_class_substate(cls, path: Sequence[str] | str) -> type[BaseState]:
10851091

10861092
if len(path) == 0:
10871093
return cls
1088-
if path[0] == cls.get_name():
1094+
if _skip_self and path[0] == cls.get_name():
10891095
if len(path) == 1:
10901096
return cls
10911097
path = path[1:]
10921098
for substate in cls.get_substates():
10931099
if path[0] == substate.get_name():
1094-
return substate.get_class_substate(path[1:])
1100+
return substate.get_class_substate(path[1:], _skip_self=False)
10951101
msg = f"Invalid path: {path}"
10961102
raise ValueError(msg)
10971103

@@ -1605,11 +1611,15 @@ def _reset_client_storage(self):
16051611
for substate in self.substates.values():
16061612
substate._reset_client_storage()
16071613

1608-
def get_substate(self, path: Sequence[str]) -> BaseState:
1614+
def get_substate(self, path: Sequence[str], _skip_self: bool = True) -> BaseState:
16091615
"""Get the substate.
16101616
16111617
Args:
16121618
path: The path to the substate.
1619+
_skip_self: If True, strip the leading segment when it matches this
1620+
state's name. Only the initial (root) call should use True;
1621+
recursive calls pass False so that a child whose minified name
1622+
collides with its parent is resolved correctly.
16131623
16141624
Returns:
16151625
The substate.
@@ -1619,14 +1629,14 @@ def get_substate(self, path: Sequence[str]) -> BaseState:
16191629
"""
16201630
if len(path) == 0:
16211631
return self
1622-
if path[0] == self.get_name():
1632+
if _skip_self and path[0] == self.get_name():
16231633
if len(path) == 1:
16241634
return self
16251635
path = path[1:]
16261636
if path[0] not in self.substates:
16271637
msg = f"Invalid path: {path}"
16281638
raise ValueError(msg)
1629-
return self.substates[path[0]].get_substate(path[1:])
1639+
return self.substates[path[0]].get_substate(path[1:], _skip_self=False)
16301640

16311641
@classmethod
16321642
def _get_potentially_dirty_states(cls) -> set[type[BaseState]]:

tests/units/test_minification.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,3 +731,104 @@ def test_is_minify_enabled_false_when_both_disabled(self, temp_minify_json):
731731
clear_config_cache()
732732

733733
assert is_minify_enabled() is False
734+
735+
736+
class TestMinifiedNameCollision:
737+
"""Tests for parent-child minified name collision in substate resolution."""
738+
739+
def test_get_class_substate_with_parent_child_name_collision(
740+
self, temp_minify_json, monkeypatch
741+
):
742+
"""Test that get_class_substate resolves correctly when parent and child
743+
share the same minified name (IDs are only sibling-unique).
744+
"""
745+
monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled")
746+
747+
# Build a hierarchy: State -> ParentState -> ChildState
748+
# where ParentState and ChildState both get minified name "b"
749+
750+
class ParentState(State):
751+
pass
752+
753+
class ChildState(ParentState):
754+
pass
755+
756+
parent_path = get_state_full_path(ParentState)
757+
child_path = get_state_full_path(ChildState)
758+
759+
config: MinifyConfig = {
760+
"version": SCHEMA_VERSION,
761+
"states": {
762+
"reflex.state.State": "a",
763+
parent_path: "b",
764+
child_path: "b", # Same minified name as parent
765+
},
766+
"events": {},
767+
}
768+
save_minify_config(config)
769+
clear_config_cache()
770+
State.get_name.cache_clear()
771+
State.get_full_name.cache_clear()
772+
State.get_class_substate.cache_clear()
773+
ParentState.get_name.cache_clear()
774+
ParentState.get_full_name.cache_clear()
775+
ChildState.get_name.cache_clear()
776+
ChildState.get_full_name.cache_clear()
777+
778+
# Verify both get the same minified name
779+
assert ParentState.get_name() == "b"
780+
assert ChildState.get_name() == "b"
781+
782+
# Full path should be a.b.b
783+
assert ChildState.get_full_name() == "a.b.b"
784+
785+
# get_class_substate should resolve a.b.b to ChildState, not ParentState
786+
resolved = State.get_class_substate("a.b.b")
787+
assert resolved is ChildState
788+
789+
def test_get_substate_with_parent_child_name_collision(
790+
self, temp_minify_json, monkeypatch
791+
):
792+
"""Test that get_substate (instance method) resolves correctly when parent
793+
and child share the same minified name.
794+
"""
795+
import reflex as rx
796+
797+
monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled")
798+
799+
class ParentState2(State):
800+
pass
801+
802+
class ChildState2(ParentState2):
803+
@rx.event
804+
def my_handler(self):
805+
pass
806+
807+
parent_path = get_state_full_path(ParentState2)
808+
child_path = get_state_full_path(ChildState2)
809+
810+
config: MinifyConfig = {
811+
"version": SCHEMA_VERSION,
812+
"states": {
813+
"reflex.state.State": "a",
814+
parent_path: "b",
815+
child_path: "b", # Same minified name as parent
816+
},
817+
"events": {},
818+
}
819+
save_minify_config(config)
820+
clear_config_cache()
821+
State.get_name.cache_clear()
822+
State.get_full_name.cache_clear()
823+
State.get_class_substate.cache_clear()
824+
ParentState2.get_name.cache_clear()
825+
ParentState2.get_full_name.cache_clear()
826+
ChildState2.get_name.cache_clear()
827+
ChildState2.get_full_name.cache_clear()
828+
829+
# Create a state instance tree
830+
root = State(_reflex_internal_init=True) # type: ignore[call-arg]
831+
832+
# Instance get_substate should resolve a.b.b to ChildState2
833+
resolved = root.get_substate(["a", "b", "b"])
834+
assert type(resolved) is ChildState2

0 commit comments

Comments
 (0)