event.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. """Define event classes to connect the frontend and backend."""
  2. from __future__ import annotations
  3. import inspect
  4. from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
  5. from pynecone import constants
  6. from pynecone.base import Base
  7. from pynecone.utils import format
  8. from pynecone.vars import BaseVar, Var
  9. class Event(Base):
  10. """An event that describes any state change in the app."""
  11. # The token to specify the client that the event is for.
  12. token: str
  13. # The event name.
  14. name: str
  15. # The routing data where event occurred
  16. router_data: Dict[str, Any] = {}
  17. # The event payload.
  18. payload: Dict[str, Any] = {}
  19. class EventHandler(Base):
  20. """An event handler responds to an event to update the state."""
  21. # The function to call in response to the event.
  22. fn: Any
  23. class Config:
  24. """The Pydantic config."""
  25. # Needed to allow serialization of Callable.
  26. frozen = True
  27. def __call__(self, *args: Var) -> EventSpec:
  28. """Pass arguments to the handler to get an event spec.
  29. This method configures event handlers that take in arguments.
  30. Args:
  31. *args: The arguments to pass to the handler.
  32. Returns:
  33. The event spec, containing both the function and args.
  34. Raises:
  35. TypeError: If the arguments are invalid.
  36. """
  37. # Get the function args.
  38. fn_args = inspect.getfullargspec(self.fn).args[1:]
  39. fn_args = (Var.create_safe(arg) for arg in fn_args)
  40. # Construct the payload.
  41. values = []
  42. for arg in args:
  43. # Special case for file uploads.
  44. if isinstance(arg, FileUpload):
  45. return EventSpec(
  46. handler=self,
  47. client_handler_name="uploadFiles",
  48. )
  49. # Otherwise, convert to JSON.
  50. try:
  51. values.append(Var.create(arg, is_string=type(arg) is str))
  52. except TypeError as e:
  53. raise TypeError(
  54. f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
  55. ) from e
  56. payload = tuple(zip(fn_args, values))
  57. # Return the event spec.
  58. return EventSpec(handler=self, args=payload)
  59. class EventSpec(Base):
  60. """An event specification.
  61. Whereas an Event object is passed during runtime, a spec is used
  62. during compile time to outline the structure of an event.
  63. """
  64. # The event handler.
  65. handler: EventHandler
  66. # The handler on the client to process event.
  67. client_handler_name: str = ""
  68. # The arguments to pass to the function.
  69. args: Tuple[Tuple[Var, Var], ...] = ()
  70. class Config:
  71. """The Pydantic config."""
  72. # Required to allow tuple fields.
  73. frozen = True
  74. class EventChain(Base):
  75. """Container for a chain of events that will be executed in order."""
  76. events: List[EventSpec]
  77. # Whether events are in fully controlled input.
  78. full_control: bool = False
  79. # State name when fully controlled.
  80. state_name: str = ""
  81. class Target(Base):
  82. """A Javascript event target."""
  83. checked: bool = False
  84. value: Any = None
  85. class FrontendEvent(Base):
  86. """A Javascript event."""
  87. target: Target = Target()
  88. key: str = ""
  89. value: Any = None
  90. # The default event argument.
  91. EVENT_ARG = BaseVar(name="_e", type_=FrontendEvent, is_local=True)
  92. class FileUpload(Base):
  93. """Class to represent a file upload."""
  94. pass
  95. # Special server-side events.
  96. def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec:
  97. """A server-side event.
  98. Args:
  99. name: The name of the event.
  100. sig: The function signature of the event.
  101. **kwargs: The arguments to pass to the event.
  102. Returns:
  103. An event spec for a server-side event.
  104. """
  105. def fn():
  106. return None
  107. fn.__qualname__ = name
  108. fn.__signature__ = sig
  109. return EventSpec(
  110. handler=EventHandler(fn=fn),
  111. args=tuple(
  112. (Var.create_safe(k), Var.create_safe(v, is_string=type(v) is str))
  113. for k, v in kwargs.items()
  114. ),
  115. )
  116. def redirect(path: Union[str, Var[str]]) -> EventSpec:
  117. """Redirect to a new path.
  118. Args:
  119. path: The path to redirect to.
  120. Returns:
  121. An event to redirect to the path.
  122. """
  123. return server_side("_redirect", get_fn_signature(redirect), path=path)
  124. def console_log(message: Union[str, Var[str]]) -> EventSpec:
  125. """Do a console.log on the browser.
  126. Args:
  127. message: The message to log.
  128. Returns:
  129. An event to log the message.
  130. """
  131. return server_side("_console", get_fn_signature(console_log), message=message)
  132. def window_alert(message: Union[str, Var[str]]) -> EventSpec:
  133. """Create a window alert on the browser.
  134. Args:
  135. message: The message to alert.
  136. Returns:
  137. An event to alert the message.
  138. """
  139. return server_side("_alert", get_fn_signature(window_alert), message=message)
  140. def set_focus(ref: str) -> EventSpec:
  141. """Set focus to specified ref.
  142. Args:
  143. ref: The ref.
  144. Returns:
  145. An event to set focus on the ref
  146. """
  147. return server_side(
  148. "_set_focus",
  149. get_fn_signature(set_focus),
  150. ref=Var.create_safe(format.format_ref(ref)),
  151. )
  152. def set_value(ref: str, value: Any) -> EventSpec:
  153. """Set the value of a ref.
  154. Args:
  155. ref: The ref.
  156. value: The value to set.
  157. Returns:
  158. An event to set the ref.
  159. """
  160. return server_side(
  161. "_set_value",
  162. get_fn_signature(set_value),
  163. ref=Var.create_safe(format.format_ref(ref)),
  164. value=value,
  165. )
  166. def set_cookie(key: str, value: str) -> EventSpec:
  167. """Set a cookie on the frontend.
  168. Args:
  169. key (str): The key identifying the cookie.
  170. value (str): The value contained in the cookie.
  171. Returns:
  172. EventSpec: An event to set a cookie.
  173. """
  174. return server_side(
  175. "_set_cookie",
  176. get_fn_signature(set_cookie),
  177. key=key,
  178. value=value,
  179. )
  180. def set_local_storage(key: str, value: str) -> EventSpec:
  181. """Set a value in the local storage on the frontend.
  182. Args:
  183. key (str): The key identifying the variable in the local storage.
  184. value (str): The value contained in the local storage.
  185. Returns:
  186. EventSpec: An event to set a key-value in local storage.
  187. """
  188. return server_side(
  189. "_set_local_storage",
  190. get_fn_signature(set_local_storage),
  191. key=key,
  192. value=value,
  193. )
  194. def set_clipboard(content: str) -> EventSpec:
  195. """Set the text in content in the clipboard.
  196. Args:
  197. content: The text to add to clipboard.
  198. Returns:
  199. EventSpec: An event to set some content in the clipboard.
  200. """
  201. return server_side(
  202. "_set_clipboard",
  203. get_fn_signature(set_clipboard),
  204. content=content,
  205. )
  206. def get_event(state, event):
  207. """Get the event from the given state.
  208. Args:
  209. state: The state.
  210. event: The event.
  211. Returns:
  212. The event.
  213. """
  214. return f"{state.get_name()}.{event}"
  215. def get_hydrate_event(state) -> str:
  216. """Get the name of the hydrate event for the state.
  217. Args:
  218. state: The state.
  219. Returns:
  220. The name of the hydrate event.
  221. """
  222. return get_event(state, constants.HYDRATE)
  223. def call_event_handler(event_handler: EventHandler, arg: Var) -> EventSpec:
  224. """Call an event handler to get the event spec.
  225. This function will inspect the function signature of the event handler.
  226. If it takes in an arg, the arg will be passed to the event handler.
  227. Otherwise, the event handler will be called with no args.
  228. Args:
  229. event_handler: The event handler.
  230. arg: The argument to pass to the event handler.
  231. Returns:
  232. The event spec from calling the event handler.
  233. """
  234. args = inspect.getfullargspec(event_handler.fn).args
  235. if len(args) == 1:
  236. return event_handler()
  237. assert (
  238. len(args) == 2
  239. ), f"Event handler {event_handler.fn} must have 1 or 2 arguments."
  240. return event_handler(arg)
  241. def call_event_fn(fn: Callable, arg: Var) -> List[EventSpec]:
  242. """Call a function to a list of event specs.
  243. The function should return either a single EventSpec or a list of EventSpecs.
  244. If the function takes in an arg, the arg will be passed to the function.
  245. Otherwise, the function will be called with no args.
  246. Args:
  247. fn: The function to call.
  248. arg: The argument to pass to the function.
  249. Returns:
  250. The event specs from calling the function.
  251. Raises:
  252. ValueError: If the lambda has an invalid signature.
  253. """
  254. # Import here to avoid circular imports.
  255. from pynecone.event import EventHandler, EventSpec
  256. # Get the args of the lambda.
  257. args = inspect.getfullargspec(fn).args
  258. # Call the lambda.
  259. if len(args) == 0:
  260. out = fn()
  261. elif len(args) == 1:
  262. out = fn(arg)
  263. else:
  264. raise ValueError(f"Lambda {fn} must have 0 or 1 arguments.")
  265. # Convert the output to a list.
  266. if not isinstance(out, List):
  267. out = [out]
  268. # Convert any event specs to event specs.
  269. events = []
  270. for e in out:
  271. # Convert handlers to event specs.
  272. if isinstance(e, EventHandler):
  273. if len(args) == 0:
  274. e = e()
  275. elif len(args) == 1:
  276. e = e(arg)
  277. # Make sure the event spec is valid.
  278. if not isinstance(e, EventSpec):
  279. raise ValueError(f"Lambda {fn} returned an invalid event spec: {e}.")
  280. # Add the event spec to the chain.
  281. events.append(e)
  282. # Return the events.
  283. return events
  284. def get_handler_args(event_spec: EventSpec, arg: Var) -> Tuple[Tuple[Var, Var], ...]:
  285. """Get the handler args for the given event spec.
  286. Args:
  287. event_spec: The event spec.
  288. arg: The controlled event argument.
  289. Returns:
  290. The handler args.
  291. """
  292. args = inspect.getfullargspec(event_spec.handler.fn).args
  293. return event_spec.args if len(args) > 1 else tuple()
  294. def fix_events(
  295. events: Optional[List[Union[EventHandler, EventSpec]]], token: str
  296. ) -> List[Event]:
  297. """Fix a list of events returned by an event handler.
  298. Args:
  299. events: The events to fix.
  300. token: The user token.
  301. Returns:
  302. The fixed events.
  303. """
  304. # If the event handler returns nothing, return an empty list.
  305. if events is None:
  306. return []
  307. # If the handler returns a single event, wrap it in a list.
  308. if not isinstance(events, List):
  309. events = [events]
  310. # Fix the events created by the handler.
  311. out = []
  312. for e in events:
  313. if not isinstance(e, (EventHandler, EventSpec)):
  314. e = EventHandler(fn=e)
  315. # Otherwise, create an event from the event spec.
  316. if isinstance(e, EventHandler):
  317. e = e()
  318. assert isinstance(e, EventSpec), f"Unexpected event type, {type(e)}."
  319. name = format.format_event_handler(e.handler)
  320. payload = {k.name: v.name for k, v in e.args}
  321. # Create an event and append it to the list.
  322. out.append(
  323. Event(
  324. token=token,
  325. name=name,
  326. payload=payload,
  327. )
  328. )
  329. return out
  330. def get_fn_signature(fn: Callable) -> inspect.Signature:
  331. """Get the signature of a function.
  332. Args:
  333. fn: The function.
  334. Returns:
  335. The signature of the function.
  336. """
  337. signature = inspect.signature(fn)
  338. new_param = inspect.Parameter(
  339. "state", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Any
  340. )
  341. return signature.replace(parameters=(new_param, *signature.parameters.values()))
  342. # A set of common event triggers.
  343. EVENT_TRIGGERS: Set[str] = {
  344. "on_focus",
  345. "on_blur",
  346. "on_click",
  347. "on_context_menu",
  348. "on_double_click",
  349. "on_mouse_down",
  350. "on_mouse_enter",
  351. "on_mouse_leave",
  352. "on_mouse_move",
  353. "on_mouse_out",
  354. "on_mouse_over",
  355. "on_mouse_up",
  356. "on_scroll",
  357. }