event.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029
  1. """Define event classes to connect the frontend and backend."""
  2. from __future__ import annotations
  3. import inspect
  4. import urllib.parse
  5. from base64 import b64encode
  6. from typing import (
  7. Any,
  8. Callable,
  9. Dict,
  10. List,
  11. Optional,
  12. Tuple,
  13. Union,
  14. get_type_hints,
  15. )
  16. from reflex import constants
  17. from reflex.base import Base
  18. from reflex.utils import format
  19. from reflex.utils.types import ArgsSpec
  20. from reflex.vars import BaseVar, Var
  21. try:
  22. from typing import Annotated
  23. except ImportError:
  24. from typing_extensions import Annotated
  25. class Event(Base):
  26. """An event that describes any state change in the app."""
  27. # The token to specify the client that the event is for.
  28. token: str
  29. # The event name.
  30. name: str
  31. # The routing data where event occurred
  32. router_data: Dict[str, Any] = {}
  33. # The event payload.
  34. payload: Dict[str, Any] = {}
  35. @property
  36. def substate_token(self) -> str:
  37. """Get the substate token for the event.
  38. Returns:
  39. The substate token.
  40. """
  41. substate = self.name.rpartition(".")[0]
  42. return f"{self.token}_{substate}"
  43. BACKGROUND_TASK_MARKER = "_reflex_background_task"
  44. def background(fn):
  45. """Decorator to mark event handler as running in the background.
  46. Args:
  47. fn: The function to decorate.
  48. Returns:
  49. The same function, but with a marker set.
  50. Raises:
  51. TypeError: If the function is not a coroutine function or async generator.
  52. """
  53. if not inspect.iscoroutinefunction(fn) and not inspect.isasyncgenfunction(fn):
  54. raise TypeError("Background task must be async function or generator.")
  55. setattr(fn, BACKGROUND_TASK_MARKER, True)
  56. return fn
  57. class EventActionsMixin(Base):
  58. """Mixin for DOM event actions."""
  59. # Whether to `preventDefault` or `stopPropagation` on the event.
  60. event_actions: Dict[str, Union[bool, int]] = {}
  61. @property
  62. def stop_propagation(self):
  63. """Stop the event from bubbling up the DOM tree.
  64. Returns:
  65. New EventHandler-like with stopPropagation set to True.
  66. """
  67. return self.copy(
  68. update={"event_actions": {"stopPropagation": True, **self.event_actions}},
  69. )
  70. @property
  71. def prevent_default(self):
  72. """Prevent the default behavior of the event.
  73. Returns:
  74. New EventHandler-like with preventDefault set to True.
  75. """
  76. return self.copy(
  77. update={"event_actions": {"preventDefault": True, **self.event_actions}},
  78. )
  79. def throttle(self, limit_ms: int):
  80. """Throttle the event handler.
  81. Args:
  82. limit_ms: The time in milliseconds to throttle the event handler.
  83. Returns:
  84. New EventHandler-like with throttle set to limit_ms.
  85. """
  86. return self.copy(
  87. update={"event_actions": {"throttle": limit_ms, **self.event_actions}},
  88. )
  89. def debounce(self, delay_ms: int):
  90. """Debounce the event handler.
  91. Args:
  92. delay_ms: The time in milliseconds to debounce the event handler.
  93. Returns:
  94. New EventHandler-like with debounce set to delay_ms.
  95. """
  96. return self.copy(
  97. update={"event_actions": {"debounce": delay_ms, **self.event_actions}},
  98. )
  99. class EventHandler(EventActionsMixin):
  100. """An event handler responds to an event to update the state."""
  101. # The function to call in response to the event.
  102. fn: Any
  103. # The full name of the state class this event handler is attached to.
  104. # Empty string means this event handler is a server side event.
  105. state_full_name: str = ""
  106. class Config:
  107. """The Pydantic config."""
  108. # Needed to allow serialization of Callable.
  109. frozen = True
  110. @classmethod
  111. def __class_getitem__(cls, args_spec: str) -> Annotated:
  112. """Get a typed EventHandler.
  113. Args:
  114. args_spec: The args_spec of the EventHandler.
  115. Returns:
  116. The EventHandler class item.
  117. """
  118. return Annotated[cls, args_spec]
  119. @property
  120. def is_background(self) -> bool:
  121. """Whether the event handler is a background task.
  122. Returns:
  123. True if the event handler is marked as a background task.
  124. """
  125. return getattr(self.fn, BACKGROUND_TASK_MARKER, False)
  126. def __call__(self, *args: Any) -> EventSpec:
  127. """Pass arguments to the handler to get an event spec.
  128. This method configures event handlers that take in arguments.
  129. Args:
  130. *args: The arguments to pass to the handler.
  131. Returns:
  132. The event spec, containing both the function and args.
  133. Raises:
  134. EventHandlerTypeError: If the arguments are invalid.
  135. """
  136. from reflex.utils.exceptions import EventHandlerTypeError
  137. # Get the function args.
  138. fn_args = inspect.getfullargspec(self.fn).args[1:]
  139. fn_args = (Var.create_safe(arg, _var_is_string=False) for arg in fn_args)
  140. # Construct the payload.
  141. values = []
  142. for arg in args:
  143. # Special case for file uploads.
  144. if isinstance(arg, FileUpload):
  145. return arg.as_event_spec(handler=self)
  146. # Otherwise, convert to JSON.
  147. try:
  148. values.append(Var.create(arg, _var_is_string=isinstance(arg, str)))
  149. except TypeError as e:
  150. raise EventHandlerTypeError(
  151. f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
  152. ) from e
  153. payload = tuple(zip(fn_args, values))
  154. # Return the event spec.
  155. return EventSpec(
  156. handler=self, args=payload, event_actions=self.event_actions.copy()
  157. )
  158. class EventSpec(EventActionsMixin):
  159. """An event specification.
  160. Whereas an Event object is passed during runtime, a spec is used
  161. during compile time to outline the structure of an event.
  162. """
  163. # The event handler.
  164. handler: EventHandler
  165. # The handler on the client to process event.
  166. client_handler_name: str = ""
  167. # The arguments to pass to the function.
  168. args: Tuple[Tuple[Var, Var], ...] = ()
  169. class Config:
  170. """The Pydantic config."""
  171. # Required to allow tuple fields.
  172. frozen = True
  173. def with_args(self, args: Tuple[Tuple[Var, Var], ...]) -> EventSpec:
  174. """Copy the event spec, with updated args.
  175. Args:
  176. args: The new args to pass to the function.
  177. Returns:
  178. A copy of the event spec, with the new args.
  179. """
  180. return type(self)(
  181. handler=self.handler,
  182. client_handler_name=self.client_handler_name,
  183. args=args,
  184. event_actions=self.event_actions.copy(),
  185. )
  186. def add_args(self, *args: Var) -> EventSpec:
  187. """Add arguments to the event spec.
  188. Args:
  189. *args: The arguments to add positionally.
  190. Returns:
  191. The event spec with the new arguments.
  192. Raises:
  193. EventHandlerTypeError: If the arguments are invalid.
  194. """
  195. from reflex.utils.exceptions import EventHandlerTypeError
  196. # Get the remaining unfilled function args.
  197. fn_args = inspect.getfullargspec(self.handler.fn).args[1 + len(self.args) :]
  198. fn_args = (Var.create_safe(arg, _var_is_string=False) for arg in fn_args)
  199. # Construct the payload.
  200. values = []
  201. for arg in args:
  202. try:
  203. values.append(Var.create(arg, _var_is_string=isinstance(arg, str)))
  204. except TypeError as e:
  205. raise EventHandlerTypeError(
  206. f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
  207. ) from e
  208. new_payload = tuple(zip(fn_args, values))
  209. return self.with_args(self.args + new_payload)
  210. class CallableEventSpec(EventSpec):
  211. """Decorate an EventSpec-returning function to act as both a EventSpec and a function.
  212. This is used as a compatibility shim for replacing EventSpec objects in the
  213. API with functions that return a family of EventSpec.
  214. """
  215. fn: Optional[Callable[..., EventSpec]] = None
  216. def __init__(self, fn: Callable[..., EventSpec] | None = None, **kwargs):
  217. """Initialize a CallableEventSpec.
  218. Args:
  219. fn: The function to decorate.
  220. **kwargs: The kwargs to pass to pydantic initializer
  221. """
  222. if fn is not None:
  223. default_event_spec = fn()
  224. super().__init__(
  225. fn=fn, # type: ignore
  226. **default_event_spec.dict(),
  227. **kwargs,
  228. )
  229. else:
  230. super().__init__(**kwargs)
  231. def __call__(self, *args, **kwargs) -> EventSpec:
  232. """Call the decorated function.
  233. Args:
  234. *args: The args to pass to the function.
  235. **kwargs: The kwargs to pass to the function.
  236. Returns:
  237. The EventSpec returned from calling the function.
  238. Raises:
  239. EventHandlerTypeError: If the CallableEventSpec has no associated function.
  240. """
  241. from reflex.utils.exceptions import EventHandlerTypeError
  242. if self.fn is None:
  243. raise EventHandlerTypeError("CallableEventSpec has no associated function.")
  244. return self.fn(*args, **kwargs)
  245. class EventChain(EventActionsMixin):
  246. """Container for a chain of events that will be executed in order."""
  247. events: List[EventSpec]
  248. args_spec: Optional[Callable]
  249. # These chains can be used for their side effects when no other events are desired.
  250. stop_propagation = EventChain(events=[], args_spec=lambda: []).stop_propagation
  251. prevent_default = EventChain(events=[], args_spec=lambda: []).prevent_default
  252. class Target(Base):
  253. """A Javascript event target."""
  254. checked: bool = False
  255. value: Any = None
  256. class FrontendEvent(Base):
  257. """A Javascript event."""
  258. target: Target = Target()
  259. key: str = ""
  260. value: Any = None
  261. class FileUpload(Base):
  262. """Class to represent a file upload."""
  263. upload_id: Optional[str] = None
  264. on_upload_progress: Optional[Union[EventHandler, Callable]] = None
  265. @staticmethod
  266. def on_upload_progress_args_spec(_prog: Dict[str, Union[int, float, bool]]):
  267. """Args spec for on_upload_progress event handler.
  268. Returns:
  269. The arg mapping passed to backend event handler
  270. """
  271. return [_prog]
  272. def as_event_spec(self, handler: EventHandler) -> EventSpec:
  273. """Get the EventSpec for the file upload.
  274. Args:
  275. handler: The event handler.
  276. Returns:
  277. The event spec for the handler.
  278. Raises:
  279. ValueError: If the on_upload_progress is not a valid event handler.
  280. """
  281. from reflex.components.core.upload import (
  282. DEFAULT_UPLOAD_ID,
  283. upload_files_context_var_data,
  284. )
  285. upload_id = self.upload_id or DEFAULT_UPLOAD_ID
  286. spec_args = [
  287. (
  288. Var.create_safe("files", _var_is_string=False),
  289. Var.create_safe(
  290. f"filesById[{Var.create_safe(upload_id, _var_is_string=True)._var_name_unwrapped}]",
  291. _var_is_string=False,
  292. )._replace(_var_data=upload_files_context_var_data),
  293. ),
  294. (
  295. Var.create_safe("upload_id", _var_is_string=False),
  296. Var.create_safe(upload_id, _var_is_string=True),
  297. ),
  298. ]
  299. if self.on_upload_progress is not None:
  300. on_upload_progress = self.on_upload_progress
  301. if isinstance(on_upload_progress, EventHandler):
  302. events = [
  303. call_event_handler(
  304. on_upload_progress,
  305. self.on_upload_progress_args_spec,
  306. ),
  307. ]
  308. elif isinstance(on_upload_progress, Callable):
  309. # Call the lambda to get the event chain.
  310. events = call_event_fn(
  311. on_upload_progress, self.on_upload_progress_args_spec
  312. ) # type: ignore
  313. else:
  314. raise ValueError(f"{on_upload_progress} is not a valid event handler.")
  315. if isinstance(events, Var):
  316. raise ValueError(f"{on_upload_progress} cannot return a var {events}.")
  317. on_upload_progress_chain = EventChain(
  318. events=events,
  319. args_spec=self.on_upload_progress_args_spec,
  320. )
  321. formatted_chain = str(format.format_prop(on_upload_progress_chain))
  322. spec_args.append(
  323. (
  324. Var.create_safe("on_upload_progress", _var_is_string=False),
  325. BaseVar(
  326. _var_name=formatted_chain.strip("{}"),
  327. _var_type=EventChain,
  328. ),
  329. ),
  330. )
  331. return EventSpec(
  332. handler=handler,
  333. client_handler_name="uploadFiles",
  334. args=tuple(spec_args),
  335. event_actions=handler.event_actions.copy(),
  336. )
  337. # Alias for rx.upload_files
  338. upload_files = FileUpload
  339. # Special server-side events.
  340. def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec:
  341. """A server-side event.
  342. Args:
  343. name: The name of the event.
  344. sig: The function signature of the event.
  345. **kwargs: The arguments to pass to the event.
  346. Returns:
  347. An event spec for a server-side event.
  348. """
  349. def fn():
  350. return None
  351. fn.__qualname__ = name
  352. fn.__signature__ = sig
  353. return EventSpec(
  354. handler=EventHandler(fn=fn),
  355. args=tuple(
  356. (
  357. Var.create_safe(k, _var_is_string=False),
  358. Var.create_safe(v, _var_is_string=isinstance(v, str)),
  359. )
  360. for k, v in kwargs.items()
  361. ),
  362. )
  363. def redirect(
  364. path: str | Var[str],
  365. external: Optional[bool] = False,
  366. replace: Optional[bool] = False,
  367. ) -> EventSpec:
  368. """Redirect to a new path.
  369. Args:
  370. path: The path to redirect to.
  371. external: Whether to open in new tab or not.
  372. replace: If True, the current page will not create a new history entry.
  373. Returns:
  374. An event to redirect to the path.
  375. """
  376. return server_side(
  377. "_redirect",
  378. get_fn_signature(redirect),
  379. path=path,
  380. external=external,
  381. replace=replace,
  382. )
  383. def console_log(message: str | Var[str]) -> EventSpec:
  384. """Do a console.log on the browser.
  385. Args:
  386. message: The message to log.
  387. Returns:
  388. An event to log the message.
  389. """
  390. return server_side("_console", get_fn_signature(console_log), message=message)
  391. def back() -> EventSpec:
  392. """Do a history.back on the browser.
  393. Returns:
  394. An event to go back one page.
  395. """
  396. return call_script("window.history.back()")
  397. def window_alert(message: str | Var[str]) -> EventSpec:
  398. """Create a window alert on the browser.
  399. Args:
  400. message: The message to alert.
  401. Returns:
  402. An event to alert the message.
  403. """
  404. return server_side("_alert", get_fn_signature(window_alert), message=message)
  405. def set_focus(ref: str) -> EventSpec:
  406. """Set focus to specified ref.
  407. Args:
  408. ref: The ref.
  409. Returns:
  410. An event to set focus on the ref
  411. """
  412. return server_side(
  413. "_set_focus",
  414. get_fn_signature(set_focus),
  415. ref=Var.create_safe(format.format_ref(ref), _var_is_string=True),
  416. )
  417. def scroll_to(elem_id: str) -> EventSpec:
  418. """Select the id of a html element for scrolling into view.
  419. Args:
  420. elem_id: the id of the element
  421. Returns:
  422. An EventSpec to scroll the page to the selected element.
  423. """
  424. js_code = f"document.getElementById('{elem_id}').scrollIntoView();"
  425. return call_script(js_code)
  426. def set_value(ref: str, value: Any) -> EventSpec:
  427. """Set the value of a ref.
  428. Args:
  429. ref: The ref.
  430. value: The value to set.
  431. Returns:
  432. An event to set the ref.
  433. """
  434. return server_side(
  435. "_set_value",
  436. get_fn_signature(set_value),
  437. ref=Var.create_safe(format.format_ref(ref), _var_is_string=True),
  438. value=value,
  439. )
  440. def remove_cookie(key: str, options: dict[str, Any] | None = None) -> EventSpec:
  441. """Remove a cookie on the frontend.
  442. Args:
  443. key: The key identifying the cookie to be removed.
  444. options: Support all the cookie options from RFC 6265
  445. Returns:
  446. EventSpec: An event to remove a cookie.
  447. """
  448. options = options or {}
  449. options["path"] = options.get("path", "/")
  450. return server_side(
  451. "_remove_cookie",
  452. get_fn_signature(remove_cookie),
  453. key=key,
  454. options=options,
  455. )
  456. def clear_local_storage() -> EventSpec:
  457. """Set a value in the local storage on the frontend.
  458. Returns:
  459. EventSpec: An event to clear the local storage.
  460. """
  461. return server_side(
  462. "_clear_local_storage",
  463. get_fn_signature(clear_local_storage),
  464. )
  465. def remove_local_storage(key: str) -> EventSpec:
  466. """Set a value in the local storage on the frontend.
  467. Args:
  468. key: The key identifying the variable in the local storage to remove.
  469. Returns:
  470. EventSpec: An event to remove an item based on the provided key in local storage.
  471. """
  472. return server_side(
  473. "_remove_local_storage",
  474. get_fn_signature(remove_local_storage),
  475. key=key,
  476. )
  477. def clear_session_storage() -> EventSpec:
  478. """Set a value in the session storage on the frontend.
  479. Returns:
  480. EventSpec: An event to clear the session storage.
  481. """
  482. return server_side(
  483. "_clear_session_storage",
  484. get_fn_signature(clear_session_storage),
  485. )
  486. def remove_session_storage(key: str) -> EventSpec:
  487. """Set a value in the session storage on the frontend.
  488. Args:
  489. key: The key identifying the variable in the session storage to remove.
  490. Returns:
  491. EventSpec: An event to remove an item based on the provided key in session storage.
  492. """
  493. return server_side(
  494. "_remove_session_storage",
  495. get_fn_signature(remove_session_storage),
  496. key=key,
  497. )
  498. def set_clipboard(content: str) -> EventSpec:
  499. """Set the text in content in the clipboard.
  500. Args:
  501. content: The text to add to clipboard.
  502. Returns:
  503. EventSpec: An event to set some content in the clipboard.
  504. """
  505. return server_side(
  506. "_set_clipboard",
  507. get_fn_signature(set_clipboard),
  508. content=content,
  509. )
  510. def download(
  511. url: str | Var | None = None,
  512. filename: Optional[str | Var] = None,
  513. data: str | bytes | Var | None = None,
  514. ) -> EventSpec:
  515. """Download the file at a given path or with the specified data.
  516. Args:
  517. url: The URL to the file to download.
  518. filename: The name that the file should be saved as after download.
  519. data: The data to download.
  520. Raises:
  521. ValueError: If the URL provided is invalid, both URL and data are provided,
  522. or the data is not an expected type.
  523. Returns:
  524. EventSpec: An event to download the associated file.
  525. """
  526. from reflex.components.core.cond import cond
  527. if isinstance(url, str):
  528. if not url.startswith("/"):
  529. raise ValueError("The URL argument should start with a /")
  530. # if filename is not provided, infer it from url
  531. if filename is None:
  532. filename = url.rpartition("/")[-1]
  533. if filename is None:
  534. filename = ""
  535. if data is not None:
  536. if url is not None:
  537. raise ValueError("Cannot provide both URL and data to download.")
  538. if isinstance(data, str):
  539. # Caller provided a plain text string to download.
  540. url = "data:text/plain," + urllib.parse.quote(data)
  541. elif isinstance(data, Var):
  542. # Need to check on the frontend if the Var already looks like a data: URI.
  543. is_data_url = data._replace(
  544. _var_name=(
  545. f"typeof {data._var_full_name} == 'string' && "
  546. f"{data._var_full_name}.startsWith('data:')"
  547. ),
  548. _var_type=bool,
  549. _var_is_string=False,
  550. _var_full_name_needs_state_prefix=False,
  551. )
  552. # If it's a data: URI, use it as is, otherwise convert the Var to JSON in a data: URI.
  553. url = cond( # type: ignore
  554. is_data_url,
  555. data,
  556. "data:text/plain," + data.to_string(), # type: ignore
  557. )
  558. elif isinstance(data, bytes):
  559. # Caller provided bytes, so base64 encode it as a data: URI.
  560. b64_data = b64encode(data).decode("utf-8")
  561. url = "data:application/octet-stream;base64," + b64_data
  562. else:
  563. raise ValueError(
  564. f"Invalid data type {type(data)} for download. Use `str` or `bytes`."
  565. )
  566. return server_side(
  567. "_download",
  568. get_fn_signature(download),
  569. url=url,
  570. filename=filename,
  571. )
  572. def _callback_arg_spec(eval_result):
  573. """ArgSpec for call_script callback function.
  574. Args:
  575. eval_result: The result of the javascript execution.
  576. Returns:
  577. Args for the callback function
  578. """
  579. return [eval_result]
  580. def call_script(
  581. javascript_code: str | Var[str],
  582. callback: EventSpec
  583. | EventHandler
  584. | Callable
  585. | List[EventSpec | EventHandler | Callable]
  586. | None = None,
  587. ) -> EventSpec:
  588. """Create an event handler that executes arbitrary javascript code.
  589. Args:
  590. javascript_code: The code to execute.
  591. callback: EventHandler that will receive the result of evaluating the javascript code.
  592. Returns:
  593. EventSpec: An event that will execute the client side javascript.
  594. """
  595. callback_kwargs = {}
  596. if callback is not None:
  597. callback_kwargs = {
  598. "callback": str(
  599. format.format_queue_events(
  600. callback,
  601. args_spec=lambda result: [result],
  602. ),
  603. ),
  604. }
  605. return server_side(
  606. "_call_script",
  607. get_fn_signature(call_script),
  608. javascript_code=javascript_code,
  609. **callback_kwargs,
  610. )
  611. def get_event(state, event):
  612. """Get the event from the given state.
  613. Args:
  614. state: The state.
  615. event: The event.
  616. Returns:
  617. The event.
  618. """
  619. return f"{state.get_name()}.{event}"
  620. def get_hydrate_event(state) -> str:
  621. """Get the name of the hydrate event for the state.
  622. Args:
  623. state: The state.
  624. Returns:
  625. The name of the hydrate event.
  626. """
  627. return get_event(state, constants.CompileVars.HYDRATE)
  628. def call_event_handler(
  629. event_handler: EventHandler | EventSpec,
  630. arg_spec: ArgsSpec,
  631. ) -> EventSpec:
  632. """Call an event handler to get the event spec.
  633. This function will inspect the function signature of the event handler.
  634. If it takes in an arg, the arg will be passed to the event handler.
  635. Otherwise, the event handler will be called with no args.
  636. Args:
  637. event_handler: The event handler.
  638. arg_spec: The lambda that define the argument(s) to pass to the event handler.
  639. Raises:
  640. ValueError: if number of arguments expected by event_handler doesn't match the spec.
  641. Returns:
  642. The event spec from calling the event handler.
  643. """
  644. parsed_args = parse_args_spec(arg_spec) # type: ignore
  645. if isinstance(event_handler, EventSpec):
  646. # Handle partial application of EventSpec args
  647. return event_handler.add_args(*parsed_args)
  648. args = inspect.getfullargspec(event_handler.fn).args
  649. if len(args) == len(["self", *parsed_args]):
  650. return event_handler(*parsed_args) # type: ignore
  651. else:
  652. source = inspect.getsource(arg_spec) # type: ignore
  653. raise ValueError(
  654. f"number of arguments in {event_handler.fn.__qualname__} "
  655. f"doesn't match the definition of the event trigger '{source.strip().strip(',')}'"
  656. )
  657. def parse_args_spec(arg_spec: ArgsSpec):
  658. """Parse the args provided in the ArgsSpec of an event trigger.
  659. Args:
  660. arg_spec: The spec of the args.
  661. Returns:
  662. The parsed args.
  663. """
  664. spec = inspect.getfullargspec(arg_spec)
  665. annotations = get_type_hints(arg_spec)
  666. return arg_spec(
  667. *[
  668. BaseVar(
  669. _var_name=f"_{l_arg}",
  670. _var_type=annotations.get(l_arg, FrontendEvent),
  671. _var_is_local=True,
  672. )
  673. for l_arg in spec.args
  674. ]
  675. )
  676. def call_event_fn(fn: Callable, arg: Union[Var, ArgsSpec]) -> list[EventSpec] | Var:
  677. """Call a function to a list of event specs.
  678. The function should return a single EventSpec, a list of EventSpecs, or a
  679. single Var. If the function takes in an arg, the arg will be passed to the
  680. function. Otherwise, the function will be called with no args.
  681. Args:
  682. fn: The function to call.
  683. arg: The argument to pass to the function.
  684. Returns:
  685. The event specs from calling the function or a Var.
  686. Raises:
  687. EventHandlerValueError: If the lambda has an invalid signature.
  688. """
  689. # Import here to avoid circular imports.
  690. from reflex.event import EventHandler, EventSpec
  691. from reflex.utils.exceptions import EventHandlerValueError
  692. # Get the args of the lambda.
  693. args = inspect.getfullargspec(fn).args
  694. if isinstance(arg, ArgsSpec):
  695. out = fn(*parse_args_spec(arg)) # type: ignore
  696. else:
  697. # Call the lambda.
  698. if len(args) == 0:
  699. out = fn()
  700. elif len(args) == 1:
  701. out = fn(arg)
  702. else:
  703. raise EventHandlerValueError(f"Lambda {fn} must have 0 or 1 arguments.")
  704. # If the function returns a Var, assume it's an EventChain and render it directly.
  705. if isinstance(out, Var):
  706. return out
  707. # Convert the output to a list.
  708. if not isinstance(out, List):
  709. out = [out]
  710. # Convert any event specs to event specs.
  711. events = []
  712. for e in out:
  713. # Convert handlers to event specs.
  714. if isinstance(e, EventHandler):
  715. if len(args) == 0:
  716. e = e()
  717. elif len(args) == 1:
  718. e = e(arg) # type: ignore
  719. # Make sure the event spec is valid.
  720. if not isinstance(e, EventSpec):
  721. raise EventHandlerValueError(
  722. f"Lambda {fn} returned an invalid event spec: {e}."
  723. )
  724. # Add the event spec to the chain.
  725. events.append(e)
  726. # Return the events.
  727. return events
  728. def get_handler_args(event_spec: EventSpec) -> tuple[tuple[Var, Var], ...]:
  729. """Get the handler args for the given event spec.
  730. Args:
  731. event_spec: The event spec.
  732. Returns:
  733. The handler args.
  734. """
  735. args = inspect.getfullargspec(event_spec.handler.fn).args
  736. return event_spec.args if len(args) > 1 else tuple()
  737. def fix_events(
  738. events: list[EventHandler | EventSpec] | None,
  739. token: str,
  740. router_data: dict[str, Any] | None = None,
  741. ) -> list[Event]:
  742. """Fix a list of events returned by an event handler.
  743. Args:
  744. events: The events to fix.
  745. token: The user token.
  746. router_data: The optional router data to set in the event.
  747. Returns:
  748. The fixed events.
  749. """
  750. # If the event handler returns nothing, return an empty list.
  751. if events is None:
  752. return []
  753. # If the handler returns a single event, wrap it in a list.
  754. if not isinstance(events, List):
  755. events = [events]
  756. # Fix the events created by the handler.
  757. out = []
  758. for e in events:
  759. if isinstance(e, Event):
  760. # If the event is already an event, append it to the list.
  761. out.append(e)
  762. continue
  763. if not isinstance(e, (EventHandler, EventSpec)):
  764. e = EventHandler(fn=e)
  765. # Otherwise, create an event from the event spec.
  766. if isinstance(e, EventHandler):
  767. e = e()
  768. assert isinstance(e, EventSpec), f"Unexpected event type, {type(e)}."
  769. name = format.format_event_handler(e.handler)
  770. payload = {k._var_name: v._decode() for k, v in e.args} # type: ignore
  771. # Filter router_data to reduce payload size
  772. event_router_data = {
  773. k: v
  774. for k, v in (router_data or {}).items()
  775. if k in constants.route.ROUTER_DATA_INCLUDE
  776. }
  777. # Create an event and append it to the list.
  778. out.append(
  779. Event(
  780. token=token,
  781. name=name,
  782. payload=payload,
  783. router_data=event_router_data,
  784. )
  785. )
  786. return out
  787. def get_fn_signature(fn: Callable) -> inspect.Signature:
  788. """Get the signature of a function.
  789. Args:
  790. fn: The function.
  791. Returns:
  792. The signature of the function.
  793. """
  794. signature = inspect.signature(fn)
  795. new_param = inspect.Parameter(
  796. "state", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Any
  797. )
  798. return signature.replace(parameters=(new_param, *signature.parameters.values()))