123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839 |
- from __future__ import annotations
- import functools
- import io
- import json
- import os.path
- import re
- import unittest.mock
- import uuid
- from collections.abc import Generator
- from contextlib import nullcontext as does_not_raise
- from pathlib import Path
- from unittest.mock import AsyncMock
- import pytest
- import sqlmodel
- from fastapi.responses import StreamingResponse
- from pytest_mock import MockerFixture
- from starlette.applications import Starlette
- from starlette.datastructures import UploadFile
- from starlette_admin.auth import AuthProvider
- from starlette_admin.contrib.sqla.admin import Admin
- from starlette_admin.contrib.sqla.view import ModelView
- import reflex as rx
- from reflex import AdminDash, constants
- from reflex.app import (
- App,
- ComponentCallable,
- OverlayFragment,
- default_overlay_component,
- process,
- upload,
- )
- from reflex.components import Component
- from reflex.components.base.bare import Bare
- from reflex.components.base.fragment import Fragment
- from reflex.components.core.cond import Cond
- from reflex.components.radix.themes.typography.text import Text
- from reflex.event import Event
- from reflex.middleware import HydrateMiddleware
- from reflex.model import Model
- from reflex.state import (
- BaseState,
- OnLoadInternalState,
- RouterData,
- State,
- StateManagerDisk,
- StateManagerMemory,
- StateManagerRedis,
- StateUpdate,
- _substate_key,
- )
- from reflex.style import Style
- from reflex.utils import console, exceptions, format
- from reflex.vars.base import computed_var
- from .conftest import chdir
- from .states import (
- ChildFileUploadState,
- FileStateBase1,
- FileUploadState,
- GenState,
- GrandChildFileUploadState,
- )
- class EmptyState(BaseState):
- """An empty state."""
- pass
- @pytest.fixture
- def index_page() -> ComponentCallable:
- """An index page.
- Returns:
- The index page.
- """
- def index():
- return rx.box("Index")
- return index
- @pytest.fixture
- def about_page() -> ComponentCallable:
- """An about page.
- Returns:
- The about page.
- """
- def about():
- return rx.box("About")
- return about
- class ATestState(BaseState):
- """A simple state for testing."""
- var: int
- @pytest.fixture()
- def test_state() -> type[BaseState]:
- """A default state.
- Returns:
- A default state.
- """
- return ATestState
- @pytest.fixture()
- def redundant_test_state() -> type[BaseState]:
- """A default state.
- Returns:
- A default state.
- """
- class RedundantTestState(BaseState):
- var: int
- return RedundantTestState
- @pytest.fixture(scope="session")
- def test_model() -> type[Model]:
- """A default model.
- Returns:
- A default model.
- """
- class TestModel(Model, table=True):
- pass
- return TestModel
- @pytest.fixture(scope="session")
- def test_model_auth() -> type[Model]:
- """A default model.
- Returns:
- A default model.
- """
- class TestModelAuth(Model, table=True):
- """A test model with auth."""
- pass
- return TestModelAuth
- @pytest.fixture()
- def test_get_engine():
- """A default database engine.
- Returns:
- A default database engine.
- """
- enable_admin = True
- url = "sqlite:///test.db"
- return sqlmodel.create_engine(
- url,
- echo=False,
- connect_args={"check_same_thread": False} if enable_admin else {},
- )
- @pytest.fixture()
- def test_custom_auth_admin() -> type[AuthProvider]:
- """A default auth provider.
- Returns:
- A default default auth provider.
- """
- class TestAuthProvider(AuthProvider):
- """A test auth provider."""
- login_path: str = "/login"
- logout_path: str = "/logout"
- def login(self): # pyright: ignore [reportIncompatibleMethodOverride]
- """Login."""
- pass
- def is_authenticated(self): # pyright: ignore [reportIncompatibleMethodOverride]
- """Is authenticated."""
- pass
- def get_admin_user(self): # pyright: ignore [reportIncompatibleMethodOverride]
- """Get admin user."""
- pass
- def logout(self): # pyright: ignore [reportIncompatibleMethodOverride]
- """Logout."""
- pass
- return TestAuthProvider
- def test_default_app(app: App):
- """Test creating an app with no args.
- Args:
- app: The app to test.
- """
- assert app._middlewares == [HydrateMiddleware()]
- assert app.style == Style()
- assert app.admin_dash is None
- def test_multiple_states_error(monkeypatch, test_state, redundant_test_state):
- """Test that an error is thrown when multiple classes subclass rx.BaseState.
- Args:
- monkeypatch: Pytest monkeypatch object.
- test_state: A test state subclassing rx.BaseState.
- redundant_test_state: Another test state subclassing rx.BaseState.
- """
- monkeypatch.delenv(constants.PYTEST_CURRENT_TEST)
- with pytest.raises(ValueError):
- App()
- def test_add_page_default_route(app: App, index_page, about_page):
- """Test adding a page to an app.
- Args:
- app: The app to test.
- index_page: The index page.
- about_page: The about page.
- """
- assert app._pages == {}
- assert app._unevaluated_pages == {}
- app.add_page(index_page)
- app._compile_page("index")
- assert app._pages.keys() == {"index"}
- app.add_page(about_page)
- app._compile_page("about")
- assert app._pages.keys() == {"index", "about"}
- def test_add_page_set_route(app: App, index_page, windows_platform: bool):
- """Test adding a page to an app.
- Args:
- app: The app to test.
- index_page: The index page.
- windows_platform: Whether the system is windows.
- """
- route = "test" if windows_platform else "/test"
- assert app._unevaluated_pages == {}
- app.add_page(index_page, route=route)
- app._compile_page("test")
- assert app._pages.keys() == {"test"}
- def test_add_page_set_route_dynamic(index_page, windows_platform: bool):
- """Test adding a page with dynamic route variable to an app.
- Args:
- index_page: The index page.
- windows_platform: Whether the system is windows.
- """
- app = App(_state=EmptyState)
- assert app._state is not None
- route = "/test/[dynamic]"
- assert app._unevaluated_pages == {}
- app.add_page(index_page, route=route)
- app._compile_page("test/[dynamic]")
- assert app._pages.keys() == {"test/[dynamic]"}
- assert "dynamic" in app._state.computed_vars
- assert app._state.computed_vars["dynamic"]._deps(objclass=EmptyState) == {
- EmptyState.get_full_name(): {constants.ROUTER},
- }
- assert constants.ROUTER in app._state()._var_dependencies
- def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool):
- """Test adding a page to an app.
- Args:
- app: The app to test.
- index_page: The index page.
- windows_platform: Whether the system is windows.
- """
- route = "test\\nested" if windows_platform else "/test/nested"
- assert app._unevaluated_pages == {}
- app.add_page(index_page, route=route)
- assert app._unevaluated_pages.keys() == {route.strip(os.path.sep)}
- def test_add_page_invalid_api_route(app: App, index_page):
- """Test adding a page with an invalid route to an app.
- Args:
- app: The app to test.
- index_page: The index page.
- """
- with pytest.raises(ValueError):
- app.add_page(index_page, route="api")
- with pytest.raises(ValueError):
- app.add_page(index_page, route="/api")
- with pytest.raises(ValueError):
- app.add_page(index_page, route="/api/")
- with pytest.raises(ValueError):
- app.add_page(index_page, route="api/foo")
- with pytest.raises(ValueError):
- app.add_page(index_page, route="/api/foo")
- # These should be fine
- app.add_page(index_page, route="api2")
- app.add_page(index_page, route="/foo/api")
- def page1():
- return rx.fragment()
- def page2():
- return rx.fragment()
- def index():
- return rx.fragment()
- @pytest.mark.parametrize(
- ("first_page", "second_page", "route"),
- [
- (index, index, None),
- (page1, page1, None),
- ],
- )
- def test_add_the_same_page(
- mocker: MockerFixture, app: App, first_page, second_page, route
- ):
- app.add_page(first_page, route=route)
- mock_object = mocker.Mock()
- mocker.patch.object(
- console,
- "warn",
- mock_object,
- )
- app.add_page(second_page, route="/" + route.strip("/") if route else None)
- assert mock_object.call_count == 1
- @pytest.mark.parametrize(
- ("first_page", "second_page", "route"),
- [
- (lambda: rx.fragment(), lambda: rx.fragment(rx.text("second")), "/"),
- (rx.fragment(rx.text("first")), rx.fragment(rx.text("second")), "/page1"),
- (
- lambda: rx.fragment(rx.text("first")),
- rx.fragment(rx.text("second")),
- "page3",
- ),
- (page1, page2, "page1"),
- ],
- )
- def test_add_duplicate_page_route_error(app: App, first_page, second_page, route):
- app.add_page(first_page, route=route)
- with pytest.raises(ValueError):
- app.add_page(second_page, route="/" + route.strip("/") if route else None)
- def test_initialize_with_admin_dashboard(test_model):
- """Test setting the admin dashboard of an app.
- Args:
- test_model: The default model.
- """
- app = App(admin_dash=AdminDash(models=[test_model]))
- assert app.admin_dash is not None
- assert len(app.admin_dash.models) > 0
- assert app.admin_dash.models[0] == test_model
- def test_initialize_with_custom_admin_dashboard(
- test_get_engine,
- test_custom_auth_admin,
- test_model_auth,
- ):
- """Test setting the custom admin dashboard of an app.
- Args:
- test_get_engine: The default database engine.
- test_model_auth: The default model for an auth admin dashboard.
- test_custom_auth_admin: The custom auth provider.
- """
- custom_auth_provider = test_custom_auth_admin()
- custom_admin = Admin(engine=test_get_engine, auth_provider=custom_auth_provider)
- app = App(admin_dash=AdminDash(models=[test_model_auth], admin=custom_admin))
- assert app.admin_dash is not None
- assert app.admin_dash.admin is not None
- assert len(app.admin_dash.models) > 0
- assert app.admin_dash.models[0] == test_model_auth
- assert app.admin_dash.admin.auth_provider == custom_auth_provider
- def test_initialize_admin_dashboard_with_view_overrides(test_model):
- """Test setting the admin dashboard of an app with view class overridden.
- Args:
- test_model: The default model.
- """
- class TestModelView(ModelView):
- pass
- app = App(
- admin_dash=AdminDash(
- models=[test_model], view_overrides={test_model: TestModelView}
- )
- )
- assert app.admin_dash is not None
- assert app.admin_dash.models == [test_model]
- assert app.admin_dash.view_overrides[test_model] == TestModelView
- @pytest.mark.asyncio
- async def test_initialize_with_state(test_state: type[ATestState], token: str):
- """Test setting the state of an app.
- Args:
- test_state: The default state.
- token: a Token.
- """
- app = App(_state=test_state)
- assert app._state == test_state
- # Get a state for a given token.
- state = await app.state_manager.get_state(_substate_key(token, test_state))
- assert isinstance(state, test_state)
- assert state.var == 0
- if isinstance(app.state_manager, StateManagerRedis):
- await app.state_manager.close()
- @pytest.mark.asyncio
- async def test_set_and_get_state(test_state):
- """Test setting and getting the state of an app with different tokens.
- Args:
- test_state: The default state.
- """
- app = App(_state=test_state)
- # Create two tokens.
- token1 = str(uuid.uuid4()) + f"_{test_state.get_full_name()}"
- token2 = str(uuid.uuid4()) + f"_{test_state.get_full_name()}"
- # Get the default state for each token.
- state1 = await app.state_manager.get_state(token1)
- state2 = await app.state_manager.get_state(token2)
- assert state1.var == 0
- assert state2.var == 0
- # Set the vars to different values.
- state1.var = 1
- state2.var = 2
- await app.state_manager.set_state(token1, state1)
- await app.state_manager.set_state(token2, state2)
- # Get the states again and check the values.
- state1 = await app.state_manager.get_state(token1)
- state2 = await app.state_manager.get_state(token2)
- assert state1.var == 1
- assert state2.var == 2
- if isinstance(app.state_manager, StateManagerRedis):
- await app.state_manager.close()
- @pytest.mark.asyncio
- async def test_dynamic_var_event(test_state: type[ATestState], token: str):
- """Test that the default handler of a dynamic generated var
- works as expected.
- Args:
- test_state: State Fixture.
- token: a Token.
- """
- state = test_state() # pyright: ignore [reportCallIssue]
- state.add_var("int_val", int, 0)
- async for result in state._process(
- Event(
- token=token,
- name=f"{test_state.get_name()}.set_int_val",
- router_data={"pathname": "/", "query": {}},
- payload={"value": 50},
- )
- ):
- assert result.delta == {test_state.get_name(): {"int_val": 50}}
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- "event_tuples",
- [
- pytest.param(
- [
- (
- "make_friend",
- {"plain_friends": ["Tommy", "another-fd"]},
- ),
- (
- "change_first_friend",
- {"plain_friends": ["Jenny", "another-fd"]},
- ),
- ],
- id="append then __setitem__",
- ),
- pytest.param(
- [
- (
- "unfriend_first_friend",
- {"plain_friends": []},
- ),
- (
- "make_friend",
- {"plain_friends": ["another-fd"]},
- ),
- ],
- id="delitem then append",
- ),
- pytest.param(
- [
- (
- "make_friends_with_colleagues",
- {"plain_friends": ["Tommy", "Peter", "Jimmy"]},
- ),
- (
- "remove_tommy",
- {"plain_friends": ["Peter", "Jimmy"]},
- ),
- (
- "remove_last_friend",
- {"plain_friends": ["Peter"]},
- ),
- (
- "unfriend_all_friends",
- {"plain_friends": []},
- ),
- ],
- id="extend, remove, pop, clear",
- ),
- pytest.param(
- [
- (
- "add_jimmy_to_second_group",
- {"friends_in_nested_list": [["Tommy"], ["Jenny", "Jimmy"]]},
- ),
- (
- "remove_first_person_from_first_group",
- {"friends_in_nested_list": [[], ["Jenny", "Jimmy"]]},
- ),
- (
- "remove_first_group",
- {"friends_in_nested_list": [["Jenny", "Jimmy"]]},
- ),
- ],
- id="nested list",
- ),
- pytest.param(
- [
- (
- "add_jimmy_to_tommy_friends",
- {"friends_in_dict": {"Tommy": ["Jenny", "Jimmy"]}},
- ),
- (
- "remove_jenny_from_tommy",
- {"friends_in_dict": {"Tommy": ["Jimmy"]}},
- ),
- (
- "tommy_has_no_fds",
- {"friends_in_dict": {"Tommy": []}},
- ),
- ],
- id="list in dict",
- ),
- ],
- )
- async def test_list_mutation_detection__plain_list(
- event_tuples: list[tuple[str, list[str]]],
- list_mutation_state: State,
- token: str,
- ):
- """Test list mutation detection
- when reassignment is not explicitly included in the logic.
- Args:
- event_tuples: From parametrization.
- list_mutation_state: A state with list mutation features.
- token: a Token.
- """
- for event_name, expected_delta in event_tuples:
- async for result in list_mutation_state._process(
- Event(
- token=token,
- name=f"{list_mutation_state.get_name()}.{event_name}",
- router_data={"pathname": "/", "query": {}},
- payload={},
- )
- ):
- # prefix keys in expected_delta with the state name
- expected_delta = {list_mutation_state.get_name(): expected_delta}
- assert result.delta == expected_delta
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- "event_tuples",
- [
- pytest.param(
- [
- (
- "add_age",
- {"details": {"name": "Tommy", "age": 20}},
- ),
- (
- "change_name",
- {"details": {"name": "Jenny", "age": 20}},
- ),
- (
- "remove_last_detail",
- {"details": {"name": "Jenny"}},
- ),
- ],
- id="update then __setitem__",
- ),
- pytest.param(
- [
- (
- "clear_details",
- {"details": {}},
- ),
- (
- "add_age",
- {"details": {"age": 20}},
- ),
- ],
- id="delitem then update",
- ),
- pytest.param(
- [
- (
- "add_age",
- {"details": {"name": "Tommy", "age": 20}},
- ),
- (
- "remove_name",
- {"details": {"age": 20}},
- ),
- (
- "pop_out_age",
- {"details": {}},
- ),
- ],
- id="add, remove, pop",
- ),
- pytest.param(
- [
- (
- "remove_home_address",
- {"address": [{}, {"work": "work address"}]},
- ),
- (
- "add_street_to_home_address",
- {
- "address": [
- {"street": "street address"},
- {"work": "work address"},
- ]
- },
- ),
- ],
- id="dict in list",
- ),
- pytest.param(
- [
- (
- "change_friend_name",
- {
- "friend_in_nested_dict": {
- "name": "Nikhil",
- "friend": {"name": "Tommy"},
- }
- },
- ),
- (
- "add_friend_age",
- {
- "friend_in_nested_dict": {
- "name": "Nikhil",
- "friend": {"name": "Tommy", "age": 30},
- }
- },
- ),
- (
- "remove_friend",
- {"friend_in_nested_dict": {"name": "Nikhil"}},
- ),
- ],
- id="nested dict",
- ),
- ],
- )
- async def test_dict_mutation_detection__plain_list(
- event_tuples: list[tuple[str, list[str]]],
- dict_mutation_state: State,
- token: str,
- ):
- """Test dict mutation detection
- when reassignment is not explicitly included in the logic.
- Args:
- event_tuples: From parametrization.
- dict_mutation_state: A state with dict mutation features.
- token: a Token.
- """
- for event_name, expected_delta in event_tuples:
- async for result in dict_mutation_state._process(
- Event(
- token=token,
- name=f"{dict_mutation_state.get_name()}.{event_name}",
- router_data={"pathname": "/", "query": {}},
- payload={},
- )
- ):
- # prefix keys in expected_delta with the state name
- expected_delta = {dict_mutation_state.get_name(): expected_delta}
- assert result.delta == expected_delta
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("state", "delta"),
- [
- (
- FileUploadState,
- {
- FileUploadState.get_full_name(): {
- "img_list": ["image1.jpg", "image2.jpg"]
- }
- },
- ),
- (
- ChildFileUploadState,
- {
- ChildFileUploadState.get_full_name(): {
- "img_list": ["image1.jpg", "image2.jpg"]
- }
- },
- ),
- (
- GrandChildFileUploadState,
- {
- GrandChildFileUploadState.get_full_name(): {
- "img_list": ["image1.jpg", "image2.jpg"]
- }
- },
- ),
- ],
- )
- async def test_upload_file(tmp_path, state, delta, token: str, mocker):
- """Test that file upload works correctly.
- Args:
- tmp_path: Temporary path.
- state: The state class.
- delta: Expected delta
- token: a Token.
- mocker: pytest mocker object.
- """
- mocker.patch(
- "reflex.state.State.class_subclasses",
- {state if state is FileUploadState else FileStateBase1},
- )
- state._tmp_path = tmp_path
- # The App state must be the "root" of the state tree
- app = App()
- app._enable_state()
- app.event_namespace.emit = AsyncMock() # pyright: ignore [reportOptionalMemberAccess]
- current_state = await app.state_manager.get_state(_substate_key(token, state))
- data = b"This is binary data"
- # Create a binary IO object and write data to it
- bio = io.BytesIO()
- bio.write(data)
- request_mock = unittest.mock.Mock()
- request_mock.headers = {
- "reflex-client-token": token,
- "reflex-event-handler": f"{state.get_full_name()}.multi_handle_upload",
- }
- file1 = UploadFile(
- filename="image1.jpg",
- file=bio,
- )
- file2 = UploadFile(
- filename="image2.jpg",
- file=bio,
- )
- async def form():
- files_mock = unittest.mock.Mock()
- def getlist(key: str):
- assert key == "files"
- return [file1, file2]
- files_mock.getlist = getlist
- return files_mock
- request_mock.form = form
- upload_fn = upload(app)
- streaming_response = await upload_fn(request_mock)
- assert isinstance(streaming_response, StreamingResponse)
- async for state_update in streaming_response.body_iterator:
- assert (
- state_update
- == StateUpdate(delta=delta, events=[], final=True).json() + "\n"
- )
- current_state = await app.state_manager.get_state(_substate_key(token, state))
- state_dict = current_state.dict()[state.get_full_name()]
- assert state_dict["img_list"] == [
- "image1.jpg",
- "image2.jpg",
- ]
- if isinstance(app.state_manager, StateManagerRedis):
- await app.state_manager.close()
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- "state",
- [FileUploadState, ChildFileUploadState, GrandChildFileUploadState],
- )
- async def test_upload_file_without_annotation(state, tmp_path, token):
- """Test that an error is thrown when there's no param annotated with rx.UploadFile or list[UploadFile].
- Args:
- state: The state class.
- tmp_path: Temporary path.
- token: a Token.
- """
- state._tmp_path = tmp_path
- app = App(_state=State)
- request_mock = unittest.mock.Mock()
- request_mock.headers = {
- "reflex-client-token": token,
- "reflex-event-handler": f"{state.get_full_name()}.handle_upload2",
- }
- async def form():
- files_mock = unittest.mock.Mock()
- def getlist(key: str):
- assert key == "files"
- return [unittest.mock.Mock(filename="image1.jpg")]
- files_mock.getlist = getlist
- return files_mock
- request_mock.form = form
- fn = upload(app)
- with pytest.raises(ValueError) as err:
- await fn(request_mock)
- assert (
- err.value.args[0]
- == f"`{state.get_full_name()}.handle_upload2` handler should have a parameter annotated as list[rx.UploadFile]"
- )
- if isinstance(app.state_manager, StateManagerRedis):
- await app.state_manager.close()
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- "state",
- [FileUploadState, ChildFileUploadState, GrandChildFileUploadState],
- )
- async def test_upload_file_background(state, tmp_path, token):
- """Test that an error is thrown handler is a background task.
- Args:
- state: The state class.
- tmp_path: Temporary path.
- token: a Token.
- """
- state._tmp_path = tmp_path
- app = App(_state=State)
- request_mock = unittest.mock.Mock()
- request_mock.headers = {
- "reflex-client-token": token,
- "reflex-event-handler": f"{state.get_full_name()}.bg_upload",
- }
- async def form():
- files_mock = unittest.mock.Mock()
- def getlist(key: str):
- assert key == "files"
- return [unittest.mock.Mock(filename="image1.jpg")]
- files_mock.getlist = getlist
- return files_mock
- request_mock.form = form
- fn = upload(app)
- with pytest.raises(TypeError) as err:
- await fn(request_mock)
- assert (
- err.value.args[0]
- == f"@rx.event(background=True) is not supported for upload handler `{state.get_full_name()}.bg_upload`."
- )
- if isinstance(app.state_manager, StateManagerRedis):
- await app.state_manager.close()
- class DynamicState(BaseState):
- """State class for testing dynamic route var.
- This is defined at module level because event handlers cannot be addressed
- correctly when the class is defined as a local.
- There are several counters:
- * loaded: counts how many times `on_load` was triggered by the hydrate middleware
- * counter: counts how many times `on_counter` was triggered by a non-navigational event
- -> these events should NOT trigger reload or recalculation of router_data dependent vars
- * side_effect_counter: counts how many times a computed var was
- recalculated when the dynamic route var was dirty
- """
- is_hydrated: bool = False
- loaded: int = 0
- counter: int = 0
- @rx.event
- def on_load(self):
- """Event handler for page on_load, should trigger for all navigation events."""
- self.loaded = self.loaded + 1
- @rx.event
- def on_counter(self):
- """Increment the counter var."""
- self.counter = self.counter + 1
- @computed_var
- def comp_dynamic(self) -> str:
- """A computed var that depends on the dynamic var.
- Returns:
- same as self.dynamic
- """
- return self.dynamic
- on_load_internal = OnLoadInternalState.on_load_internal.fn # pyright: ignore [reportFunctionMemberAccess]
- def test_dynamic_arg_shadow(
- index_page: ComponentCallable,
- windows_platform: bool,
- token: str,
- app_module_mock: unittest.mock.Mock,
- mocker,
- ):
- """Create app with dynamic route var and try to add a page with a dynamic arg that shadows a state var.
- Args:
- index_page: The index page.
- windows_platform: Whether the system is windows.
- token: a Token.
- app_module_mock: Mocked app module.
- mocker: pytest mocker object.
- """
- arg_name = "counter"
- route = f"/test/[{arg_name}]"
- app = app_module_mock.app = App(_state=DynamicState)
- assert app._state is not None
- with pytest.raises(NameError):
- app.add_page(index_page, route=route, on_load=DynamicState.on_load)
- def test_multiple_dynamic_args(
- index_page: ComponentCallable,
- windows_platform: bool,
- token: str,
- app_module_mock: unittest.mock.Mock,
- mocker,
- ):
- """Create app with multiple dynamic route vars with the same name.
- Args:
- index_page: The index page.
- windows_platform: Whether the system is windows.
- token: a Token.
- app_module_mock: Mocked app module.
- mocker: pytest mocker object.
- """
- arg_name = "my_arg"
- route = f"/test/[{arg_name}]"
- route2 = f"/test2/[{arg_name}]"
- app = app_module_mock.app = App(_state=EmptyState)
- app.add_page(index_page, route=route)
- app.add_page(index_page, route=route2)
- @pytest.mark.asyncio
- async def test_dynamic_route_var_route_change_completed_on_load(
- index_page: ComponentCallable,
- windows_platform: bool,
- token: str,
- app_module_mock: unittest.mock.Mock,
- mocker,
- ):
- """Create app with dynamic route var, and simulate navigation.
- on_load should fire, allowing any additional vars to be updated before the
- initial page hydrate.
- Args:
- index_page: The index page.
- windows_platform: Whether the system is windows.
- token: a Token.
- app_module_mock: Mocked app module.
- mocker: pytest mocker object.
- """
- arg_name = "dynamic"
- route = f"/test/[{arg_name}]"
- app = app_module_mock.app = App(_state=DynamicState)
- assert app._state is not None
- assert arg_name not in app._state.vars
- app.add_page(index_page, route=route, on_load=DynamicState.on_load)
- assert arg_name in app._state.vars
- assert arg_name in app._state.computed_vars
- assert app._state.computed_vars[arg_name]._deps(objclass=DynamicState) == {
- DynamicState.get_full_name(): {constants.ROUTER},
- }
- assert constants.ROUTER in app._state()._var_dependencies
- substate_token = _substate_key(token, DynamicState)
- sid = "mock_sid"
- client_ip = "127.0.0.1"
- async with app.state_manager.modify_state(substate_token) as state:
- state.router_data = {"simulate": "hydrated"}
- assert state.dynamic == ""
- exp_vals = ["foo", "foobar", "baz"]
- def _event(name, val, **kwargs):
- return Event(
- token=kwargs.pop("token", token),
- name=name,
- router_data=kwargs.pop(
- "router_data", {"pathname": route, "query": {arg_name: val}}
- ),
- payload=kwargs.pop("payload", {}),
- **kwargs,
- )
- def _dynamic_state_event(name, val, **kwargs):
- return _event(
- name=format.format_event_handler(getattr(DynamicState, name)),
- val=val,
- **kwargs,
- )
- prev_exp_val = ""
- for exp_index, exp_val in enumerate(exp_vals):
- on_load_internal = _event(
- name=f"{state.get_full_name()}.{constants.CompileVars.ON_LOAD_INTERNAL.rpartition('.')[2]}",
- val=exp_val,
- )
- exp_router_data = {
- "headers": {},
- "ip": client_ip,
- "sid": sid,
- "token": token,
- **on_load_internal.router_data,
- }
- exp_router = RouterData(exp_router_data)
- process_coro = process(
- app,
- event=on_load_internal,
- sid=sid,
- headers={},
- client_ip=client_ip,
- )
- update = await process_coro.__anext__()
- # route change (on_load_internal) triggers: [call on_load events, call set_is_hydrated(True)]
- assert update == StateUpdate(
- delta={
- state.get_name(): {
- arg_name: exp_val,
- f"comp_{arg_name}": exp_val,
- constants.CompileVars.IS_HYDRATED: False,
- "router": exp_router,
- }
- },
- events=[
- _dynamic_state_event(
- name="on_load",
- val=exp_val,
- ),
- _event(
- name=f"{State.get_name()}.set_is_hydrated",
- payload={"value": True},
- val=exp_val,
- router_data={},
- ),
- ],
- )
- if isinstance(app.state_manager, StateManagerRedis):
- # When redis is used, the state is not updated until the processing is complete
- state = await app.state_manager.get_state(substate_token)
- assert state.dynamic == prev_exp_val
- # complete the processing
- with pytest.raises(StopAsyncIteration):
- await process_coro.__anext__()
- # check that router data was written to the state_manager store
- state = await app.state_manager.get_state(substate_token)
- assert state.dynamic == exp_val
- process_coro = process(
- app,
- event=_dynamic_state_event(name="on_load", val=exp_val),
- sid=sid,
- headers={},
- client_ip=client_ip,
- )
- on_load_update = await process_coro.__anext__()
- assert on_load_update == StateUpdate(
- delta={
- state.get_name(): {
- "loaded": exp_index + 1,
- },
- },
- events=[],
- )
- # complete the processing
- with pytest.raises(StopAsyncIteration):
- await process_coro.__anext__()
- process_coro = process(
- app,
- event=_dynamic_state_event(
- name="set_is_hydrated", payload={"value": True}, val=exp_val
- ),
- sid=sid,
- headers={},
- client_ip=client_ip,
- )
- on_set_is_hydrated_update = await process_coro.__anext__()
- assert on_set_is_hydrated_update == StateUpdate(
- delta={
- state.get_name(): {
- "is_hydrated": True,
- },
- },
- events=[],
- )
- # complete the processing
- with pytest.raises(StopAsyncIteration):
- await process_coro.__anext__()
- # a simple state update event should NOT trigger on_load or route var side effects
- process_coro = process(
- app,
- event=_dynamic_state_event(name="on_counter", val=exp_val),
- sid=sid,
- headers={},
- client_ip=client_ip,
- )
- update = await process_coro.__anext__()
- assert update == StateUpdate(
- delta={
- state.get_name(): {
- "counter": exp_index + 1,
- }
- },
- events=[],
- )
- # complete the processing
- with pytest.raises(StopAsyncIteration):
- await process_coro.__anext__()
- prev_exp_val = exp_val
- state = await app.state_manager.get_state(substate_token)
- assert state.loaded == len(exp_vals)
- assert state.counter == len(exp_vals)
- if isinstance(app.state_manager, StateManagerRedis):
- await app.state_manager.close()
- @pytest.mark.asyncio
- async def test_process_events(mocker, token: str):
- """Test that an event is processed properly and that it is postprocessed
- n+1 times. Also check that the processing flag of the last stateupdate is set to
- False.
- Args:
- mocker: mocker object.
- token: a Token.
- """
- router_data = {
- "pathname": "/",
- "query": {},
- "token": token,
- "sid": "mock_sid",
- "headers": {},
- "ip": "127.0.0.1",
- }
- app = App(_state=GenState)
- mocker.patch.object(app, "_postprocess", AsyncMock())
- event = Event(
- token=token,
- name=f"{GenState.get_name()}.go",
- payload={"c": 5},
- router_data=router_data,
- )
- async with app.state_manager.modify_state(event.substate_token) as state:
- state.router_data = {"simulate": "hydrated"}
- async for _update in process(app, event, "mock_sid", {}, "127.0.0.1"):
- pass
- assert (await app.state_manager.get_state(event.substate_token)).value == 5
- assert app._postprocess.call_count == 6 # pyright: ignore [reportFunctionMemberAccess]
- if isinstance(app.state_manager, StateManagerRedis):
- await app.state_manager.close()
- @pytest.mark.parametrize(
- ("state", "overlay_component", "exp_page_child"),
- [
- (None, default_overlay_component, None),
- (None, None, None),
- (None, Text.create("foo"), Text),
- (State, default_overlay_component, Fragment),
- (State, None, None),
- (State, Text.create("foo"), Text),
- (State, lambda: Text.create("foo"), Text),
- ],
- )
- def test_overlay_component(
- state: type[State] | None,
- overlay_component: Component | ComponentCallable | None,
- exp_page_child: type[Component] | None,
- ):
- """Test that the overlay component is set correctly.
- Args:
- state: The state class to pass to App.
- overlay_component: The overlay_component to pass to App.
- exp_page_child: The type of the expected child in the page fragment.
- """
- app = App(_state=state, overlay_component=overlay_component)
- app._setup_overlay_component()
- if exp_page_child is None:
- assert app.overlay_component is None
- elif isinstance(exp_page_child, OverlayFragment):
- assert app.overlay_component is not None
- generated_component = app._generate_component(app.overlay_component)
- assert isinstance(generated_component, OverlayFragment)
- assert isinstance(
- generated_component.children[0],
- Cond, # ConnectionModal is a Cond under the hood
- )
- else:
- assert app.overlay_component is not None
- assert isinstance(
- app._generate_component(app.overlay_component),
- exp_page_child,
- )
- app.add_page(rx.box("Index"), route="/test")
- # overlay components are wrapped during compile only
- app._compile_page("test")
- app._setup_overlay_component()
- page = app._pages["test"]
- if exp_page_child is not None:
- assert len(page.children) == 3
- children_types = (type(child) for child in page.children)
- assert exp_page_child in children_types # pyright: ignore [reportOperatorIssue]
- else:
- assert len(page.children) == 2
- @pytest.fixture
- def compilable_app(tmp_path) -> Generator[tuple[App, Path], None, None]:
- """Fixture for an app that can be compiled.
- Args:
- tmp_path: Temporary path.
- Yields:
- Tuple containing (app instance, Path to ".web" directory)
- The working directory is set to the app dir (parent of .web),
- allowing app.compile() to be called.
- """
- app_path = tmp_path / "app"
- web_dir = app_path / ".web"
- web_dir.mkdir(parents=True)
- (web_dir / constants.PackageJson.PATH).touch()
- app = App(theme=None)
- app._get_frontend_packages = unittest.mock.Mock()
- with chdir(app_path):
- yield app, web_dir
- @pytest.mark.parametrize(
- "react_strict_mode",
- [True, False],
- )
- def test_app_wrap_compile_theme(
- react_strict_mode: bool, compilable_app: tuple[App, Path], mocker
- ):
- """Test that the radix theme component wraps the app.
- Args:
- react_strict_mode: Whether to use React Strict Mode.
- compilable_app: compilable_app fixture.
- mocker: pytest mocker object.
- """
- conf = rx.Config(app_name="testing", react_strict_mode=react_strict_mode)
- mocker.patch("reflex.config._get_config", return_value=conf)
- app, web_dir = compilable_app
- app.theme = rx.theme(accent_color="plum")
- app._compile()
- app_js_contents = (web_dir / "pages" / "_app.js").read_text()
- app_js_lines = [
- line.strip() for line in app_js_contents.splitlines() if line.strip()
- ]
- lines = "".join(app_js_lines)
- expected = (
- "function AppWrap({children}) {"
- "return ("
- + ("jsx(StrictMode,{}," if react_strict_mode else "")
- + "jsx(RadixThemesColorModeProvider,{},"
- "jsx(RadixThemesTheme,{accentColor:\"plum\",css:{...theme.styles.global[':root'], ...theme.styles.global.body}},"
- "jsx(Fragment,{},"
- "jsx(MemoizedToastProvider,{},),"
- "jsx(Fragment,{},"
- "children,"
- "),"
- "),"
- "),"
- ")" + (",)" if react_strict_mode else "") + ")"
- "}"
- )
- assert expected in lines
- @pytest.mark.parametrize(
- "react_strict_mode",
- [True, False],
- )
- def test_app_wrap_priority(
- react_strict_mode: bool, compilable_app: tuple[App, Path], mocker
- ):
- """Test that the app wrap components are wrapped in the correct order.
- Args:
- react_strict_mode: Whether to use React Strict Mode.
- compilable_app: compilable_app fixture.
- mocker: pytest mocker object.
- """
- conf = rx.Config(app_name="testing", react_strict_mode=react_strict_mode)
- mocker.patch("reflex.config._get_config", return_value=conf)
- app, web_dir = compilable_app
- class Fragment1(Component):
- tag = "Fragment1"
- def _get_app_wrap_components(self) -> dict[tuple[int, str], Component]: # pyright: ignore [reportIncompatibleMethodOverride]
- return {(99, "Box"): rx.box()}
- class Fragment2(Component):
- tag = "Fragment2"
- def _get_app_wrap_components(self) -> dict[tuple[int, str], Component]: # pyright: ignore [reportIncompatibleMethodOverride]
- return {(50, "Text"): rx.text()}
- class Fragment3(Component):
- tag = "Fragment3"
- def _get_app_wrap_components(self) -> dict[tuple[int, str], Component]: # pyright: ignore [reportIncompatibleMethodOverride]
- return {(10, "Fragment2"): Fragment2.create()}
- def page():
- return Fragment1.create(Fragment3.create())
- app.add_page(page)
- app._compile()
- app_js_contents = (web_dir / "pages" / "_app.js").read_text()
- app_js_lines = [
- line.strip() for line in app_js_contents.splitlines() if line.strip()
- ]
- lines = "".join(app_js_lines)
- expected = (
- "function AppWrap({children}) {"
- "return ("
- + ("jsx(StrictMode,{}," if react_strict_mode else "")
- + "jsx(RadixThemesBox,{},"
- 'jsx(RadixThemesText,{as:"p"},'
- "jsx(RadixThemesColorModeProvider,{},"
- "jsx(Fragment2,{},"
- "jsx(Fragment,{},"
- "jsx(MemoizedToastProvider,{},),"
- "jsx(Fragment,{},"
- "children"
- ",),),),),)" + (",)" if react_strict_mode else "")
- )
- assert expected in lines
- def test_app_state_determination():
- """Test that the stateless status of an app is determined correctly."""
- a1 = App()
- assert a1._state is None
- # No state, no router, no event handlers.
- a1.add_page(rx.box("Index"), route="/")
- assert a1._state is None
- # Add a page with `on_load` enables state.
- a1.add_page(rx.box("About"), route="/about", on_load=rx.console_log(""))
- a1._compile_page("about")
- assert a1._state is not None
- a2 = App()
- assert a2._state is None
- # Referencing a state Var enables state.
- a2.add_page(rx.box(rx.text(GenState.value)), route="/")
- a2._compile_page("index")
- assert a2._state is not None
- a3 = App()
- assert a3._state is None
- # Referencing router enables state.
- a3.add_page(rx.box(rx.text(State.router.page.full_path)), route="/")
- a3._compile_page("index")
- assert a3._state is not None
- a4 = App()
- assert a4._state is None
- a4.add_page(rx.box(rx.button("Click", on_click=rx.console_log(""))), route="/")
- assert a4._state is None
- a4.add_page(
- rx.box(rx.button("Click", on_click=DynamicState.on_counter)), route="/page2"
- )
- a4._compile_page("page2")
- assert a4._state is not None
- def test_raise_on_state():
- """Test that the state is set."""
- # state kwargs is deprecated, we just make sure the app is created anyway.
- _app = App(_state=State)
- assert _app._state is not None
- assert issubclass(_app._state, State)
- def test_call_app():
- """Test that the app can be called."""
- app = App()
- app._compile = unittest.mock.Mock()
- api = app()
- assert isinstance(api, Starlette)
- def test_app_with_optional_endpoints():
- from reflex.components.core.upload import Upload
- app = App()
- Upload.is_used = True
- app._add_optional_endpoints()
- # TODO: verify the availability of the endpoints in app.api
- def test_app_state_manager():
- app = App()
- with pytest.raises(ValueError):
- app.state_manager
- app._enable_state()
- assert app.state_manager is not None
- assert isinstance(
- app.state_manager, (StateManagerMemory, StateManagerDisk, StateManagerRedis)
- )
- def test_generate_component():
- def index():
- return rx.box("Index")
- def index_mismatch():
- return rx.match(
- 1,
- (1, rx.box("Index")),
- (2, "About"),
- "Bar",
- )
- comp = App._generate_component(index)
- assert isinstance(comp, Component)
- with pytest.raises(exceptions.MatchTypeError):
- App._generate_component(index_mismatch)
- def test_add_page_component_returning_tuple():
- """Test that a component or render method returning a
- tuple is unpacked in a Fragment.
- """
- app = App()
- def index():
- return rx.text("first"), rx.text("second")
- def page2():
- return (rx.text("third"),)
- app.add_page(index)
- app.add_page(page2)
- app._compile_page("index")
- app._compile_page("page2")
- fragment_wrapper = app._pages["index"].children[0]
- assert isinstance(fragment_wrapper, Fragment)
- first_text = fragment_wrapper.children[0]
- assert isinstance(first_text, Text)
- assert isinstance(first_text.children[0], Bare)
- assert str(first_text.children[0].contents) == '"first"'
- second_text = fragment_wrapper.children[1]
- assert isinstance(second_text, Text)
- assert isinstance(second_text.children[0], Bare)
- assert str(second_text.children[0].contents) == '"second"'
- # Test page with trailing comma.
- page2_fragment_wrapper = app._pages["page2"].children[0]
- assert isinstance(page2_fragment_wrapper, Fragment)
- third_text = page2_fragment_wrapper.children[0]
- assert isinstance(third_text, Text)
- assert isinstance(third_text.children[0], Bare)
- assert str(third_text.children[0].contents) == '"third"'
- @pytest.mark.parametrize("export", (True, False))
- def test_app_with_transpile_packages(compilable_app: tuple[App, Path], export: bool):
- class C1(rx.Component):
- library = "foo@1.2.3"
- tag = "Foo"
- transpile_packages: list[str] = ["foo"]
- class C2(rx.Component):
- library = "bar@4.5.6"
- tag = "Bar"
- transpile_packages: list[str] = ["bar@4.5.6"]
- class C3(rx.NoSSRComponent):
- library = "baz@7.8.10"
- tag = "Baz"
- transpile_packages: list[str] = ["baz@7.8.9"]
- class C4(rx.NoSSRComponent):
- library = "quuc@2.3.4"
- tag = "Quuc"
- transpile_packages: list[str] = ["quuc"]
- class C5(rx.Component):
- library = "quuc"
- tag = "Quuc"
- app, web_dir = compilable_app
- page = Fragment.create(
- C1.create(), C2.create(), C3.create(), C4.create(), C5.create()
- )
- app.add_page(page, route="/")
- app._compile(export=export)
- next_config = (web_dir / "next.config.js").read_text()
- transpile_packages_match = re.search(r"transpilePackages: (\[.*?\])", next_config)
- transpile_packages_json = transpile_packages_match.group(1) # pyright: ignore [reportOptionalMemberAccess]
- transpile_packages = sorted(json.loads(transpile_packages_json))
- assert transpile_packages == [
- "bar",
- "foo",
- "quuc",
- ]
- if export:
- assert 'output: "export"' in next_config
- assert f'distDir: "{constants.Dirs.STATIC}"' in next_config
- else:
- assert 'output: "export"' not in next_config
- assert f'distDir: "{constants.Dirs.STATIC}"' not in next_config
- def test_app_with_valid_var_dependencies(compilable_app: tuple[App, Path]):
- app, _ = compilable_app
- class ValidDepState(BaseState):
- base: int = 0
- _backend: int = 0
- @computed_var()
- def foo(self) -> str:
- return "foo"
- @computed_var(deps=["_backend", "base", foo])
- def bar(self) -> str:
- return "bar"
- class Child1(ValidDepState):
- @computed_var(deps=["base", ValidDepState.bar])
- def other(self) -> str:
- return "other"
- class Child2(ValidDepState):
- @computed_var(deps=["base", Child1.other])
- def other(self) -> str:
- return "other"
- app._state = ValidDepState
- app._compile()
- def test_app_with_invalid_var_dependencies(compilable_app: tuple[App, Path]):
- app, _ = compilable_app
- class InvalidDepState(BaseState):
- @computed_var(deps=["foolksjdf"])
- def bar(self) -> str:
- return "bar"
- app._state = InvalidDepState
- with pytest.raises(exceptions.VarDependencyError):
- app._compile()
- # Test custom exception handlers
- def valid_custom_handler(exception: Exception, logger: str = "test"):
- print("Custom Backend Exception")
- print(exception)
- def custom_exception_handler_with_wrong_arg_order(
- logger: str,
- exception: Exception, # Should be first
- ):
- print("Custom Backend Exception")
- print(exception)
- def custom_exception_handler_with_wrong_argspec(
- exception: str, # Should be Exception
- ):
- print("Custom Backend Exception")
- print(exception)
- class DummyExceptionHandler:
- """Dummy exception handler class."""
- def handle(self, exception: Exception):
- """Handle the exception.
- Args:
- exception: The exception.
- """
- print("Custom Backend Exception")
- print(exception)
- custom_exception_handlers = {
- "lambda": lambda exception: print("Custom Exception Handler", exception),
- "wrong_argspec": custom_exception_handler_with_wrong_argspec,
- "wrong_arg_order": custom_exception_handler_with_wrong_arg_order,
- "valid": valid_custom_handler,
- "partial": functools.partial(valid_custom_handler, logger="test"),
- "method": DummyExceptionHandler().handle,
- }
- @pytest.mark.parametrize(
- "handler_fn, expected",
- [
- pytest.param(
- custom_exception_handlers["partial"],
- pytest.raises(ValueError),
- id="partial",
- ),
- pytest.param(
- custom_exception_handlers["lambda"],
- pytest.raises(ValueError),
- id="lambda",
- ),
- pytest.param(
- custom_exception_handlers["wrong_argspec"],
- pytest.raises(ValueError),
- id="wrong_argspec",
- ),
- pytest.param(
- custom_exception_handlers["wrong_arg_order"],
- pytest.raises(ValueError),
- id="wrong_arg_order",
- ),
- pytest.param(
- custom_exception_handlers["valid"],
- does_not_raise(),
- id="valid_handler",
- ),
- pytest.param(
- custom_exception_handlers["method"],
- does_not_raise(),
- id="valid_class_method",
- ),
- ],
- )
- def test_frontend_exception_handler_validation(handler_fn, expected):
- """Test that the custom frontend exception handler is properly validated.
- Args:
- handler_fn: The handler function.
- expected: The expected result.
- """
- with expected:
- rx.App(frontend_exception_handler=handler_fn)._validate_exception_handlers()
- def backend_exception_handler_with_wrong_return_type(exception: Exception) -> int:
- """Custom backend exception handler with wrong return type.
- Args:
- exception: The exception.
- Returns:
- int: The wrong return type.
- """
- print("Custom Backend Exception")
- print(exception)
- return 5
- @pytest.mark.parametrize(
- "handler_fn, expected",
- [
- pytest.param(
- backend_exception_handler_with_wrong_return_type,
- pytest.raises(ValueError),
- id="wrong_return_type",
- ),
- pytest.param(
- custom_exception_handlers["partial"],
- pytest.raises(ValueError),
- id="partial",
- ),
- pytest.param(
- custom_exception_handlers["lambda"],
- pytest.raises(ValueError),
- id="lambda",
- ),
- pytest.param(
- custom_exception_handlers["wrong_argspec"],
- pytest.raises(ValueError),
- id="wrong_argspec",
- ),
- pytest.param(
- custom_exception_handlers["wrong_arg_order"],
- pytest.raises(ValueError),
- id="wrong_arg_order",
- ),
- pytest.param(
- custom_exception_handlers["valid"],
- does_not_raise(),
- id="valid_handler",
- ),
- pytest.param(
- custom_exception_handlers["method"],
- does_not_raise(),
- id="valid_class_method",
- ),
- ],
- )
- def test_backend_exception_handler_validation(handler_fn, expected):
- """Test that the custom backend exception handler is properly validated.
- Args:
- handler_fn: The handler function.
- expected: The expected result.
- """
- with expected:
- rx.App(backend_exception_handler=handler_fn)._validate_exception_handlers()
|