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

hello

"), + (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"}), - '', + { + "name": "box", + "contents": "", + "props": {"color": "red", "textAlign": "center"}, + }, ), ( Tag( @@ -140,30 +146,44 @@ def test_add_props(): props={"color": "red", "textAlign": "center"}, contents="text", ), - 'text', + { + "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) ?

True content

:

False content

}" + 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" diff --git a/tests/test_utils.py b/tests/test_utils.py index 0114c1728..9c1f979aa 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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 )