From 6e74cb00a3fd6ba8d01030b7b65d7355e784e6a8 Mon Sep 17 00:00:00 2001 From: PeterYusuke <58464065+PeterYusuke@users.noreply.github.com> Date: Thu, 9 Mar 2023 07:39:01 +0900 Subject: [PATCH] Hot loading asset folder on dev (#643) --- poetry.lock | 55 ++++++++++++++++++++++++---- pynecone/utils.py | 14 +++++++ pynecone/watch.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 4 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 pynecone/watch.py diff --git a/poetry.lock b/poetry.lock index 971e33c03..16cde14a2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -798,14 +798,14 @@ plugins = ["importlib-metadata"] [[package]] name = "pyright" -version = "1.1.296" +version = "1.1.297" description = "Command line wrapper for pyright" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.296-py3-none-any.whl", hash = "sha256:51cc5f05807b1fb53f9f0e14736b8f772b500a3ba4e0edeb99727e68e700d9ea"}, - {file = "pyright-1.1.296.tar.gz", hash = "sha256:6c3cd394473e55a516ebe443d02b83e63456ef29f052dcf8e64e7875c1418fa6"}, + {file = "pyright-1.1.297-py3-none-any.whl", hash = "sha256:3fd6528280eb649f8b64b7ece55299f01e340d29f4cf257da876957e3ee24062"}, + {file = "pyright-1.1.297.tar.gz", hash = "sha256:89082de2fbd240fa75767b57824f4d8516f2fb9005047265a67b895547c6272f"}, ] [package.dependencies] @@ -1043,14 +1043,14 @@ files = [ [[package]] name = "setuptools" -version = "67.4.0" +version = "67.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"}, - {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"}, + {file = "setuptools-67.5.1-py3-none-any.whl", hash = "sha256:1c39d42bda4cb89f7fdcad52b6762e3c309ec8f8715b27c684176b7d71283242"}, + {file = "setuptools-67.5.1.tar.gz", hash = "sha256:15136a251127da2d2e77ac7a1bc231eb504654f7e3346d93613a13f2e2787535"}, ] [package.extras] @@ -1335,6 +1335,47 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +[[package]] +name = "watchdog" +version = "2.3.1" +description = "Filesystem events monitoring" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "watchdog-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1f1200d4ec53b88bf04ab636f9133cb703eb19768a39351cee649de21a33697"}, + {file = "watchdog-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:564e7739abd4bd348aeafbf71cc006b6c0ccda3160c7053c4a53b67d14091d42"}, + {file = "watchdog-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:95ad708a9454050a46f741ba5e2f3468655ea22da1114e4c40b8cbdaca572565"}, + {file = "watchdog-2.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a073c91a6ef0dda488087669586768195c3080c66866144880f03445ca23ef16"}, + {file = "watchdog-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa8b028750b43e80eea9946d01925168eeadb488dfdef1d82be4b1e28067f375"}, + {file = "watchdog-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:964fd236cd443933268ae49b59706569c8b741073dbfd7ca705492bae9d39aab"}, + {file = "watchdog-2.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:91fd146d723392b3e6eb1ac21f122fcce149a194a2ba0a82c5e4d0ee29cd954c"}, + {file = "watchdog-2.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efe3252137392a471a2174d721e1037a0e6a5da7beb72a021e662b7000a9903f"}, + {file = "watchdog-2.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85bf2263290591b7c5fa01140601b64c831be88084de41efbcba6ea289874f44"}, + {file = "watchdog-2.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8f2df370cd8e4e18499dd0bfdef476431bcc396108b97195d9448d90924e3131"}, + {file = "watchdog-2.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ea5d86d1bcf4a9d24610aa2f6f25492f441960cf04aed2bd9a97db439b643a7b"}, + {file = "watchdog-2.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6f5d0f7eac86807275eba40b577c671b306f6f335ba63a5c5a348da151aba0fc"}, + {file = "watchdog-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b848c71ef2b15d0ef02f69da8cc120d335cec0ed82a3fa7779e27a5a8527225"}, + {file = "watchdog-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d9878be36d2b9271e3abaa6f4f051b363ff54dbbe7e7df1af3c920e4311ee43"}, + {file = "watchdog-2.3.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4cd61f98cb37143206818cb1786d2438626aa78d682a8f2ecee239055a9771d5"}, + {file = "watchdog-2.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3d2dbcf1acd96e7a9c9aefed201c47c8e311075105d94ce5e899f118155709fd"}, + {file = "watchdog-2.3.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03f342a9432fe08107defbe8e405a2cb922c5d00c4c6c168c68b633c64ce6190"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7a596f9415a378d0339681efc08d2249e48975daae391d58f2e22a3673b977cf"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:0e1dd6d449267cc7d6935d7fe27ee0426af6ee16578eed93bacb1be9ff824d2d"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_i686.whl", hash = "sha256:7a1876f660e32027a1a46f8a0fa5747ad4fcf86cb451860eae61a26e102c8c79"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:2caf77ae137935c1466f8cefd4a3aec7017b6969f425d086e6a528241cba7256"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:53f3e95081280898d9e4fc51c5c69017715929e4eea1ab45801d5e903dd518ad"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:9da7acb9af7e4a272089bd2af0171d23e0d6271385c51d4d9bde91fe918c53ed"}, + {file = "watchdog-2.3.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8a4d484e846dcd75e96b96d80d80445302621be40e293bfdf34a631cab3b33dc"}, + {file = "watchdog-2.3.1-py3-none-win32.whl", hash = "sha256:a74155398434937ac2780fd257c045954de5b11b5c52fc844e2199ce3eecf4cf"}, + {file = "watchdog-2.3.1-py3-none-win_amd64.whl", hash = "sha256:5defe4f0918a2a1a4afbe4dbb967f743ac3a93d546ea4674567806375b024adb"}, + {file = "watchdog-2.3.1-py3-none-win_ia64.whl", hash = "sha256:4109cccf214b7e3462e8403ab1e5b17b302ecce6c103eb2fc3afa534a7f27b96"}, + {file = "watchdog-2.3.1.tar.gz", hash = "sha256:d9f9ed26ed22a9d331820a8432c3680707ea8b54121ddcc9dc7d9f2ceeb36906"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + [[package]] name = "websockets" version = "10.4" @@ -1433,4 +1474,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "5ca32932250a2a3f00c95b0bdd77d9702b82e951958ee5ca29180f11174ac8e4" +content-hash = "0ed6ff121b610ef2f2993889abbea9b3caa231f92964ab2fdc70667a814ad630" diff --git a/pynecone/utils.py b/pynecone/utils.py index 42b95651c..5813a3b98 100644 --- a/pynecone/utils.py +++ b/pynecone/utils.py @@ -43,6 +43,7 @@ from rich.prompt import Prompt from pynecone import constants from pynecone.base import Base +from pynecone.watch import AssetFolderWatch if TYPE_CHECKING: from pynecone.app import App @@ -590,6 +591,16 @@ def posix_export(backend: bool = True, frontend: bool = True): os.system(cmd) +def start_watching_assets_folder(root): + """Start watching assets folder. + + Args: + root: root path of the project. + """ + asset_watch = AssetFolderWatch(root) + asset_watch.start() + + def setup_frontend(root: Path): """Set up the frontend. @@ -622,6 +633,9 @@ def run_frontend(app: App, root: Path, port: str): # Set up the frontend. setup_frontend(root) + # start watching asset folder + start_watching_assets_folder(root) + # Compile the frontend. app.compile(force_compile=True) diff --git a/pynecone/watch.py b/pynecone/watch.py new file mode 100644 index 000000000..783ea299f --- /dev/null +++ b/pynecone/watch.py @@ -0,0 +1,93 @@ +"""General utility functions.""" + +import contextlib +import os +import shutil +import time + +from watchdog.events import FileSystemEvent, FileSystemEventHandler +from watchdog.observers import Observer + +from pynecone.constants import APP_ASSETS_DIR, WEB_ASSETS_DIR + + +class AssetFolderWatch: + """Asset folder watch class.""" + + def __init__(self, root): + """Initialize the Watch Class. + + Args: + root: root path of the public. + """ + self.path = str(root / APP_ASSETS_DIR) + self.event_handler = AssetFolderHandler(root) + + def start(self): + """Start watching asset folder.""" + self.observer = Observer() + self.observer.schedule(self.event_handler, self.path, recursive=True) + self.observer.start() + + +class AssetFolderHandler(FileSystemEventHandler): + """Asset folder event handler.""" + + def __init__(self, root): + """Initialize the AssetFolderHandler Class. + + Args: + root: root path of the public. + """ + super().__init__() + self.root = root + + def on_modified(self, event: FileSystemEvent): + """Event handler when a file or folder was modified. + This is called every time after a file is created, modified and deleted. + + Args: + event: Event information. + """ + dest_path = self.get_dest_path(event.src_path) + + # wait 1 sec for fully saved + time.sleep(1) + + if os.path.isfile(event.src_path): + with contextlib.suppress(PermissionError): + shutil.copyfile(event.src_path, dest_path) + if os.path.isdir(event.src_path): + if os.path.exists(dest_path): + shutil.rmtree(dest_path) + with contextlib.suppress(PermissionError): + shutil.copytree(event.src_path, dest_path) + + def on_deleted(self, event: FileSystemEvent): + """Event hander when a file or folder was deleted. + + Args: + event: Event infomation. + """ + dest_path = self.get_dest_path(event.src_path) + + if os.path.isfile(dest_path): + # when event is about a file, pass + # this will be deleted at on_modified function + return + + if os.path.exists(dest_path): + shutil.rmtree(dest_path) + + def get_dest_path(self, src_path: str) -> str: + """Get public file path. + + Args: + src_path: The asset file path. + + Returns: + The public file path. + """ + return src_path.replace( + str(self.root / APP_ASSETS_DIR), str(self.root / WEB_ASSETS_DIR) + ) diff --git a/pyproject.toml b/pyproject.toml index 5364d2d7c..d4dcf3ab7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ psutil = "^5.9.4" websockets = "^10.4" cloudpickle = "^2.2.1" python-multipart = "^0.0.5" +watchdog = "^2.3.1" [tool.poetry.group.dev.dependencies] pytest = "^7.1.2"