event.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. """Define event classes to connect the frontend and backend."""
  2. from __future__ import annotations
  3. import inspect
  4. from typing import (
  5. TYPE_CHECKING,
  6. Any,
  7. Callable,
  8. Dict,
  9. List,
  10. Optional,
  11. Tuple,
  12. Type,
  13. Union,
  14. )
  15. from reflex import constants
  16. from reflex.base import Base
  17. from reflex.utils import console, format
  18. from reflex.utils.types import ArgsSpec
  19. from reflex.vars import BaseVar, Var
  20. if TYPE_CHECKING:
  21. from reflex.state import State
  22. class Event(Base):
  23. """An event that describes any state change in the app."""
  24. # The token to specify the client that the event is for.
  25. token: str
  26. # The event name.
  27. name: str
  28. # The routing data where event occurred
  29. router_data: Dict[str, Any] = {}
  30. # The event payload.
  31. payload: Dict[str, Any] = {}
  32. BACKGROUND_TASK_MARKER = "_reflex_background_task"
  33. def background(fn):
  34. """Decorator to mark event handler as running in the background.
  35. Args:
  36. fn: The function to decorate.
  37. Returns:
  38. The same function, but with a marker set.
  39. Raises:
  40. TypeError: If the function is not a coroutine function or async generator.
  41. """
  42. if not inspect.iscoroutinefunction(fn) and not inspect.isasyncgenfunction(fn):
  43. raise TypeError("Background task must be async function or generator.")
  44. setattr(fn, BACKGROUND_TASK_MARKER, True)
  45. return fn
  46. def _no_chain_background_task(
  47. state_cls: Type["State"], name: str, fn: Callable
  48. ) -> Callable:
  49. """Protect against directly chaining a background task from another event handler.
  50. Args:
  51. state_cls: The state class that the event handler is in.
  52. name: The name of the background task.
  53. fn: The background task coroutine function / generator.
  54. Returns:
  55. A compatible coroutine function / generator that raises a runtime error.
  56. Raises:
  57. TypeError: If the background task is not async.
  58. """
  59. call = f"{state_cls.__name__}.{name}"
  60. message = (
  61. f"Cannot directly call background task {name!r}, use "
  62. f"`yield {call}` or `return {call}` instead."
  63. )
  64. if inspect.iscoroutinefunction(fn):
  65. async def _no_chain_background_task_co(*args, **kwargs):
  66. raise RuntimeError(message)
  67. return _no_chain_background_task_co
  68. if inspect.isasyncgenfunction(fn):
  69. async def _no_chain_background_task_gen(*args, **kwargs):
  70. yield
  71. raise RuntimeError(message)
  72. return _no_chain_background_task_gen
  73. raise TypeError(f"{fn} is marked as a background task, but is not async.")
  74. class EventHandler(Base):
  75. """An event handler responds to an event to update the state."""
  76. # The function to call in response to the event.
  77. fn: Any
  78. class Config:
  79. """The Pydantic config."""
  80. # Needed to allow serialization of Callable.
  81. frozen = True
  82. @property
  83. def is_background(self) -> bool:
  84. """Whether the event handler is a background task.
  85. Returns:
  86. True if the event handler is marked as a background task.
  87. """
  88. return getattr(self.fn, BACKGROUND_TASK_MARKER, False)
  89. def __call__(self, *args: Var) -> EventSpec:
  90. """Pass arguments to the handler to get an event spec.
  91. This method configures event handlers that take in arguments.
  92. Args:
  93. *args: The arguments to pass to the handler.
  94. Returns:
  95. The event spec, containing both the function and args.
  96. Raises:
  97. TypeError: If the arguments are invalid.
  98. """
  99. # Get the function args.
  100. fn_args = inspect.getfullargspec(self.fn).args[1:]
  101. fn_args = (Var.create_safe(arg) for arg in fn_args)
  102. # Construct the payload.
  103. values = []
  104. for arg in args:
  105. # Special case for file uploads.
  106. if isinstance(arg, FileUpload):
  107. return EventSpec(
  108. handler=self,
  109. client_handler_name="uploadFiles",
  110. # `files` is defined in the Upload component's _use_hooks
  111. args=((Var.create_safe("files"), Var.create_safe("files")),),
  112. )
  113. # Otherwise, convert to JSON.
  114. try:
  115. values.append(Var.create(arg, is_string=type(arg) is str))
  116. except TypeError as e:
  117. raise TypeError(
  118. f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
  119. ) from e
  120. payload = tuple(zip(fn_args, values))
  121. # Return the event spec.
  122. return EventSpec(handler=self, args=payload)
  123. class EventSpec(Base):
  124. """An event specification.
  125. Whereas an Event object is passed during runtime, a spec is used
  126. during compile time to outline the structure of an event.
  127. """
  128. # The event handler.
  129. handler: EventHandler
  130. # The handler on the client to process event.
  131. client_handler_name: str = ""
  132. # The arguments to pass to the function.
  133. args: Tuple[Tuple[Var, Var], ...] = ()
  134. class Config:
  135. """The Pydantic config."""
  136. # Required to allow tuple fields.
  137. frozen = True
  138. class EventChain(Base):
  139. """Container for a chain of events that will be executed in order."""
  140. events: List[EventSpec]
  141. args_spec: Optional[Callable]
  142. class Target(Base):
  143. """A Javascript event target."""
  144. checked: bool = False
  145. value: Any = None
  146. class FrontendEvent(Base):
  147. """A Javascript event."""
  148. target: Target = Target()
  149. key: str = ""
  150. value: Any = None
  151. # The default event argument.
  152. EVENT_ARG = BaseVar(name="_e", type_=FrontendEvent, is_local=True)
  153. class FileUpload(Base):
  154. """Class to represent a file upload."""
  155. pass
  156. # Special server-side events.
  157. def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec:
  158. """A server-side event.
  159. Args:
  160. name: The name of the event.
  161. sig: The function signature of the event.
  162. **kwargs: The arguments to pass to the event.
  163. Returns:
  164. An event spec for a server-side event.
  165. """
  166. def fn():
  167. return None
  168. fn.__qualname__ = name
  169. fn.__signature__ = sig
  170. return EventSpec(
  171. handler=EventHandler(fn=fn),
  172. args=tuple(
  173. (Var.create_safe(k), Var.create_safe(v, is_string=type(v) is str))
  174. for k, v in kwargs.items()
  175. ),
  176. )
  177. def redirect(path: str | Var[str], external: Optional[bool] = False) -> EventSpec:
  178. """Redirect to a new path.
  179. Args:
  180. path: The path to redirect to.
  181. external: Whether to open in new tab or not.
  182. Returns:
  183. An event to redirect to the path.
  184. """
  185. return server_side(
  186. "_redirect", get_fn_signature(redirect), path=path, external=external
  187. )
  188. def console_log(message: str | Var[str]) -> EventSpec:
  189. """Do a console.log on the browser.
  190. Args:
  191. message: The message to log.
  192. Returns:
  193. An event to log the message.
  194. """
  195. return server_side("_console", get_fn_signature(console_log), message=message)
  196. def window_alert(message: str | Var[str]) -> EventSpec:
  197. """Create a window alert on the browser.
  198. Args:
  199. message: The message to alert.
  200. Returns:
  201. An event to alert the message.
  202. """
  203. return server_side("_alert", get_fn_signature(window_alert), message=message)
  204. def set_focus(ref: str) -> EventSpec:
  205. """Set focus to specified ref.
  206. Args:
  207. ref: The ref.
  208. Returns:
  209. An event to set focus on the ref
  210. """
  211. return server_side(
  212. "_set_focus",
  213. get_fn_signature(set_focus),
  214. ref=Var.create_safe(format.format_ref(ref)),
  215. )
  216. def set_value(ref: str, value: Any) -> EventSpec:
  217. """Set the value of a ref.
  218. Args:
  219. ref: The ref.
  220. value: The value to set.
  221. Returns:
  222. An event to set the ref.
  223. """
  224. return server_side(
  225. "_set_value",
  226. get_fn_signature(set_value),
  227. ref=Var.create_safe(format.format_ref(ref)),
  228. value=value,
  229. )
  230. def set_cookie(key: str, value: str) -> EventSpec:
  231. """Set a cookie on the frontend.
  232. Args:
  233. key: The key identifying the cookie.
  234. value: The value contained in the cookie.
  235. Returns:
  236. EventSpec: An event to set a cookie.
  237. """
  238. console.deprecate(
  239. feature_name=f"rx.set_cookie",
  240. reason="and has been replaced by rx.Cookie, which can be used as a state var",
  241. deprecation_version="0.2.9",
  242. removal_version="0.2.10",
  243. )
  244. return server_side(
  245. "_set_cookie",
  246. get_fn_signature(set_cookie),
  247. key=key,
  248. value=value,
  249. )
  250. def remove_cookie(key: str, options: dict[str, Any] = {}) -> EventSpec: # noqa: B006
  251. """Remove a cookie on the frontend.
  252. Args:
  253. key: The key identifying the cookie to be removed.
  254. options: Support all the cookie options from RFC 6265
  255. Returns:
  256. EventSpec: An event to remove a cookie.
  257. """
  258. return server_side(
  259. "_remove_cookie",
  260. get_fn_signature(remove_cookie),
  261. key=key,
  262. options=options,
  263. )
  264. def set_local_storage(key: str, value: str) -> EventSpec:
  265. """Set a value in the local storage on the frontend.
  266. Args:
  267. key: The key identifying the variable in the local storage.
  268. value: The value contained in the local storage.
  269. Returns:
  270. EventSpec: An event to set a key-value in local storage.
  271. """
  272. console.deprecate(
  273. feature_name=f"rx.set_local_storage",
  274. reason="and has been replaced by rx.LocalStorage, which can be used as a state var",
  275. deprecation_version="0.2.9",
  276. removal_version="0.2.10",
  277. )
  278. return server_side(
  279. "_set_local_storage",
  280. get_fn_signature(set_local_storage),
  281. key=key,
  282. value=value,
  283. )
  284. def clear_local_storage() -> EventSpec:
  285. """Set a value in the local storage on the frontend.
  286. Returns:
  287. EventSpec: An event to clear the local storage.
  288. """
  289. return server_side(
  290. "_clear_local_storage",
  291. get_fn_signature(clear_local_storage),
  292. )
  293. def remove_local_storage(key: str) -> EventSpec:
  294. """Set a value in the local storage on the frontend.
  295. Args:
  296. key: The key identifying the variable in the local storage to remove.
  297. Returns:
  298. EventSpec: An event to remove an item based on the provided key in local storage.
  299. """
  300. return server_side(
  301. "_remove_local_storage",
  302. get_fn_signature(clear_local_storage),
  303. key=key,
  304. )
  305. def set_clipboard(content: str) -> EventSpec:
  306. """Set the text in content in the clipboard.
  307. Args:
  308. content: The text to add to clipboard.
  309. Returns:
  310. EventSpec: An event to set some content in the clipboard.
  311. """
  312. return server_side(
  313. "_set_clipboard",
  314. get_fn_signature(set_clipboard),
  315. content=content,
  316. )
  317. def download(url: str, filename: Optional[str] = None) -> EventSpec:
  318. """Download the file at a given path.
  319. Args:
  320. url : The URL to the file to download.
  321. filename : The name that the file should be saved as after download.
  322. Raises:
  323. ValueError: If the URL provided is invalid.
  324. Returns:
  325. EventSpec: An event to download the associated file.
  326. """
  327. if not url.startswith("/"):
  328. raise ValueError("The URL argument should start with a /")
  329. # if filename is not provided, infer it from url
  330. if filename is None:
  331. filename = url.rpartition("/")[-1]
  332. return server_side(
  333. "_download",
  334. get_fn_signature(download),
  335. url=url,
  336. filename=filename,
  337. )
  338. def call_script(javascript_code: str) -> EventSpec:
  339. """Create an event handler that executes arbitrary javascript code.
  340. Args:
  341. javascript_code: The code to execute.
  342. Returns:
  343. EventSpec: An event that will execute the client side javascript.
  344. """
  345. return server_side(
  346. "_call_script",
  347. get_fn_signature(call_script),
  348. javascript_code=javascript_code,
  349. )
  350. def get_event(state, event):
  351. """Get the event from the given state.
  352. Args:
  353. state: The state.
  354. event: The event.
  355. Returns:
  356. The event.
  357. """
  358. return f"{state.get_name()}.{event}"
  359. def get_hydrate_event(state) -> str:
  360. """Get the name of the hydrate event for the state.
  361. Args:
  362. state: The state.
  363. Returns:
  364. The name of the hydrate event.
  365. """
  366. return get_event(state, constants.CompileVars.HYDRATE)
  367. def call_event_handler(
  368. event_handler: EventHandler, arg_spec: Union[Var, ArgsSpec]
  369. ) -> EventSpec:
  370. """Call an event handler to get the event spec.
  371. This function will inspect the function signature of the event handler.
  372. If it takes in an arg, the arg will be passed to the event handler.
  373. Otherwise, the event handler will be called with no args.
  374. Args:
  375. event_handler: The event handler.
  376. arg_spec: The lambda that define the argument(s) to pass to the event handler.
  377. Raises:
  378. ValueError: if number of arguments expected by event_handler doesn't match the spec.
  379. Returns:
  380. The event spec from calling the event handler.
  381. """
  382. args = inspect.getfullargspec(event_handler.fn).args
  383. # handle new API using lambda to define triggers
  384. if isinstance(arg_spec, ArgsSpec):
  385. parsed_args = parse_args_spec(arg_spec) # type: ignore
  386. if len(args) == len(["self", *parsed_args]):
  387. return event_handler(*parsed_args) # type: ignore
  388. else:
  389. source = inspect.getsource(arg_spec) # type: ignore
  390. raise ValueError(
  391. f"number of arguments in {event_handler.fn.__name__} "
  392. f"doesn't match the definition '{source.strip().strip(',')}'"
  393. )
  394. else:
  395. console.deprecate(
  396. feature_name="EVENT_ARG API for triggers",
  397. reason="Replaced by new API using lambda allow arbitrary number of args",
  398. deprecation_version="0.2.8",
  399. removal_version="0.2.9",
  400. )
  401. if len(args) == 1:
  402. return event_handler()
  403. assert (
  404. len(args) == 2
  405. ), f"Event handler {event_handler.fn} must have 1 or 2 arguments."
  406. return event_handler(arg_spec) # type: ignore
  407. def parse_args_spec(arg_spec: ArgsSpec):
  408. """Parse the args provided in the ArgsSpec of an event trigger.
  409. Args:
  410. arg_spec: The spec of the args.
  411. Returns:
  412. The parsed args.
  413. """
  414. spec = inspect.getfullargspec(arg_spec)
  415. return arg_spec(
  416. *[
  417. BaseVar(
  418. name=f"_{l_arg}",
  419. type_=spec.annotations.get(l_arg, FrontendEvent),
  420. is_local=True,
  421. )
  422. for l_arg in spec.args
  423. ]
  424. )
  425. def call_event_fn(fn: Callable, arg: Union[Var, ArgsSpec]) -> list[EventSpec]:
  426. """Call a function to a list of event specs.
  427. The function should return either a single EventSpec or a list of EventSpecs.
  428. If the function takes in an arg, the arg will be passed to the function.
  429. Otherwise, the function will be called with no args.
  430. Args:
  431. fn: The function to call.
  432. arg: The argument to pass to the function.
  433. Returns:
  434. The event specs from calling the function.
  435. Raises:
  436. ValueError: If the lambda has an invalid signature.
  437. """
  438. # Import here to avoid circular imports.
  439. from reflex.event import EventHandler, EventSpec
  440. # Get the args of the lambda.
  441. args = inspect.getfullargspec(fn).args
  442. if isinstance(arg, ArgsSpec):
  443. out = fn(*parse_args_spec(arg)) # type: ignore
  444. else:
  445. # Call the lambda.
  446. if len(args) == 0:
  447. out = fn()
  448. elif len(args) == 1:
  449. out = fn(arg)
  450. else:
  451. raise ValueError(f"Lambda {fn} must have 0 or 1 arguments.")
  452. # Convert the output to a list.
  453. if not isinstance(out, List):
  454. out = [out]
  455. # Convert any event specs to event specs.
  456. events = []
  457. for e in out:
  458. # Convert handlers to event specs.
  459. if isinstance(e, EventHandler):
  460. if len(args) == 0:
  461. e = e()
  462. elif len(args) == 1:
  463. e = e(arg) # type: ignore
  464. # Make sure the event spec is valid.
  465. if not isinstance(e, EventSpec):
  466. raise ValueError(f"Lambda {fn} returned an invalid event spec: {e}.")
  467. # Add the event spec to the chain.
  468. events.append(e)
  469. # Return the events.
  470. return events
  471. def get_handler_args(event_spec: EventSpec) -> tuple[tuple[Var, Var], ...]:
  472. """Get the handler args for the given event spec.
  473. Args:
  474. event_spec: The event spec.
  475. Returns:
  476. The handler args.
  477. """
  478. args = inspect.getfullargspec(event_spec.handler.fn).args
  479. return event_spec.args if len(args) > 1 else tuple()
  480. def fix_events(
  481. events: list[EventHandler | EventSpec] | None,
  482. token: str,
  483. router_data: dict[str, Any] | None = None,
  484. ) -> list[Event]:
  485. """Fix a list of events returned by an event handler.
  486. Args:
  487. events: The events to fix.
  488. token: The user token.
  489. router_data: The optional router data to set in the event.
  490. Returns:
  491. The fixed events.
  492. """
  493. # If the event handler returns nothing, return an empty list.
  494. if events is None:
  495. return []
  496. # If the handler returns a single event, wrap it in a list.
  497. if not isinstance(events, List):
  498. events = [events]
  499. # Fix the events created by the handler.
  500. out = []
  501. for e in events:
  502. if not isinstance(e, (EventHandler, EventSpec)):
  503. e = EventHandler(fn=e)
  504. # Otherwise, create an event from the event spec.
  505. if isinstance(e, EventHandler):
  506. e = e()
  507. assert isinstance(e, EventSpec), f"Unexpected event type, {type(e)}."
  508. name = format.format_event_handler(e.handler)
  509. payload = {k.name: v._decode() for k, v in e.args} # type: ignore
  510. # Create an event and append it to the list.
  511. out.append(
  512. Event(
  513. token=token,
  514. name=name,
  515. payload=payload,
  516. router_data=router_data or {},
  517. )
  518. )
  519. return out
  520. def get_fn_signature(fn: Callable) -> inspect.Signature:
  521. """Get the signature of a function.
  522. Args:
  523. fn: The function.
  524. Returns:
  525. The signature of the function.
  526. """
  527. signature = inspect.signature(fn)
  528. new_param = inspect.Parameter(
  529. "state", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Any
  530. )
  531. return signature.replace(parameters=(new_param, *signature.parameters.values()))