This commit is contained in:
Khaleel Al-Adhami 2024-10-11 17:23:47 -07:00
commit a0a5dcb831
12 changed files with 533 additions and 12 deletions

View File

@ -44,7 +44,8 @@ class Bun(SimpleNamespace):
DEFAULT_PATH = ROOT_PATH / "bin" / ("bun" if not IS_WINDOWS else "bun.exe") DEFAULT_PATH = ROOT_PATH / "bin" / ("bun" if not IS_WINDOWS else "bun.exe")
# URL to bun install script. # URL to bun install script.
INSTALL_URL = "https://bun.sh/install" INSTALL_URL = "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/bun_install.sh"
# URL to windows install script. # URL to windows install script.
WINDOWS_INSTALL_URL = ( WINDOWS_INSTALL_URL = (
"https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/install.ps1" "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/install.ps1"

View File

@ -392,6 +392,8 @@ class EventChain(EventActionsMixin):
args_spec: Optional[Callable] = dataclasses.field(default=None) args_spec: Optional[Callable] = dataclasses.field(default=None)
invocation: Optional[Var] = dataclasses.field(default=None)
# These chains can be used for their side effects when no other events are desired. # These chains can be used for their side effects when no other events are desired.
stop_propagation = EventChain(events=[], args_spec=lambda: []).stop_propagation stop_propagation = EventChain(events=[], args_spec=lambda: []).stop_propagation
@ -1360,10 +1362,15 @@ class LiteralEventChainVar(CachedVarOperation, LiteralVar, EventChainVar):
arg_def = ("...args",) arg_def = ("...args",)
arg_def_expr = Var(_js_expr="args") arg_def_expr = Var(_js_expr="args")
if self._var_value.invocation is None:
invocation = FunctionStringVar.create("addEvents")
else:
invocation = self._var_value.invocation
return str( return str(
ArgsFunctionOperation.create( ArgsFunctionOperation.create(
arg_def, arg_def,
FunctionStringVar.create("addEvents").call( invocation.call(
LiteralVar.create( LiteralVar.create(
[LiteralVar.create(event) for event in self._var_value.events] [LiteralVar.create(event) for event in self._var_value.events]
), ),

3
reflex/istate/dynamic.py Normal file
View File

@ -0,0 +1,3 @@
"""A container for dynamically generated states."""
# This page intentionally left blank.

View File

@ -10,6 +10,7 @@ import functools
import inspect import inspect
import os import os
import pickle import pickle
import sys
import uuid import uuid
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import defaultdict from collections import defaultdict
@ -60,6 +61,7 @@ import wrapt
from redis.asyncio import Redis from redis.asyncio import Redis
from redis.exceptions import ResponseError from redis.exceptions import ResponseError
import reflex.istate.dynamic
from reflex import constants from reflex import constants
from reflex.base import Base from reflex.base import Base
from reflex.event import ( from reflex.event import (
@ -425,6 +427,10 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if mixin: if mixin:
return return
# Handle locally-defined states for pickling.
if "<locals>" in cls.__qualname__:
cls._handle_local_def()
# Validate the module name. # Validate the module name.
cls._validate_module_name() cls._validate_module_name()
@ -471,7 +477,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
new_backend_vars.update( new_backend_vars.update(
{ {
name: cls._get_var_default(name, annotation_value) name: cls._get_var_default(name, annotation_value)
for name, annotation_value in get_type_hints(cls).items() for name, annotation_value in cls._get_type_hints().items()
if name not in new_backend_vars if name not in new_backend_vars
and types.is_backend_base_variable(name, cls) and types.is_backend_base_variable(name, cls)
} }
@ -647,6 +653,39 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
) )
] ]
@classmethod
def _handle_local_def(cls):
"""Handle locally-defined states for pickling."""
known_names = dir(reflex.istate.dynamic)
proposed_name = cls.__name__
for ix in range(len(known_names)):
if proposed_name not in known_names:
break
proposed_name = f"{cls.__name__}_{ix}"
setattr(reflex.istate.dynamic, proposed_name, cls)
cls.__original_name__ = cls.__name__
cls.__original_module__ = cls.__module__
cls.__name__ = cls.__qualname__ = proposed_name
cls.__module__ = reflex.istate.dynamic.__name__
@classmethod
def _get_type_hints(cls) -> dict[str, Any]:
"""Get the type hints for this class.
If the class is dynamic, evaluate the type hints with the original
module in the local namespace.
Returns:
The type hints dict.
"""
original_module = getattr(cls, "__original_module__", None)
if original_module is not None:
localns = sys.modules[original_module].__dict__
else:
localns = None
return get_type_hints(cls, localns=localns)
@classmethod @classmethod
def _init_var_dependency_dicts(cls): def _init_var_dependency_dicts(cls):
"""Initialize the var dependency tracking dicts. """Initialize the var dependency tracking dicts.
@ -1210,7 +1249,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
name not in self.vars name not in self.vars
and name not in self.get_skip_vars() and name not in self.get_skip_vars()
and not name.startswith("__") and not name.startswith("__")
and not name.startswith(f"_{type(self).__name__}__") and not name.startswith(
f"_{getattr(type(self), '__original_name__', type(self).__name__)}__"
)
): ):
raise SetUndefinedStateVarError( raise SetUndefinedStateVarError(
f"The state variable '{name}' has not been defined in '{type(self).__name__}'. " f"The state variable '{name}' has not been defined in '{type(self).__name__}'. "
@ -2199,10 +2240,13 @@ class ComponentState(State, mixin=True):
cls._per_component_state_instance_count += 1 cls._per_component_state_instance_count += 1
state_cls_name = f"{cls.__name__}_n{cls._per_component_state_instance_count}" state_cls_name = f"{cls.__name__}_n{cls._per_component_state_instance_count}"
component_state = type( component_state = type(
state_cls_name, (cls, State), {"__module__": __name__}, mixin=False state_cls_name,
(cls, State),
{"__module__": reflex.istate.dynamic.__name__},
mixin=False,
) )
# Save a reference to the dynamic state for pickle/unpickle. # Save a reference to the dynamic state for pickle/unpickle.
globals()[state_cls_name] = component_state setattr(reflex.istate.dynamic, state_cls_name, component_state)
component = component_state.get_component(*children, **props) component = component_state.get_component(*children, **props)
component.State = component_state component.State = component_state
return component return component

View File

@ -218,7 +218,7 @@ def update_json_file(file_path: str | Path, update_dict: dict[str, int | str]):
# Read the existing json object from the file. # Read the existing json object from the file.
json_object = {} json_object = {}
if fp.stat().st_size == 0: if fp.stat().st_size:
with open(fp) as f: with open(fp) as f:
json_object = json.load(f) json_object = json.load(f)

View File

@ -221,6 +221,27 @@ def is_literal(cls: GenericType) -> bool:
return get_origin(cls) is Literal return get_origin(cls) is Literal
def has_args(cls) -> bool:
"""Check if the class has generic parameters.
Args:
cls: The class to check.
Returns:
Whether the class has generic
"""
if get_args(cls):
return True
# Check if the class inherits from a generic class (using __orig_bases__)
if hasattr(cls, "__orig_bases__"):
for base in cls.__orig_bases__:
if get_args(base):
return True
return False
def is_optional(cls: GenericType) -> bool: def is_optional(cls: GenericType) -> bool:
"""Check if a class is an Optional. """Check if a class is an Optional.
@ -526,6 +547,10 @@ def is_backend_base_variable(name: str, cls: Type) -> bool:
if name.startswith(f"_{cls.__name__}__"): if name.startswith(f"_{cls.__name__}__"):
return False return False
# Extract the namespace of the original module if defined (dynamic substates).
if callable(getattr(cls, "_get_type_hints", None)):
hints = cls._get_type_hints()
else:
hints = get_type_hints(cls) hints = get_type_hints(cls)
if name in hints: if name in hints:
hint = get_origin(hints[name]) hint = get_origin(hints[name])

View File

@ -56,7 +56,7 @@ from reflex.utils.imports import (
ParsedImportDict, ParsedImportDict,
parse_imports, parse_imports,
) )
from reflex.utils.types import GenericType, Self, get_origin from reflex.utils.types import GenericType, Self, get_origin, has_args
if TYPE_CHECKING: if TYPE_CHECKING:
from reflex.state import BaseState from reflex.state import BaseState
@ -1273,6 +1273,11 @@ def figure_out_type(value: Any) -> types.GenericType:
Returns: Returns:
The type of the value. The type of the value.
""" """
if isinstance(value, Var):
return value._var_type
type_ = type(value)
if has_args(type_):
return type_
if isinstance(value, list): if isinstance(value, list):
return List[unionize(*(figure_out_type(v) for v in value))] return List[unionize(*(figure_out_type(v) for v in value))]
if isinstance(value, set): if isinstance(value, set):
@ -1284,8 +1289,6 @@ def figure_out_type(value: Any) -> types.GenericType:
unionize(*(figure_out_type(k) for k in value)), unionize(*(figure_out_type(k) for k in value)),
unionize(*(figure_out_type(v) for v in value.values())), unionize(*(figure_out_type(v) for v in value.values())),
] ]
if isinstance(value, Var):
return value._var_type
return type(value) return type(value)

311
scripts/bun_install.sh Normal file
View File

@ -0,0 +1,311 @@
#!/usr/bin/env bash
set -euo pipefail
platform=$(uname -ms)
if [[ ${OS:-} = Windows_NT ]]; then
if [[ $platform != MINGW64* ]]; then
powershell -c "irm bun.sh/install.ps1|iex"
exit $?
fi
fi
# Reset
Color_Off=''
# Regular Colors
Red=''
Green=''
Dim='' # White
# Bold
Bold_White=''
Bold_Green=''
if [[ -t 1 ]]; then
# Reset
Color_Off='\033[0m' # Text Reset
# Regular Colors
Red='\033[0;31m' # Red
Green='\033[0;32m' # Green
Dim='\033[0;2m' # White
# Bold
Bold_Green='\033[1;32m' # Bold Green
Bold_White='\033[1m' # Bold White
fi
error() {
echo -e "${Red}error${Color_Off}:" "$@" >&2
exit 1
}
info() {
echo -e "${Dim}$@ ${Color_Off}"
}
info_bold() {
echo -e "${Bold_White}$@ ${Color_Off}"
}
success() {
echo -e "${Green}$@ ${Color_Off}"
}
command -v unzip >/dev/null ||
error 'unzip is required to install bun'
if [[ $# -gt 2 ]]; then
error 'Too many arguments, only 2 are allowed. The first can be a specific tag of bun to install. (e.g. "bun-v0.1.4") The second can be a build variant of bun to install. (e.g. "debug-info")'
fi
case $platform in
'Darwin x86_64')
target=darwin-x64
;;
'Darwin arm64')
target=darwin-aarch64
;;
'Linux aarch64' | 'Linux arm64')
target=linux-aarch64
;;
'MINGW64'*)
target=windows-x64
;;
'Linux x86_64' | *)
target=linux-x64
;;
esac
if [[ $target = darwin-x64 ]]; then
# Is this process running in Rosetta?
# redirect stderr to devnull to avoid error message when not running in Rosetta
if [[ $(sysctl -n sysctl.proc_translated 2>/dev/null) = 1 ]]; then
target=darwin-aarch64
info "Your shell is running in Rosetta 2. Downloading bun for $target instead"
fi
fi
GITHUB=${GITHUB-"https://github.com"}
github_repo="$GITHUB/oven-sh/bun"
if [[ $target = darwin-x64 ]]; then
# If AVX2 isn't supported, use the -baseline build
if [[ $(sysctl -a | grep machdep.cpu | grep AVX2) == '' ]]; then
target=darwin-x64-baseline
fi
fi
if [[ $target = linux-x64 ]]; then
# If AVX2 isn't supported, use the -baseline build
if [[ $(cat /proc/cpuinfo | grep avx2) = '' ]]; then
target=linux-x64-baseline
fi
fi
exe_name=bun
if [[ $# = 2 && $2 = debug-info ]]; then
target=$target-profile
exe_name=bun-profile
info "You requested a debug build of bun. More information will be shown if a crash occurs."
fi
if [[ $# = 0 ]]; then
bun_uri=$github_repo/releases/latest/download/bun-$target.zip
else
bun_uri=$github_repo/releases/download/$1/bun-$target.zip
fi
install_env=BUN_INSTALL
bin_env=\$$install_env/bin
install_dir=${!install_env:-$HOME/.bun}
bin_dir=$install_dir/bin
exe=$bin_dir/bun
if [[ ! -d $bin_dir ]]; then
mkdir -p "$bin_dir" ||
error "Failed to create install directory \"$bin_dir\""
fi
curl --fail --location --progress-bar --output "$exe.zip" "$bun_uri" ||
error "Failed to download bun from \"$bun_uri\""
unzip -oqd "$bin_dir" "$exe.zip" ||
error 'Failed to extract bun'
mv "$bin_dir/bun-$target/$exe_name" "$exe" ||
error 'Failed to move extracted bun to destination'
chmod +x "$exe" ||
error 'Failed to set permissions on bun executable'
rm -r "$bin_dir/bun-$target" "$exe.zip"
tildify() {
if [[ $1 = $HOME/* ]]; then
local replacement=\~/
echo "${1/$HOME\//$replacement}"
else
echo "$1"
fi
}
success "bun was installed successfully to $Bold_Green$(tildify "$exe")"
if command -v bun >/dev/null; then
# Install completions, but we don't care if it fails
IS_BUN_AUTO_UPDATE=true $exe completions &>/dev/null || :
echo "Run 'bun --help' to get started"
exit
fi
refresh_command=''
tilde_bin_dir=$(tildify "$bin_dir")
quoted_install_dir=\"${install_dir//\"/\\\"}\"
if [[ $quoted_install_dir = \"$HOME/* ]]; then
quoted_install_dir=${quoted_install_dir/$HOME\//\$HOME/}
fi
echo
case $(basename "$SHELL") in
fish)
# Install completions, but we don't care if it fails
IS_BUN_AUTO_UPDATE=true SHELL=fish $exe completions &>/dev/null || :
commands=(
"set --export $install_env $quoted_install_dir"
"set --export PATH $bin_env \$PATH"
)
fish_config=$HOME/.config/fish/config.fish
tilde_fish_config=$(tildify "$fish_config")
if [[ -w $fish_config ]]; then
{
echo -e '\n# bun'
for command in "${commands[@]}"; do
echo "$command"
done
} >>"$fish_config"
info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_fish_config\""
refresh_command="source $tilde_fish_config"
else
echo "Manually add the directory to $tilde_fish_config (or similar):"
for command in "${commands[@]}"; do
info_bold " $command"
done
fi
;;
zsh)
# Install completions, but we don't care if it fails
IS_BUN_AUTO_UPDATE=true SHELL=zsh $exe completions &>/dev/null || :
commands=(
"export $install_env=$quoted_install_dir"
"export PATH=\"$bin_env:\$PATH\""
)
zsh_config=$HOME/.zshrc
tilde_zsh_config=$(tildify "$zsh_config")
if [[ -w $zsh_config ]]; then
{
echo -e '\n# bun'
for command in "${commands[@]}"; do
echo "$command"
done
} >>"$zsh_config"
info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_zsh_config\""
refresh_command="exec $SHELL"
else
echo "Manually add the directory to $tilde_zsh_config (or similar):"
for command in "${commands[@]}"; do
info_bold " $command"
done
fi
;;
bash)
# Install completions, but we don't care if it fails
IS_BUN_AUTO_UPDATE=true SHELL=bash $exe completions &>/dev/null || :
commands=(
"export $install_env=$quoted_install_dir"
"export PATH=\"$bin_env:\$PATH\""
)
bash_configs=(
"$HOME/.bashrc"
"$HOME/.bash_profile"
)
if [[ ${XDG_CONFIG_HOME:-} ]]; then
bash_configs+=(
"$XDG_CONFIG_HOME/.bash_profile"
"$XDG_CONFIG_HOME/.bashrc"
"$XDG_CONFIG_HOME/bash_profile"
"$XDG_CONFIG_HOME/bashrc"
)
fi
set_manually=true
for bash_config in "${bash_configs[@]}"; do
tilde_bash_config=$(tildify "$bash_config")
if [[ -w $bash_config ]]; then
{
echo -e '\n# bun'
for command in "${commands[@]}"; do
echo "$command"
done
} >>"$bash_config"
info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_bash_config\""
refresh_command="source $bash_config"
set_manually=false
break
fi
done
if [[ $set_manually = true ]]; then
echo "Manually add the directory to $tilde_bash_config (or similar):"
for command in "${commands[@]}"; do
info_bold " $command"
done
fi
;;
*)
echo 'Manually add the directory to ~/.bashrc (or similar):'
info_bold " export $install_env=$quoted_install_dir"
info_bold " export PATH=\"$bin_env:\$PATH\""
;;
esac
echo
info "To get started, run:"
echo
if [[ $refresh_command ]]; then
info_bold " $refresh_command"
fi
info_bold " bun --help"

View File

@ -20,6 +20,8 @@ def ComponentStateApp():
E = TypeVar("E") E = TypeVar("E")
class MultiCounter(rx.ComponentState, Generic[E]): class MultiCounter(rx.ComponentState, Generic[E]):
"""ComponentState style."""
count: int = 0 count: int = 0
_be: E _be: E
_be_int: int _be_int: int
@ -43,16 +45,47 @@ def ComponentStateApp():
**props, **props,
) )
def multi_counter_func(id: str = "default") -> rx.Component:
"""Local-substate style.
Args:
id: identifier for this instance
Returns:
A new instance of the component with its own state.
"""
class _Counter(rx.State):
count: int = 0
def increment(self):
self.count += 1
return rx.vstack(
rx.heading(_Counter.count, id=f"count-{id}"),
rx.button(
"Increment",
on_click=_Counter.increment,
id=f"button-{id}",
),
State=_Counter,
)
app = rx.App(state=rx.State) # noqa app = rx.App(state=rx.State) # noqa
@rx.page() @rx.page()
def index(): def index():
mc_a = MultiCounter.create(id="a") mc_a = MultiCounter.create(id="a")
mc_b = MultiCounter.create(id="b") mc_b = MultiCounter.create(id="b")
mc_c = multi_counter_func(id="c")
mc_d = multi_counter_func(id="d")
assert mc_a.State != mc_b.State assert mc_a.State != mc_b.State
assert mc_c.State != mc_d.State
return rx.vstack( return rx.vstack(
mc_a, mc_a,
mc_b, mc_b,
mc_c,
mc_d,
rx.button( rx.button(
"Inc A", "Inc A",
on_click=mc_a.State.increment, # type: ignore on_click=mc_a.State.increment, # type: ignore
@ -150,3 +183,21 @@ async def test_component_state_app(component_state_app: AppHarness):
b_state = root_state.substates[b_state_name] b_state = root_state.substates[b_state_name]
assert b_state._backend_vars != b_state.backend_vars assert b_state._backend_vars != b_state.backend_vars
assert b_state._be == b_state._backend_vars["_be"] == 2 assert b_state._be == b_state._backend_vars["_be"] == 2
# Check locally-defined substate style
count_c = driver.find_element(By.ID, "count-c")
count_d = driver.find_element(By.ID, "count-d")
button_c = driver.find_element(By.ID, "button-c")
button_d = driver.find_element(By.ID, "button-d")
assert component_state_app.poll_for_content(count_c, exp_not_equal="") == "0"
assert component_state_app.poll_for_content(count_d, exp_not_equal="") == "0"
button_c.click()
assert component_state_app.poll_for_content(count_c, exp_not_equal="0") == "1"
assert component_state_app.poll_for_content(count_d, exp_not_equal="") == "0"
button_c.click()
assert component_state_app.poll_for_content(count_c, exp_not_equal="1") == "2"
assert component_state_app.poll_for_content(count_d, exp_not_equal="") == "0"
button_d.click()
assert component_state_app.poll_for_content(count_c, exp_not_equal="1") == "2"
assert component_state_app.poll_for_content(count_d, exp_not_equal="0") == "1"

View File

@ -2502,7 +2502,10 @@ def test_mutable_copy_vars(mutable_state: MutableTestState, copy_func: Callable)
def test_duplicate_substate_class(mocker): def test_duplicate_substate_class(mocker):
# Neuter pytest escape hatch, because we want to test duplicate detection.
mocker.patch("reflex.state.is_testing_env", lambda: False) mocker.patch("reflex.state.is_testing_env", lambda: False)
# Neuter <locals> state handling since these _are_ defined inside a function.
mocker.patch("reflex.state.BaseState._handle_local_def", lambda: None)
with pytest.raises(ValueError): with pytest.raises(ValueError):
class TestState(BaseState): class TestState(BaseState):

View File

@ -1,4 +1,4 @@
from typing import Any, List, Literal, Tuple, Union from typing import Any, Dict, List, Literal, Tuple, Union
import pytest import pytest
@ -45,3 +45,48 @@ def test_issubclass(
cls: types.GenericType, cls_check: types.GenericType, expected: bool cls: types.GenericType, cls_check: types.GenericType, expected: bool
) -> None: ) -> None:
assert types._issubclass(cls, cls_check) == expected assert types._issubclass(cls, cls_check) == expected
class CustomDict(dict[str, str]):
"""A custom dict with generic arguments."""
pass
class ChildCustomDict(CustomDict):
"""A child of CustomDict."""
pass
class GenericDict(dict):
"""A generic dict with no generic arguments."""
pass
class ChildGenericDict(GenericDict):
"""A child of GenericDict."""
pass
@pytest.mark.parametrize(
"cls,expected",
[
(int, False),
(str, False),
(float, False),
(Tuple[int], True),
(List[int], True),
(Union[int, str], True),
(Union[str, int], True),
(Dict[str, int], True),
(CustomDict, True),
(ChildCustomDict, True),
(GenericDict, False),
(ChildGenericDict, False),
],
)
def test_has_args(cls, expected: bool) -> None:
assert types.has_args(cls) == expected

View File

@ -5,6 +5,30 @@ import pytest
from reflex.vars.base import figure_out_type from reflex.vars.base import figure_out_type
class CustomDict(dict[str, str]):
"""A custom dict with generic arguments."""
pass
class ChildCustomDict(CustomDict):
"""A child of CustomDict."""
pass
class GenericDict(dict):
"""A generic dict with no generic arguments."""
pass
class ChildGenericDict(GenericDict):
"""A child of GenericDict."""
pass
@pytest.mark.parametrize( @pytest.mark.parametrize(
("value", "expected"), ("value", "expected"),
[ [
@ -15,6 +39,10 @@ from reflex.vars.base import figure_out_type
([1, 2.0, "a"], List[Union[int, float, str]]), ([1, 2.0, "a"], List[Union[int, float, str]]),
({"a": 1, "b": 2}, Dict[str, int]), ({"a": 1, "b": 2}, Dict[str, int]),
({"a": 1, 2: "b"}, Dict[Union[int, str], Union[str, int]]), ({"a": 1, 2: "b"}, Dict[Union[int, str], Union[str, int]]),
(CustomDict(), CustomDict),
(ChildCustomDict(), ChildCustomDict),
(GenericDict({1: 1}), Dict[int, int]),
(ChildGenericDict({1: 1}), Dict[int, int]),
], ],
) )
def test_figure_out_type(value, expected): def test_figure_out_type(value, expected):