Skip to content

Commit

Permalink
Merge branch 'main' into lendemor/lift_restriction_on_node_version
Browse files Browse the repository at this point in the history
  • Loading branch information
Lendemor committed Sep 25, 2024
2 parents c8a48c7 + 74d1c47 commit 2f71c94
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 8 deletions.
152 changes: 152 additions & 0 deletions integration/test_dynamic_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Integration tests for var operations."""

import time
from typing import Callable, Generator, TypeVar

import pytest
from selenium.webdriver.common.by import By

from reflex.testing import AppHarness

# pyright: reportOptionalMemberAccess=false, reportGeneralTypeIssues=false, reportUnknownMemberType=false


def DynamicComponents():
"""App with var operations."""
import reflex as rx

class DynamicComponentsState(rx.State):
button: rx.Component = rx.button(
"Click me",
custom_attrs={
"id": "button",
},
)

def got_clicked(self):
self.button = rx.button(
"Clicked",
custom_attrs={
"id": "button",
},
)

@rx.var
def client_token_component(self) -> rx.Component:
return rx.vstack(
rx.el.input(
custom_attrs={
"id": "token",
},
value=self.router.session.client_token,
is_read_only=True,
),
rx.button(
"Update",
custom_attrs={
"id": "update",
},
on_click=DynamicComponentsState.got_clicked,
),
)

app = rx.App()

@app.add_page
def index():
return rx.vstack(
DynamicComponentsState.client_token_component,
DynamicComponentsState.button,
)


@pytest.fixture(scope="module")
def dynamic_components(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""Start VarOperations app at tmp_path via AppHarness.
Args:
tmp_path_factory: pytest tmp_path_factory fixture
Yields:
running AppHarness instance
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("dynamic_components"),
app_source=DynamicComponents, # type: ignore
) as harness:
assert harness.app_instance is not None, "app is not running"
yield harness


T = TypeVar("T")


def poll_for_result(
f: Callable[[], T], exception=Exception, max_attempts=5, seconds_between_attempts=1
) -> T:
"""Poll for a result from a function.
Args:
f: function to call
exception: exception to catch
max_attempts: maximum number of attempts
seconds_between_attempts: seconds to wait between
Returns:
Result of the function
Raises:
AssertionError: if the function does not return a value
"""
attempts = 0
while attempts < max_attempts:
try:
return f()
except exception:
attempts += 1
time.sleep(seconds_between_attempts)
raise AssertionError("Function did not return a value")


@pytest.fixture
def driver(dynamic_components: AppHarness):
"""Get an instance of the browser open to the dynamic components app.
Args:
dynamic_components: AppHarness for the dynamic components
Yields:
WebDriver instance.
"""
driver = dynamic_components.frontend()
try:
token_input = poll_for_result(lambda: driver.find_element(By.ID, "token"))
assert token_input
# wait for the backend connection to send the token
token = dynamic_components.poll_for_value(token_input)
assert token is not None

yield driver
finally:
driver.quit()


def test_dynamic_components(driver, dynamic_components: AppHarness):
"""Test that the var operations produce the right results.
Args:
driver: selenium WebDriver open to the app
dynamic_components: AppHarness for the dynamic components
"""
button = poll_for_result(lambda: driver.find_element(By.ID, "button"))
assert button
assert button.text == "Click me"

update_button = driver.find_element(By.ID, "update")
assert update_button
update_button.click()

assert (
dynamic_components.poll_for_content(button, exp_not_equal="Click me")
== "Clicked"
)
8 changes: 7 additions & 1 deletion reflex/components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
)
from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var
from reflex.vars.sequence import LiteralArrayVar


class BaseComponent(Base, ABC):
Expand Down Expand Up @@ -496,7 +497,12 @@ def __init__(self, *args, **kwargs):
# Convert class_name to str if it's list
class_name = kwargs.get("class_name", "")
if isinstance(class_name, (List, tuple)):
kwargs["class_name"] = " ".join(class_name)
if any(isinstance(c, Var) for c in class_name):
kwargs["class_name"] = LiteralArrayVar.create(
class_name, _var_type=List[str]
).join(" ")
else:
kwargs["class_name"] = " ".join(class_name)

# Construct the component.
super().__init__(*args, **kwargs)
Expand Down
24 changes: 19 additions & 5 deletions reflex/components/dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,20 @@ def make_component(component: Component) -> str:
)
] = None

libs_in_window = [
"react",
"@radix-ui/themes",
]

imports = {}
for lib, names in component._get_all_imports().items():
if (
not lib.startswith((".", "/"))
and not lib.startswith("http")
and lib != "react"
and all(
not lib.startswith(lib_in_window)
for lib_in_window in libs_in_window
)
):
imports[get_cdn_url(lib)] = names
else:
Expand All @@ -83,10 +91,16 @@ def make_component(component: Component) -> str:
)
+ "]"
)
elif 'from "react"' in line:
module_code_lines[ix] = line.replace(
"import ", "const ", 1
).replace(' from "react"', " = window.__reflex.react", 1)
else:
for lib in libs_in_window:
if f'from "{lib}"' in line:
module_code_lines[ix] = (
line.replace("import ", "const ", 1)
.replace(
f' from "{lib}"', f" = window.__reflex['{lib}']", 1
)
.replace(" as ", ": ")
)
if line.startswith("export function"):
module_code_lines[ix] = line.replace(
"export function", "export default function", 1
Expand Down
2 changes: 1 addition & 1 deletion reflex/components/radix/themes/layout/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def create(
"""
# Apply the default classname
given_class_name = props.pop("class_name", [])
if isinstance(given_class_name, str):
if not isinstance(given_class_name, list):
given_class_name = [given_class_name]
props["class_name"] = ["rx-Stack", *given_class_name]

Expand Down
2 changes: 1 addition & 1 deletion reflex/constants/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class Commands(SimpleNamespace):
"@emotion/react": "11.11.1",
"axios": "1.6.0",
"json5": "2.2.3",
"next": "14.0.1",
"next": "14.2.13",
"next-sitemap": "4.1.8",
"next-themes": "0.2.1",
"react": "18.2.0",
Expand Down
43 changes: 43 additions & 0 deletions reflex/vars/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,29 @@ def create(
else:
return only_string.to(StringVar, only_string._var_type)

if len(
literal_strings := [
s
for s in filtered_strings_and_vals
if isinstance(s, (str, LiteralStringVar))
]
) == len(filtered_strings_and_vals):
return LiteralStringVar.create(
"".join(
s._var_value if isinstance(s, LiteralStringVar) else s
for s in literal_strings
),
_var_type=_var_type,
_var_data=VarData.merge(
_var_data,
*(
s._get_all_var_data()
for s in filtered_strings_and_vals
if isinstance(s, Var)
),
),
)

concat_result = ConcatVarOperation.create(
*filtered_strings_and_vals,
_var_data=_var_data,
Expand Down Expand Up @@ -736,6 +759,26 @@ def join(self, sep: Any = "") -> StringVar:
"""
if not isinstance(sep, (StringVar, str)):
raise_unsupported_operand_types("join", (type(self), type(sep)))
if (
isinstance(self, LiteralArrayVar)
and (
len(
args := [
x
for x in self._var_value
if isinstance(x, (LiteralStringVar, str))
]
)
== len(self._var_value)
)
and isinstance(sep, (LiteralStringVar, str))
):
sep_str = sep._var_value if isinstance(sep, LiteralStringVar) else sep
return LiteralStringVar.create(
sep_str.join(
i._var_value if isinstance(i, LiteralStringVar) else i for i in args
)
)
return array_join_operation(self, sep)

def reverse(self) -> ArrayVar[ARRAY_VAR_TYPE]:
Expand Down
10 changes: 10 additions & 0 deletions tests/units/components/test_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,16 @@ def handler2(self, arg):
[FORMATTED_TEST_VAR],
id="fstring-class_name",
),
pytest.param(
rx.fragment(class_name=f"foo{TEST_VAR}bar other-class"),
[LiteralVar.create(f"{FORMATTED_TEST_VAR} other-class")],
id="fstring-dual-class_name",
),
pytest.param(
rx.fragment(class_name=[TEST_VAR, "other-class"]),
[LiteralVar.create([TEST_VAR, "other-class"]).join(" ")],
id="fstring-dual-class_name",
),
pytest.param(
rx.fragment(special_props=[TEST_VAR]),
[TEST_VAR],
Expand Down

0 comments on commit 2f71c94

Please sign in to comment.