Add upload component (#622)

This commit is contained in:
Nikhil Rao 2023-03-03 19:38:58 -08:00 committed by GitHub
parent 8ba22ed92d
commit f7138bd53f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 326 additions and 39 deletions

40
poetry.lock generated
View File

@ -480,14 +480,14 @@ files = [
[[package]] [[package]]
name = "platformdirs" 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\"." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, {file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"},
{file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, {file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"},
] ]
[package.dependencies] [package.dependencies]
@ -648,14 +648,14 @@ dev = ["twine (>=3.4.1)"]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "7.2.1" version = "7.2.2"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"},
{file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"},
] ]
[package.dependencies] [package.dependencies]
@ -725,6 +725,20 @@ files = [
asyncio-client = ["aiohttp (>=3.4)"] asyncio-client = ["aiohttp (>=3.4)"]
client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] 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]] [[package]]
name = "python-socketio" name = "python-socketio"
version = "5.7.2" 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 = ["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"] 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]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.0" version = "1.3.0"
@ -1210,4 +1236,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "13e3d8aa740b5a1b24ec4cdd394791f1650ac3bcb618e4430f1620238c6e8a6d" content-hash = "b7272a6016a5b9fb3eea7ce834b9f539919e8f71c7f849d626adb2ee7a354d2f"

Binary file not shown.

View File

@ -23,6 +23,7 @@
"react": "^17.0.2", "react": "^17.0.2",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-dropzone": "^14.2.3",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
"react-plotly.js": "^2.6.0", "react-plotly.js": "^2.6.0",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",

View File

@ -1,3 +1,3 @@
{ {
"version": "0.1.18" "version": "0.1.19"
} }

View File

@ -1,5 +1,6 @@
// State management for Pynecone web apps. // 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. // Global variable to hold the token.
let token; let token;
@ -103,12 +104,19 @@ export const applyEvent = async (event, router, socket) => {
* Process an event off the event queue. * Process an event off the event queue.
* @param state The state with the event queue. * @param state The state with the event queue.
* @param setState The function to set the state. * @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 setResult The function to set the result.
* @param router The router object. * @param router The router object.
* @param socket The socket object to send the event on. * @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 we are already processing an event, or there are no events to process, return.
if (result.processing || state.events.length == 0) { if (result.processing || state.events.length == 0) {
return; return;
@ -118,7 +126,7 @@ export const updateState = async (state, setState, result, setResult, router, so
setResult({ ...result, processing: true }); setResult({ ...result, processing: true });
// Pop the next event off the queue and apply it. // 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. // Set new events to avoid reprocessing the same event.
setState({ ...state, events: state.events }); 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); const eventSent = await applyEvent(event, router, socket);
if (!eventSent) { if (!eventSent) {
// If no event was sent, set processing to false and return. // 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 socket The socket object to connect.
* @param state The state object to apply the deltas to. * @param state The state object to apply the deltas to.
* @param setState The function to set the state. * @param setState The function to set the state.
* @param result The current result.
* @param setResult The function to set the result. * @param setResult The function to set the result.
* @param endpoint The endpoint to connect to. * @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 // Get backend URL object from the endpoint
const endpoint_url = new URL(endpoint) const endpoint_url = new URL(endpoint);
// Create the socket. // Create the socket.
socket.current = io(endpoint, { socket.current = io(endpoint, {
path: endpoint_url['pathname'], path: endpoint_url["pathname"],
transports: transports, transports: transports,
autoUnref: false, autoUnref: false,
}); });
// Once the socket is open, hydrate the page. // Once the socket is open, hydrate the page.
socket.current.on('connect', () => { socket.current.on("connect", () => {
updateState(state, setState, result, setResult, router, socket.current); updateState(state, setState, result, setResult, router, socket.current);
}); });
// On each received message, apply the delta and set the result. // 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); update = JSON.parse(update);
applyDelta(state, update.delta); applyDelta(state, update.delta);
setResult({ 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. * Create an event object.
* @param name The name of the event. * @param name The name of the event.

View File

@ -3,14 +3,21 @@
Anything imported here will be available in the default Pynecone import as `pc.*`. 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 .base import Base
from .components import * from .components import *
from .components.component import custom_component as component from .components.component import custom_component as component
from .components.graphing.victory import data from .components.graphing.victory import data
from .config import Config from .config import Config
from .constants import Env, Transports 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 .middleware import Middleware
from .model import Model, session from .model import Model, session
from .route import route from .route import route

View File

@ -2,7 +2,7 @@
from typing import Any, Callable, Coroutine, Dict, List, Optional, Type, Union 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 fastapi.middleware import cors
from socketio import ASGIApp, AsyncNamespace, AsyncServer from socketio import ASGIApp, AsyncNamespace, AsyncServer
@ -124,6 +124,9 @@ class App(Base):
# To test the server. # To test the server.
self.api.get(str(constants.Endpoint.PING))(ping) self.api.get(str(constants.Endpoint.PING))(ping)
# To upload files.
self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
def add_cors(self): def add_cors(self):
"""Add CORS middleware to the app.""" """Add CORS middleware to the app."""
self.api.add_middleware( self.api.add_middleware(
@ -131,6 +134,7 @@ class App(Base):
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
allow_origins=["*"],
) )
def preprocess(self, state: State, event: Event) -> Optional[Delta]: def preprocess(self, state: State, event: Event) -> Optional[Delta]:
@ -428,6 +432,38 @@ async def ping() -> str:
return "pong" 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): class EventNamespace(AsyncNamespace):
"""The event namespace.""" """The event namespace."""

View File

@ -15,7 +15,7 @@ from pynecone.style import Style
DEFAULT_IMPORTS: ImportDict = { DEFAULT_IMPORTS: ImportDict = {
"react": {"useEffect", "useRef", "useState"}, "react": {"useEffect", "useRef", "useState"},
"next/router": {"useRouter"}, "next/router": {"useRouter"},
f"/{constants.STATE_PATH}": {"connect", "updateState", "E"}, f"/{constants.STATE_PATH}": {"connect", "updateState", "uploadFiles", "E"},
"": {"focus-visible/dist/focus-visible"}, "": {"focus-visible/dist/focus-visible"},
"@chakra-ui/react": {constants.USE_COLOR_MODE}, "@chakra-ui/react": {constants.USE_COLOR_MODE},
} }

View File

@ -146,6 +146,14 @@ EVENT_FN = join(
"}})", "}})",
] ]
).format ).format
UPLOAD_FN = join(
[
"const File = files => {set_state}({{",
" ...{state},",
" files,",
"}})",
]
).format
# Effects. # Effects.

View File

@ -85,9 +85,11 @@ def compile_constants() -> str:
Returns: Returns:
A string of all the compiled constants. A string of all the compiled constants.
""" """
endpoint = constants.Endpoint.EVENT
return templates.join( 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( initial_state.update(
{ {
"events": [{"name": utils.get_hydrate_event(state)}], "events": [{"name": utils.get_hydrate_event(state)}],
"files": [],
} }
) )
initial_state = utils.format_state(initial_state) initial_state = utils.format_state(initial_state)
@ -137,7 +140,12 @@ def compile_events(state: Type[State]) -> str:
""" """
state_name = state.get_name() state_name = state.get_name()
state_setter = templates.format_state_setter(state_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: def compile_effects(state: Type[State]) -> str:

View File

@ -112,6 +112,7 @@ slider_thumb = SliderThumb.create
slider_track = SliderTrack.create slider_track = SliderTrack.create
switch = Switch.create switch = Switch.create
text_area = TextArea.create text_area = TextArea.create
upload = Upload.create
area = Area.create area = Area.create
bar = Bar.create bar = Bar.create
box_plot = BoxPlot.create box_plot = BoxPlot.create

View File

@ -50,6 +50,9 @@ class Component(Base, ABC):
# The class name for the component. # The class name for the component.
class_name: Any = None class_name: Any = None
# Special component props.
special_props: Set[Var] = set()
@classmethod @classmethod
def __init_subclass__(cls, **kwargs): def __init_subclass__(cls, **kwargs):
"""Set default properties. """Set default properties.
@ -290,7 +293,7 @@ class Component(Base, ABC):
# Create the base tag. # Create the base tag.
alias = self.get_alias() alias = self.get_alias()
name = alias if alias is not None else self.tag 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. # Add component props to the tag.
props = {attr: getattr(self, attr) for attr in self.get_props()} props = {attr: getattr(self, attr) for attr in self.get_props()}

View File

@ -27,5 +27,6 @@ from .select import Option, Select
from .slider import Slider, SliderFilledTrack, SliderMark, SliderThumb, SliderTrack from .slider import Slider, SliderFilledTrack, SliderMark, SliderThumb, SliderTrack
from .switch import Switch from .switch import Switch
from .textarea import TextArea from .textarea import TextArea
from .upload import Upload
__all__ = [f for f in dir() if f[0].isupper()] # type: ignore __all__ = [f for f in dir() if f[0].isupper()] # type: ignore

View File

@ -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

View File

@ -1,4 +1,4 @@
"""An image component.""" """An icon component."""
from pynecone import utils from pynecone import utils
from pynecone.components.component import Component from pynecone.components.component import Component

View File

@ -5,7 +5,7 @@ from __future__ import annotations
import json import json
import os import os
import re 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.graph_objects import Figure
from plotly.io import to_json from plotly.io import to_json
@ -31,6 +31,12 @@ class Tag(Base):
# The inner contents of the tag. # The inner contents of the tag.
contents: str = "" 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): def __init__(self, *args, **kwargs):
"""Initialize the tag. """Initialize the tag.
@ -68,8 +74,15 @@ class Tag(Base):
# Handle event props. # Handle event props.
elif isinstance(prop, EventChain): elif isinstance(prop, EventChain):
local_args = ",".join(prop.events[0].local_args) 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. # Handle other types.
elif isinstance(prop, str): elif isinstance(prop, str):
@ -125,6 +138,11 @@ class Tag(Base):
""" """
# Get the tag props. # Get the tag props.
props_str = self.format_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: if len(props_str) > 0:
props_str = " " + props_str props_str = " " + props_str
@ -132,10 +150,16 @@ class Tag(Base):
# If there is no inner content, we don't need a closing tag. # If there is no inner content, we don't need a closing tag.
tag_str = utils.wrap(f"{self.name}{props_str}/", "<") tag_str = utils.wrap(f"{self.name}{props_str}/", "<")
else: 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. # Otherwise wrap it in opening and closing tags.
open = utils.wrap(f"{self.name}{props_str}", "<") open = utils.wrap(f"{self.name}{props_str}", "<")
close = utils.wrap(f"/{self.name}", "<") close = utils.wrap(f"/{self.name}", "<")
tag_str = utils.wrap(self.contents, open, close) tag_str = utils.wrap(contents, open, close)
return tag_str return tag_str

View File

@ -168,6 +168,7 @@ class Endpoint(Enum):
PING = "ping" PING = "ping"
EVENT = "event" EVENT = "event"
UPLOAD = "upload"
def __str__(self) -> str: def __str__(self) -> str:
"""Get the string representation of the endpoint. """Get the string representation of the endpoint.

View File

@ -62,6 +62,9 @@ class EventHandler(Base):
values.append(arg.full_name) values.append(arg.full_name)
continue continue
if isinstance(arg, FileUpload):
return EventSpec(handler=self, upload=True)
# Otherwise, convert to JSON. # Otherwise, convert to JSON.
try: try:
values.append(json.dumps(arg, ensure_ascii=False)) values.append(json.dumps(arg, ensure_ascii=False))
@ -91,6 +94,9 @@ class EventSpec(Base):
# The arguments to pass to the function. # The arguments to pass to the function.
args: Tuple[Any, ...] = () args: Tuple[Any, ...] = ()
# Whether to upload files.
upload: bool = False
class Config: class Config:
"""The Pydantic config.""" """The Pydantic config."""
@ -122,6 +128,12 @@ class FrontendEvent(Base):
EVENT_ARG = BaseVar(name="_e", type_=FrontendEvent, is_local=True) EVENT_ARG = BaseVar(name="_e", type_=FrontendEvent, is_local=True)
class FileUpload(Base):
"""Class to represent a file upload."""
pass
# Special server-side events. # Special server-side events.
def redirect(path: str) -> EventSpec: def redirect(path: str) -> EventSpec:
"""Redirect to a new path. """Redirect to a new path.

View File

@ -1138,21 +1138,21 @@ def format_cond(
return expr return expr
def format_event_handler(handler: EventHandler) -> str: def get_event_handler_parts(handler: EventHandler) -> Tuple[str, str]:
"""Format an event handler. """Get the state and function name of an event handler.
Args: Args:
handler: The event handler to format. handler: The event handler to get the parts of.
Returns: Returns:
The formatted function. The state and function name.
""" """
# Get the class that defines the event handler. # Get the class that defines the event handler.
parts = handler.fn.__qualname__.split(".") parts = handler.fn.__qualname__.split(".")
# If there's no enclosing class, just return the function name. # If there's no enclosing class, just return the function name.
if len(parts) == 1: if len(parts) == 1:
return parts[-1] return ("", parts[-1])
# Get the state and the function name. # Get the state and the function name.
state_name, name = parts[-2:] 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] state = vars(sys.modules[handler.fn.__module__])[state_name]
except Exception: except Exception:
# If the state isn't in the module, just return the function name. # If the state isn't in the module, just return the function name.
return handler.fn.__qualname__ return ("", handler.fn.__qualname__)
return ".".join([state.get_full_name(), name])
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: 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, '{')})" 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]: def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]:
"""Convert back query params name to python-friendly case. """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()} return {k.replace("-", "_"): v for k, v in params.items()}
# Set of unique variable names.
USED_VARIABLES = set() USED_VARIABLES = set()

View File

@ -38,6 +38,7 @@ python-socketio = "^5.7.2"
psutil = "^5.9.4" psutil = "^5.9.4"
websockets = "^10.4" websockets = "^10.4"
cloudpickle = "^2.2.1" cloudpickle = "^2.2.1"
python-multipart = "^0.0.5"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^7.1.2" pytest = "^7.1.2"