[Fix 477] Use jinja2 for templating (#915)

This commit is contained in:
PeterYusuke 2023-05-10 06:34:47 +09:00 committed by GitHub
parent dc2dff9323
commit 3b88e7c329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 691 additions and 542 deletions

88
poetry.lock generated
View File

@ -463,6 +463,84 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, {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]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "1.0.0" version = "1.0.0"
@ -1063,18 +1141,18 @@ files = [
[[package]] [[package]]
name = "redis" name = "redis"
version = "4.5.4" version = "4.5.5"
description = "Python client for Redis database and key-value store" description = "Python client for Redis database and key-value store"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "redis-4.5.4-py3-none-any.whl", hash = "sha256:2c19e6767c474f2e85167909061d525ed65bea9301c0770bb151e041b7ac89a2"}, {file = "redis-4.5.5-py3-none-any.whl", hash = "sha256:77929bc7f5dab9adf3acba2d3bb7d7658f1e0c2f1cafe7eb36434e751c471119"},
{file = "redis-4.5.4.tar.gz", hash = "sha256:73ec35da4da267d6847e47f68730fdd5f62e2ca69e3ef5885c6a78a9374c3893"}, {file = "redis-4.5.5.tar.gz", hash = "sha256:dc87a0bdef6c8bfe1ef1e1c40be7034390c2ae02d92dcd0c7ca1729443899880"},
] ]
[package.dependencies] [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\""} importlib-metadata = {version = ">=1.0", markers = "python_version < \"3.8\""}
typing-extensions = {version = "*", 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] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "8c9f764a830657316f774fb679895c89d6874460582f4dbf8b2edbd4f534b262" content-hash = "43e53d9fff649b6a939ec953411b8932e512600b5af3edc0cbbbbb6c5576168b"

View File

@ -1,7 +1,6 @@
"""Compiler for the pynecone apps.""" """Compiler for the pynecone apps."""
from __future__ import annotations from __future__ import annotations
import json
from functools import wraps from functools import wraps
from typing import Callable, List, Set, Tuple, Type 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.components.component import Component, CustomComponent
from pynecone.state import State from pynecone.state import State
from pynecone.style import Style from pynecone.style import Style
from pynecone.utils import imports, path_ops from pynecone.utils import imports
from pynecone.var import ImportVar from pynecone.var import ImportVar
# Imports to be included in every Pynecone app. # Imports to be included in every Pynecone app.
@ -42,7 +41,7 @@ def _compile_document_root(root: Component) -> str:
Returns: Returns:
The compiled document root. The compiled document root.
""" """
return templates.DOCUMENT_ROOT( return templates.DOCUMENT_ROOT.render(
imports=utils.compile_imports(root.get_imports()), imports=utils.compile_imports(root.get_imports()),
document=root.render(), document=root.render(),
) )
@ -57,7 +56,7 @@ def _compile_theme(theme: dict) -> str:
Returns: Returns:
The compiled theme. 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: 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. # Merge the default imports with the app-specific imports.
imports = utils.merge_imports(DEFAULT_IMPORTS, component.get_imports()) imports = utils.merge_imports(DEFAULT_IMPORTS, component.get_imports())
imports = utils.compile_imports(imports)
# Compile the code to render the component. # Compile the code to render the component.
return templates.PAGE( return templates.PAGE.render(
imports=utils.compile_imports(imports), imports=imports,
custom_code=path_ops.join(component.get_custom_code()), custom_codes=component.get_custom_code(),
constants=utils.compile_constants(), endpoints={
state=utils.compile_state(state), constant.name: constant.get_url() for constant in constants.Endpoint
events=utils.compile_events(state), },
effects=utils.compile_effects(state), initial_state=utils.compile_state(state),
hooks=path_ops.join(component.get_hooks()), state_name=state.get_name(),
hooks=component.get_hooks(),
render=component.render(), 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")}, "react": {ImportVar(tag="memo")},
f"/{constants.STATE_PATH}": {ImportVar(tag="E"), ImportVar(tag="isTrue")}, f"/{constants.STATE_PATH}": {ImportVar(tag="E"), ImportVar(tag="isTrue")},
} }
component_defs = [] component_renders = []
# Compile each component. # Compile each component.
for component in components: for component in components:
component_def, component_imports = utils.compile_custom_component(component) component_render, component_imports = utils.compile_custom_component(component)
component_defs.append(component_def) component_renders.append(component_render)
imports = utils.merge_imports(imports, component_imports) imports = utils.merge_imports(imports, component_imports)
# Compile the components page. # Compile the components page.
return templates.COMPONENTS( return templates.COMPONENTS.render(
imports=utils.compile_imports(imports), imports=utils.compile_imports(imports),
components=path_ops.join(component_defs), components=component_renders,
) )

View File

@ -1,163 +1,75 @@
"""Templates to use in the pynecone compiler.""" """Templates to use in the pynecone compiler."""
from typing import Optional, Set from jinja2 import Environment, FileSystemLoader, Template
from pynecone import constants from pynecone import constants
from pynecone.utils import path_ops 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. # Template for the Pynecone config file.
PCCONFIG = f"""import pynecone as pc PCCONFIG = get_template("app/pcconfig.py.jinja2")
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)
# Code to render a NextJS Document root. # Code to render a NextJS Document root.
DOCUMENT_ROOT = path_ops.join( DOCUMENT_ROOT = get_template("web/pages/_document.js.jinja2")
[
"{imports}",
"export default function Document() {{",
"return (",
"{document}",
")",
"}}",
]
).format
# Template for the theme file. # 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. # Code to render a single NextJS page.
PAGE = path_ops.join( PAGE = get_template("web/pages/index.js.jinja2")
[
"{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
# Code to render the custom components page. # Code to render the custom components page.
COMPONENTS = path_ops.join( COMPONENTS = get_template("web/pages/custom_component.js.jinja2")
[
"{imports}",
"{components}",
]
).format
# 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( FULL_CONTROL = path_ops.join(
[ [
"{{setState(prev => ({{", "{{setState(prev => ({{",
@ -167,52 +79,3 @@ FULL_CONTROL = path_ops.join(
")}}", ")}}",
] ]
).format ).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

View File

@ -1,11 +1,9 @@
"""Common utility functions used in the compiler.""" """Common utility functions used in the compiler."""
import json
import os import os
from typing import Dict, List, Optional, Set, Tuple, Type from typing import Dict, List, Optional, Set, Tuple, Type
from pynecone import constants from pynecone import constants
from pynecone.compiler import templates
from pynecone.components.base import ( from pynecone.components.base import (
Body, Body,
ColorModeScript, ColorModeScript,
@ -31,15 +29,16 @@ from pynecone.var import ImportVar
merge_imports = imports.merge_imports 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. """Compile an import statement.
Args: Args:
lib: The library to import from.
fields: The set of fields to import from the library. fields: The set of fields to import from the library.
Returns: 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. # Check for default imports.
defaults = {field for field in fields if field.is_default} 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. # Get the default import, and the specific imports.
default = next(iter({field.name for field in defaults}), "") default = next(iter({field.name for field in defaults}), "")
rest = {field.name for field in fields - 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. """Compile an import dict.
Args: Args:
imports: The import dict to compile. imports: The import dict to compile.
Returns: Returns:
The compiled import dict. The list of import dict.
""" """
return path_ops.join( import_dicts = []
[compile_import_statement(lib, fields) for lib, fields in imports.items()] 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: def get_import_dict(lib: str, default: str = "", rest: Optional[Set] = None) -> Dict:
"""Compile a constant declaration. """Get dictionary for import template.
Args: Args:
name: The name of the constant. lib: The importing react library.
value: The value of the constant. default: The default module to import.
rest: The rest module to import.
Returns: 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: def compile_state(state: Type[State]) -> Dict:
"""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:
"""Compile the state of the app. """Compile the state of the app.
Args: Args:
state: The app state object. state: The app state object.
Returns: Returns:
A string of the compiled state. A dictionary of the compiled state.
""" """
initial_state = state().dict() initial_state = state().dict()
initial_state.update( initial_state.update(
@ -108,77 +108,12 @@ def compile_state(state: Type[State]) -> str:
"files": [], "files": [],
} }
) )
initial_state = format.format_state(initial_state) return 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()
def compile_custom_component( def compile_custom_component(
component: CustomComponent, component: CustomComponent,
) -> Tuple[str, imports.ImportDict]: ) -> Tuple[dict, imports.ImportDict]:
"""Compile a custom component. """Compile a custom component.
Args: Args:
@ -198,15 +133,15 @@ def compile_custom_component(
} }
# Concatenate the props. # 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. # Compile the component.
return ( return (
templates.COMPONENT( {
name=component.tag, "name": component.tag,
props=props, "props": props,
render=render, "render": render.render(),
), },
imports, imports,
) )

View File

@ -1,6 +1,6 @@
"""Display the title of the current page.""" """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.base.bare import Bare
from pynecone.components.component import Component from pynecone.components.component import Component
@ -11,18 +11,17 @@ class Title(Component):
tag = "title" tag = "title"
def render(self) -> str: def render(self) -> Dict:
"""Render the title component. """Render the title component.
Returns: Returns:
The rendered title component. The rendered title component.
""" """
tag = self._render()
# Make sure the title is a single string. # Make sure the title is a single string.
assert len(self.children) == 1 and isinstance( assert len(self.children) == 1 and isinstance(
self.children[0], Bare self.children[0], Bare
), "Title must be a single string." ), "Title must be a single string."
return str(tag.set(contents=str(self.children[0].contents))) return super().render()
class Meta(Component): class Meta(Component):

View File

@ -21,7 +21,7 @@ from pynecone.event import (
get_handler_args, get_handler_args,
) )
from pynecone.style import Style 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 from pynecone.var import BaseVar, ImportVar, Var
@ -289,7 +289,7 @@ class Component(Base, ABC):
Returns: Returns:
The code to render the component. The code to render the component.
""" """
return self.render() return format.json_dumps(self.render())
def __str__(self) -> str: def __str__(self) -> str:
"""Represent the component in React. """Represent the component in React.
@ -297,7 +297,7 @@ class Component(Base, ABC):
Returns: Returns:
The code to render the component. The code to render the component.
""" """
return self.render() return format.json_dumps(self.render())
def _render(self) -> Tag: def _render(self) -> Tag:
"""Define how to render the component in React. """Define how to render the component in React.
@ -393,14 +393,14 @@ class Component(Base, ABC):
child.add_style(style) child.add_style(style)
return self return self
def render(self) -> str: def render(self) -> Dict:
"""Render the component. """Render the component.
Returns: Returns:
The code to render the component. The dictionary for template of component.
""" """
tag = self._render() tag = self._render()
return str( return dict(
tag.add_props( tag.add_props(
**self.event_triggers, **self.event_triggers,
key=self.key, key=self.key,
@ -408,10 +408,10 @@ class Component(Base, ABC):
id=self.id, id=self.id,
class_name=self.class_name, class_name=self.class_name,
).set( ).set(
contents=path_ops.join( children=[child.render() for child in self.children],
[str(tag.contents)] + [child.render() for child in self.children] contents=str(tag.contents),
).strip(), props=tag.format_props(),
) ),
) )
def _get_custom_code(self) -> Optional[str]: def _get_custom_code(self) -> Optional[str]:

View File

@ -1,7 +1,7 @@
"""Create a list of components from an iterable.""" """Create a list of components from an iterable."""
from __future__ import annotations from __future__ import annotations
from typing import Any, Optional from typing import Any, Dict, Optional
from pynecone.components.component import Component from pynecone.components.component import Component
from pynecone.components.layout.fragment import Fragment from pynecone.components.layout.fragment import Fragment
@ -24,7 +24,7 @@ class Cond(Component):
@classmethod @classmethod
def create( def create(
cls, cond: Var, comp1: Component, comp2: Optional[Component] = None cls, cond: Var, comp1: Component, comp2: Optional[Component]
) -> Component: ) -> Component:
"""Create a conditional component. """Create a conditional component.
@ -37,8 +37,10 @@ class Cond(Component):
The conditional component. The conditional component.
""" """
# Wrap everything in fragments. # Wrap everything in fragments.
comp1 = Fragment.create(comp1) if comp1.__class__.__name__ != "Fragment":
comp2 = Fragment.create(comp2) if comp2 else Fragment.create() 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( return Fragment.create(
cls( cls(
cond=cond, cond=cond,
@ -55,6 +57,26 @@ class Cond(Component):
false_value=self.comp2.render(), 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): def cond(condition: Any, c1: Any, c2: Any = None):
"""Create a conditional component or Prop. """Create a conditional component or Prop.

View File

@ -4,8 +4,8 @@ from __future__ import annotations
from typing import Any, Callable, List from typing import Any, Callable, List
from pynecone.components.component import Component from pynecone.components.component import Component
from pynecone.components.tags import IterTag, Tag from pynecone.components.tags import IterTag
from pynecone.var import BaseVar, Var from pynecone.var import BaseVar, Var, get_unique_variable_name
class Foreach(Component): class Foreach(Component):
@ -49,5 +49,38 @@ class Foreach(Component):
**props, **props,
) )
def _render(self) -> Tag: def _render(self) -> IterTag:
return IterTag(iterable=self.iterable, render_fn=self.render_fn) 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,
)

View File

@ -1,9 +1,8 @@
"""Tag to conditionally render components.""" """Tag to conditionally render components."""
from typing import Any from typing import Any, Dict, Optional
from pynecone.components.tags.tag import Tag from pynecone.components.tags.tag import Tag
from pynecone.utils import format
from pynecone.var import Var from pynecone.var import Var
@ -14,20 +13,7 @@ class CondTag(Tag):
cond: Var[Any] cond: Var[Any]
# The code to render if the condition is true. # The code to render if the condition is true.
true_value: str true_value: Dict
# The code to render if the condition is false. # The code to render if the condition is false.
false_value: str false_value: Optional[Dict]
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,
)

View File

@ -2,11 +2,10 @@
from __future__ import annotations from __future__ import annotations
import inspect 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.components.tags.tag import Tag
from pynecone.utils import format from pynecone.var import Var
from pynecone.var import BaseVar, Var, get_unique_variable_name
if TYPE_CHECKING: if TYPE_CHECKING:
from pynecone.components.component import Component from pynecone.components.component import Component
@ -83,24 +82,3 @@ class IterTag(Tag):
component.key = index component.key = index
return component 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})",
"{",
)

View File

@ -3,9 +3,8 @@
from __future__ import annotations from __future__ import annotations
import json import json
import os
import re 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.graph_objects import Figure
from plotly.io import to_json from plotly.io import to_json
@ -37,6 +36,9 @@ class Tag(Base):
# Special props that aren't key value pairs. # Special props that aren't key value pairs.
special_props: Set[Var] = set() special_props: Set[Var] = set()
# The children components.
children: List[Any] = []
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Initialize the tag. """Initialize the tag.
@ -117,55 +119,22 @@ class Tag(Base):
assert isinstance(prop, str), "The prop must be a string." assert isinstance(prop, str), "The prop must be a string."
return format.wrap(prop, "{", check_first=False) return format.wrap(prop, "{", check_first=False)
def format_props(self) -> str: def format_props(self) -> List:
"""Format the tag's props. """Format the tag's props.
Returns: Returns:
The formatted props. The formatted props list.
""" """
# If there are no props, return an empty string. # If there are no props, return an empty string.
if len(self.props) == 0: if len(self.props) == 0:
return "" return []
# Format all the props. # Format all the props.
return os.linesep.join( return [
f"{name}={self.format_prop(prop)}" f"{name}={self.format_prop(prop)}"
for name, prop in sorted(self.props.items()) for name, prop in sorted(self.props.items())
if prop is not None if prop is not None
) ] + [str(prop) for prop in self.special_props]
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
def add_props(self, **kwargs: Optional[Any]) -> Tag: def add_props(self, **kwargs: Optional[Any]) -> Tag:
"""Add props to the tag. """Add props to the tag.

View File

@ -33,6 +33,8 @@ TEMPLATE_DIR = os.path.join(ROOT_DIR, MODULE_NAME, ".templates")
WEB_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, "web") WEB_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, "web")
# The assets subdirectory of the template directory. # The assets subdirectory of the template directory.
ASSETS_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, APP_ASSETS_DIR) 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 frontend directories in a project.
# The web folder where the NextJS app is compiled to. # The web folder where the NextJS app is compiled to.

View File

@ -1,7 +1,8 @@
"""Base class definition for raw HTML elements.""" """Base class definition for raw HTML elements."""
from typing import Dict
from pynecone.components.component import Component from pynecone.components.component import Component
from pynecone.utils import path_ops
class Element(Component): class Element(Component):
@ -12,14 +13,14 @@ class Element(Component):
prop. prop.
""" """
def render(self) -> str: def render(self) -> Dict:
"""Render the element. """Render the element.
Returns: Returns:
The code to render the element. The code to render the element.
""" """
tag = self._render() tag = self._render()
return str( return dict(
tag.add_props( tag.add_props(
**self.event_triggers, **self.event_triggers,
key=self.key, key=self.key,
@ -27,9 +28,8 @@ class Element(Component):
style=self.style, style=self.style,
class_name=self.class_name, class_name=self.class_name,
).set( ).set(
contents=path_ops.join( contents=str(tag.contents),
[str(tag.contents)] + [child.render() for child in self.children] children=[child.render() for child in self.children],
).strip(),
) )
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export default {{ theme|json_dumps }}

View File

@ -306,11 +306,9 @@ def format_upload_event(event_spec: EventSpec) -> str:
Returns: Returns:
The compiled event. The compiled event.
""" """
from pynecone.compiler import templates
state, name = get_event_handler_parts(event_spec.handler) state, name = get_event_handler_parts(event_spec.handler)
parent_state = state.split(".")[0] 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: def format_full_control_event(event_chain: EventChain) -> str:

View File

@ -151,7 +151,7 @@ def create_config(app_name: str):
config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config" config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config"
with open(constants.CONFIG_FILE, "w") as f: 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: def create_web_directory(root: Path) -> str:

View File

@ -40,6 +40,7 @@ websockets = "^10.4"
cloudpickle = "^2.2.1" cloudpickle = "^2.2.1"
python-multipart = "^0.0.5" python-multipart = "^0.0.5"
watchdog = "^2.3.1" watchdog = "^2.3.1"
jinja2 = "^3.1.2"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^7.1.2" pytest = "^7.1.2"

View File

@ -1,4 +1,4 @@
from typing import Set from typing import List, Set
import pytest import pytest
@ -8,51 +8,55 @@ from pynecone.var import ImportVar
@pytest.mark.parametrize( @pytest.mark.parametrize(
"lib,fields,output", "fields,test_default,test_rest",
[ [
( (
"axios",
{ImportVar(tag="axios", is_default=True)}, {ImportVar(tag="axios", is_default=True)},
'import axios from "axios"', "axios",
set(),
), ),
( (
"axios",
{ImportVar(tag="foo"), ImportVar(tag="bar")}, {ImportVar(tag="foo"), ImportVar(tag="bar")},
'import {bar, foo} from "axios"', "",
{"foo", "bar"},
), ),
( (
"axios",
{ {
ImportVar(tag="axios", is_default=True), ImportVar(tag="axios", is_default=True),
ImportVar(tag="foo"), ImportVar(tag="foo"),
ImportVar(tag="bar"), 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. """Test the compile_import_statement function.
Args: Args:
lib: The library name.
fields: The fields to import. 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( @pytest.mark.parametrize(
"import_dict,output", "import_dict,test_dicts",
[ [
({}, ""), ({}, []),
( (
{"axios": {ImportVar(tag="axios", is_default=True)}}, {"axios": {ImportVar(tag="axios", is_default=True)}},
'import axios from "axios"', [{"lib": "axios", "default": "axios", "rest": set()}],
), ),
( (
{"axios": {ImportVar(tag="foo"), ImportVar(tag="bar")}}, {"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)}, "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")}}, {"": {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")}, "": {ImportVar(tag="lib1.js"), ImportVar(tag="lib2.js")},
"axios": {ImportVar(tag="axios", is_default=True)}, "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( def test_compile_imports(import_dict: imports.ImportDict, test_dicts: List[dict]):
import_dict: imports.ImportDict, output: str, windows_platform: bool
):
"""Test the compile_imports function. """Test the compile_imports function.
Args: Args:
import_dict: The import dictionary. import_dict: The import dictionary.
output: The expected output. test_dicts: The expected output.
windows_platform: whether system is windows.
""" """
assert utils.compile_imports(import_dict) == ( imports = utils.compile_imports(import_dict)
output.replace("\n", "\r\n") if windows_platform else output 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( # @pytest.mark.parametrize(
"name,value,output", # "name,value,output",
[ # [
("foo", "bar", 'const foo = "bar"'), # ("foo", "bar", 'const foo = "bar"'),
("num", 1, "const num = 1"), # ("num", 1, "const num = 1"),
("check", False, "const check = false"), # ("check", False, "const check = false"),
("arr", [1, 2, 3], "const arr = [1, 2, 3]"), # ("arr", [1, 2, 3], "const arr = [1, 2, 3]"),
("obj", {"foo": "bar"}, 'const obj = {"foo": "bar"}'), # ("obj", {"foo": "bar"}, 'const obj = {"foo": "bar"}'),
], # ],
) # )
def test_compile_constant_declaration(name: str, value: str, output: str): # def test_compile_constant_declaration(name: str, value: str, output: str):
"""Test the compile_constant_declaration function. # """Test the compile_constant_declaration function.
Args: # Args:
name: The name of the constant. # name: The name of the constant.
value: The value of the constant. # value: The value of the constant.
output: The expected output. # output: The expected output.
""" # """
assert utils.compile_constant_declaration(name, value) == output # assert utils.compile_constant_declaration(name, value) == output

View File

@ -19,5 +19,5 @@ def test_fstrings(contents, expected):
contents: The contents of the component. contents: The contents of the component.
expected: The expected output. expected: The expected output.
""" """
comp = Bare.create(contents) comp = Bare.create(contents).render()
assert str(comp) == expected assert comp["contents"] == expected

View File

@ -1,5 +1,3 @@
import os
import pandas as pd import pandas as pd
import pytest import pytest
@ -37,11 +35,12 @@ def test_validate_data_table(data_table_state: pc.Var, expected):
props["columns"] = data_table_state.columns props["columns"] = data_table_state.columns
data_table_component = data_table(**props) data_table_component = data_table(**props)
assert ( data_table_dict = data_table_component.render()
str(data_table_component)
== f"<DataTableGrid columns={{{expected}.columns}}{os.linesep}data={{" assert data_table_dict["props"] == [
f"{expected}.data}}/>" f"columns={{{expected}.columns}}",
) f"data={{{expected}.data}}",
]
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -1,5 +1,3 @@
import os
import pytest import pytest
import pynecone as pc import pynecone as pc
@ -49,14 +47,36 @@ def test_upload_component_render(upload_component):
Args: Args:
upload_component: component fixture upload_component: component fixture
""" """
uplaod = upload_component.render()
# upload
assert uplaod["name"] == "ReactDropzone"
assert uplaod["props"] == [
"multiple={true}",
"onDrop={e => File(e)}",
]
assert uplaod["args"] == ("getRootProps", "getInputProps")
# box inside of upload
[box] = uplaod["children"]
assert box["name"] == "Box"
assert box["props"] == [
'sx={{"border": "1px dotted black"}}',
"{...getRootProps()}",
]
# input, button and text inside of box
[input, button, text] = box["children"]
assert input["name"] == "Input"
assert input["props"] == ['type="file"', "{...getInputProps()}"]
assert button["name"] == "Button"
assert button["children"][0]["contents"] == "{`select file`}"
assert text["name"] == "Text"
assert ( assert (
str(upload_component) == f"<ReactDropzone multiple={{true}}{os.linesep}" text["children"][0]["contents"]
"onDrop={e => File(e)}>{({getRootProps, getInputProps}) => (<Box " == "{`Drag and drop files here or click to select files`}"
'sx={{"border": "1px dotted black"}}{...getRootProps()}><Input '
f'type="file"{{...getInputProps()}}/>{os.linesep}'
f"<Button>{{`select file`}}</Button>{os.linesep}"
"<Text>{`Drag and drop files here or click to select "
"files`}</Text></Box>)}</ReactDropzone>"
) )
@ -66,14 +86,11 @@ def test_upload_component_with_props_render(upload_component_with_props):
Args: Args:
upload_component_with_props: component fixture upload_component_with_props: component fixture
""" """
assert ( uplaod = upload_component_with_props.render()
str(upload_component_with_props) == f"<ReactDropzone maxFiles={{2}}{os.linesep}"
f"multiple={{true}}{os.linesep}" assert uplaod["props"] == [
f"noDrag={{true}}{os.linesep}" "maxFiles={2}",
"onDrop={e => File(e)}>{({getRootProps, getInputProps}) => (<Box " "multiple={true}",
'sx={{"border": "1px dotted black"}}{...getRootProps()}><Input ' "noDrag={true}",
f'type="file"{{...getInputProps()}}/>{os.linesep}' "onDrop={e => File(e)}",
f"<Button>{{`select file`}}</Button>{os.linesep}" ]
"<Text>{`Drag and drop files here or click to select "
"files`}</Text></Box>)}</ReactDropzone>"
)

View File

@ -38,12 +38,27 @@ def test_validate_cond(cond_state: pc.Var):
Text.create("cond is True"), Text.create("cond is True"),
Text.create("cond is False"), Text.create("cond is False"),
) )
cond_dict = cond_component.render() if type(cond_component) == Fragment else {}
assert cond_dict["name"] == "Fragment"
assert str(cond_component) == ( [condition] = cond_dict["children"]
"<Fragment>{isTrue(cond_state.value) ? " assert condition["cond_state"] == "isTrue(cond_state.value)"
"<Fragment><Text>{`cond is True`}</Text></Fragment> : "
"<Fragment><Text>{`cond is False`}</Text></Fragment>}</Fragment>" # true value
) true_value = condition["true_value"]
assert true_value["name"] == "Fragment"
[true_value_text] = true_value["children"]
assert true_value_text["name"] == "Text"
assert true_value_text["children"][0]["contents"] == "{`cond is True`}"
# false value
false_value = condition["false_value"]
assert false_value["name"] == "Fragment"
[false_value_text] = false_value["children"]
assert false_value_text["name"] == "Text"
assert false_value_text["children"][0]["contents"] == "{`cond is False`}"
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -1,4 +1,4 @@
from typing import Any, Dict from typing import Any, Dict, List
import pytest import pytest
@ -71,25 +71,24 @@ def test_format_prop(prop: Var, formatted: str):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"props,formatted", "props,test_props",
[ [
({}, ""), ({}, []),
({"key": 1}, "key={1}"), ({"key": 1}, ["key={1}"]),
({"key": "value"}, 'key="value"'), ({"key": "value"}, ['key="value"']),
({"key": True, "key2": "value2"}, 'key={true}\nkey2="value2"'), ({"key": True, "key2": "value2"}, ["key={true}", 'key2="value2"']),
], ],
) )
def test_format_props(props: Dict[str, Var], formatted: str, windows_platform: bool): def test_format_props(props: Dict[str, Var], test_props: List):
"""Test that the formatted props are correct. """Test that the formatted props are correct.
Args: Args:
props: The props to test. props: The props to test.
formatted: The expected formatted props. test_props: The expected props.
windows_platform: Whether the system is windows.
""" """
assert Tag(props=props).format_props() == ( tag_props = Tag(props=props).format_props()
formatted.replace("\n", "\r\n") if windows_platform else formatted for i, tag_prop in enumerate(tag_props):
) assert tag_prop == test_props[i]
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -126,13 +125,20 @@ def test_add_props():
@pytest.mark.parametrize( @pytest.mark.parametrize(
"tag,expected", "tag,expected",
[ [
(Tag(), "</>"), (Tag(), {"name": "", "contents": "", "props": {}}),
(Tag(name="br"), "<br/>"), (Tag(name="br"), {"name": "br", "contents": "", "props": {}}),
(Tag(contents="hello"), "<>hello</>"), (Tag(contents="hello"), {"name": "", "contents": "hello", "props": {}}),
(Tag(name="h1", contents="hello"), "<h1>hello</h1>"), (
Tag(name="h1", contents="hello"),
{"name": "h1", "contents": "hello", "props": {}},
),
( (
Tag(name="box", props={"color": "red", "textAlign": "center"}), Tag(name="box", props={"color": "red", "textAlign": "center"}),
'<box color="red"\ntextAlign="center"/>', {
"name": "box",
"contents": "",
"props": {"color": "red", "textAlign": "center"},
},
), ),
( (
Tag( Tag(
@ -140,30 +146,44 @@ def test_add_props():
props={"color": "red", "textAlign": "center"}, props={"color": "red", "textAlign": "center"},
contents="text", contents="text",
), ),
'<box color="red"\ntextAlign="center">text</box>', {
"name": "box",
"contents": "text",
"props": {"color": "red", "textAlign": "center"},
},
), ),
], ],
) )
def test_format_tag(tag: Tag, expected: str, windows_platform: bool): def test_format_tag(tag: Tag, expected: Dict):
"""Test that the formatted tag is correct. """Test that the tag dict is correct.
Args: Args:
tag: The tag to test. tag: The tag to test.
expected: The expected formatted tag. expected: The expected tag dictionary.
windows_platform: Whether the system is windows.
""" """
expected = expected.replace("\n", "\r\n") if windows_platform else expected tag_dict = dict(tag)
assert str(tag) == expected assert tag_dict["name"] == expected["name"]
assert tag_dict["contents"] == expected["contents"]
assert tag_dict["props"] == expected["props"]
def test_format_cond_tag(): def test_format_cond_tag():
"""Test that the formatted cond tag is correct.""" """Test that the cond tag dict is correct."""
tag = CondTag( tag = CondTag(
true_value=str(Tag(name="h1", contents="True content")), true_value=dict(Tag(name="h1", contents="True content")),
false_value=str(Tag(name="h2", contents="False content")), false_value=dict(Tag(name="h2", contents="False content")),
cond=BaseVar(name="logged_in", type_=bool), cond=BaseVar(name="logged_in", type_=bool),
) )
assert ( tag_dict = dict(tag)
str(tag) cond, true_value, false_value = (
== "{isTrue(logged_in) ? <h1>True content</h1> : <h2>False content</h2>}" tag_dict["cond"],
tag_dict["true_value"],
tag_dict["false_value"],
) )
assert cond == "logged_in"
assert true_value["name"] == "h1"
assert true_value["contents"] == "True content"
assert false_value["name"] == "h2"
assert false_value["contents"] == "False content"

View File

@ -335,7 +335,7 @@ def test_create_config(app_name, expected_config_name, mocker):
mocker.patch("builtins.open") mocker.patch("builtins.open")
tmpl_mock = mocker.patch("pynecone.compiler.templates.PCCONFIG") tmpl_mock = mocker.patch("pynecone.compiler.templates.PCCONFIG")
prerequisites.create_config(app_name) prerequisites.create_config(app_name)
tmpl_mock.format.assert_called_with( tmpl_mock.render.assert_called_with(
app_name=app_name, config_name=expected_config_name app_name=app_name, config_name=expected_config_name
) )