123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- """Define event classes to connect the frontend and backend."""
- from __future__ import annotations
- import inspect
- from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
- from pynecone import constants
- from pynecone.base import Base
- from pynecone.utils import format
- from pynecone.vars import BaseVar, Var
- class Event(Base):
- """An event that describes any state change in the app."""
- # The token to specify the client that the event is for.
- token: str
- # The event name.
- name: str
- # The routing data where event occurred
- router_data: Dict[str, Any] = {}
- # The event payload.
- payload: Dict[str, Any] = {}
- class EventHandler(Base):
- """An event handler responds to an event to update the state."""
- # The function to call in response to the event.
- fn: Any
- class Config:
- """The Pydantic config."""
- # Needed to allow serialization of Callable.
- frozen = True
- def __call__(self, *args: Var) -> EventSpec:
- """Pass arguments to the handler to get an event spec.
- This method configures event handlers that take in arguments.
- Args:
- *args: The arguments to pass to the handler.
- Returns:
- The event spec, containing both the function and args.
- Raises:
- TypeError: If the arguments are invalid.
- """
- # Get the function args.
- fn_args = inspect.getfullargspec(self.fn).args[1:]
- fn_args = (Var.create_safe(arg) for arg in fn_args)
- # Construct the payload.
- values = []
- for arg in args:
- # Special case for file uploads.
- if isinstance(arg, FileUpload):
- return EventSpec(
- handler=self,
- client_handler_name="uploadFiles",
- )
- # Otherwise, convert to JSON.
- try:
- values.append(Var.create(arg, is_string=type(arg) is str))
- except TypeError as e:
- raise TypeError(
- f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
- ) from e
- payload = tuple(zip(fn_args, values))
- # Return the event spec.
- return EventSpec(handler=self, args=payload)
- class EventSpec(Base):
- """An event specification.
- Whereas an Event object is passed during runtime, a spec is used
- during compile time to outline the structure of an event.
- """
- # The event handler.
- handler: EventHandler
- # The handler on the client to process event.
- client_handler_name: str = ""
- # The arguments to pass to the function.
- args: Tuple[Tuple[Var, Var], ...] = ()
- class Config:
- """The Pydantic config."""
- # Required to allow tuple fields.
- frozen = True
- class EventChain(Base):
- """Container for a chain of events that will be executed in order."""
- events: List[EventSpec]
- # Whether events are in fully controlled input.
- full_control: bool = False
- # State name when fully controlled.
- state_name: str = ""
- class Target(Base):
- """A Javascript event target."""
- checked: bool = False
- value: Any = None
- class FrontendEvent(Base):
- """A Javascript event."""
- target: Target = Target()
- key: str = ""
- value: Any = None
- # The default event argument.
- EVENT_ARG = BaseVar(name="_e", type_=FrontendEvent, is_local=True)
- class FileUpload(Base):
- """Class to represent a file upload."""
- pass
- # Special server-side events.
- def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec:
- """A server-side event.
- Args:
- name: The name of the event.
- sig: The function signature of the event.
- **kwargs: The arguments to pass to the event.
- Returns:
- An event spec for a server-side event.
- """
- def fn():
- return None
- fn.__qualname__ = name
- fn.__signature__ = sig
- return EventSpec(
- handler=EventHandler(fn=fn),
- args=tuple(
- (Var.create_safe(k), Var.create_safe(v, is_string=type(v) is str))
- for k, v in kwargs.items()
- ),
- )
- def redirect(path: Union[str, Var[str]]) -> EventSpec:
- """Redirect to a new path.
- Args:
- path: The path to redirect to.
- Returns:
- An event to redirect to the path.
- """
- return server_side("_redirect", get_fn_signature(redirect), path=path)
- def console_log(message: Union[str, Var[str]]) -> EventSpec:
- """Do a console.log on the browser.
- Args:
- message: The message to log.
- Returns:
- An event to log the message.
- """
- return server_side("_console", get_fn_signature(console_log), message=message)
- def window_alert(message: Union[str, Var[str]]) -> EventSpec:
- """Create a window alert on the browser.
- Args:
- message: The message to alert.
- Returns:
- An event to alert the message.
- """
- return server_side("_alert", get_fn_signature(window_alert), message=message)
- def set_focus(ref: str) -> EventSpec:
- """Set focus to specified ref.
- Args:
- ref: The ref.
- Returns:
- An event to set focus on the ref
- """
- return server_side(
- "_set_focus",
- get_fn_signature(set_focus),
- ref=Var.create_safe(format.format_ref(ref)),
- )
- def set_value(ref: str, value: Any) -> EventSpec:
- """Set the value of a ref.
- Args:
- ref: The ref.
- value: The value to set.
- Returns:
- An event to set the ref.
- """
- return server_side(
- "_set_value",
- get_fn_signature(set_value),
- ref=Var.create_safe(format.format_ref(ref)),
- value=value,
- )
- def set_cookie(key: str, value: str) -> EventSpec:
- """Set a cookie on the frontend.
- Args:
- key (str): The key identifying the cookie.
- value (str): The value contained in the cookie.
- Returns:
- EventSpec: An event to set a cookie.
- """
- return server_side(
- "_set_cookie",
- get_fn_signature(set_cookie),
- key=key,
- value=value,
- )
- def set_local_storage(key: str, value: str) -> EventSpec:
- """Set a value in the local storage on the frontend.
- Args:
- key (str): The key identifying the variable in the local storage.
- value (str): The value contained in the local storage.
- Returns:
- EventSpec: An event to set a key-value in local storage.
- """
- return server_side(
- "_set_local_storage",
- get_fn_signature(set_local_storage),
- key=key,
- value=value,
- )
- def set_clipboard(content: str) -> EventSpec:
- """Set the text in content in the clipboard.
- Args:
- content: The text to add to clipboard.
- Returns:
- EventSpec: An event to set some content in the clipboard.
- """
- return server_side(
- "_set_clipboard",
- get_fn_signature(set_clipboard),
- content=content,
- )
- def get_event(state, event):
- """Get the event from the given state.
- Args:
- state: The state.
- event: The event.
- Returns:
- The event.
- """
- return f"{state.get_name()}.{event}"
- def get_hydrate_event(state) -> str:
- """Get the name of the hydrate event for the state.
- Args:
- state: The state.
- Returns:
- The name of the hydrate event.
- """
- return get_event(state, constants.HYDRATE)
- def call_event_handler(event_handler: EventHandler, arg: Var) -> EventSpec:
- """Call an event handler to get the event spec.
- This function will inspect the function signature of the event handler.
- If it takes in an arg, the arg will be passed to the event handler.
- Otherwise, the event handler will be called with no args.
- Args:
- event_handler: The event handler.
- arg: The argument to pass to the event handler.
- Returns:
- The event spec from calling the event handler.
- """
- args = inspect.getfullargspec(event_handler.fn).args
- if len(args) == 1:
- return event_handler()
- assert (
- len(args) == 2
- ), f"Event handler {event_handler.fn} must have 1 or 2 arguments."
- return event_handler(arg)
- def call_event_fn(fn: Callable, arg: Var) -> List[EventSpec]:
- """Call a function to a list of event specs.
- The function should return either a single EventSpec or a list of EventSpecs.
- If the function takes in an arg, the arg will be passed to the function.
- Otherwise, the function will be called with no args.
- Args:
- fn: The function to call.
- arg: The argument to pass to the function.
- Returns:
- The event specs from calling the function.
- Raises:
- ValueError: If the lambda has an invalid signature.
- """
- # Import here to avoid circular imports.
- from pynecone.event import EventHandler, EventSpec
- # Get the args of the lambda.
- args = inspect.getfullargspec(fn).args
- # Call the lambda.
- if len(args) == 0:
- out = fn()
- elif len(args) == 1:
- out = fn(arg)
- else:
- raise ValueError(f"Lambda {fn} must have 0 or 1 arguments.")
- # Convert the output to a list.
- if not isinstance(out, List):
- out = [out]
- # Convert any event specs to event specs.
- events = []
- for e in out:
- # Convert handlers to event specs.
- if isinstance(e, EventHandler):
- if len(args) == 0:
- e = e()
- elif len(args) == 1:
- e = e(arg)
- # Make sure the event spec is valid.
- if not isinstance(e, EventSpec):
- raise ValueError(f"Lambda {fn} returned an invalid event spec: {e}.")
- # Add the event spec to the chain.
- events.append(e)
- # Return the events.
- return events
- def get_handler_args(event_spec: EventSpec, arg: Var) -> Tuple[Tuple[Var, Var], ...]:
- """Get the handler args for the given event spec.
- Args:
- event_spec: The event spec.
- arg: The controlled event argument.
- Returns:
- The handler args.
- """
- args = inspect.getfullargspec(event_spec.handler.fn).args
- return event_spec.args if len(args) > 1 else tuple()
- def fix_events(
- events: Optional[List[Union[EventHandler, EventSpec]]], token: str
- ) -> List[Event]:
- """Fix a list of events returned by an event handler.
- Args:
- events: The events to fix.
- token: The user token.
- Returns:
- The fixed events.
- """
- # If the event handler returns nothing, return an empty list.
- if events is None:
- return []
- # If the handler returns a single event, wrap it in a list.
- if not isinstance(events, List):
- events = [events]
- # Fix the events created by the handler.
- out = []
- for e in events:
- if not isinstance(e, (EventHandler, EventSpec)):
- e = EventHandler(fn=e)
- # Otherwise, create an event from the event spec.
- if isinstance(e, EventHandler):
- e = e()
- assert isinstance(e, EventSpec), f"Unexpected event type, {type(e)}."
- name = format.format_event_handler(e.handler)
- payload = {k.name: v.name for k, v in e.args}
- # Create an event and append it to the list.
- out.append(
- Event(
- token=token,
- name=name,
- payload=payload,
- )
- )
- return out
- def get_fn_signature(fn: Callable) -> inspect.Signature:
- """Get the signature of a function.
- Args:
- fn: The function.
- Returns:
- The signature of the function.
- """
- signature = inspect.signature(fn)
- new_param = inspect.Parameter(
- "state", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Any
- )
- return signature.replace(parameters=(new_param, *signature.parameters.values()))
- # A set of common event triggers.
- EVENT_TRIGGERS: Set[str] = {
- "on_focus",
- "on_blur",
- "on_click",
- "on_context_menu",
- "on_double_click",
- "on_mouse_down",
- "on_mouse_enter",
- "on_mouse_leave",
- "on_mouse_move",
- "on_mouse_out",
- "on_mouse_over",
- "on_mouse_up",
- "on_scroll",
- }
|