[Fix 477] Use jinja2 for templating (#915)
This commit is contained in:
parent
dc2dff9323
commit
3b88e7c329
88
poetry.lock
generated
88
poetry.lock
generated
@ -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"
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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]:
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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]
|
||||
|
@ -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})",
|
||||
"{",
|
||||
)
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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],
|
||||
)
|
||||
)
|
||||
|
||||
|
10
pynecone/templates/app/pcconfig.py.jinja2
Normal file
10
pynecone/templates/app/pcconfig.py.jinja2
Normal 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,
|
||||
)
|
9
pynecone/templates/web/pages/_document.js.jinja2
Normal file
9
pynecone/templates/web/pages/_document.js.jinja2
Normal 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 %}
|
13
pynecone/templates/web/pages/base_page.js.jinja2
Normal file
13
pynecone/templates/web/pages/base_page.js.jinja2
Normal 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 %}
|
10
pynecone/templates/web/pages/custom_component.js.jinja2
Normal file
10
pynecone/templates/web/pages/custom_component.js.jinja2
Normal 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 %}
|
66
pynecone/templates/web/pages/index.js.jinja2
Normal file
66
pynecone/templates/web/pages/index.js.jinja2
Normal 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 %}
|
110
pynecone/templates/web/pages/utils.js.jinja2
Normal file
110
pynecone/templates/web/pages/utils.js.jinja2
Normal 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 %}
|
1
pynecone/templates/web/utils/theme.js.jinja2
Normal file
1
pynecone/templates/web/utils/theme.js.jinja2
Normal file
@ -0,0 +1 @@
|
||||
export default {{ theme|json_dumps }}
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"<DataTableGrid columns={{{expected}.columns}}{os.linesep}data={{"
|
||||
f"{expected}.data}}/>"
|
||||
)
|
||||
data_table_dict = data_table_component.render()
|
||||
|
||||
assert data_table_dict["props"] == [
|
||||
f"columns={{{expected}.columns}}",
|
||||
f"data={{{expected}.data}}",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -1,5 +1,3 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import pynecone as pc
|
||||
@ -49,14 +47,36 @@ def test_upload_component_render(upload_component):
|
||||
Args:
|
||||
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 (
|
||||
str(upload_component) == f"<ReactDropzone multiple={{true}}{os.linesep}"
|
||||
"onDrop={e => File(e)}>{({getRootProps, getInputProps}) => (<Box "
|
||||
'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>"
|
||||
text["children"][0]["contents"]
|
||||
== "{`Drag and drop files here or click to select files`}"
|
||||
)
|
||||
|
||||
|
||||
@ -66,14 +86,11 @@ def test_upload_component_with_props_render(upload_component_with_props):
|
||||
Args:
|
||||
upload_component_with_props: component fixture
|
||||
"""
|
||||
assert (
|
||||
str(upload_component_with_props) == f"<ReactDropzone maxFiles={{2}}{os.linesep}"
|
||||
f"multiple={{true}}{os.linesep}"
|
||||
f"noDrag={{true}}{os.linesep}"
|
||||
"onDrop={e => File(e)}>{({getRootProps, getInputProps}) => (<Box "
|
||||
'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>"
|
||||
)
|
||||
uplaod = upload_component_with_props.render()
|
||||
|
||||
assert uplaod["props"] == [
|
||||
"maxFiles={2}",
|
||||
"multiple={true}",
|
||||
"noDrag={true}",
|
||||
"onDrop={e => File(e)}",
|
||||
]
|
||||
|
@ -38,12 +38,27 @@ def test_validate_cond(cond_state: pc.Var):
|
||||
Text.create("cond is True"),
|
||||
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) == (
|
||||
"<Fragment>{isTrue(cond_state.value) ? "
|
||||
"<Fragment><Text>{`cond is True`}</Text></Fragment> : "
|
||||
"<Fragment><Text>{`cond is False`}</Text></Fragment>}</Fragment>"
|
||||
)
|
||||
[condition] = cond_dict["children"]
|
||||
assert condition["cond_state"] == "isTrue(cond_state.value)"
|
||||
|
||||
# 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(
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pytest
|
||||
|
||||
@ -71,25 +71,24 @@ def test_format_prop(prop: Var, formatted: str):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"props,formatted",
|
||||
"props,test_props",
|
||||
[
|
||||
({}, ""),
|
||||
({"key": 1}, "key={1}"),
|
||||
({"key": "value"}, 'key="value"'),
|
||||
({"key": True, "key2": "value2"}, 'key={true}\nkey2="value2"'),
|
||||
({}, []),
|
||||
({"key": 1}, ["key={1}"]),
|
||||
({"key": "value"}, ['key="value"']),
|
||||
({"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.
|
||||
|
||||
Args:
|
||||
props: The props to test.
|
||||
formatted: The expected formatted props.
|
||||
windows_platform: Whether the system is windows.
|
||||
test_props: The expected props.
|
||||
"""
|
||||
assert Tag(props=props).format_props() == (
|
||||
formatted.replace("\n", "\r\n") if windows_platform else formatted
|
||||
)
|
||||
tag_props = Tag(props=props).format_props()
|
||||
for i, tag_prop in enumerate(tag_props):
|
||||
assert tag_prop == test_props[i]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -126,13 +125,20 @@ def test_add_props():
|
||||
@pytest.mark.parametrize(
|
||||
"tag,expected",
|
||||
[
|
||||
(Tag(), "</>"),
|
||||
(Tag(name="br"), "<br/>"),
|
||||
(Tag(contents="hello"), "<>hello</>"),
|
||||
(Tag(name="h1", contents="hello"), "<h1>hello</h1>"),
|
||||
(Tag(), {"name": "", "contents": "", "props": {}}),
|
||||
(Tag(name="br"), {"name": "br", "contents": "", "props": {}}),
|
||||
(Tag(contents="hello"), {"name": "", "contents": "hello", "props": {}}),
|
||||
(
|
||||
Tag(name="h1", contents="hello"),
|
||||
{"name": "h1", "contents": "hello", "props": {}},
|
||||
),
|
||||
(
|
||||
Tag(name="box", props={"color": "red", "textAlign": "center"}),
|
||||
'<box color="red"\ntextAlign="center"/>',
|
||||
{
|
||||
"name": "box",
|
||||
"contents": "",
|
||||
"props": {"color": "red", "textAlign": "center"},
|
||||
},
|
||||
),
|
||||
(
|
||||
Tag(
|
||||
@ -140,30 +146,44 @@ def test_add_props():
|
||||
props={"color": "red", "textAlign": "center"},
|
||||
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):
|
||||
"""Test that the formatted tag is correct.
|
||||
def test_format_tag(tag: Tag, expected: Dict):
|
||||
"""Test that the tag dict is correct.
|
||||
|
||||
Args:
|
||||
tag: The tag to test.
|
||||
expected: The expected formatted tag.
|
||||
windows_platform: Whether the system is windows.
|
||||
expected: The expected tag dictionary.
|
||||
"""
|
||||
expected = expected.replace("\n", "\r\n") if windows_platform else expected
|
||||
assert str(tag) == expected
|
||||
tag_dict = dict(tag)
|
||||
assert tag_dict["name"] == expected["name"]
|
||||
assert tag_dict["contents"] == expected["contents"]
|
||||
assert tag_dict["props"] == expected["props"]
|
||||
|
||||
|
||||
def test_format_cond_tag():
|
||||
"""Test that the formatted cond tag is correct."""
|
||||
"""Test that the cond tag dict is correct."""
|
||||
tag = CondTag(
|
||||
true_value=str(Tag(name="h1", contents="True content")),
|
||||
false_value=str(Tag(name="h2", contents="False content")),
|
||||
true_value=dict(Tag(name="h1", contents="True content")),
|
||||
false_value=dict(Tag(name="h2", contents="False content")),
|
||||
cond=BaseVar(name="logged_in", type_=bool),
|
||||
)
|
||||
assert (
|
||||
str(tag)
|
||||
== "{isTrue(logged_in) ? <h1>True content</h1> : <h2>False content</h2>}"
|
||||
tag_dict = dict(tag)
|
||||
cond, true_value, false_value = (
|
||||
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"
|
||||
|
@ -335,7 +335,7 @@ def test_create_config(app_name, expected_config_name, mocker):
|
||||
mocker.patch("builtins.open")
|
||||
tmpl_mock = mocker.patch("pynecone.compiler.templates.PCCONFIG")
|
||||
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
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user