[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"},
]
[[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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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})",
"{",
)

View File

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

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")
# 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.

View File

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

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

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"
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:

View File

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

View File

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

View File

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

View File

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

View File

@ -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)}",
]

View File

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

View File

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

View File

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