diff --git a/.gitignore b/.gitignore index fc50258b7..beebaa81b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ bun.lockb poetry.lock dist/* -pynetree/ +examples/ diff --git a/pynecone/app.py b/pynecone/app.py index 7fcbbd378..17879e582 100644 --- a/pynecone/app.py +++ b/pynecone/app.py @@ -196,7 +196,7 @@ class App(Base): v = BaseVar( name=match.groups()[0], type_=str, - state="router.query", + state=f"{constants.ROUTER}.query", ) args.append(v) diff --git a/pynecone/components/component.py b/pynecone/components/component.py index ed560eef2..ed6c0bd41 100644 --- a/pynecone/components/component.py +++ b/pynecone/components/component.py @@ -224,7 +224,7 @@ class Component(Base, ABC): # Add component props to the tag. props = {attr: getattr(self, attr) for attr in self.get_props()} - + # Special case for props named `type_`. if hasattr(self, "type_"): props["type"] = getattr(self, "type_") diff --git a/pynecone/components/datadisplay/datatable.py b/pynecone/components/datadisplay/datatable.py index f80c4ca08..5b40654d0 100644 --- a/pynecone/components/datadisplay/datatable.py +++ b/pynecone/components/datadisplay/datatable.py @@ -1,7 +1,8 @@ """Table components.""" -from typing import Any, Dict, List, Union +from typing import Any, List +from pynecone import utils from pynecone.components.component import Component from pynecone.components.tags import Tag from pynecone.var import Var @@ -44,7 +45,7 @@ import "gridjs/dist/theme/mermaid.css"; """ def _render(self) -> Tag: - if type(self.data).__name__ == "DataFrame": + if utils.is_dataframe(type(self.data)): self.columns = Var.create(list(self.data.columns.values.tolist())) # type: ignore self.data = Var.create(list(self.data.values.tolist())) # type: ignore diff --git a/pynecone/constants.py b/pynecone/constants.py index 4f42c6b0e..7348ae4b5 100644 --- a/pynecone/constants.py +++ b/pynecone/constants.py @@ -94,6 +94,8 @@ CONFIG_MODULE = "pcconfig" CONFIG_FILE = f"{CONFIG_MODULE}.{PY_EXT}" # The deployment URL. PRODUCTION_BACKEND_URL = "https://{username}-{app_name}.api.pynecone.app" +# Token expiration time in seconds. +TOKEN_EXPIRATION = 60 * 60 # Env modes diff --git a/pynecone/state.py b/pynecone/state.py index c219de1e8..c7cae1dbd 100644 --- a/pynecone/state.py +++ b/pynecone/state.py @@ -8,7 +8,7 @@ import traceback from abc import ABC from typing import Any, Callable, ClassVar, Dict, List, Optional, Sequence, Set, Type -from pynecone import utils +from pynecone import constants, utils from pynecone.base import Base from pynecone.event import Event, EventHandler, EventSpec, window_alert from pynecone.var import BaseVar, ComputedVar, Var @@ -320,35 +320,6 @@ class State(Base, ABC): Returns: The state update after processing the event. """ - - def fix_events(events): - if events is None: - return [] - if not isinstance(events, List): - events = [events] - out = [] - for e in events: - if isinstance(e, Event): - out.append( - Event( - token=event.token, - name=e.name, - payload=e.payload, - ) - ) - else: - if isinstance(e, EventHandler): - e = e() - assert isinstance(e, EventSpec) - out.append( - Event( - token=event.token, - name=utils.to_snake_case(e.handler.fn.__qualname__), - payload=dict(e.args), - ) - ) - return out - # Get the event handler. path = event.name.split(".") path, name = path[:-1], path[-1] @@ -367,10 +338,16 @@ class State(Base, ABC): print(error) return StateUpdate(events=[window_alert(error)]) - # Return the substate and the delta. - events = fix_events(events) + # Fix the returned events. + events = utils.fix_events(events, event.token) + + # Get the delta after processing the event. delta = self.get_delta() + + # Reset the dirty vars. self.clean() + + # Return the state update. return StateUpdate(delta=delta, events=events) def get_delta(self) -> Delta: @@ -379,15 +356,21 @@ class State(Base, ABC): Returns: The delta for the state. """ + # Return the dirty vars, as well as all computed vars. delta = { self.get_full_name(): { prop: getattr(self, prop) for prop in self.dirty_vars | set(self.computed_vars.keys()) } } + # Recursively find the substate deltas. for substate in self.dirty_substates: delta.update(self.substates[substate].get_delta()) + + # Format the delta. delta = utils.format_state(delta) + + # Return the delta. return delta def mark_dirty(self): @@ -398,8 +381,11 @@ class State(Base, ABC): def clean(self): """Reset the dirty vars.""" + # Recursively clean the substates. for substate in self.dirty_substates: self.substates[substate].clean() + + # Clean this state. self.dirty_vars = set() self.dirty_substates = set() @@ -463,7 +449,7 @@ class StateManager(Base): states: Dict[str, State] = {} # The token expiration time (s). - token_expiration: int = 60 * 60 + token_expiration: int = constants.TOKEN_EXPIRATION def __init__(self, *args, **kwargs): """Initialize the state manager. diff --git a/pynecone/utils.py b/pynecone/utils.py index b78f76f6e..87d6a41fb 100644 --- a/pynecone/utils.py +++ b/pynecone/utils.py @@ -36,7 +36,7 @@ from pynecone import constants if TYPE_CHECKING: from pynecone.components.component import ImportDict - from pynecone.event import EventHandler, EventSpec + from pynecone.event import Event, EventHandler, EventSpec from pynecone.var import Var @@ -621,6 +621,18 @@ def get_default_app_name() -> str: return os.getcwd().split(os.path.sep)[-1].replace("-", "_") +def is_dataframe(value: Type) -> bool: + """Check if the given value is a dataframe. + + Args: + value: The value to check. + + Returns: + Whether the value is a dataframe. + """ + return value.__name__ == "DataFrame" + + def format_state(value: Dict) -> Dict: """Recursively format values in the given state. @@ -632,9 +644,8 @@ def format_state(value: Dict) -> Dict: """ if isinstance(value, go.Figure): return json.loads(to_json(value))["data"] - import pandas as pd - if isinstance(value, pd.DataFrame): + if is_dataframe(type(value)): return { "columns": value.columns.tolist(), "data": value.values.tolist(), @@ -657,6 +668,26 @@ def get_event(state, event): return f"{state.get_name()}.{event}" +def format_string(string: str) -> str: + """Format the given string as a JS string literal.. + + Args: + string: The string to format. + + Returns: + The formatted string. + """ + # Escale backticks. + string = string.replace("\`", "`") # type: ignore + string = string.replace("`", "\`") # type: ignore + + # Wrap the string so it looks like {`string`}. + string = wrap(string, "`") + string = wrap(string, "{") + + return string + + def call_event_handler(event_handler: EventHandler, arg: Var) -> EventSpec: """Call an event handler to get the event spec. @@ -723,6 +754,53 @@ def get_handler_args(event_spec: EventSpec, arg: Var) -> Tuple[Tuple[str, str], return ((args[1], arg.name),) +def fix_events(events: Optional[List[Event]], 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 it is already an event, don't modify it. + if isinstance(e, Event): + name = e.name + payload = e.payload + + # Otherwise, create an event from the event spec. + else: + if isinstance(e, EventHandler): + e = e() + assert isinstance(e, EventSpec), f"Unexpected event type, {type(e)}." + name = to_snake_case(e.handler.fn.__qualname__) + payload = dict(e.args) + + # Create an event and append it to the list. + out.append( + Event( + token=token, + name=name, + payload=payload, + ) + ) + + return out + + def merge_imports(*imports) -> ImportDict: """Merge two import dicts together. diff --git a/pynecone/var.py b/pynecone/var.py index 32c55372f..5222e3a53 100644 --- a/pynecone/var.py +++ b/pynecone/var.py @@ -123,10 +123,7 @@ class Var(ABC): else: out = utils.wrap(self.full_name, "{") if self.is_string: - out = out.replace("\`", "`") # type: ignore - out = out.replace("`", "\`") # type: ignore - out = utils.wrap(out, "`") - out = utils.wrap(out, "{") + out = utils.format_string(out) return out def __getitem__(self, i) -> Var: @@ -140,7 +137,6 @@ class Var(ABC): """ # The type of the indexed var. type_ = str - import pandas as pd # Convert any vars to local vars. if isinstance(i, Var): @@ -154,7 +150,7 @@ class Var(ABC): type_ = utils.get_args(self.type_)[0] else: type_ = Any - elif utils._issubclass(self.type_, Union[dict, pd.DataFrame]): + elif utils.is_dataframe(self.type_): if isinstance(i, str): i = utils.wrap(i, '"') if isinstance(self.type_, _GenericAlias):