From 189700ecb3a5b119cf47e7bf30c47ee3e3f43fba Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Fri, 11 Oct 2024 15:43:54 -0700 Subject: [PATCH 1/6] Change bun link (#4162) * change bun to download from us * make it use branch --- reflex/constants/installer.py | 3 +- scripts/bun_install.sh | 311 ++++++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 scripts/bun_install.sh diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 12e4c9f20..627db5ae8 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -44,7 +44,8 @@ class Bun(SimpleNamespace): DEFAULT_PATH = ROOT_PATH / "bin" / ("bun" if not IS_WINDOWS else "bun.exe") # URL to bun install script. - INSTALL_URL = "https://bun.sh/install" + INSTALL_URL = "https://raw.githubusercontent.com/reflex-dev/reflex/change-bun-link/scripts/bun_install.sh" + # URL to windows install script. WINDOWS_INSTALL_URL = ( "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/install.ps1" diff --git a/scripts/bun_install.sh b/scripts/bun_install.sh new file mode 100644 index 000000000..08a0817f6 --- /dev/null +++ b/scripts/bun_install.sh @@ -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" From 1eb92fa39b42fc6f4605dfec748bcc3562d682e2 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Fri, 11 Oct 2024 15:51:30 -0700 Subject: [PATCH 2/6] change bun install link to main (#4164) --- reflex/constants/installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 627db5ae8..b12d56c78 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -44,7 +44,7 @@ class Bun(SimpleNamespace): DEFAULT_PATH = ROOT_PATH / "bin" / ("bun" if not IS_WINDOWS else "bun.exe") # URL to bun install script. - INSTALL_URL = "https://raw.githubusercontent.com/reflex-dev/reflex/change-bun-link/scripts/bun_install.sh" + INSTALL_URL = "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/bun_install.sh" # URL to windows install script. WINDOWS_INSTALL_URL = ( From 0311dae5689c43e6bb16eb4e0f0b0d81bb4d9fde Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Fri, 11 Oct 2024 16:49:01 -0700 Subject: [PATCH 3/6] only read if it's *not* empty (#4165) --- reflex/utils/path_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/utils/path_ops.py b/reflex/utils/path_ops.py index 79596716c..f597e0075 100644 --- a/reflex/utils/path_ops.py +++ b/reflex/utils/path_ops.py @@ -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. json_object = {} - if fp.stat().st_size == 0: + if fp.stat().st_size: with open(fp) as f: json_object = json.load(f) From a2862bd1025b929fd48e26361410ad8110ae8e78 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 11 Oct 2024 16:49:40 -0700 Subject: [PATCH 4/6] Allow setting a different invocation function for EventChain (#4160) In rx.call_script scenario, the EventChain must call `queueEvents` and `processEvent` instead of `addEvents`, because the former are in scope in the call_script eval environment where `addEvents` is not. This is an escape hatch for certain wrapping scenarios. --- reflex/event.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/reflex/event.py b/reflex/event.py index d2bebaa3f..659532ce2 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -389,6 +389,8 @@ class EventChain(EventActionsMixin): 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. stop_propagation = EventChain(events=[], args_spec=lambda: []).stop_propagation @@ -1323,10 +1325,15 @@ class LiteralEventChainVar(CachedVarOperation, LiteralVar, EventChainVar): arg_def = ("...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( ArgsFunctionOperation.create( arg_def, - FunctionStringVar.create("addEvents").call( + invocation.call( LiteralVar.create( [LiteralVar.create(event) for event in self._var_value.events] ), From 736b2a6ea9e9be697cab3e5c03f29a4a7bf31741 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 11 Oct 2024 16:51:10 -0700 Subject: [PATCH 5/6] Handle rx.State subclasses defined in function (#4129) * Handle rx.State subclasses defined in function * create a new container module: `reflex.istate.dynamic` to save references to dynamically generated substates. * for substates with `` in the name, copy these to the container module and update the name to avoid duplication. * add test for "poor man" ComponentState Fix #4128 * test_state: disable local def handling for dupe-detection test * Track the original module and name for type hint evaluation Also use the original name when checking for the "mangled name" pattern when doing undeclared Var assignment checking. --- reflex/istate/dynamic.py | 3 ++ reflex/state.py | 52 +++++++++++++++++++++-- reflex/utils/types.py | 6 ++- tests/integration/test_component_state.py | 51 ++++++++++++++++++++++ tests/units/test_state.py | 3 ++ 5 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 reflex/istate/dynamic.py diff --git a/reflex/istate/dynamic.py b/reflex/istate/dynamic.py new file mode 100644 index 000000000..e5da36c26 --- /dev/null +++ b/reflex/istate/dynamic.py @@ -0,0 +1,3 @@ +"""A container for dynamically generated states.""" + +# This page intentionally left blank. diff --git a/reflex/state.py b/reflex/state.py index 38289d081..7b338b4d6 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -10,6 +10,7 @@ import functools import inspect import os import pickle +import sys import uuid from abc import ABC, abstractmethod from collections import defaultdict @@ -60,6 +61,7 @@ import wrapt from redis.asyncio import Redis from redis.exceptions import ResponseError +import reflex.istate.dynamic from reflex import constants from reflex.base import Base from reflex.event import ( @@ -425,6 +427,10 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): if mixin: return + # Handle locally-defined states for pickling. + if "" in cls.__qualname__: + cls._handle_local_def() + # Validate the module name. cls._validate_module_name() @@ -471,7 +477,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): new_backend_vars.update( { 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 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 def _init_var_dependency_dicts(cls): """Initialize the var dependency tracking dicts. @@ -1210,7 +1249,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): name not in self.vars and name not in self.get_skip_vars() 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( 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 state_cls_name = f"{cls.__name__}_n{cls._per_component_state_instance_count}" 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. - globals()[state_cls_name] = component_state + setattr(reflex.istate.dynamic, state_cls_name, component_state) component = component_state.get_component(*children, **props) component.State = component_state return component diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 6bedf5b61..5e963a7cb 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -525,7 +525,11 @@ def is_backend_base_variable(name: str, cls: Type) -> bool: if name.startswith(f"_{cls.__name__}__"): return False - hints = get_type_hints(cls) + # 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) if name in hints: hint = get_origin(hints[name]) if hint == ClassVar: diff --git a/tests/integration/test_component_state.py b/tests/integration/test_component_state.py index 7b35f8116..1555577d1 100644 --- a/tests/integration/test_component_state.py +++ b/tests/integration/test_component_state.py @@ -20,6 +20,8 @@ def ComponentStateApp(): E = TypeVar("E") class MultiCounter(rx.ComponentState, Generic[E]): + """ComponentState style.""" + count: int = 0 _be: E _be_int: int @@ -42,16 +44,47 @@ def ComponentStateApp(): **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 @rx.page() def index(): mc_a = MultiCounter.create(id="a") 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_c.State != mc_d.State return rx.vstack( mc_a, mc_b, + mc_c, + mc_d, rx.button( "Inc A", on_click=mc_a.State.increment, # type: ignore @@ -149,3 +182,21 @@ async def test_component_state_app(component_state_app: AppHarness): b_state = root_state.substates[b_state_name] assert b_state._backend_vars != b_state.backend_vars 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" diff --git a/tests/units/test_state.py b/tests/units/test_state.py index ae74cacce..610d69110 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -2502,7 +2502,10 @@ def test_mutable_copy_vars(mutable_state: MutableTestState, copy_func: Callable) 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) + # Neuter state handling since these _are_ defined inside a function. + mocker.patch("reflex.state.BaseState._handle_local_def", lambda: None) with pytest.raises(ValueError): class TestState(BaseState): From 0889276e24be8e459506e1f591ca19dec3944e8c Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Sat, 12 Oct 2024 02:08:39 +0200 Subject: [PATCH 6/6] Do not auto-determine generic args if already supplied (#4148) * add failing test for figure_out_type * do not auto-determine generic args if already supplied * move has_args to utils.types, add tests for it --- reflex/utils/types.py | 21 +++++++++++++++ reflex/vars/base.py | 9 ++++--- tests/units/utils/test_types.py | 47 ++++++++++++++++++++++++++++++++- tests/units/vars/test_base.py | 28 ++++++++++++++++++++ 4 files changed, 101 insertions(+), 4 deletions(-) diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 5e963a7cb..3f3b05258 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -220,6 +220,27 @@ def is_literal(cls: GenericType) -> bool: 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: """Check if a class is an Optional. diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 7bc5e4d92..df9a0e122 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -56,7 +56,7 @@ from reflex.utils.imports import ( ParsedImportDict, 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: from reflex.state import BaseState @@ -1266,6 +1266,11 @@ def figure_out_type(value: Any) -> types.GenericType: Returns: 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): return List[unionize(*(figure_out_type(v) for v in value))] if isinstance(value, set): @@ -1277,8 +1282,6 @@ def figure_out_type(value: Any) -> types.GenericType: unionize(*(figure_out_type(k) for k in value)), unionize(*(figure_out_type(v) for v in value.values())), ] - if isinstance(value, Var): - return value._var_type return type(value) diff --git a/tests/units/utils/test_types.py b/tests/units/utils/test_types.py index fc9261e04..623aacc1f 100644 --- a/tests/units/utils/test_types.py +++ b/tests/units/utils/test_types.py @@ -1,4 +1,4 @@ -from typing import Any, List, Literal, Tuple, Union +from typing import Any, Dict, List, Literal, Tuple, Union import pytest @@ -45,3 +45,48 @@ def test_issubclass( cls: types.GenericType, cls_check: types.GenericType, expected: bool ) -> None: 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 diff --git a/tests/units/vars/test_base.py b/tests/units/vars/test_base.py index f83d79373..68bc0c38e 100644 --- a/tests/units/vars/test_base.py +++ b/tests/units/vars/test_base.py @@ -5,6 +5,30 @@ import pytest 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( ("value", "expected"), [ @@ -15,6 +39,10 @@ from reflex.vars.base import figure_out_type ([1, 2.0, "a"], List[Union[int, float, str]]), ({"a": 1, "b": 2}, Dict[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):