From 3b88e7c329a2dcba7a5526b333b2482057ff388c Mon Sep 17 00:00:00 2001
From: PeterYusuke <58464065+PeterYusuke@users.noreply.github.com>
Date: Wed, 10 May 2023 06:34:47 +0900
Subject: [PATCH] [Fix 477] Use jinja2 for templating (#915)
---
poetry.lock | 88 ++++++-
pynecone/compiler/compiler.py | 36 +--
pynecone/compiler/templates.py | 247 ++++--------------
pynecone/compiler/utils.py | 147 +++--------
pynecone/components/base/meta.py | 7 +-
pynecone/components/component.py | 20 +-
pynecone/components/layout/cond.py | 30 ++-
pynecone/components/layout/foreach.py | 39 ++-
pynecone/components/tags/cond_tag.py | 20 +-
pynecone/components/tags/iter_tag.py | 26 +-
pynecone/components/tags/tag.py | 49 +---
pynecone/constants.py | 2 +
pynecone/el/element.py | 12 +-
pynecone/templates/app/pcconfig.py.jinja2 | 10 +
.../templates/web/pages/_document.js.jinja2 | 9 +
.../templates/web/pages/base_page.js.jinja2 | 13 +
.../web/pages/custom_component.js.jinja2 | 10 +
pynecone/templates/web/pages/index.js.jinja2 | 66 +++++
pynecone/templates/web/pages/utils.js.jinja2 | 110 ++++++++
pynecone/templates/web/utils/theme.js.jinja2 | 1 +
pynecone/utils/format.py | 4 +-
pynecone/utils/prerequisites.py | 2 +-
pyproject.toml | 1 +
tests/compiler/test_compiler.py | 103 ++++----
tests/components/base/test_bare.py | 4 +-
.../components/datadisplay/test_datatable.py | 13 +-
tests/components/forms/test_uploads.py | 57 ++--
tests/components/layout/test_cond.py | 25 +-
tests/components/test_tag.py | 80 +++---
tests/test_utils.py | 2 +-
30 files changed, 691 insertions(+), 542 deletions(-)
create mode 100644 pynecone/templates/app/pcconfig.py.jinja2
create mode 100644 pynecone/templates/web/pages/_document.js.jinja2
create mode 100644 pynecone/templates/web/pages/base_page.js.jinja2
create mode 100644 pynecone/templates/web/pages/custom_component.js.jinja2
create mode 100644 pynecone/templates/web/pages/index.js.jinja2
create mode 100644 pynecone/templates/web/pages/utils.js.jinja2
create mode 100644 pynecone/templates/web/utils/theme.js.jinja2
diff --git a/poetry.lock b/poetry.lock
index 55cd70c49..efdf81a70 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -463,6 +463,84 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
+[[package]]
+name = "jinja2"
+version = "3.1.2"
+description = "A very fast and expressive template engine."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
+ {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.2"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
+ {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
+ {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
+ {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
+ {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
+ {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
+ {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
+]
+
[[package]]
name = "mypy-extensions"
version = "1.0.0"
@@ -1063,18 +1141,18 @@ files = [
[[package]]
name = "redis"
-version = "4.5.4"
+version = "4.5.5"
description = "Python client for Redis database and key-value store"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "redis-4.5.4-py3-none-any.whl", hash = "sha256:2c19e6767c474f2e85167909061d525ed65bea9301c0770bb151e041b7ac89a2"},
- {file = "redis-4.5.4.tar.gz", hash = "sha256:73ec35da4da267d6847e47f68730fdd5f62e2ca69e3ef5885c6a78a9374c3893"},
+ {file = "redis-4.5.5-py3-none-any.whl", hash = "sha256:77929bc7f5dab9adf3acba2d3bb7d7658f1e0c2f1cafe7eb36434e751c471119"},
+ {file = "redis-4.5.5.tar.gz", hash = "sha256:dc87a0bdef6c8bfe1ef1e1c40be7034390c2ae02d92dcd0c7ca1729443899880"},
]
[package.dependencies]
-async-timeout = {version = ">=4.0.2", markers = "python_version <= \"3.11.2\""}
+async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""}
importlib-metadata = {version = ">=1.0", markers = "python_version < \"3.8\""}
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
@@ -1600,4 +1678,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata]
lock-version = "2.0"
python-versions = "^3.7"
-content-hash = "8c9f764a830657316f774fb679895c89d6874460582f4dbf8b2edbd4f534b262"
+content-hash = "43e53d9fff649b6a939ec953411b8932e512600b5af3edc0cbbbbb6c5576168b"
diff --git a/pynecone/compiler/compiler.py b/pynecone/compiler/compiler.py
index c54621279..fae707013 100644
--- a/pynecone/compiler/compiler.py
+++ b/pynecone/compiler/compiler.py
@@ -1,7 +1,6 @@
"""Compiler for the pynecone apps."""
from __future__ import annotations
-import json
from functools import wraps
from typing import Callable, List, Set, Tuple, Type
@@ -10,7 +9,7 @@ from pynecone.compiler import templates, utils
from pynecone.components.component import Component, CustomComponent
from pynecone.state import State
from pynecone.style import Style
-from pynecone.utils import imports, path_ops
+from pynecone.utils import imports
from pynecone.var import ImportVar
# Imports to be included in every Pynecone app.
@@ -42,7 +41,7 @@ def _compile_document_root(root: Component) -> str:
Returns:
The compiled document root.
"""
- return templates.DOCUMENT_ROOT(
+ return templates.DOCUMENT_ROOT.render(
imports=utils.compile_imports(root.get_imports()),
document=root.render(),
)
@@ -57,7 +56,7 @@ def _compile_theme(theme: dict) -> str:
Returns:
The compiled theme.
"""
- return templates.THEME(theme=json.dumps(theme))
+ return templates.THEME.render(theme=theme)
def _compile_page(component: Component, state: Type[State]) -> str:
@@ -72,17 +71,20 @@ def _compile_page(component: Component, state: Type[State]) -> str:
"""
# Merge the default imports with the app-specific imports.
imports = utils.merge_imports(DEFAULT_IMPORTS, component.get_imports())
+ imports = utils.compile_imports(imports)
# Compile the code to render the component.
- return templates.PAGE(
- imports=utils.compile_imports(imports),
- custom_code=path_ops.join(component.get_custom_code()),
- constants=utils.compile_constants(),
- state=utils.compile_state(state),
- events=utils.compile_events(state),
- effects=utils.compile_effects(state),
- hooks=path_ops.join(component.get_hooks()),
+ return templates.PAGE.render(
+ imports=imports,
+ custom_codes=component.get_custom_code(),
+ endpoints={
+ constant.name: constant.get_url() for constant in constants.Endpoint
+ },
+ initial_state=utils.compile_state(state),
+ state_name=state.get_name(),
+ hooks=component.get_hooks(),
render=component.render(),
+ transports=constants.Transports.POLLING_WEBSOCKET.get_transports(),
)
@@ -99,18 +101,18 @@ def _compile_components(components: Set[CustomComponent]) -> str:
"react": {ImportVar(tag="memo")},
f"/{constants.STATE_PATH}": {ImportVar(tag="E"), ImportVar(tag="isTrue")},
}
- component_defs = []
+ component_renders = []
# Compile each component.
for component in components:
- component_def, component_imports = utils.compile_custom_component(component)
- component_defs.append(component_def)
+ component_render, component_imports = utils.compile_custom_component(component)
+ component_renders.append(component_render)
imports = utils.merge_imports(imports, component_imports)
# Compile the components page.
- return templates.COMPONENTS(
+ return templates.COMPONENTS.render(
imports=utils.compile_imports(imports),
- components=path_ops.join(component_defs),
+ components=component_renders,
)
diff --git a/pynecone/compiler/templates.py b/pynecone/compiler/templates.py
index a5c24e7ce..c97bea9a5 100644
--- a/pynecone/compiler/templates.py
+++ b/pynecone/compiler/templates.py
@@ -1,163 +1,75 @@
"""Templates to use in the pynecone compiler."""
-from typing import Optional, Set
+from jinja2 import Environment, FileSystemLoader, Template
from pynecone import constants
from pynecone.utils import path_ops
+from pynecone.utils.format import json_dumps
+
+
+class PyneconeJinjaEnvironment(Environment):
+ """The template class for jinja environment."""
+
+ def __init__(self) -> None:
+ """Set default environment."""
+ extensions = ["jinja2.ext.debug"]
+ super().__init__(
+ extensions=extensions,
+ trim_blocks=True,
+ lstrip_blocks=True,
+ )
+ self.filters["json_dumps"] = json_dumps
+ self.filters["react_setter"] = lambda state: f"set{state.capitalize()}"
+ self.loader = FileSystemLoader(constants.JINJA_TEMPLATE_DIR)
+ self.globals["const"] = {
+ "socket": constants.SOCKET,
+ "result": constants.RESULT,
+ "router": constants.ROUTER,
+ "event_endpoint": constants.Endpoint.EVENT.name,
+ "events": constants.EVENTS,
+ "state": constants.STATE,
+ "processing": constants.PROCESSING,
+ "initial_result": {
+ constants.STATE: None,
+ constants.EVENTS: [],
+ constants.PROCESSING: False,
+ },
+ "color_mode": constants.COLOR_MODE,
+ "toggle_color_mode": constants.TOGGLE_COLOR_MODE,
+ "use_color_mode": constants.USE_COLOR_MODE,
+ }
+
+
+def get_template(name: str) -> Template:
+ """Get render function that work with a template.
+
+ Args:
+ name: The template name. "/" is used as the path separator.
+
+ Returns:
+ A render function.
+ """
+ return PyneconeJinjaEnvironment().get_template(name=name)
+
# Template for the Pynecone config file.
-PCCONFIG = f"""import pynecone as pc
-
-class {{config_name}}(pc.Config):
- pass
-
-config = {{config_name}}(
- app_name="{{app_name}}",
- db_url="{constants.DB_URL}",
- env=pc.Env.DEV,
-)
-"""
-
-# Javascript formatting.
-CONST = "const {name} = {value}".format
-PROP = "{object}.{property}".format
-IMPORT_LIB = 'import "{lib}"'.format
-IMPORT_FIELDS = 'import {default}{others} from "{lib}"'.format
-
-
-def format_import(lib: str, default: str = "", rest: Optional[Set[str]] = None) -> str:
- """Format an import statement.
-
- Args:
- lib: The library to import from.
- default: The default field to import.
- rest: The set of fields to import from the library.
-
- Returns:
- The compiled import statement.
- """
- # Handle the case of direct imports with no libraries.
- if not lib:
- assert not default, "No default field allowed for empty library."
- assert rest is not None and len(rest) > 0, "No fields to import."
- return path_ops.join([IMPORT_LIB(lib=lib) for lib in sorted(rest)])
-
- # Handle importing from a library.
- rest = rest or set()
- if len(default) == 0 and len(rest) == 0:
- # Handle the case of importing a library with no fields.
- return IMPORT_LIB(lib=lib)
- # Handle importing specific fields from a library.
- others = f'{{{", ".join(sorted(rest))}}}' if len(rest) > 0 else ""
- if default != "" and len(rest) > 0:
- default += ", "
- return IMPORT_FIELDS(default=default, others=others, lib=lib)
-
+PCCONFIG = get_template("app/pcconfig.py.jinja2")
# Code to render a NextJS Document root.
-DOCUMENT_ROOT = path_ops.join(
- [
- "{imports}",
- "export default function Document() {{",
- "return (",
- "{document}",
- ")",
- "}}",
- ]
-).format
+DOCUMENT_ROOT = get_template("web/pages/_document.js.jinja2")
# Template for the theme file.
-THEME = "export default {theme}".format
+THEME = get_template("web/utils/theme.js.jinja2")
# Code to render a single NextJS page.
-PAGE = path_ops.join(
- [
- "{imports}",
- "{custom_code}",
- "{constants}",
- "export default function Component() {{",
- "{state}",
- "{events}",
- "{effects}",
- "{hooks}",
- "return (",
- "{render}",
- ")",
- "}}",
- ]
-).format
-
-# Code to render a single exported custom component.
-COMPONENT = path_ops.join(
- [
- "export const {name} = memo(({{{props}}}) => (",
- "{render}",
- "))",
- ]
-).format
+PAGE = get_template("web/pages/index.js.jinja2")
# Code to render the custom components page.
-COMPONENTS = path_ops.join(
- [
- "{imports}",
- "{components}",
- ]
-).format
+COMPONENTS = get_template("web/pages/custom_component.js.jinja2")
+# Sitemap config file.
+SITEMAP_CONFIG = "module.exports = {config}".format
-# React state declarations.
-USE_STATE = CONST(
- name="[{state}, {set_state}]", value="useState({initial_state})"
-).format
-
-
-def format_state_setter(state: str) -> str:
- """Format a state setter.
-
- Args:
- state: The name of the state variable.
-
- Returns:
- The compiled state setter.
- """
- return f"set{state[0].upper() + state[1:]}"
-
-
-def format_state(
- state: str,
- initial_state: str,
-) -> str:
- """Format a state declaration.
-
- Args:
- state: The name of the state variable.
- initial_state: The initial state of the state variable.
-
- Returns:
- The compiled state declaration.
- """
- set_state = format_state_setter(state)
- return USE_STATE(state=state, set_state=set_state, initial_state=initial_state)
-
-
-# Events.
-EVENT_ENDPOINT = constants.Endpoint.EVENT.name
-EVENT_FN = path_ops.join(
- [
- "const Event = events => {set_state}({{",
- " ...{state},",
- " events: [...{state}.events, ...events],",
- "}})",
- ]
-).format
-UPLOAD_FN = path_ops.join(
- [
- "const File = files => {set_state}({{",
- " ...{state},",
- " files,",
- "}})",
- ]
-).format
FULL_CONTROL = path_ops.join(
[
"{{setState(prev => ({{",
@@ -167,52 +79,3 @@ FULL_CONTROL = path_ops.join(
")}}",
]
).format
-
-# Effects.
-ROUTER = constants.ROUTER
-RESULT = constants.RESULT
-PROCESSING = constants.PROCESSING
-SOCKET = constants.SOCKET
-STATE = constants.STATE
-EVENTS = constants.EVENTS
-SET_RESULT = format_state_setter(RESULT)
-READY = f"const {{ isReady }} = {ROUTER};"
-USE_EFFECT = path_ops.join(
- [
- "useEffect(() => {{",
- " if(!isReady) {{",
- " return;",
- " }}",
- f" if (!{SOCKET}.current) {{{{",
- f" connect({SOCKET}, {{state}}, {{set_state}}, {RESULT}, {SET_RESULT}, {ROUTER}, {EVENT_ENDPOINT}, {{transports}})",
- " }}",
- " const update = async () => {{",
- f" if ({RESULT}.{STATE} != null) {{{{",
- f" {{set_state}}({{{{",
- f" ...{RESULT}.{STATE},",
- f" events: [...{{state}}.{EVENTS}, ...{RESULT}.{EVENTS}],",
- " }})",
- f" {SET_RESULT}({{{{",
- f" {STATE}: null,",
- f" {EVENTS}: [],",
- f" {PROCESSING}: false,",
- " }})",
- " }}",
- f" await updateState({{state}}, {{set_state}}, {RESULT}, {SET_RESULT}, {ROUTER}, {SOCKET}.current)",
- " }}",
- " update()",
- "}})",
- ]
-).format
-
-# Routing
-ROUTER = f"const {constants.ROUTER} = useRouter()"
-
-# Sockets.
-SOCKET = "const socket = useRef(null)"
-
-# Color toggle
-COLORTOGGLE = f"const {{ {constants.COLOR_MODE}, {constants.TOGGLE_COLOR_MODE} }} = {constants.USE_COLOR_MODE}()"
-
-# Sitemap config file.
-SITEMAP_CONFIG = "module.exports = {config}".format
diff --git a/pynecone/compiler/utils.py b/pynecone/compiler/utils.py
index a7dba4f61..d569f3cd2 100644
--- a/pynecone/compiler/utils.py
+++ b/pynecone/compiler/utils.py
@@ -1,11 +1,9 @@
"""Common utility functions used in the compiler."""
-import json
import os
from typing import Dict, List, Optional, Set, Tuple, Type
from pynecone import constants
-from pynecone.compiler import templates
from pynecone.components.base import (
Body,
ColorModeScript,
@@ -31,15 +29,16 @@ from pynecone.var import ImportVar
merge_imports = imports.merge_imports
-def compile_import_statement(lib: str, fields: Set[ImportVar]) -> str:
+def compile_import_statement(fields: Set[ImportVar]) -> Tuple[str, Set[str]]:
"""Compile an import statement.
Args:
- lib: The library to import from.
fields: The set of fields to import from the library.
Returns:
- The compiled import statement.
+ The libraries for default and rest.
+ default: default library. When install "import def from library".
+ rest: rest of libraries. When install "import {rest1, rest2} from library"
"""
# Check for default imports.
defaults = {field for field in fields if field.is_default}
@@ -48,58 +47,59 @@ def compile_import_statement(lib: str, fields: Set[ImportVar]) -> str:
# Get the default import, and the specific imports.
default = next(iter({field.name for field in defaults}), "")
rest = {field.name for field in fields - defaults}
- return templates.format_import(lib=lib, default=default, rest=rest)
+
+ return default, rest
-def compile_imports(imports: imports.ImportDict) -> str:
+def compile_imports(imports: imports.ImportDict) -> List[dict]:
"""Compile an import dict.
Args:
imports: The import dict to compile.
Returns:
- The compiled import dict.
+ The list of import dict.
"""
- return path_ops.join(
- [compile_import_statement(lib, fields) for lib, fields in imports.items()]
- )
+ import_dicts = []
+ for lib, fields in imports.items():
+ default, rest = compile_import_statement(fields)
+ if not lib:
+ assert not default, "No default field allowed for empty library."
+ assert rest is not None and len(rest) > 0, "No fields to import."
+ for module in sorted(rest):
+ import_dicts.append(get_import_dict(module))
+ continue
+
+ import_dicts.append(get_import_dict(lib, default, rest))
+ return import_dicts
-def compile_constant_declaration(name: str, value: str) -> str:
- """Compile a constant declaration.
+def get_import_dict(lib: str, default: str = "", rest: Optional[Set] = None) -> Dict:
+ """Get dictionary for import template.
Args:
- name: The name of the constant.
- value: The value of the constant.
+ lib: The importing react library.
+ default: The default module to import.
+ rest: The rest module to import.
Returns:
- The compiled constant declaration.
+ A dictionary for import template.
"""
- return templates.CONST(name=name, value=json.dumps(value))
+ return {
+ "lib": lib,
+ "default": default,
+ "rest": rest if rest else set(),
+ }
-def compile_constants() -> str:
- """Compile all the necessary constants.
-
- Returns:
- A string of all the compiled constants.
- """
- return path_ops.join(
- [
- compile_constant_declaration(name=endpoint.name, value=endpoint.get_url())
- for endpoint in constants.Endpoint
- ]
- )
-
-
-def compile_state(state: Type[State]) -> str:
+def compile_state(state: Type[State]) -> Dict:
"""Compile the state of the app.
Args:
state: The app state object.
Returns:
- A string of the compiled state.
+ A dictionary of the compiled state.
"""
initial_state = state().dict()
initial_state.update(
@@ -108,77 +108,12 @@ def compile_state(state: Type[State]) -> str:
"files": [],
}
)
- initial_state = format.format_state(initial_state)
- synced_state = templates.format_state(
- state=state.get_name(), initial_state=json.dumps(initial_state)
- )
- initial_result = {
- constants.STATE: None,
- constants.EVENTS: [],
- constants.PROCESSING: False,
- }
- result = templates.format_state(
- state="result",
- initial_state=json.dumps(initial_result),
- )
- router = templates.ROUTER
- socket = templates.SOCKET
- ready = templates.READY
- color_toggle = templates.COLORTOGGLE
- return path_ops.join([synced_state, result, router, socket, ready, color_toggle])
-
-
-def compile_events(state: Type[State]) -> str:
- """Compile all the events for a given component.
-
- Args:
- state: The state class for the component.
-
- Returns:
- A string of the compiled events for the component.
- """
- state_name = state.get_name()
- state_setter = templates.format_state_setter(state_name)
- return path_ops.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:
- """Compile all the effects for a given component.
-
- Args:
- state: The state class for the component.
-
- Returns:
- A string of the compiled effects for the component.
- """
- state_name = state.get_name()
- set_state = templates.format_state_setter(state_name)
- transports = constants.Transports.POLLING_WEBSOCKET.get_transports()
- return templates.USE_EFFECT(
- state=state_name, set_state=set_state, transports=transports
- )
-
-
-def compile_render(component: Component) -> str:
- """Compile the component's render method.
-
- Args:
- component: The component to compile the render method for.
-
- Returns:
- A string of the compiled render method.
- """
- return component.render()
+ return format.format_state(initial_state)
def compile_custom_component(
component: CustomComponent,
-) -> Tuple[str, imports.ImportDict]:
+) -> Tuple[dict, imports.ImportDict]:
"""Compile a custom component.
Args:
@@ -198,15 +133,15 @@ def compile_custom_component(
}
# Concatenate the props.
- props = ", ".join([prop.name for prop in component.get_prop_vars()])
+ props = [prop.name for prop in component.get_prop_vars()]
# Compile the component.
return (
- templates.COMPONENT(
- name=component.tag,
- props=props,
- render=render,
- ),
+ {
+ "name": component.tag,
+ "props": props,
+ "render": render.render(),
+ },
imports,
)
diff --git a/pynecone/components/base/meta.py b/pynecone/components/base/meta.py
index 99d0c9167..a6d922461 100644
--- a/pynecone/components/base/meta.py
+++ b/pynecone/components/base/meta.py
@@ -1,6 +1,6 @@
"""Display the title of the current page."""
-from typing import Optional
+from typing import Dict, Optional
from pynecone.components.base.bare import Bare
from pynecone.components.component import Component
@@ -11,18 +11,17 @@ class Title(Component):
tag = "title"
- def render(self) -> str:
+ def render(self) -> Dict:
"""Render the title component.
Returns:
The rendered title component.
"""
- tag = self._render()
# Make sure the title is a single string.
assert len(self.children) == 1 and isinstance(
self.children[0], Bare
), "Title must be a single string."
- return str(tag.set(contents=str(self.children[0].contents)))
+ return super().render()
class Meta(Component):
diff --git a/pynecone/components/component.py b/pynecone/components/component.py
index 94e319194..efa1e4a86 100644
--- a/pynecone/components/component.py
+++ b/pynecone/components/component.py
@@ -21,7 +21,7 @@ from pynecone.event import (
get_handler_args,
)
from pynecone.style import Style
-from pynecone.utils import format, imports, path_ops, types
+from pynecone.utils import format, imports, types
from pynecone.var import BaseVar, ImportVar, Var
@@ -289,7 +289,7 @@ class Component(Base, ABC):
Returns:
The code to render the component.
"""
- return self.render()
+ return format.json_dumps(self.render())
def __str__(self) -> str:
"""Represent the component in React.
@@ -297,7 +297,7 @@ class Component(Base, ABC):
Returns:
The code to render the component.
"""
- return self.render()
+ return format.json_dumps(self.render())
def _render(self) -> Tag:
"""Define how to render the component in React.
@@ -393,14 +393,14 @@ class Component(Base, ABC):
child.add_style(style)
return self
- def render(self) -> str:
+ def render(self) -> Dict:
"""Render the component.
Returns:
- The code to render the component.
+ The dictionary for template of component.
"""
tag = self._render()
- return str(
+ return dict(
tag.add_props(
**self.event_triggers,
key=self.key,
@@ -408,10 +408,10 @@ class Component(Base, ABC):
id=self.id,
class_name=self.class_name,
).set(
- contents=path_ops.join(
- [str(tag.contents)] + [child.render() for child in self.children]
- ).strip(),
- )
+ children=[child.render() for child in self.children],
+ contents=str(tag.contents),
+ props=tag.format_props(),
+ ),
)
def _get_custom_code(self) -> Optional[str]:
diff --git a/pynecone/components/layout/cond.py b/pynecone/components/layout/cond.py
index 913bce8be..543194ee6 100644
--- a/pynecone/components/layout/cond.py
+++ b/pynecone/components/layout/cond.py
@@ -1,7 +1,7 @@
"""Create a list of components from an iterable."""
from __future__ import annotations
-from typing import Any, Optional
+from typing import Any, Dict, Optional
from pynecone.components.component import Component
from pynecone.components.layout.fragment import Fragment
@@ -24,7 +24,7 @@ class Cond(Component):
@classmethod
def create(
- cls, cond: Var, comp1: Component, comp2: Optional[Component] = None
+ cls, cond: Var, comp1: Component, comp2: Optional[Component]
) -> Component:
"""Create a conditional component.
@@ -37,8 +37,10 @@ class Cond(Component):
The conditional component.
"""
# Wrap everything in fragments.
- comp1 = Fragment.create(comp1)
- comp2 = Fragment.create(comp2) if comp2 else Fragment.create()
+ if comp1.__class__.__name__ != "Fragment":
+ comp1 = Fragment.create(comp1)
+ if comp2 is None or comp2.__class__.__name__ != "Fragment":
+ comp2 = Fragment.create(comp2) if comp2 else Fragment.create()
return Fragment.create(
cls(
cond=cond,
@@ -55,6 +57,26 @@ class Cond(Component):
false_value=self.comp2.render(),
)
+ def render(self) -> Dict:
+ """Render the component.
+
+ Returns:
+ The dictionary for template of component.
+ """
+ tag = self._render()
+ return dict(
+ tag.add_props(
+ **self.event_triggers,
+ key=self.key,
+ sx=self.style,
+ id=self.id,
+ class_name=self.class_name,
+ ).set(
+ props=tag.format_props(),
+ ),
+ cond_state=f"isTrue({self.cond.full_name})",
+ )
+
def cond(condition: Any, c1: Any, c2: Any = None):
"""Create a conditional component or Prop.
diff --git a/pynecone/components/layout/foreach.py b/pynecone/components/layout/foreach.py
index 33a8c37cc..8380bb3d5 100644
--- a/pynecone/components/layout/foreach.py
+++ b/pynecone/components/layout/foreach.py
@@ -4,8 +4,8 @@ from __future__ import annotations
from typing import Any, Callable, List
from pynecone.components.component import Component
-from pynecone.components.tags import IterTag, Tag
-from pynecone.var import BaseVar, Var
+from pynecone.components.tags import IterTag
+from pynecone.var import BaseVar, Var, get_unique_variable_name
class Foreach(Component):
@@ -49,5 +49,38 @@ class Foreach(Component):
**props,
)
- def _render(self) -> Tag:
+ def _render(self) -> IterTag:
return IterTag(iterable=self.iterable, render_fn=self.render_fn)
+
+ def render(self):
+ """Render the component.
+
+ Returns:
+ The dictionary for template of component.
+ """
+ tag = self._render()
+ try:
+ type_ = self.iterable.type_.__args__[0]
+ except Exception:
+ type_ = Any
+ arg = BaseVar(
+ name=get_unique_variable_name(),
+ type_=type_,
+ )
+ index_arg = tag.get_index_var_arg()
+ component = tag.render_component(self.render_fn, arg)
+ return dict(
+ tag.add_props(
+ **self.event_triggers,
+ key=self.key,
+ sx=self.style,
+ id=self.id,
+ class_name=self.class_name,
+ ).set(
+ children=[component.render()],
+ props=tag.format_props(),
+ ),
+ iterable_state=tag.iterable.full_name,
+ arg_name=arg.name,
+ arg_index=index_arg,
+ )
diff --git a/pynecone/components/tags/cond_tag.py b/pynecone/components/tags/cond_tag.py
index f6c6cc888..f66a2fe9b 100644
--- a/pynecone/components/tags/cond_tag.py
+++ b/pynecone/components/tags/cond_tag.py
@@ -1,9 +1,8 @@
"""Tag to conditionally render components."""
-from typing import Any
+from typing import Any, Dict, Optional
from pynecone.components.tags.tag import Tag
-from pynecone.utils import format
from pynecone.var import Var
@@ -14,20 +13,7 @@ class CondTag(Tag):
cond: Var[Any]
# The code to render if the condition is true.
- true_value: str
+ true_value: Dict
# The code to render if the condition is false.
- false_value: str
-
- def __str__(self) -> str:
- """Render the tag as a React string.
-
- Returns:
- The React code to render the tag.
- """
- assert self.cond is not None, "The condition must be set."
- return format.format_cond(
- cond=self.cond.full_name,
- true_value=self.true_value,
- false_value=self.false_value,
- )
+ false_value: Optional[Dict]
diff --git a/pynecone/components/tags/iter_tag.py b/pynecone/components/tags/iter_tag.py
index 51572d9ce..cac7bfd4c 100644
--- a/pynecone/components/tags/iter_tag.py
+++ b/pynecone/components/tags/iter_tag.py
@@ -2,11 +2,10 @@
from __future__ import annotations
import inspect
-from typing import TYPE_CHECKING, Any, Callable, List
+from typing import TYPE_CHECKING, Callable, List
from pynecone.components.tags.tag import Tag
-from pynecone.utils import format
-from pynecone.var import BaseVar, Var, get_unique_variable_name
+from pynecone.var import Var
if TYPE_CHECKING:
from pynecone.components.component import Component
@@ -83,24 +82,3 @@ class IterTag(Tag):
component.key = index
return component
-
- def __str__(self) -> str:
- """Render the tag as a React string.
-
- Returns:
- The React code to render the tag.
- """
- try:
- type_ = self.iterable.type_.__args__[0]
- except Exception:
- type_ = Any
- arg = BaseVar(
- name=get_unique_variable_name(),
- type_=type_,
- )
- index_arg = self.get_index_var_arg()
- component = self.render_component(self.render_fn, arg)
- return format.wrap(
- f"{self.iterable.full_name}.map(({arg.name}, {index_arg}) => {component})",
- "{",
- )
diff --git a/pynecone/components/tags/tag.py b/pynecone/components/tags/tag.py
index f23e975ee..a8795e6dd 100644
--- a/pynecone/components/tags/tag.py
+++ b/pynecone/components/tags/tag.py
@@ -3,9 +3,8 @@
from __future__ import annotations
import json
-import os
import re
-from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Tuple, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
from plotly.graph_objects import Figure
from plotly.io import to_json
@@ -37,6 +36,9 @@ class Tag(Base):
# Special props that aren't key value pairs.
special_props: Set[Var] = set()
+ # The children components.
+ children: List[Any] = []
+
def __init__(self, *args, **kwargs):
"""Initialize the tag.
@@ -117,55 +119,22 @@ class Tag(Base):
assert isinstance(prop, str), "The prop must be a string."
return format.wrap(prop, "{", check_first=False)
- def format_props(self) -> str:
+ def format_props(self) -> List:
"""Format the tag's props.
Returns:
- The formatted props.
+ The formatted props list.
"""
# If there are no props, return an empty string.
if len(self.props) == 0:
- return ""
+ return []
# Format all the props.
- return os.linesep.join(
+ return [
f"{name}={self.format_prop(prop)}"
for name, prop in sorted(self.props.items())
if prop is not None
- )
-
- def __str__(self) -> str:
- """Render the tag as a React string.
-
- Returns:
- The React code to render the tag.
- """
- # 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
-
- if len(self.contents) == 0:
- # If there is no inner content, we don't need a closing tag.
- tag_str = format.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 = format.wrap(f"{self.name}{props_str}", "<")
- close = format.wrap(f"/{self.name}", "<")
- tag_str = format.wrap(contents, open, close)
-
- return tag_str
+ ] + [str(prop) for prop in self.special_props]
def add_props(self, **kwargs: Optional[Any]) -> Tag:
"""Add props to the tag.
diff --git a/pynecone/constants.py b/pynecone/constants.py
index 44ad25eea..d7f01b7d3 100644
--- a/pynecone/constants.py
+++ b/pynecone/constants.py
@@ -33,6 +33,8 @@ TEMPLATE_DIR = os.path.join(ROOT_DIR, MODULE_NAME, ".templates")
WEB_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, "web")
# The assets subdirectory of the template directory.
ASSETS_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, APP_ASSETS_DIR)
+# The jinja template directory.
+JINJA_TEMPLATE_DIR = os.path.join(ROOT_DIR, MODULE_NAME, "templates")
# The frontend directories in a project.
# The web folder where the NextJS app is compiled to.
diff --git a/pynecone/el/element.py b/pynecone/el/element.py
index 849a60305..2b17e1250 100644
--- a/pynecone/el/element.py
+++ b/pynecone/el/element.py
@@ -1,7 +1,8 @@
"""Base class definition for raw HTML elements."""
+from typing import Dict
+
from pynecone.components.component import Component
-from pynecone.utils import path_ops
class Element(Component):
@@ -12,14 +13,14 @@ class Element(Component):
prop.
"""
- def render(self) -> str:
+ def render(self) -> Dict:
"""Render the element.
Returns:
The code to render the element.
"""
tag = self._render()
- return str(
+ return dict(
tag.add_props(
**self.event_triggers,
key=self.key,
@@ -27,9 +28,8 @@ class Element(Component):
style=self.style,
class_name=self.class_name,
).set(
- contents=path_ops.join(
- [str(tag.contents)] + [child.render() for child in self.children]
- ).strip(),
+ contents=str(tag.contents),
+ children=[child.render() for child in self.children],
)
)
diff --git a/pynecone/templates/app/pcconfig.py.jinja2 b/pynecone/templates/app/pcconfig.py.jinja2
new file mode 100644
index 000000000..da215ed10
--- /dev/null
+++ b/pynecone/templates/app/pcconfig.py.jinja2
@@ -0,0 +1,10 @@
+import pynecone as pc
+
+class {{ config_name }}(pc.Config):
+ pass
+
+config = {{ config_name }}(
+ app_name="{{ app_name }}",
+ db_url="{{ db_url }}",
+ env=pc.Env.DEV,
+)
diff --git a/pynecone/templates/web/pages/_document.js.jinja2 b/pynecone/templates/web/pages/_document.js.jinja2
new file mode 100644
index 000000000..f17c42069
--- /dev/null
+++ b/pynecone/templates/web/pages/_document.js.jinja2
@@ -0,0 +1,9 @@
+{% extends "web/pages/base_page.js.jinja2" %}
+
+{% block export %}
+export default function Document() {
+ return (
+ {{utils.render(document, indent_width=4)}}
+ )
+}
+{% endblock %}
diff --git a/pynecone/templates/web/pages/base_page.js.jinja2 b/pynecone/templates/web/pages/base_page.js.jinja2
new file mode 100644
index 000000000..0f3868aa4
--- /dev/null
+++ b/pynecone/templates/web/pages/base_page.js.jinja2
@@ -0,0 +1,13 @@
+{% import 'web/pages/utils.js.jinja2' as utils %}
+
+{%- block imports_libs %}
+{% for module in imports%}
+ {{- utils.get_import(module) }}
+{% endfor %}
+{% endblock %}
+
+{% block declaration %}
+{% endblock %}
+
+{% block export %}
+{% endblock %}
diff --git a/pynecone/templates/web/pages/custom_component.js.jinja2 b/pynecone/templates/web/pages/custom_component.js.jinja2
new file mode 100644
index 000000000..89d5d7abb
--- /dev/null
+++ b/pynecone/templates/web/pages/custom_component.js.jinja2
@@ -0,0 +1,10 @@
+{% extends "web/pages/base_page.js.jinja2" %}
+
+{% block export %}
+{% for component in components %}
+
+export const {{component.name}} = memo(({ {{-component.props|join(", ")-}} }) => (
+ {{utils.render(component.render)}}
+))
+{% endfor %}
+{% endblock %}
diff --git a/pynecone/templates/web/pages/index.js.jinja2 b/pynecone/templates/web/pages/index.js.jinja2
new file mode 100644
index 000000000..819f7c673
--- /dev/null
+++ b/pynecone/templates/web/pages/index.js.jinja2
@@ -0,0 +1,66 @@
+{% extends "web/pages/base_page.js.jinja2" %}
+
+{% block declaration %}
+{% for custom_code in custom_codes %}
+{{custom_code}}
+{% endfor %}
+
+{% for name, url in endpoints.items() %}
+const {{name}} = {{url|json_dumps}}
+{% endfor %}
+{% endblock %}
+
+{% block export %}
+export default function Component() {
+ const [{{state_name}}, {{state_name|react_setter}}] = useState({{initial_state|json_dumps}})
+ const [{{const.result}}, {{const.result|react_setter}}] = useState({{const.initial_result|json_dumps}})
+ const {{const.router}} = useRouter()
+ const {{const.socket}} = useRef(null)
+ const { isReady } = {{const.router}}
+ const { {{const.color_mode}}, {{const.toggle_color_mode}} } = {{const.use_color_mode}}()
+
+ const Event = events => {{state_name|react_setter}}({
+ ...{{state_name}},
+ events: [...{{state_name}}.events, ...events],
+ })
+
+ const File = files => {{state_name|react_setter}}({
+ ...{{state_name}},
+ files,
+ })
+
+ useEffect(()=>{
+ if(!isReady) {
+ return;
+ }
+ if (!{{const.socket}}.current) {
+ connect({{const.socket}}, {{state_name}}, {{state_name|react_setter}}, {{const.result}}, {{const.result|react_setter}}, {{const.router}}, {{const.event_endpoint}}, {{transports}})
+ }
+ const update = async () => {
+ if ({{const.result}}.{{const.state}} != null){
+ {{state_name|react_setter}}({
+ ...{{const.result}}.{{const.state}},
+ events: [...{{state_name}}.{{const.events}}, ...{{const.result}}.{{const.events}}],
+ })
+
+ {{const.result|react_setter}}({
+ {{const.state}}: null,
+ {{const.events}}: [],
+ {{const.processing}}: false,
+ })
+ }
+
+ await updateState({{state_name}}, {{state_name|react_setter}}, {{const.result}}, {{const.result|react_setter}}, {{const.router}}, {{const.socket}}.current)
+ }
+ update()
+ })
+
+ {% for hook in hooks %}
+ {{ hook }}
+ {% endfor %}
+
+ return (
+ {{utils.render(render, indent_width=4)}}
+ )
+}
+{% endblock %}
diff --git a/pynecone/templates/web/pages/utils.js.jinja2 b/pynecone/templates/web/pages/utils.js.jinja2
new file mode 100644
index 000000000..7ec0b01f9
--- /dev/null
+++ b/pynecone/templates/web/pages/utils.js.jinja2
@@ -0,0 +1,110 @@
+{# Renderting components recursively. #}
+{# Args: #}
+{# component: component dictionary #}
+{# indent_width: indent width #}
+{% macro render(component, indent_width=2) %}
+{% filter indent(width=indent_width) %}
+ {%- if component is not mapping %}
+ {{- component }}
+ {%- elif component.iterable %}
+ {{- render_iterable_tag(component) }}
+ {%- elif component.cond %}
+ {{- render_condition_tag(component) }}
+ {%- elif component.children|length %}
+ {{- render_tag(component) }}
+ {%- else %}
+ {{- render_self_close_tag(component) }}
+ {%- endif %}
+{% endfilter %}
+{% endmacro %}
+
+{# Renderting self close tag. #}
+{# Args: #}
+{# component: component dictionary #}
+{% macro render_self_close_tag(component) %}
+{%- if component.name|length %}
+<{{ component.name }} {{- render_props(component.props) }}/>
+{%- else %}
+ {{- component.contents }}
+{%- endif %}
+{% endmacro %}
+
+{# Renderting close tag with args and props. #}
+{# Args: #}
+{# component: component dictionary #}
+{% macro render_tag(component) %}
+<{{component.name}} {{- render_props(component.props) }}>
+{%- if component.args is not none -%}
+ {{- render_arg_content(component) }}
+{%- else -%}
+ {{ component.contents }}
+ {% for child in component.children %}
+ {{ render(child) }}
+ {% endfor %}
+{%- endif -%}
+{{component.name}}>
+{%- endmacro %}
+
+
+{# Renderting condition component. #}
+{# Args: #}
+{# component: component dictionary #}
+{% macro render_condition_tag(component) %}
+{ {{- component.cond_state }} ? (
+ {{ render(component.true_value) }}
+) : (
+ {{ render(component.false_value) }}
+)}
+{%- endmacro %}
+
+
+{# Renderting iterable component. #}
+{# Args: #}
+{# component: component dictionary #}
+{% macro render_iterable_tag(component) %}
+{ {{- component.iterable_state }}.map(({{ component.arg_name }}, {{ component.arg_index }}) => (
+ {% for child in component.children %}
+ {{ render(child) }}
+ {% endfor %}
+))}
+{%- endmacro %}
+
+
+{# Renderting props of a component. #}
+{# Args: #}
+{# component: component dictionary #}
+{% macro render_props(props) %}
+{% if props|length %} {{ props|join(" ") }}{% endif %}
+{% endmacro %}
+
+
+{# Renderting content with args. #}
+{# Args: #}
+{# component: component dictionary #}
+{% macro render_arg_content(component) %}
+{% filter indent(width=2) %}
+{# no string below for a line break #}
+
+{({ {{component.args|join(", ")}} }) => (
+ {% for child in component.children %}
+ {{ render(child) }}
+ {% endfor %}
+)}
+{% endfilter %}
+{% endmacro %}
+
+
+{# Get react libraries import . #}
+{# Args: #}
+{# module: react module dictionary #}
+{% macro get_import(module)%}
+{%- if module.default|length and module.rest|length -%}
+ import {{module.default}}, { {{module.rest|sort|join(", ")}} } from "{{module.lib}}"
+{%- elif module.default|length -%}
+ import {{module.default}} from "{{module.lib}}"
+{%- elif module.rest|length -%}
+ import { {{module.rest|sort|join(", ")}} } from "{{module.lib}}"
+{%- else -%}
+ import "{{module.lib}}"
+{%- endif -%}
+{% endmacro %}
diff --git a/pynecone/templates/web/utils/theme.js.jinja2 b/pynecone/templates/web/utils/theme.js.jinja2
new file mode 100644
index 000000000..74f861c5a
--- /dev/null
+++ b/pynecone/templates/web/utils/theme.js.jinja2
@@ -0,0 +1 @@
+export default {{ theme|json_dumps }}
diff --git a/pynecone/utils/format.py b/pynecone/utils/format.py
index cdb2ea780..a8143c7ca 100644
--- a/pynecone/utils/format.py
+++ b/pynecone/utils/format.py
@@ -306,11 +306,9 @@ def format_upload_event(event_spec: EventSpec) -> str:
Returns:
The compiled event.
"""
- from pynecone.compiler import templates
-
state, name = get_event_handler_parts(event_spec.handler)
parent_state = state.split(".")[0]
- return f'uploadFiles({parent_state}, {templates.RESULT}, {templates.SET_RESULT}, {parent_state}.files, "{state}.{name}",UPLOAD)'
+ return f'uploadFiles({parent_state}, {constants.RESULT}, set{constants.RESULT.capitalize()}, {parent_state}.files, "{state}.{name}",UPLOAD)'
def format_full_control_event(event_chain: EventChain) -> str:
diff --git a/pynecone/utils/prerequisites.py b/pynecone/utils/prerequisites.py
index 11b83633f..7adc4aea7 100644
--- a/pynecone/utils/prerequisites.py
+++ b/pynecone/utils/prerequisites.py
@@ -151,7 +151,7 @@ def create_config(app_name: str):
config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config"
with open(constants.CONFIG_FILE, "w") as f:
- f.write(templates.PCCONFIG.format(app_name=app_name, config_name=config_name))
+ f.write(templates.PCCONFIG.render(app_name=app_name, config_name=config_name))
def create_web_directory(root: Path) -> str:
diff --git a/pyproject.toml b/pyproject.toml
index ac97d6b9a..56efe6a12 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,6 +40,7 @@ websockets = "^10.4"
cloudpickle = "^2.2.1"
python-multipart = "^0.0.5"
watchdog = "^2.3.1"
+jinja2 = "^3.1.2"
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.2"
diff --git a/tests/compiler/test_compiler.py b/tests/compiler/test_compiler.py
index a1bab5eae..f70026716 100644
--- a/tests/compiler/test_compiler.py
+++ b/tests/compiler/test_compiler.py
@@ -1,4 +1,4 @@
-from typing import Set
+from typing import List, Set
import pytest
@@ -8,51 +8,55 @@ from pynecone.var import ImportVar
@pytest.mark.parametrize(
- "lib,fields,output",
+ "fields,test_default,test_rest",
[
(
- "axios",
{ImportVar(tag="axios", is_default=True)},
- 'import axios from "axios"',
+ "axios",
+ set(),
),
(
- "axios",
{ImportVar(tag="foo"), ImportVar(tag="bar")},
- 'import {bar, foo} from "axios"',
+ "",
+ {"foo", "bar"},
),
(
- "axios",
{
ImportVar(tag="axios", is_default=True),
ImportVar(tag="foo"),
ImportVar(tag="bar"),
},
- "import " "axios, " "{bar, " "foo} from " '"axios"',
+ "axios",
+ {"foo", "bar"},
),
],
)
-def test_compile_import_statement(lib: str, fields: Set[ImportVar], output: str):
+def test_compile_import_statement(
+ fields: Set[ImportVar], test_default: str, test_rest: str
+):
"""Test the compile_import_statement function.
Args:
- lib: The library name.
fields: The fields to import.
- output: The expected output.
+ test_default: The expected output of default library.
+ test_rest: The expected output rest libraries.
"""
- assert utils.compile_import_statement(lib, fields) == output
+ default, rest = utils.compile_import_statement(fields)
+ assert default == test_default
+ assert rest == test_rest
@pytest.mark.parametrize(
- "import_dict,output",
+ "import_dict,test_dicts",
[
- ({}, ""),
+ ({}, []),
(
{"axios": {ImportVar(tag="axios", is_default=True)}},
- 'import axios from "axios"',
+ [{"lib": "axios", "default": "axios", "rest": set()}],
),
(
{"axios": {ImportVar(tag="foo"), ImportVar(tag="bar")}},
- 'import {bar, foo} from "axios"',
+ [{"lib": "axios", "default": "", "rest": {"foo", "bar"}}],
),
(
{
@@ -63,52 +67,61 @@ def test_compile_import_statement(lib: str, fields: Set[ImportVar], output: str)
},
"react": {ImportVar(tag="react", is_default=True)},
},
- 'import axios, {bar, foo} from "axios"\nimport react from "react"',
+ [
+ {"lib": "axios", "default": "axios", "rest": {"foo", "bar"}},
+ {"lib": "react", "default": "react", "rest": set()},
+ ],
),
(
{"": {ImportVar(tag="lib1.js"), ImportVar(tag="lib2.js")}},
- 'import "lib1.js"\nimport "lib2.js"',
+ [
+ {"lib": "lib1.js", "default": "", "rest": set()},
+ {"lib": "lib2.js", "default": "", "rest": set()},
+ ],
),
(
{
"": {ImportVar(tag="lib1.js"), ImportVar(tag="lib2.js")},
"axios": {ImportVar(tag="axios", is_default=True)},
},
- 'import "lib1.js"\nimport "lib2.js"\nimport axios from "axios"',
+ [
+ {"lib": "lib1.js", "default": "", "rest": set()},
+ {"lib": "lib2.js", "default": "", "rest": set()},
+ {"lib": "axios", "default": "axios", "rest": set()},
+ ],
),
],
)
-def test_compile_imports(
- import_dict: imports.ImportDict, output: str, windows_platform: bool
-):
+def test_compile_imports(import_dict: imports.ImportDict, test_dicts: List[dict]):
"""Test the compile_imports function.
Args:
import_dict: The import dictionary.
- output: The expected output.
- windows_platform: whether system is windows.
+ test_dicts: The expected output.
"""
- assert utils.compile_imports(import_dict) == (
- output.replace("\n", "\r\n") if windows_platform else output
- )
+ imports = utils.compile_imports(import_dict)
+ for import_dict, test_dict in zip(imports, test_dicts):
+ assert import_dict["lib"] == test_dict["lib"]
+ assert import_dict["default"] == test_dict["default"]
+ assert import_dict["rest"] == test_dict["rest"]
-@pytest.mark.parametrize(
- "name,value,output",
- [
- ("foo", "bar", 'const foo = "bar"'),
- ("num", 1, "const num = 1"),
- ("check", False, "const check = false"),
- ("arr", [1, 2, 3], "const arr = [1, 2, 3]"),
- ("obj", {"foo": "bar"}, 'const obj = {"foo": "bar"}'),
- ],
-)
-def test_compile_constant_declaration(name: str, value: str, output: str):
- """Test the compile_constant_declaration function.
+# @pytest.mark.parametrize(
+# "name,value,output",
+# [
+# ("foo", "bar", 'const foo = "bar"'),
+# ("num", 1, "const num = 1"),
+# ("check", False, "const check = false"),
+# ("arr", [1, 2, 3], "const arr = [1, 2, 3]"),
+# ("obj", {"foo": "bar"}, 'const obj = {"foo": "bar"}'),
+# ],
+# )
+# def test_compile_constant_declaration(name: str, value: str, output: str):
+# """Test the compile_constant_declaration function.
- Args:
- name: The name of the constant.
- value: The value of the constant.
- output: The expected output.
- """
- assert utils.compile_constant_declaration(name, value) == output
+# Args:
+# name: The name of the constant.
+# value: The value of the constant.
+# output: The expected output.
+# """
+# assert utils.compile_constant_declaration(name, value) == output
diff --git a/tests/components/base/test_bare.py b/tests/components/base/test_bare.py
index 04d061044..de0c68f25 100644
--- a/tests/components/base/test_bare.py
+++ b/tests/components/base/test_bare.py
@@ -19,5 +19,5 @@ def test_fstrings(contents, expected):
contents: The contents of the component.
expected: The expected output.
"""
- comp = Bare.create(contents)
- assert str(comp) == expected
+ comp = Bare.create(contents).render()
+ assert comp["contents"] == expected
diff --git a/tests/components/datadisplay/test_datatable.py b/tests/components/datadisplay/test_datatable.py
index f733cc63c..f6e06f8ab 100644
--- a/tests/components/datadisplay/test_datatable.py
+++ b/tests/components/datadisplay/test_datatable.py
@@ -1,5 +1,3 @@
-import os
-
import pandas as pd
import pytest
@@ -37,11 +35,12 @@ def test_validate_data_table(data_table_state: pc.Var, expected):
props["columns"] = data_table_state.columns
data_table_component = data_table(**props)
- assert (
- str(data_table_component)
- == f"
"),
- (Tag(contents="hello"), "<>hello>"),
- (Tag(name="h1", contents="hello"), "