From f7138bd53f8d55a7856fc9e70d1e9609318e96c1 Mon Sep 17 00:00:00 2001 From: Nikhil Rao Date: Fri, 3 Mar 2023 19:38:58 -0800 Subject: [PATCH] Add upload component (#622) --- poetry.lock | 40 +++++++++-- pynecone/.templates/web/bun.lockb | Bin 259735 -> 261125 bytes pynecone/.templates/web/package.json | 1 + pynecone/.templates/web/pynecone.json | 4 +- pynecone/.templates/web/utils/state.js | 89 ++++++++++++++++++++++--- pynecone/__init__.py | 11 ++- pynecone/app.py | 38 ++++++++++- pynecone/compiler/compiler.py | 2 +- pynecone/compiler/templates.py | 8 +++ pynecone/compiler/utils.py | 14 +++- pynecone/components/__init__.py | 1 + pynecone/components/component.py | 5 +- pynecone/components/forms/__init__.py | 1 + pynecone/components/forms/upload.py | 57 ++++++++++++++++ pynecone/components/media/icon.py | 2 +- pynecone/components/tags/tag.py | 32 +++++++-- pynecone/constants.py | 1 + pynecone/event.py | 12 ++++ pynecone/utils.py | 46 +++++++++++-- pyproject.toml | 1 + 20 files changed, 326 insertions(+), 39 deletions(-) create mode 100644 pynecone/components/forms/upload.py diff --git a/poetry.lock b/poetry.lock index 148964ce0..76c6f8ab6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -480,14 +480,14 @@ files = [ [[package]] name = "platformdirs" -version = "3.0.0" +version = "3.1.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, - {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, + {file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"}, + {file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"}, ] [package.dependencies] @@ -648,14 +648,14 @@ dev = ["twine (>=3.4.1)"] [[package]] name = "pytest" -version = "7.2.1" +version = "7.2.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, - {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, + {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, + {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, ] [package.dependencies] @@ -725,6 +725,20 @@ files = [ asyncio-client = ["aiohttp (>=3.4)"] client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] +[[package]] +name = "python-multipart" +version = "0.0.5" +description = "A streaming multipart parser for Python" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, +] + +[package.dependencies] +six = ">=1.4.0" + [[package]] name = "python-socketio" version = "5.7.2" @@ -847,6 +861,18 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-g testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.0" @@ -1210,4 +1236,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "13e3d8aa740b5a1b24ec4cdd394791f1650ac3bcb618e4430f1620238c6e8a6d" +content-hash = "b7272a6016a5b9fb3eea7ce834b9f539919e8f71c7f849d626adb2ee7a354d2f" diff --git a/pynecone/.templates/web/bun.lockb b/pynecone/.templates/web/bun.lockb index 1da69bbaa49751ee2879a3bb31e97c03a2664329..565ca1346b9fec2c0962b98f1d57f8c93731cef2 100755 GIT binary patch delta 69715 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 ZI_v?jGqS-uXtd_PicZIFdMgsw{U09%$Ke0~ delta 68848 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 MeZOa*`1{)b0qL3|J^%m! diff --git a/pynecone/.templates/web/package.json b/pynecone/.templates/web/package.json index 38817fc3d..8fd3486a4 100644 --- a/pynecone/.templates/web/package.json +++ b/pynecone/.templates/web/package.json @@ -23,6 +23,7 @@ "react": "^17.0.2", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^17.0.2", + "react-dropzone": "^14.2.3", "react-markdown": "^8.0.3", "react-plotly.js": "^2.6.0", "react-syntax-highlighter": "^15.5.0", diff --git a/pynecone/.templates/web/pynecone.json b/pynecone/.templates/web/pynecone.json index eb98579f2..cf29361fa 100644 --- a/pynecone/.templates/web/pynecone.json +++ b/pynecone/.templates/web/pynecone.json @@ -1,3 +1,3 @@ { - "version": "0.1.18" -} \ No newline at end of file + "version": "0.1.19" +} diff --git a/pynecone/.templates/web/utils/state.js b/pynecone/.templates/web/utils/state.js index fedf85b8e..0babb5fb2 100644 --- a/pynecone/.templates/web/utils/state.js +++ b/pynecone/.templates/web/utils/state.js @@ -1,5 +1,6 @@ // State management for Pynecone web apps. -import io from 'socket.io-client'; +import axios from "axios"; +import io from "socket.io-client"; // Global variable to hold the token. let token; @@ -103,12 +104,19 @@ export const applyEvent = async (event, router, socket) => { * Process an event off the event queue. * @param state The state with the event queue. * @param setState The function to set the state. - * @param result The current result + * @param result The current result. * @param setResult The function to set the result. * @param router The router object. * @param socket The socket object to send the event on. */ -export const updateState = async (state, setState, result, setResult, router, socket) => { +export const updateState = async ( + state, + setState, + result, + setResult, + router, + socket +) => { // If we are already processing an event, or there are no events to process, return. if (result.processing || state.events.length == 0) { return; @@ -118,7 +126,7 @@ export const updateState = async (state, setState, result, setResult, router, so setResult({ ...result, processing: true }); // Pop the next event off the queue and apply it. - const event = state.events.shift() + const event = state.events.shift(); // Set new events to avoid reprocessing the same event. setState({ ...state, events: state.events }); @@ -127,7 +135,7 @@ export const updateState = async (state, setState, result, setResult, router, so const eventSent = await applyEvent(event, router, socket); if (!eventSent) { // If no event was sent, set processing to false and return. - setResult({...state, processing: false}) + setResult({ ...state, processing: false }); } }; @@ -136,26 +144,37 @@ export const updateState = async (state, setState, result, setResult, router, so * @param socket The socket object to connect. * @param state The state object to apply the deltas to. * @param setState The function to set the state. + * @param result The current result. * @param setResult The function to set the result. * @param endpoint The endpoint to connect to. + * @param transports The transports to use. */ -export const connect = async (socket, state, setState, result, setResult, router, endpoint, transports) => { +export const connect = async ( + socket, + state, + setState, + result, + setResult, + router, + endpoint, + transports +) => { // Get backend URL object from the endpoint - const endpoint_url = new URL(endpoint) + const endpoint_url = new URL(endpoint); // Create the socket. socket.current = io(endpoint, { - path: endpoint_url['pathname'], + path: endpoint_url["pathname"], transports: transports, autoUnref: false, }); // Once the socket is open, hydrate the page. - socket.current.on('connect', () => { + socket.current.on("connect", () => { updateState(state, setState, result, setResult, router, socket.current); }); // On each received message, apply the delta and set the result. - socket.current.on('event', function (update) { + socket.current.on("event", function (update) { update = JSON.parse(update); applyDelta(state, update.delta); setResult({ @@ -166,6 +185,56 @@ export const connect = async (socket, state, setState, result, setResult, router }); }; +/** + * Upload files to the server. + * + * @param state The state to apply the delta to. + * @param setResult The function to set the result. + * @param files The files to upload. + * @param handler The handler to use. + * @param endpoint The endpoint to upload to. + */ +export const uploadFiles = async ( + state, + result, + setResult, + files, + handler, + endpoint +) => { + // If we are already processing an event, or there are no upload files, return. + if (result.processing || files.length == 0) { + return; + } + + // Set processing to true to block other events from being processed. + setResult({ ...result, processing: true }); + + // Currently only supports uploading one file. + const file = files[0]; + const headers = { + "Content-Type": file.type, + }; + const formdata = new FormData(); + + // Add the token and handler to the file name. + formdata.append("file", file, getToken() + ":" + handler + ":" + file.name); + + // Send the file to the server. + await axios.post(endpoint, formdata, headers).then((response) => { + // Apply the delta and set the result. + const update = response.data; + applyDelta(state, update.delta); + + // Set processing to false and return. + setResult({ + processing: false, + state: state, + events: update.events, + }); + }); +}; + /** * Create an event object. * @param name The name of the event. diff --git a/pynecone/__init__.py b/pynecone/__init__.py index 9027df00b..c32255c38 100644 --- a/pynecone/__init__.py +++ b/pynecone/__init__.py @@ -3,14 +3,21 @@ Anything imported here will be available in the default Pynecone import as `pc.*`. """ -from .app import App +from .app import App, UploadFile from .base import Base from .components import * from .components.component import custom_component as component from .components.graphing.victory import data from .config import Config from .constants import Env, Transports -from .event import EVENT_ARG, EventChain, console_log, redirect, window_alert +from .event import ( + EVENT_ARG, + EventChain, + console_log, + redirect, + window_alert, +) +from .event import FileUpload as upload_files from .middleware import Middleware from .model import Model, session from .route import route diff --git a/pynecone/app.py b/pynecone/app.py index d2d5cc22d..95ce2bab6 100644 --- a/pynecone/app.py +++ b/pynecone/app.py @@ -2,7 +2,7 @@ from typing import Any, Callable, Coroutine, Dict, List, Optional, Type, Union -from fastapi import FastAPI +from fastapi import FastAPI, UploadFile from fastapi.middleware import cors from socketio import ASGIApp, AsyncNamespace, AsyncServer @@ -124,6 +124,9 @@ class App(Base): # To test the server. self.api.get(str(constants.Endpoint.PING))(ping) + # To upload files. + self.api.post(str(constants.Endpoint.UPLOAD))(upload(self)) + def add_cors(self): """Add CORS middleware to the app.""" self.api.add_middleware( @@ -131,6 +134,7 @@ class App(Base): allow_credentials=True, allow_methods=["*"], allow_headers=["*"], + allow_origins=["*"], ) def preprocess(self, state: State, event: Event) -> Optional[Delta]: @@ -428,6 +432,38 @@ async def ping() -> str: return "pong" +def upload(app: App): + """Upload a file. + + Args: + app: The app to upload the file for. + + Returns: + The upload function. + """ + + async def upload_file(file: UploadFile): + """Upload a file. + + Args: + file: The file to upload. + + Returns: + The state update after processing the event. + """ + # Get the token and filename. + token, handler, filename = file.filename.split(":", 2) + file.filename = filename + + # Get the state for the session. + state = app.state_manager.get_state(token) + event = Event(token=token, name=handler, payload={"file": file}) + update = await state.process(event) + return update + + return upload_file + + class EventNamespace(AsyncNamespace): """The event namespace.""" diff --git a/pynecone/compiler/compiler.py b/pynecone/compiler/compiler.py index 297fbfc27..42213b90a 100644 --- a/pynecone/compiler/compiler.py +++ b/pynecone/compiler/compiler.py @@ -15,7 +15,7 @@ from pynecone.style import Style DEFAULT_IMPORTS: ImportDict = { "react": {"useEffect", "useRef", "useState"}, "next/router": {"useRouter"}, - f"/{constants.STATE_PATH}": {"connect", "updateState", "E"}, + f"/{constants.STATE_PATH}": {"connect", "updateState", "uploadFiles", "E"}, "": {"focus-visible/dist/focus-visible"}, "@chakra-ui/react": {constants.USE_COLOR_MODE}, } diff --git a/pynecone/compiler/templates.py b/pynecone/compiler/templates.py index c70f4424b..e0acad7fd 100644 --- a/pynecone/compiler/templates.py +++ b/pynecone/compiler/templates.py @@ -146,6 +146,14 @@ EVENT_FN = join( "}})", ] ).format +UPLOAD_FN = join( + [ + "const File = files => {set_state}({{", + " ...{state},", + " files,", + "}})", + ] +).format # Effects. diff --git a/pynecone/compiler/utils.py b/pynecone/compiler/utils.py index 17acf9b01..1c49adc5b 100644 --- a/pynecone/compiler/utils.py +++ b/pynecone/compiler/utils.py @@ -85,9 +85,11 @@ def compile_constants() -> str: Returns: A string of all the compiled constants. """ - endpoint = constants.Endpoint.EVENT return templates.join( - [compile_constant_declaration(name=endpoint.name, value=endpoint.get_url())] + [ + compile_constant_declaration(name=endpoint.name, value=endpoint.get_url()) + for endpoint in constants.Endpoint + ] ) @@ -104,6 +106,7 @@ def compile_state(state: Type[State]) -> str: initial_state.update( { "events": [{"name": utils.get_hydrate_event(state)}], + "files": [], } ) initial_state = utils.format_state(initial_state) @@ -137,7 +140,12 @@ def compile_events(state: Type[State]) -> str: """ state_name = state.get_name() state_setter = templates.format_state_setter(state_name) - return templates.EVENT_FN(state=state_name, set_state=state_setter) + return templates.join( + [ + templates.EVENT_FN(state=state_name, set_state=state_setter), + templates.UPLOAD_FN(state=state_name, set_state=state_setter), + ] + ) def compile_effects(state: Type[State]) -> str: diff --git a/pynecone/components/__init__.py b/pynecone/components/__init__.py index cf34bcb6b..48965191b 100644 --- a/pynecone/components/__init__.py +++ b/pynecone/components/__init__.py @@ -112,6 +112,7 @@ slider_thumb = SliderThumb.create slider_track = SliderTrack.create switch = Switch.create text_area = TextArea.create +upload = Upload.create area = Area.create bar = Bar.create box_plot = BoxPlot.create diff --git a/pynecone/components/component.py b/pynecone/components/component.py index a175a8003..c2c9ce45d 100644 --- a/pynecone/components/component.py +++ b/pynecone/components/component.py @@ -50,6 +50,9 @@ class Component(Base, ABC): # The class name for the component. class_name: Any = None + # Special component props. + special_props: Set[Var] = set() + @classmethod def __init_subclass__(cls, **kwargs): """Set default properties. @@ -290,7 +293,7 @@ class Component(Base, ABC): # Create the base tag. alias = self.get_alias() name = alias if alias is not None else self.tag - tag = Tag(name=name) + tag = Tag(name=name, special_props=self.special_props) # Add component props to the tag. props = {attr: getattr(self, attr) for attr in self.get_props()} diff --git a/pynecone/components/forms/__init__.py b/pynecone/components/forms/__init__.py index 3ee68238d..1d424c742 100644 --- a/pynecone/components/forms/__init__.py +++ b/pynecone/components/forms/__init__.py @@ -27,5 +27,6 @@ from .select import Option, Select from .slider import Slider, SliderFilledTrack, SliderMark, SliderThumb, SliderTrack from .switch import Switch from .textarea import TextArea +from .upload import Upload __all__ = [f for f in dir() if f[0].isupper()] # type: ignore diff --git a/pynecone/components/forms/upload.py b/pynecone/components/forms/upload.py new file mode 100644 index 000000000..5717a5d2d --- /dev/null +++ b/pynecone/components/forms/upload.py @@ -0,0 +1,57 @@ +"""A file upload component.""" + +from typing import Dict + +from pynecone.components.component import EVENT_ARG, Component +from pynecone.components.forms.input import Input +from pynecone.components.layout.box import Box +from pynecone.event import EventChain +from pynecone.var import BaseVar, Var + +upload_file = BaseVar(name="e => File(e)", type_=EventChain) + + +class Upload(Component): + """A file upload component.""" + + library = "react-dropzone" + + tag = "ReactDropzone" + + @classmethod + def create(cls, *children, **props) -> Component: + """Create an upload component. + + Args: + children: The children of the component. + props: The properties of the component. + + Returns: + The upload component. + """ + # The file input to use. + upload = Input.create(type_="file") + upload.special_props = {BaseVar(name="{...getInputProps()}", type_=None)} + + # The dropzone to use. + zone = Box.create(upload, *children, **props) + zone.special_props = {BaseVar(name="{...getRootProps()}", type_=None)} + + # Create the component. + return super().create(zone, on_drop=upload_file) + + @classmethod + def get_controlled_triggers(cls) -> Dict[str, Var]: + """Get the event triggers that pass the component's value to the handler. + + Returns: + A dict mapping the event trigger to the var that is passed to the handler. + """ + return { + "on_drop": EVENT_ARG, + } + + def _render(self): + out = super()._render() + out.args = ("getRootProps", "getInputProps") + return out diff --git a/pynecone/components/media/icon.py b/pynecone/components/media/icon.py index 6408e42b8..f224ad2aa 100644 --- a/pynecone/components/media/icon.py +++ b/pynecone/components/media/icon.py @@ -1,4 +1,4 @@ -"""An image component.""" +"""An icon component.""" from pynecone import utils from pynecone.components.component import Component diff --git a/pynecone/components/tags/tag.py b/pynecone/components/tags/tag.py index 87f2550fc..9fd801b9e 100644 --- a/pynecone/components/tags/tag.py +++ b/pynecone/components/tags/tag.py @@ -5,7 +5,7 @@ from __future__ import annotations import json import os import re -from typing import TYPE_CHECKING, Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Tuple, Union from plotly.graph_objects import Figure from plotly.io import to_json @@ -31,6 +31,12 @@ class Tag(Base): # The inner contents of the tag. contents: str = "" + # Args to pass to the tag. + args: Optional[Tuple[str, ...]] = None + + # Special props that aren't key value pairs. + special_props: Set[Var] = set() + def __init__(self, *args, **kwargs): """Initialize the tag. @@ -68,8 +74,15 @@ class Tag(Base): # Handle event props. elif isinstance(prop, EventChain): local_args = ",".join(prop.events[0].local_args) - events = ",".join([utils.format_event(event) for event in prop.events]) - prop = f"({local_args}) => Event([{events}])" + + if len(prop.events) == 1 and prop.events[0].upload: + # Special case for upload events. + event = utils.format_upload_event(prop.events[0]) + else: + # All other events. + chain = ",".join([utils.format_event(event) for event in prop.events]) + event = f"Event([{chain}])" + prop = f"({local_args}) => {event}" # Handle other types. elif isinstance(prop, str): @@ -125,6 +138,11 @@ class Tag(Base): """ # Get the tag props. props_str = self.format_props() + + # Add the special props. + props_str += " ".join([str(prop) for prop in self.special_props]) + + # Add a space if there are props. if len(props_str) > 0: props_str = " " + props_str @@ -132,10 +150,16 @@ class Tag(Base): # If there is no inner content, we don't need a closing tag. tag_str = utils.wrap(f"{self.name}{props_str}/", "<") else: + if self.args is not None: + # If there are args, wrap the tag in a function call. + args_str = ", ".join(self.args) + contents = f"{{({{{args_str}}}) => ({self.contents})}}" + else: + contents = self.contents # Otherwise wrap it in opening and closing tags. open = utils.wrap(f"{self.name}{props_str}", "<") close = utils.wrap(f"/{self.name}", "<") - tag_str = utils.wrap(self.contents, open, close) + tag_str = utils.wrap(contents, open, close) return tag_str diff --git a/pynecone/constants.py b/pynecone/constants.py index a6054616b..9429ad940 100644 --- a/pynecone/constants.py +++ b/pynecone/constants.py @@ -168,6 +168,7 @@ class Endpoint(Enum): PING = "ping" EVENT = "event" + UPLOAD = "upload" def __str__(self) -> str: """Get the string representation of the endpoint. diff --git a/pynecone/event.py b/pynecone/event.py index a09248b78..2cefc99c1 100644 --- a/pynecone/event.py +++ b/pynecone/event.py @@ -62,6 +62,9 @@ class EventHandler(Base): values.append(arg.full_name) continue + if isinstance(arg, FileUpload): + return EventSpec(handler=self, upload=True) + # Otherwise, convert to JSON. try: values.append(json.dumps(arg, ensure_ascii=False)) @@ -91,6 +94,9 @@ class EventSpec(Base): # The arguments to pass to the function. args: Tuple[Any, ...] = () + # Whether to upload files. + upload: bool = False + class Config: """The Pydantic config.""" @@ -122,6 +128,12 @@ class FrontendEvent(Base): 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 redirect(path: str) -> EventSpec: """Redirect to a new path. diff --git a/pynecone/utils.py b/pynecone/utils.py index acb016042..0dd737fe1 100644 --- a/pynecone/utils.py +++ b/pynecone/utils.py @@ -1138,21 +1138,21 @@ def format_cond( return expr -def format_event_handler(handler: EventHandler) -> str: - """Format an event handler. +def get_event_handler_parts(handler: EventHandler) -> Tuple[str, str]: + """Get the state and function name of an event handler. Args: - handler: The event handler to format. + handler: The event handler to get the parts of. Returns: - The formatted function. + The state and function name. """ # Get the class that defines the event handler. parts = handler.fn.__qualname__.split(".") # If there's no enclosing class, just return the function name. if len(parts) == 1: - return parts[-1] + return ("", parts[-1]) # Get the state and the function name. state_name, name = parts[-2:] @@ -1163,8 +1163,24 @@ def format_event_handler(handler: EventHandler) -> str: state = vars(sys.modules[handler.fn.__module__])[state_name] except Exception: # If the state isn't in the module, just return the function name. - return handler.fn.__qualname__ - return ".".join([state.get_full_name(), name]) + return ("", handler.fn.__qualname__) + + return (state.get_full_name(), name) + + +def format_event_handler(handler: EventHandler) -> str: + """Format an event handler. + + Args: + handler: The event handler to format. + + Returns: + The formatted function. + """ + state, name = get_event_handler_parts(handler) + if state == "": + return name + return f"{state}.{name}" def format_event(event_spec: EventSpec) -> str: @@ -1180,6 +1196,21 @@ def format_event(event_spec: EventSpec) -> str: return f"E(\"{format_event_handler(event_spec.handler)}\", {wrap(args, '{')})" +def format_upload_event(event_spec: EventSpec) -> str: + """Format an upload event. + + Args: + event_spec: The event to format. + + Returns: + The compiled event. + """ + from pynecone.compiler import templates + + state, name = get_event_handler_parts(event_spec.handler) + return f'uploadFiles({state}, {templates.RESULT}, {templates.SET_RESULT}, {state}.files, "{name}", UPLOAD)' + + def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]: """Convert back query params name to python-friendly case. @@ -1193,6 +1224,7 @@ def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]: return {k.replace("-", "_"): v for k, v in params.items()} +# Set of unique variable names. USED_VARIABLES = set() diff --git a/pyproject.toml b/pyproject.toml index a244936e7..1d9b524bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ python-socketio = "^5.7.2" psutil = "^5.9.4" websockets = "^10.4" cloudpickle = "^2.2.1" +python-multipart = "^0.0.5" [tool.poetry.group.dev.dependencies] pytest = "^7.1.2"