diff --git a/pyi_hashes.json b/pyi_hashes.json index 04bea99bb5c..d4862c56641 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -29,7 +29,7 @@ "reflex/components/el/element.pyi": "c5974a92fbc310e42d0f6cfdd13472f4", "reflex/components/el/elements/__init__.pyi": "29512d7a6b29c6dc5ff68d3b31f26528", "reflex/components/el/elements/base.pyi": "7d1163c7221bb16691ce179646ce8515", - "reflex/components/el/elements/forms.pyi": "a2099706131a8cf364b2bf9a6f958cdc", + "reflex/components/el/elements/forms.pyi": "d83a10ac3d4177183c3911f8732835cc", "reflex/components/el/elements/inline.pyi": "8bc2bbf8f3fd8bb9b5910c0e888e5386", "reflex/components/el/elements/media.pyi": "66846a0c74fbe772811cd6577b2796d0", "reflex/components/el/elements/metadata.pyi": "bace81e70eaa42adbf50702c166dcd65", diff --git a/pyproject.toml b/pyproject.toml index 9a17a40e050..1e78c040fef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ dependencies = [ "click >=8.2", "granian[reload] >=2.4.0", "httpx >=0.23.3,<1.0", - "jinja2 >=3.1.2,<4.0", "packaging >=24.2,<26", "platformdirs >=4.3.7,<5.0", "psutil >=7.0.0,<8.0; sys_platform == 'win32'", diff --git a/reflex/.templates/jinja/app/rxconfig.py.jinja2 b/reflex/.templates/jinja/app/rxconfig.py.jinja2 deleted file mode 100644 index 242f9fa831d..00000000000 --- a/reflex/.templates/jinja/app/rxconfig.py.jinja2 +++ /dev/null @@ -1,9 +0,0 @@ -import reflex as rx - -config = rx.Config( - app_name="{{ app_name }}", - plugins=[ - rx.plugins.SitemapPlugin(), - rx.plugins.TailwindV4Plugin(), - ], -) diff --git a/reflex/.templates/jinja/custom_components/README.md.jinja2 b/reflex/.templates/jinja/custom_components/README.md.jinja2 deleted file mode 100644 index b7aec4d9d4a..00000000000 --- a/reflex/.templates/jinja/custom_components/README.md.jinja2 +++ /dev/null @@ -1,9 +0,0 @@ -# {{ module_name }} - -A Reflex custom component {{ module_name }}. - -## Installation - -```bash -pip install {{ package_name }} -``` diff --git a/reflex/.templates/jinja/custom_components/__init__.py.jinja2 b/reflex/.templates/jinja/custom_components/__init__.py.jinja2 deleted file mode 100644 index 96c74063eac..00000000000 --- a/reflex/.templates/jinja/custom_components/__init__.py.jinja2 +++ /dev/null @@ -1 +0,0 @@ -from .{{ module_name }} import * \ No newline at end of file diff --git a/reflex/.templates/jinja/custom_components/demo_app.py.jinja2 b/reflex/.templates/jinja/custom_components/demo_app.py.jinja2 deleted file mode 100644 index eb73c12c010..00000000000 --- a/reflex/.templates/jinja/custom_components/demo_app.py.jinja2 +++ /dev/null @@ -1,39 +0,0 @@ -"""Welcome to Reflex! This file showcases the custom component in a basic app.""" - -from rxconfig import config - -import reflex as rx - -from {{ custom_component_module_dir }} import {{ module_name }} - -filename = f"{config.app_name}/{config.app_name}.py" - - -class State(rx.State): - """The app state.""" - - pass - - -def index() -> rx.Component: - return rx.center( - rx.theme_panel(), - rx.vstack( - rx.heading("Welcome to Reflex!", size="9"), - rx.text( - "Test your custom component by editing ", - rx.code(filename), - font_size="2em", - ), - {{ module_name }}(), - align="center", - spacing="7", - ), - height="100vh", - ) - - -# Add state and page to the app. -app = rx.App() -app.add_page(index) - diff --git a/reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 b/reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 deleted file mode 100644 index abfd998fd62..00000000000 --- a/reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 +++ /dev/null @@ -1,25 +0,0 @@ -[build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "{{ package_name }}" -version = "0.0.1" -description = "Reflex custom component {{ module_name }}" -readme = "README.md" -license = { text = "Apache-2.0" } -requires-python = ">=3.10" -authors = [{ name = "", email = "YOUREMAIL@domain.com" }] -keywords = ["reflex","reflex-custom-components"] - -dependencies = ["reflex>={{ reflex_version }}"] - -classifiers = ["Development Status :: 4 - Beta"] - -[project.urls] - -[project.optional-dependencies] -dev = ["build", "twine"] - -[tool.setuptools.packages.find] -where = ["custom_components"] diff --git a/reflex/.templates/jinja/custom_components/src.py.jinja2 b/reflex/.templates/jinja/custom_components/src.py.jinja2 deleted file mode 100644 index dc6e7c245b5..00000000000 --- a/reflex/.templates/jinja/custom_components/src.py.jinja2 +++ /dev/null @@ -1,57 +0,0 @@ -"""Reflex custom component {{ component_class_name }}.""" - -# For wrapping react guide, visit https://reflex.dev/docs/wrapping-react/overview/ - -import reflex as rx - -# Some libraries you want to wrap may require dynamic imports. -# This is because they they may not be compatible with Server-Side Rendering (SSR). -# To handle this in Reflex, all you need to do is subclass `NoSSRComponent` instead. -# For example: -# from reflex.components.component import NoSSRComponent -# class {{ component_class_name }}(NoSSRComponent): -# pass - - -class {{ component_class_name }}(rx.Component): - """{{ component_class_name }} component.""" - - # The React library to wrap. - library = "Fill-Me" - - # The React component tag. - tag = "Fill-Me" - - # If the tag is the default export from the module, you must set is_default = True. - # This is normally used when components don't have curly braces around them when importing. - # is_default = True - - # If you are wrapping another components with the same tag as a component in your project - # you can use aliases to differentiate between them and avoid naming conflicts. - # alias = "Other{{ component_class_name }}" - - # The props of the React component. - # Note: when Reflex compiles the component to Javascript, - # `snake_case` property names are automatically formatted as `camelCase`. - # The prop names may be defined in `camelCase` as well. - # some_prop: rx.Var[str] = "some default value" - # some_other_prop: rx.Var[int] = 1 - - # By default Reflex will install the library you have specified in the library property. - # However, sometimes you may need to install other libraries to use a component. - # In this case you can use the lib_dependencies property to specify other libraries to install. - # lib_dependencies: list[str] = [] - - # Event triggers declaration if any. - # Below is equivalent to merging `{ "on_change": lambda e: [e] }` - # onto the default event triggers of parent/base Component. - # The function defined for the `on_change` trigger maps event for the javascript - # trigger to what will be passed to the backend event handler function. - # on_change: rx.EventHandler[lambda e: [e]] - - # To add custom code to your component - # def _get_custom_code(self) -> str: - # return "const customCode = 'customCode';" - - -{{ module_name }} = {{ component_class_name }}.create diff --git a/reflex/.templates/jinja/web/package.json.jinja2 b/reflex/.templates/jinja/web/package.json.jinja2 deleted file mode 100644 index 77a0b27aa06..00000000000 --- a/reflex/.templates/jinja/web/package.json.jinja2 +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "reflex", - "type": "module", - "scripts": { - "dev": "{{ scripts.dev }}", - "export": "{{ scripts.export }}", - "prod": "{{ scripts.prod }}" - }, - "dependencies": { - {% for package, version in dependencies.items() %} - "{{ package }}": "{{ version }}"{% if not loop.last %},{% endif %} - - {% endfor %} - }, - "devDependencies": { - {% for package, version in dev_dependencies.items() %} - "{{ package }}": "{{ version }}"{% if not loop.last %},{% endif %} - - {% endfor %} - }, - "overrides": { - {% for package, version in overrides.items() %} - "{{ package }}": "{{ version }}"{% if not loop.last %},{% endif %} - - {% endfor %} - } -} \ No newline at end of file diff --git a/reflex/.templates/jinja/web/pages/_app.js.jinja2 b/reflex/.templates/jinja/web/pages/_app.js.jinja2 deleted file mode 100644 index 79b0c21ca27..00000000000 --- a/reflex/.templates/jinja/web/pages/_app.js.jinja2 +++ /dev/null @@ -1,62 +0,0 @@ -{% extends "web/pages/base_page.js.jinja2" %} -{% from "web/pages/macros.js.jinja2" import renderHooks %} - -{% block early_imports %} -import reflexGlobalStyles from '$/styles/__reflex_global_styles.css?url'; -{% endblock %} - -{% block declaration %} -import { EventLoopProvider, StateProvider, defaultColorMode } from "$/utils/context"; -import { ThemeProvider } from '$/utils/react-theme'; -import { Layout as AppLayout } from './_document'; -import { Outlet } from 'react-router'; -{% for library_alias, library_path in window_libraries %} -import * as {{library_alias}} from "{{library_path}}"; -{% endfor %} - -{% for custom_code in custom_codes %} -{{custom_code}} -{% endfor %} -{% endblock %} - -{% block export %} -export const links = () => [ - { rel: 'stylesheet', href: reflexGlobalStyles, type: 'text/css' } -]; - -function AppWrap({children}) { - {{ renderHooks(hooks) }} - - return ( - {{utils.render(render)}} - ) -} - - -export function Layout({children}) { - useEffect(() => { - // Make contexts and state objects available globally for dynamic eval'd components - let windowImports = { -{% for library_alias, library_path in window_libraries %} - "{{library_path}}": {{library_alias}}, -{% endfor %} - }; - window["__reflex"] = windowImports; - }, []); - - return jsx(AppLayout, {}, - jsx(ThemeProvider, {defaultTheme: defaultColorMode, attribute: "class"}, - jsx(StateProvider, {}, - jsx(EventLoopProvider, {}, - jsx(AppWrap, {}, children) - ) - ) - ) - ); -} - -export default function App() { - return jsx(Outlet, {}); -} - -{% endblock %} diff --git a/reflex/.templates/jinja/web/pages/_document.js.jinja2 b/reflex/.templates/jinja/web/pages/_document.js.jinja2 deleted file mode 100644 index 3306f035f7f..00000000000 --- a/reflex/.templates/jinja/web/pages/_document.js.jinja2 +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "web/pages/base_page.js.jinja2" %} - -{% block export %} -export function Layout({children}) { - return ( - {{utils.render(document)}} - ) -} -{% endblock %} diff --git a/reflex/.templates/jinja/web/pages/base_page.js.jinja2 b/reflex/.templates/jinja/web/pages/base_page.js.jinja2 deleted file mode 100644 index ff430845550..00000000000 --- a/reflex/.templates/jinja/web/pages/base_page.js.jinja2 +++ /dev/null @@ -1,21 +0,0 @@ -{% import 'web/pages/utils.js.jinja2' as utils %} - -{% block early_imports %} -{% endblock %} - -{%- block imports_libs %} - -{% for module in imports%} - {{- utils.get_import(module) }} -{% endfor %} - -{% for dynamic_import in dynamic_imports %} -{{dynamic_import}} -{% endfor %} -{% endblock %} - -{% block declaration %} -{% endblock %} - -{% block export %} -{% endblock %} diff --git a/reflex/.templates/jinja/web/pages/component.js.jinja2 b/reflex/.templates/jinja/web/pages/component.js.jinja2 deleted file mode 100644 index 1a08c64c6a6..00000000000 --- a/reflex/.templates/jinja/web/pages/component.js.jinja2 +++ /dev/null @@ -1,2 +0,0 @@ -{% import 'web/pages/utils.js.jinja2' as utils %} -{{utils.render(component.render())}} \ No newline at end of file diff --git a/reflex/.templates/jinja/web/pages/custom_component.js.jinja2 b/reflex/.templates/jinja/web/pages/custom_component.js.jinja2 deleted file mode 100644 index 159a27b2767..00000000000 --- a/reflex/.templates/jinja/web/pages/custom_component.js.jinja2 +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "web/pages/base_page.js.jinja2" %} -{% from "web/pages/macros.js.jinja2" import renderHooks %} - -{% block declaration %} -{% for custom_code in custom_codes %} -{{custom_code}} -{% endfor %} -{% endblock %} - -{% block export %} -{% for component in components %} - -export const {{component.name}} = memo(({ {{-component.props|join(", ")-}} }) => { - {{ renderHooks(component.hooks) }} - - return( - {{utils.render(component.render)}} - ) - -}) -{% endfor %} -{% endblock %} diff --git a/reflex/.templates/jinja/web/pages/index.js.jinja2 b/reflex/.templates/jinja/web/pages/index.js.jinja2 deleted file mode 100644 index 98a2aefd6df..00000000000 --- a/reflex/.templates/jinja/web/pages/index.js.jinja2 +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "web/pages/base_page.js.jinja2" %} -{% from "web/pages/macros.js.jinja2" import renderHooks %} - -{% block declaration %} -{% for custom_code in custom_codes %} -{{custom_code}} -{% endfor %} -{% endblock %} - -{% block export %} -export default function Component() { - {{ renderHooks(hooks)}} - - return ( - {{utils.render(render)}} - ) -} -{% endblock %} diff --git a/reflex/.templates/jinja/web/pages/macros.js.jinja2 b/reflex/.templates/jinja/web/pages/macros.js.jinja2 deleted file mode 100644 index 68810d896df..00000000000 --- a/reflex/.templates/jinja/web/pages/macros.js.jinja2 +++ /dev/null @@ -1,38 +0,0 @@ -{% macro renderHooks(hooks) %} - {% set sorted_hooks = sort_hooks(hooks) %} - - {# Render the grouped hooks #} - {% for hook, _ in sorted_hooks[const.hook_position.INTERNAL] %} - {{ hook }} - {% endfor %} - - {% for hook, _ in sorted_hooks[const.hook_position.PRE_TRIGGER] %} - {{ hook }} - {% endfor %} - - {% for hook, _ in sorted_hooks[const.hook_position.POST_TRIGGER] %} - {{ hook }} - {% endfor %} -{% endmacro %} - -{% macro renderHooksWithMemo(hooks, memo)%} - {% set sorted_hooks = sort_hooks(hooks) %} - - {# Render the grouped hooks #} - {% for hook, _ in sorted_hooks[const.hook_position.INTERNAL] %} - {{ hook }} - {% endfor %} - - {% for hook, _ in sorted_hooks[const.hook_position.PRE_TRIGGER] %} - {{ hook }} - {% endfor %} - - {% for hook in memo %} - {{ hook }} - {% endfor %} - - {% for hook, _ in sorted_hooks[const.hook_position.POST_TRIGGER] %} - {{ hook }} - {% endfor %} - -{% endmacro %} \ No newline at end of file diff --git a/reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 b/reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 deleted file mode 100644 index 6e618396f09..00000000000 --- a/reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +++ /dev/null @@ -1,15 +0,0 @@ -{% import 'web/pages/utils.js.jinja2' as utils %} -{% from 'web/pages/macros.js.jinja2' import renderHooksWithMemo %} -{% set all_hooks = component._get_all_hooks() %} - -{% if export %} -export function {{tag_name}} () { -{% else %} -function {{tag_name}} () { -{% endif %} - {{ renderHooksWithMemo(all_hooks, memo_trigger_hooks) }} - - return ( - {{utils.render(component.render())}} - ) -} diff --git a/reflex/.templates/jinja/web/pages/stateful_components.js.jinja2 b/reflex/.templates/jinja/web/pages/stateful_components.js.jinja2 deleted file mode 100644 index 420c7ca5d06..00000000000 --- a/reflex/.templates/jinja/web/pages/stateful_components.js.jinja2 +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "web/pages/base_page.js.jinja2" %} - -{% block export %} -{{ memoized_code }} -{% endblock %} diff --git a/reflex/.templates/jinja/web/pages/utils.js.jinja2 b/reflex/.templates/jinja/web/pages/utils.js.jinja2 deleted file mode 100644 index 14c62f18dd8..00000000000 --- a/reflex/.templates/jinja/web/pages/utils.js.jinja2 +++ /dev/null @@ -1,93 +0,0 @@ -{# Rendering components recursively. #} -{# Args: #} -{# component: component dictionary #} -{% macro render(component) %} -{%- if component is not mapping %}{{ component }} -{%- elif "iterable" in component %}{{ render_iterable_tag(component) }} -{%- elif component.name == "match"%}{{ render_match_tag(component) }} -{%- elif "cond" in component %}{{ render_condition_tag(component) }} -{%- elif component.children|length %}{{ render_tag(component) }} -{%- else %}{{ render_self_close_tag(component) }} -{%- endif %} -{% endmacro %} - -{# Rendering self close tag. #} -{# Args: #} -{# component: component dictionary #} -{% macro render_self_close_tag(component) %} -{% if component.name|length %} -jsx({{ component.name }},{{ render_props(component.props) }},{{ component.contents }}) -{% elif component.contents|length -%}{{ component.contents }} -{% else %}"" -{% endif %} -{% endmacro %} - -{# Rendering close tag with args and props. #} -{# Args: #} -{# component: component dictionary #} -{% macro render_tag(component) %} -jsx( -{% if component.name|length %}{{ component.name }}{% else %}Fragment{% endif %}, -{{ render_props(component.props) }}, -{% if component.contents|length %}{{ component.contents }},{% endif %} -{% for child in component.children %}{% if child is mapping or child|length %}{{ render(child) }},{% endif %}{% endfor %} -) -{%- endmacro %} - - -{# Rendering condition component. #} -{# Args: #} -{# component: component dictionary #} -{% macro render_condition_tag(component) %} -({{ component.cond_state }} ? ({{ render(component.true_value) }}) : ({{ render(component.false_value) }})) -{%- endmacro %} - - -{# Rendering 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 %} - - -{# Rendering props of a component. #} -{# Args: #} -{# component: component dictionary #} -{% macro render_props(props) %}{{ "{" }}{% if props|length %}{{ props|join(",") }}{% endif %}{{ "}" }}{% endmacro %} - -{# Rendering Match component. #} -{# Args: #} -{# component: component dictionary #} -{% macro render_match_tag(component) %} -(() => { - switch (JSON.stringify({{ component.cond._js_expr }})) { - {% for case in component.match_cases %} - {% for condition in case[:-1] %} - case JSON.stringify({{ condition._js_expr }}): - {% endfor %} - return {{ render(case[-1]) }}; - break; - {% endfor %} - default: - return {{ render(component.default) }}; - break; - } -})() -{% 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/reflex/.templates/jinja/web/styles/styles.css.jinja2 b/reflex/.templates/jinja/web/styles/styles.css.jinja2 deleted file mode 100644 index b98657d5d34..00000000000 --- a/reflex/.templates/jinja/web/styles/styles.css.jinja2 +++ /dev/null @@ -1,6 +0,0 @@ -{%- block imports_styles %} -@layer __reflex_base; -{% for sheet_name in stylesheets %} - {{- "@import url('" + sheet_name + "'); " }} -{% endfor %} -{% endblock %} diff --git a/reflex/.templates/jinja/web/utils/context.js.jinja2 b/reflex/.templates/jinja/web/utils/context.js.jinja2 deleted file mode 100644 index 85699cbb6c0..00000000000 --- a/reflex/.templates/jinja/web/utils/context.js.jinja2 +++ /dev/null @@ -1,129 +0,0 @@ -import { createContext, useContext, useMemo, useReducer, useState, createElement, useEffect } from "react" -import { applyDelta, Event, hydrateClientStorage, useEventLoop, refs } from "$/utils/state" -import { jsx } from "@emotion/react"; - -{% if initial_state %} -export const initialState = {{ initial_state|json_dumps }} -{% else %} -export const initialState = {} -{% endif %} - -export const defaultColorMode = {{ default_color_mode }} -export const ColorModeContext = createContext(null); -export const UploadFilesContext = createContext(null); -export const DispatchContext = createContext(null); -export const StateContexts = { - {% for state_name in initial_state %} - {{state_name|var_name}}: createContext(null), - {% endfor %} -} -export const EventLoopContext = createContext(null); -{% if client_storage %} -export const clientStorage = {{ client_storage|json_dumps }} -{% else %} -export const clientStorage = {} -{% endif %} - -{% if state_name %} -export const state_name = "{{state_name}}" - -export const exception_state_name = "{{const.frontend_exception_state}}" - -// These events are triggered on initial load and each page navigation. -export const onLoadInternalEvent = () => { - const internal_events = []; - - // Get tracked cookie and local storage vars to send to the backend. - const client_storage_vars = hydrateClientStorage(clientStorage); - // But only send the vars if any are actually set in the browser. - if (client_storage_vars && Object.keys(client_storage_vars).length !== 0) { - internal_events.push( - Event( - '{{state_name}}.{{const.update_vars_internal}}', - {vars: client_storage_vars}, - ), - ); - } - - // `on_load_internal` triggers the correct on_load event(s) for the current page. - // If the page does not define any on_load event, this will just set `is_hydrated = true`. - internal_events.push(Event('{{state_name}}.{{const.on_load_internal}}')); - - return internal_events; -} - -// The following events are sent when the websocket connects or reconnects. -export const initialEvents = () => [ - Event('{{state_name}}.{{const.hydrate}}'), - ...onLoadInternalEvent() -] -{% else %} -export const state_name = undefined - -export const exception_state_name = undefined - -export const onLoadInternalEvent = () => [] - -export const initialEvents = () => [] -{% endif %} - -export const isDevMode = {{ is_dev_mode|json_dumps }} - -export function UploadFilesProvider({ children }) { - const [filesById, setFilesById] = useState({}) - refs["__clear_selected_files"] = (id) => setFilesById(filesById => { - const newFilesById = {...filesById} - delete newFilesById[id] - return newFilesById - }) - return createElement( - UploadFilesContext.Provider, - { value: [filesById, setFilesById] }, - children - ); -} - -export function ClientSide(component) { - return ({ children, ...props }) => { - const [Component, setComponent] = useState(null); - useEffect(() => { - setComponent(component); - }, []); - return Component ? jsx(Component, props, children) : null; - }; -} - -export function EventLoopProvider({ children }) { - const dispatch = useContext(DispatchContext) - const [addEvents, connectErrors] = useEventLoop( - dispatch, - initialEvents, - clientStorage, - ) - return createElement( - EventLoopContext.Provider, - { value: [addEvents, connectErrors] }, - children - ); -} - -export function StateProvider({ children }) { - {% for state_name in initial_state %} - const [{{state_name|var_name}}, dispatch_{{state_name|var_name}}] = useReducer(applyDelta, initialState["{{state_name}}"]) - {% endfor %} - const dispatchers = useMemo(() => { - return { - {% for state_name in initial_state %} - "{{state_name}}": dispatch_{{state_name|var_name}}, - {% endfor %} - } - }, []) - - return ( - {% for state_name in initial_state %} - createElement(StateContexts.{{state_name|var_name}},{value: {{state_name|var_name}}}, - {% endfor %} - createElement(DispatchContext, {value: dispatchers}, children) - {% for state_name in initial_state %}){% endfor %} - ) -} diff --git a/reflex/.templates/jinja/web/utils/theme.js.jinja2 b/reflex/.templates/jinja/web/utils/theme.js.jinja2 deleted file mode 100644 index 819abd23223..00000000000 --- a/reflex/.templates/jinja/web/utils/theme.js.jinja2 +++ /dev/null @@ -1 +0,0 @@ -export default {{ theme }} \ No newline at end of file diff --git a/reflex/.templates/jinja/web/vite.config.js.jinja2 b/reflex/.templates/jinja/web/vite.config.js.jinja2 deleted file mode 100644 index 4dfc723d9e5..00000000000 --- a/reflex/.templates/jinja/web/vite.config.js.jinja2 +++ /dev/null @@ -1,74 +0,0 @@ -import { fileURLToPath, URL } from "url"; -import { reactRouter } from "@react-router/dev/vite"; -import { defineConfig } from "vite"; -import safariCacheBustPlugin from "./vite-plugin-safari-cachebust"; - -// Ensure that bun always uses the react-dom/server.node functions. -function alwaysUseReactDomServerNode() { - return { - name: "vite-plugin-always-use-react-dom-server-node", - enforce: "pre", - - resolveId(source, importer) { - if ( - typeof importer === "string" && - importer.endsWith("/entry.server.node.tsx") && - source.includes("react-dom/server") - ) { - return this.resolve("react-dom/server.node", importer, { - skipSelf: true, - }); - } - return null; - }, - }; -} - -export default defineConfig((config) => ({ - plugins: [ - alwaysUseReactDomServerNode(), - reactRouter(), - safariCacheBustPlugin(), - ], - build: { - assetsDir: "{{base}}assets".slice(1), - rollupOptions: { - jsx: {}, - output: { - advancedChunks: { - groups: [ - { - test: /env.json/, - name: "reflex-env", - }, - ], - }, - }, - }, - }, - experimental: { - enableNativePlugin: false, - }, - server: { - port: process.env.PORT, - watch: { - ignored: [ - "**/.web/backend/**", - "**/.web/reflex.install_frontend_packages.cached", - ], - }, - }, - resolve: { - mainFields: ["browser", "module", "jsnext"], - alias: [ - { - find: "$", - replacement: fileURLToPath(new URL("./", import.meta.url)), - }, - { - find: "@", - replacement: fileURLToPath(new URL("./public", import.meta.url)), - }, - ], - }, -})); diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index ca46be9d1be..8c73077a6e2 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -53,7 +53,7 @@ def _compile_document_root(root: Component) -> str: """ document_root_imports = root._get_all_imports() _apply_common_imports(document_root_imports) - return templates.DOCUMENT_ROOT.render( + return templates.document_root_template( imports=utils.compile_imports(document_root_imports), document=root.render(), ) @@ -93,7 +93,7 @@ def _compile_app(app_root: Component) -> str: app_root_imports = app_root._get_all_imports() _apply_common_imports(app_root_imports) - return templates.APP_ROOT.render( + return templates.app_root_template( imports=utils.compile_imports(app_root_imports), custom_codes=app_root._get_all_custom_code(), hooks=app_root._get_all_hooks(), @@ -112,7 +112,7 @@ def _compile_theme(theme: str) -> str: Returns: The compiled theme. """ - return templates.THEME.render(theme=theme) + return templates.theme_template(theme=theme) def _compile_contexts(state: type[BaseState] | None, theme: Component | None) -> str: @@ -130,17 +130,17 @@ def _compile_contexts(state: type[BaseState] | None, theme: Component | None) -> appearance = LiteralVar.create(SYSTEM_COLOR_MODE) return ( - templates.CONTEXT.render( + templates.context_template( initial_state=utils.compile_state(state), state_name=state.get_name(), client_storage=utils.compile_client_storage(state), is_dev_mode=not is_prod_mode(), - default_color_mode=appearance, + default_color_mode=str(appearance), ) if state - else templates.CONTEXT.render( + else templates.context_template( is_dev_mode=not is_prod_mode(), - default_color_mode=appearance, + default_color_mode=str(appearance), ) ) @@ -159,7 +159,7 @@ def _compile_page(component: BaseComponent) -> str: imports = utils.compile_imports(imports) # Compile the code to render the component. - return templates.PAGE.render( + return templates.page_template( imports=imports, dynamic_imports=component._get_all_dynamic_imports(), custom_codes=component._get_all_custom_code(), @@ -321,7 +321,7 @@ def _compile_root_stylesheet(stylesheets: list[str], reset_style: bool = True) - 'The `libsass` package is required to compile sass/scss stylesheet files. Run `pip install "libsass>=0.23.0"`.' ) - return templates.STYLE.render(stylesheets=sheets) + return templates.styles_template(stylesheets=sheets) def _compile_component(component: Component | StatefulComponent) -> str: @@ -333,7 +333,7 @@ def _compile_component(component: Component | StatefulComponent) -> str: Returns: The compiled component. """ - return templates.COMPONENT.render(component=component) + return templates.component_template(component=component) def _compile_components( @@ -376,7 +376,7 @@ def _compile_components( # Compile the components page. return ( - templates.COMPONENTS.render( + templates.custom_component_template( imports=utils.compile_imports(imports), components=component_renders, dynamic_imports=dynamic_imports, @@ -456,7 +456,7 @@ def get_shared_components_recursive(component: BaseComponent): if rendered_components: _apply_common_imports(all_imports) - return templates.STATEFUL_COMPONENTS.render( + return templates.stateful_components_template( imports=utils.compile_imports(all_imports), memoized_code="\n".join(rendered_components), ) diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index cd610eee50e..3547dd4e955 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -2,15 +2,23 @@ from __future__ import annotations -from jinja2 import Environment, FileSystemLoader, Template +import json +from collections.abc import Iterable, Mapping +from typing import TYPE_CHECKING, Any from reflex import constants from reflex.constants import Hooks from reflex.utils.format import format_state_name, json_dumps from reflex.vars.base import VarData +if TYPE_CHECKING: + from reflex.compiler.utils import _ImportDict + from reflex.components.component import Component, StatefulComponent -def _sort_hooks(hooks: dict[str, VarData | None]): + +def _sort_hooks( + hooks: dict[str, VarData | None], +) -> tuple[list[str], list[str], list[str]]: """Sort the hooks by their position. Args: @@ -19,155 +27,674 @@ def _sort_hooks(hooks: dict[str, VarData | None]): Returns: The sorted hooks. """ - sorted_hooks = { - Hooks.HookPosition.INTERNAL: [], - Hooks.HookPosition.PRE_TRIGGER: [], - Hooks.HookPosition.POST_TRIGGER: [], - } + internal_hooks = [] + pre_trigger_hooks = [] + post_trigger_hooks = [] for hook, data in hooks.items(): if data and data.position and data.position == Hooks.HookPosition.INTERNAL: - sorted_hooks[Hooks.HookPosition.INTERNAL].append((hook, data)) + internal_hooks.append(hook) elif not data or ( not data.position or data.position == constants.Hooks.HookPosition.PRE_TRIGGER ): - sorted_hooks[Hooks.HookPosition.PRE_TRIGGER].append((hook, data)) + pre_trigger_hooks.append(hook) elif ( data and data.position and data.position == constants.Hooks.HookPosition.POST_TRIGGER ): - sorted_hooks[Hooks.HookPosition.POST_TRIGGER].append((hook, data)) + post_trigger_hooks.append(hook) + + return internal_hooks, pre_trigger_hooks, post_trigger_hooks + + +class _RenderUtils: + @staticmethod + def render(component: Mapping[str, Any] | str) -> str: + if isinstance(component, str): + return component or "null" + if "iterable" in component: + return _RenderUtils.render_iterable_tag(component) + if "match_cases" in component: + return _RenderUtils.render_match_tag(component) + if "cond_state" in component: + return _RenderUtils.render_condition_tag(component) + if (contents := component.get("contents")) is not None: + return contents or "null" + return _RenderUtils.render_tag(component) + + @staticmethod + def render_tag(component: Mapping[str, Any]) -> str: + name = component.get("name") or "Fragment" + props = f"{{{','.join(component['props'])}}}" + rendered_children = [ + _RenderUtils.render(child) + for child in component.get("children", []) + if child + ] + + return f"jsx({name},{props},{','.join(rendered_children)})" + + @staticmethod + def render_condition_tag(component: Any) -> str: + return f"({component['cond_state']}?({_RenderUtils.render(component['true_value'])}):({_RenderUtils.render(component['false_value'])}))" + + @staticmethod + def render_iterable_tag(component: Any) -> str: + children_rendered = "".join( + [_RenderUtils.render(child) for child in component.get("children", [])] + ) + return f"{component['iterable_state']}.map(({component['arg_name']},{component['arg_index']})=>({children_rendered}))" + + @staticmethod + def render_match_tag(component: Any) -> str: + cases_code = "" + for conditions, return_value in component["match_cases"]: + for condition in conditions: + cases_code += f" case JSON.stringify({condition}):\n" + cases_code += f""" return {_RenderUtils.render(return_value)}; + break; +""" + + return f"""(() => {{ + switch (JSON.stringify({component["cond"]})) {{ +{cases_code} default: + return {_RenderUtils.render(component["default"])}; + break; + }} +}})()""" + + @staticmethod + def get_import(module: _ImportDict) -> str: + default_import = module["default"] + rest_imports = module["rest"] + + if default_import and rest_imports: + rest_imports_str = ",".join(sorted(rest_imports)) + return f'import {default_import}, {{{rest_imports_str}}} from "{module["lib"]}"' + if default_import: + return f'import {default_import} from "{module["lib"]}"' + if rest_imports: + rest_imports_str = ",".join(sorted(rest_imports)) + return f'import {{{rest_imports_str}}} from "{module["lib"]}"' + return f'import "{module["lib"]}"' + + +def rxconfig_template(app_name: str): + """Template for the Reflex config file. + + Args: + app_name: The name of the application. + + Returns: + Rendered Reflex config file content as string. + """ + return f"""import reflex as rx + +config = rx.Config( + app_name="{app_name}", + plugins=[ + rx.plugins.SitemapPlugin(), + rx.plugins.TailwindV4Plugin(), + ] +)""" + - return sorted_hooks +def document_root_template(*, imports: list[_ImportDict], document: dict[str, Any]): + """Template for the document root. + Args: + imports: List of import statements. + document: Document root component. -class ReflexJinjaEnvironment(Environment): - """The template class for jinja environment.""" + Returns: + Rendered document root code as string. + """ + imports_rendered = "\n".join([_RenderUtils.get_import(mod) for mod in imports]) + return f"""{imports_rendered} + +export function Layout({{children}}) {{ + return ( + {_RenderUtils.render(document)} + ) +}}""" + + +def app_root_template( + *, + imports: list[_ImportDict], + custom_codes: set[str], + hooks: dict[str, VarData | None], + window_libraries: list[tuple[str, str]], + render: dict[str, Any], + dynamic_imports: set[str], +): + """Template for the App root. + + Args: + imports: The list of import statements. + custom_codes: The set of custom code snippets. + hooks: The dictionary of hooks. + window_libraries: The list of window libraries. + render: The dictionary of render functions. + dynamic_imports: The set of dynamic imports. - def __init__(self) -> None: - """Set default environment.""" - super().__init__( - trim_blocks=True, - lstrip_blocks=True, - auto_reload=False, + Returns: + Rendered App root component as string. + """ + imports_str = "\n".join([_RenderUtils.get_import(mod) for mod in imports]) + dynamic_imports_str = "\n".join(dynamic_imports) + + custom_code_str = "\n".join(custom_codes) + + import_window_libraries = "\n".join( + [ + f'import * as {lib_alias} from "{lib_path}";' + for lib_alias, lib_path in window_libraries + ] + ) + + window_imports_str = "\n".join( + [f' "{lib_path}": {lib_alias},' for lib_alias, lib_path in window_libraries] + ) + + return f""" +import reflexGlobalStyles from '$/styles/__reflex_global_styles.css?url'; +{imports_str} +{dynamic_imports_str} +import {{ EventLoopProvider, StateProvider, defaultColorMode }} from "$/utils/context"; +import {{ ThemeProvider }} from '$/utils/react-theme'; +import {{ Layout as AppLayout }} from './_document'; +import {{ Outlet }} from 'react-router'; +{import_window_libraries} + +{custom_code_str} + +export const links = () => [ + {{ rel: 'stylesheet', href: reflexGlobalStyles, type: 'text/css' }} +]; + +function AppWrap({{children}}) {{ +{_render_hooks(hooks)} +return ({_RenderUtils.render(render)}) +}} + + +export function Layout({{children}}) {{ + useEffect(() => {{ + // Make contexts and state objects available globally for dynamic eval'd components + let windowImports = {{ + {window_imports_str} + }}; + window["__reflex"] = windowImports; + }}, []); + + return jsx(AppLayout, {{}}, + jsx(ThemeProvider, {{defaultTheme: defaultColorMode, attribute: "class"}}, + jsx(StateProvider, {{}}, + jsx(EventLoopProvider, {{}}, + jsx(AppWrap, {{}}, children) ) - self.filters["json_dumps"] = json_dumps - self.filters["react_setter"] = lambda state: f"set{state.capitalize()}" - self.filters["var_name"] = format_state_name - self.loader = FileSystemLoader(constants.Templates.Dirs.JINJA_TEMPLATE) - self.globals["const"] = { - "socket": constants.CompileVars.SOCKET, - "result": constants.CompileVars.RESULT, - "router": constants.CompileVars.ROUTER, - "event_endpoint": constants.Endpoint.EVENT.name, - "events": constants.CompileVars.EVENTS, - "state": constants.CompileVars.STATE, - "final": constants.CompileVars.FINAL, - "processing": constants.CompileVars.PROCESSING, - "initial_result": { - constants.CompileVars.STATE: None, - constants.CompileVars.EVENTS: [], - constants.CompileVars.FINAL: True, - constants.CompileVars.PROCESSING: False, - }, - "color_mode": constants.ColorMode.NAME, - "resolved_color_mode": constants.ColorMode.RESOLVED_NAME, - "toggle_color_mode": constants.ColorMode.TOGGLE, - "set_color_mode": constants.ColorMode.SET, - "use_color_mode": constants.ColorMode.USE, - "hydrate": constants.CompileVars.HYDRATE, - "on_load_internal": constants.CompileVars.ON_LOAD_INTERNAL, - "update_vars_internal": constants.CompileVars.UPDATE_VARS_INTERNAL, - "frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL, - "hook_position": constants.Hooks.HookPosition, - } - self.globals["sort_hooks"] = _sort_hooks + ) + ) + ); +}} + +export default function App() {{ + return jsx(Outlet, {{}}); +}} +""" -def get_template(name: str) -> Template: - """Get render function that work with a template. + +def theme_template(theme: str): + """Template for the theme file. Args: - name: The template name. "/" is used as the path separator. + theme: The theme to render. Returns: - A render function. + Rendered theme file content as string. """ - return ReflexJinjaEnvironment().get_template(name=name) + return f"""export default {theme}""" -def from_string(source: str) -> Template: - """Get render function that work with a template. +def context_template( + *, + is_dev_mode: bool, + default_color_mode: str, + initial_state: dict[str, Any] | None = None, + state_name: str | None = None, + client_storage: dict[str, dict[str, dict[str, Any]]] | None = None, +): + """Template for the context file. Args: - source: The template source. + initial_state: The initial state for the context. + state_name: The name of the state. + client_storage: The client storage for the context. + is_dev_mode: Whether the app is in development mode. + default_color_mode: The default color mode for the context. Returns: - A render function. + Rendered context file content as string. + """ + initial_state = initial_state or {} + state_contexts_str = "".join( + [ + f"{format_state_name(state_name)}: createContext(null)," + for state_name in initial_state + ] + ) + + state_str = ( + rf""" +export const state_name = "{state_name}" + +export const exception_state_name = "{constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL}" + +// These events are triggered on initial load and each page navigation. +export const onLoadInternalEvent = () => {{ + const internal_events = []; + + // Get tracked cookie and local storage vars to send to the backend. + const client_storage_vars = hydrateClientStorage(clientStorage); + // But only send the vars if any are actually set in the browser. + if (client_storage_vars && Object.keys(client_storage_vars).length !== 0) {{ + internal_events.push( + Event( + '{state_name}.{constants.CompileVars.UPDATE_VARS_INTERNAL}', + {{vars: client_storage_vars}}, + ), + ); + }} + + // `on_load_internal` triggers the correct on_load event(s) for the current page. + // If the page does not define any on_load event, this will just set `is_hydrated = true`. + internal_events.push(Event('{state_name}.{constants.CompileVars.ON_LOAD_INTERNAL}')); + + return internal_events; +}} + +// The following events are sent when the websocket connects or reconnects. +export const initialEvents = () => [ + Event('{state_name}.{constants.CompileVars.HYDRATE}'), + ...onLoadInternalEvent() +] """ - return ReflexJinjaEnvironment().from_string(source=source) + if state_name + else """ +export const state_name = undefined + +export const exception_state_name = undefined + +export const onLoadInternalEvent = () => [] + +export const initialEvents = () => [] +""" + ) + + state_reducer_str = "\n".join( + rf'const [{format_state_name(state_name)}, dispatch_{format_state_name(state_name)}] = useReducer(applyDelta, initialState["{state_name}"])' + for state_name in initial_state + ) + + create_state_contexts_str = "\n".join( + rf"createElement(StateContexts.{format_state_name(state_name)},{{value: {format_state_name(state_name)}}}," + for state_name in initial_state + ) + + dispatchers_str = "\n".join( + f'"{state_name}": dispatch_{format_state_name(state_name)},' + for state_name in initial_state + ) + + return rf"""import {{ createContext, useContext, useMemo, useReducer, useState, createElement, useEffect }} from "react" +import {{ applyDelta, Event, hydrateClientStorage, useEventLoop, refs }} from "$/utils/state" +import {{ jsx }} from "@emotion/react"; + +export const initialState = {"{}" if not initial_state else json_dumps(initial_state)} + +export const defaultColorMode = {default_color_mode} +export const ColorModeContext = createContext(null); +export const UploadFilesContext = createContext(null); +export const DispatchContext = createContext(null); +export const StateContexts = {{{state_contexts_str}}}; +export const EventLoopContext = createContext(null); +export const clientStorage = {"{}" if client_storage is None else json_dumps(client_storage)} + +{state_str} + +export const isDevMode = {json.dumps(is_dev_mode)}; + +export function UploadFilesProvider({{ children }}) {{ + const [filesById, setFilesById] = useState({{}}) + refs["__clear_selected_files"] = (id) => setFilesById(filesById => {{ + const newFilesById = {{...filesById}} + delete newFilesById[id] + return newFilesById + }}) + return createElement( + UploadFilesContext.Provider, + {{ value: [filesById, setFilesById] }}, + children + ); +}} + +export function ClientSide(component) {{ + return ({{ children, ...props }}) => {{ + const [Component, setComponent] = useState(null); + useEffect(() => {{ + setComponent(component); + }}, []); + return Component ? jsx(Component, props, children) : null; + }}; +}} + +export function EventLoopProvider({{ children }}) {{ + const dispatch = useContext(DispatchContext) + const [addEvents, connectErrors] = useEventLoop( + dispatch, + initialEvents, + clientStorage, + ) + return createElement( + EventLoopContext.Provider, + {{ value: [addEvents, connectErrors] }}, + children + ); +}} + +export function StateProvider({{ children }}) {{ + {state_reducer_str} + const dispatchers = useMemo(() => {{ + return {{ + {dispatchers_str} + }} + }}, []) + + return ( + {create_state_contexts_str} + createElement(DispatchContext, {{value: dispatchers}}, children) + {")" * len(initial_state)} + ) +}}""" + + +def component_template(component: Component | StatefulComponent): + """Template to render a component tag. + Args: + component: The component to render. -# Template for the Reflex config file. -RXCONFIG = get_template("app/rxconfig.py.jinja2") + Returns: + Rendered component as string. + """ + return _RenderUtils.render(component.render()) -# Code to render the Document root. -DOCUMENT_ROOT = get_template("web/pages/_document.js.jinja2") -# Code to render App root. -APP_ROOT = get_template("web/pages/_app.js.jinja2") +def page_template( + imports: Iterable[_ImportDict], + dynamic_imports: Iterable[str], + custom_codes: Iterable[str], + hooks: dict[str, VarData | None], + render: dict[str, Any], +): + """Template for a single react page. -# Template for the theme file. -THEME = get_template("web/utils/theme.js.jinja2") + Args: + imports: List of import statements. + dynamic_imports: List of dynamic import statements. + custom_codes: List of custom code snippets. + hooks: Dictionary of hooks. + render: Render function for the component. -# Template for the context file. -CONTEXT = get_template("web/utils/context.js.jinja2") + Returns: + Rendered React page component as string. + """ + imports_str = "\n".join([_RenderUtils.get_import(imp) for imp in imports]) + custom_code_str = "\n".join(custom_codes) + dynamic_imports_str = "\n".join(dynamic_imports) -# Template to render a component tag. -COMPONENT = get_template("web/pages/component.js.jinja2") + hooks_str = _render_hooks(hooks) + return f"""{imports_str} -# Code to render a single react page. -PAGE = get_template("web/pages/index.js.jinja2") +{dynamic_imports_str} -# Code to render the custom components page. -COMPONENTS = get_template("web/pages/custom_component.js.jinja2") +{custom_code_str} -# Code to render Component instances as part of StatefulComponent -STATEFUL_COMPONENT = get_template("web/pages/stateful_component.js.jinja2") +export default function Component() {{ +{hooks_str} -# Code to render StatefulComponent to an external file to be shared -STATEFUL_COMPONENTS = get_template("web/pages/stateful_components.js.jinja2") + return ( + {_RenderUtils.render(render)} + ) +}}""" -# Sitemap config file. -SITEMAP_CONFIG = "module.exports = {config}".format -# Code to render the root stylesheet. -STYLE = get_template("web/styles/styles.css.jinja2") +def package_json_template( + scripts: dict[str, str], + dependencies: dict[str, str], + dev_dependencies: dict[str, str], + overrides: dict[str, str], +): + """Template for package.json. -# Code that generate the package json file -PACKAGE_JSON = get_template("web/package.json.jinja2") + Args: + scripts: The scripts to include in the package.json file. + dependencies: The dependencies to include in the package.json file. + dev_dependencies: The devDependencies to include in the package.json file. + overrides: The overrides to include in the package.json file. -# Code that generate the vite.config.js file -VITE_CONFIG = get_template("web/vite.config.js.jinja2") + Returns: + Rendered package.json content as string. + """ + return json.dumps( + { + "name": "reflex", + "type": "module", + "scripts": scripts, + "dependencies": dependencies, + "devDependencies": dev_dependencies, + "overrides": overrides, + } + ) -# Template containing some macros used in the web pages. -MACROS = get_template("web/pages/macros.js.jinja2") -# Code that generate the pyproject.toml file for custom components. -CUSTOM_COMPONENTS_PYPROJECT_TOML = get_template( - "custom_components/pyproject.toml.jinja2" -) +def vite_config_template(base: str): + """Template for vite.config.js. -# Code that generates the README file for custom components. -CUSTOM_COMPONENTS_README = get_template("custom_components/README.md.jinja2") + Args: + base: The base path for the Vite config. -# Code that generates the source file for custom components. -CUSTOM_COMPONENTS_SOURCE = get_template("custom_components/src.py.jinja2") + Returns: + Rendered vite.config.js content as string. + """ + return rf"""import {{ fileURLToPath, URL }} from "url"; +import {{ reactRouter }} from "@react-router/dev/vite"; +import {{ defineConfig }} from "vite"; +import safariCacheBustPlugin from "./vite-plugin-safari-cachebust"; + +// Ensure that bun always uses the react-dom/server.node functions. +function alwaysUseReactDomServerNode() {{ + return {{ + name: "vite-plugin-always-use-react-dom-server-node", + enforce: "pre", + + resolveId(source, importer) {{ + if ( + typeof importer === "string" && + importer.endsWith("/entry.server.node.tsx") && + source.includes("react-dom/server") + ) {{ + return this.resolve("react-dom/server.node", importer, {{ + skipSelf: true, + }}); + }} + return null; + }}, + }}; +}} + +export default defineConfig((config) => ({{ + plugins: [ + alwaysUseReactDomServerNode(), + reactRouter(), + safariCacheBustPlugin(), + ], + build: {{ + assetsDir: "{base}assets".slice(1), + rollupOptions: {{ + jsx: {{}}, + output: {{ + advancedChunks: {{ + groups: [ + {{ + test: /env.json/, + name: "reflex-env", + }}, + ], + }}, + }}, + }}, + }}, + experimental: {{ + enableNativePlugin: false, + }}, + server: {{ + port: process.env.PORT, + watch: {{ + ignored: [ + "**/.web/backend/**", + "**/.web/reflex.install_frontend_packages.cached", + ], + }}, + }}, + resolve: {{ + mainFields: ["browser", "module", "jsnext"], + alias: [ + {{ + find: "$", + replacement: fileURLToPath(new URL("./", import.meta.url)), + }}, + {{ + find: "@", + replacement: fileURLToPath(new URL("./public", import.meta.url)), + }}, + ], + }}, +}}));""" + + +def stateful_component_template( + tag_name: str, memo_trigger_hooks: list[str], component: Component, export: bool +): + """Template for stateful component. -# Code that generates the init file for custom components. -CUSTOM_COMPONENTS_INIT_FILE = get_template("custom_components/__init__.py.jinja2") + Args: + tag_name: The tag name for the component. + memo_trigger_hooks: The memo trigger hooks for the component. + component: The component to render. + export: Whether to export the component. -# Code that generates the demo app main py file for testing custom components. -CUSTOM_COMPONENTS_DEMO_APP = get_template("custom_components/demo_app.py.jinja2") + Returns: + Rendered stateful component code as string. + """ + all_hooks = component._get_all_hooks() + return f""" +{"export " if export else ""}function {tag_name} () {{ + {_render_hooks(all_hooks, memo_trigger_hooks)} + return ( + {_RenderUtils.render(component.render())} + ) +}} +""" + + +def stateful_components_template(imports: list[_ImportDict], memoized_code: str) -> str: + """Template for stateful components. + + Args: + imports: List of import statements. + memoized_code: Memoized code for stateful components. + + Returns: + Rendered stateful components code as string. + """ + imports_str = "\n".join([_RenderUtils.get_import(imp) for imp in imports]) + return f"{imports_str}\n{memoized_code}" + + +def custom_component_template( + imports: list[_ImportDict], + components: list[dict[str, Any]], + dynamic_imports: Iterable[str], + custom_codes: Iterable[str], +) -> str: + """Template for custom component. + + Args: + imports: List of import statements. + components: List of component definitions. + dynamic_imports: List of dynamic import statements. + custom_codes: List of custom code snippets. + + Returns: + Rendered custom component code as string. + """ + imports_str = "\n".join([_RenderUtils.get_import(imp) for imp in imports]) + dynamic_imports_str = "\n".join(dynamic_imports) + custom_code_str = "\n".join(custom_codes) + + components_code = "" + for component in components: + components_code += f""" +export const {component["name"]} = memo(({{ {", ".join(component.get("props", []))} }}) => {{ + {_render_hooks(component.get("hooks", {}))} + return( + {_RenderUtils.render(component["render"])} + ) +}}); +""" + + return f""" +{imports_str} + +{dynamic_imports_str} + +{custom_code_str} + +{components_code}""" + + +def styles_template(stylesheets: list[str]) -> str: + """Template for styles.css. + + Args: + stylesheets: List of stylesheets to include. + + Returns: + Rendered styles.css content as string. + """ + return "@layer __reflex_base;\n" + "\n".join( + [f"@import url('{sheet_name}');" for sheet_name in stylesheets] + ) + + +def _render_hooks(hooks: dict[str, VarData | None], memo: list | None = None) -> str: + """Render hooks for macros. + + Args: + hooks: Dictionary of hooks to render. + memo: Optional list of memo hooks. + + Returns: + Rendered hooks code as string. + """ + internal, pre_trigger, post_trigger = _sort_hooks(hooks) + internal_str = "\n".join(internal) + pre_trigger_str = "\n".join(pre_trigger) + post_trigger_str = "\n".join(post_trigger) + memo_str = "\n".join(memo) if memo is not None else "" + return f"{internal_str}\n{pre_trigger_str}\n{memo_str}\n{post_trigger_str}" diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 5392353d7e4..29b43f54a9e 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -8,7 +8,7 @@ from collections.abc import Mapping, Sequence from datetime import datetime from pathlib import Path -from typing import Any +from typing import Any, TypedDict from urllib.parse import urlparse from reflex import constants @@ -90,7 +90,13 @@ def validate_imports(import_dict: ParsedImportDict): used_tags[import_name] = lib -def compile_imports(import_dict: ParsedImportDict) -> list[dict]: +class _ImportDict(TypedDict): + lib: str + default: str + rest: list[str] + + +def compile_imports(import_dict: ParsedImportDict) -> list[_ImportDict]: """Compile an import dict. Args: @@ -104,7 +110,7 @@ def compile_imports(import_dict: ParsedImportDict) -> list[dict]: """ collapsed_import_dict: ParsedImportDict = imports.collapse_imports(import_dict) validate_imports(collapsed_import_dict) - import_dicts = [] + import_dicts: list[_ImportDict] = [] for lib, fields in collapsed_import_dict.items(): # prevent lib from being rendered on the page if all imports are non rendered kind if not any(f.render for f in fields): @@ -139,7 +145,9 @@ def compile_imports(import_dict: ParsedImportDict) -> list[dict]: return import_dicts -def get_import_dict(lib: str, default: str = "", rest: list[str] | None = None) -> dict: +def get_import_dict( + lib: str, default: str = "", rest: list[str] | None = None +) -> _ImportDict: """Get dictionary for import template. Args: @@ -150,11 +158,11 @@ def get_import_dict(lib: str, default: str = "", rest: list[str] | None = None) Returns: A dictionary for import template. """ - return { - "lib": lib, - "default": default, - "rest": rest if rest else [], - } + return _ImportDict( + lib=lib, + default=default, + rest=rest if rest else [], + ) def save_error(error: Exception) -> str: @@ -237,23 +245,20 @@ def _compile_client_storage_field( def _compile_client_storage_recursive( state: type[BaseState], -) -> tuple[dict[str, dict], dict[str, dict], dict[str, dict]]: +) -> tuple[ + dict[str, dict[str, Any]], dict[str, dict[str, Any]], dict[str, dict[str, Any]] +]: """Compile the client-side storage for the given state recursively. Args: state: The app state object. Returns: - A tuple of the compiled client-side storage info: - ( - cookies: dict[str, dict], - local_storage: dict[str, dict[str, str]] - session_storage: dict[str, dict[str, str]] - ). + A tuple of the compiled client-side storage info: (cookies, local_storage, session_storage). """ - cookies = {} - local_storage = {} - session_storage = {} + cookies: dict[str, dict[str, Any]] = {} + local_storage: dict[str, dict[str, Any]] = {} + session_storage: dict[str, dict[str, Any]] = {} state_name = state.get_full_name() for name, field in state.__fields__.items(): if name in state.inherited_vars: @@ -261,6 +266,8 @@ def _compile_client_storage_recursive( continue state_key = f"{state_name}.{name}" + FIELD_MARKER field_type, options = _compile_client_storage_field(field) + if field_type is None or options is None: + continue if field_type is Cookie: cookies[state_key] = options elif field_type is LocalStorage: @@ -281,7 +288,9 @@ def _compile_client_storage_recursive( return cookies, local_storage, session_storage -def compile_client_storage(state: type[BaseState]) -> dict[str, dict]: +def compile_client_storage( + state: type[BaseState], +) -> dict[str, dict[str, dict[str, Any]]]: """Compile the client-side storage for the given state. Args: diff --git a/reflex/components/base/bare.py b/reflex/components/base/bare.py index 3ebffe55709..0bf53857a7d 100644 --- a/reflex/components/base/bare.py +++ b/reflex/components/base/bare.py @@ -188,6 +188,23 @@ def _render(self) -> Tag: return Tagless(contents=f"{contents.to_string()!s}") return Tagless(contents=f"{contents!s}") + def render(self) -> dict: + """Render the component as a dictionary. + + This is overridden to provide a short performant path for rendering. + + Returns: + The rendered component. + """ + contents = ( + Var.create(self.contents) + if not isinstance(self.contents, Var) + else self.contents + ) + if isinstance(contents, (BooleanVar, ObjectVar)): + return {"contents": f"{contents.to_string()!s}"} + return {"contents": f"{contents!s}"} + def _add_style_recursive( self, style: ComponentStyle, theme: Component | None = None ) -> Component: diff --git a/reflex/components/component.py b/reflex/components/component.py index b6bbd28c1e9..ecee612ec52 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -22,7 +22,7 @@ import reflex.state from reflex import constants -from reflex.compiler.templates import STATEFUL_COMPONENT +from reflex.compiler.templates import stateful_component_template from reflex.components.core.breakpoints import Breakpoints from reflex.components.dynamic import load_dynamic_serializer from reflex.components.field import BaseField, FieldBasedMeta @@ -51,7 +51,7 @@ ) from reflex.style import Style, format_as_emotion from reflex.utils import console, format, imports, types -from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports +from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict from reflex.vars import VarData from reflex.vars.base import ( CachedVarOperation, @@ -1270,7 +1270,6 @@ def render(self) -> dict: rendered_dict = dict( tag.set( children=[child.render() for child in self.children], - contents=str(tag.contents), ) ) self._replace_prop_names(rendered_dict) @@ -1498,7 +1497,7 @@ def _iter_parent_classes_names(cls) -> Iterator[str]: yield clz.__name__ @classmethod - def _iter_parent_classes_with_method(cls, method: str) -> Iterator[type[Component]]: + def _iter_parent_classes_with_method(cls, method: str) -> Sequence[type[Component]]: """Iterate through parent classes that define a given method. Used for handling the `add_*` API functions that internally simulate a super() call chain. @@ -1506,12 +1505,13 @@ def _iter_parent_classes_with_method(cls, method: str) -> Iterator[type[Componen Args: method: The method to look for. - Yields: - The parent classes that define the method (differently than the base). + Returns: + A sequence of parent classes that define the method (differently than the base). """ seen_methods = ( {getattr(Component, method)} if hasattr(Component, method) else set() ) + clzs: list[type[Component]] = [] for clz in cls.mro(): if clz is Component: break @@ -1521,7 +1521,8 @@ def _iter_parent_classes_with_method(cls, method: str) -> Iterator[type[Componen if not callable(method_func) or method_func in seen_methods: continue seen_methods.add(method_func) - yield clz + clzs.append(clz) + return clzs def _get_custom_code(self) -> str | None: """Get custom code for the component. @@ -1644,18 +1645,18 @@ def _get_imports(self) -> ParsedImportDict: Returns: The imports needed by the component. """ - _imports = {} - - # Import this component's tag from the main library. - if self.library is not None and self.tag is not None: - _imports[self.library] = self.import_var + _imports = ( + {self.library: [self.import_var]} + if self.library is not None and self.tag is not None + else {} + ) # Get static imports required for event processing. event_imports = Imports.EVENTS if self.event_triggers else {} # Collect imports from Vars used directly by this component. var_imports = [ - var_data.imports + dict(var_data.imports) for var in self._get_vars() if (var_data := var._get_all_var_data()) is not None ] @@ -1665,16 +1666,16 @@ def _get_imports(self) -> ParsedImportDict: list_of_import_dict = clz.add_imports(self) if not isinstance(list_of_import_dict, list): - list_of_import_dict = [list_of_import_dict] - - added_import_dicts.extend( - [parse_imports(import_dict) for import_dict in list_of_import_dict] - ) + added_import_dicts.append(imports.parse_imports(list_of_import_dict)) + else: + added_import_dicts.extend( + [imports.parse_imports(item) for item in list_of_import_dict] + ) - return imports.merge_imports( + return imports.merge_parsed_imports( self._get_dependencies_imports(), self._get_hooks_imports(), - {**_imports}, + _imports, event_imports, *var_imports, *added_import_dicts, @@ -1689,7 +1690,7 @@ def _get_all_imports(self, collapse: bool = False) -> ParsedImportDict: Returns: The import dict with the required imports. """ - _imports = imports.merge_imports( + _imports = imports.merge_parsed_imports( self._get_imports(), *[child._get_all_imports() for child in self.children] ) return imports.collapse_imports(_imports) if collapse else _imports @@ -2470,7 +2471,7 @@ def _render_stateful_code( if not self.tag: return "" # Render the code for this component and hooks. - return STATEFUL_COMPONENT.render( + return stateful_component_template( tag_name=self.tag, memo_trigger_hooks=self.memo_trigger_hooks, component=self.component, @@ -2796,6 +2797,9 @@ def render_dict_to_var(tag: dict | Component | str) -> Var: return render_dict_to_var(tag.render()) return Var.create(tag) + if "contents" in tag: + return Var(tag["contents"]) + if "iterable" in tag: function_return = LiteralArrayVar.create( [render_dict_to_var(child.render()) for child in tag["children"]] @@ -2813,27 +2817,30 @@ def render_dict_to_var(tag: dict | Component | str) -> Var: func, ) - if tag["name"] == "match": - element = tag["cond"] + if "match_cases" in tag: + element = Var(tag["cond"]) conditionals = render_dict_to_var(tag["default"]) for case in tag["match_cases"][::-1]: - condition = case[0].to_string() == element.to_string() - for pattern in case[1:-1]: - condition = condition | (pattern.to_string() == element.to_string()) + conditions, return_value = case + condition = Var.create(False) + for pattern in conditions: + condition = condition | ( + Var(pattern).to_string() == element.to_string() + ) conditionals = ternary_operation( condition, - render_dict_to_var(case[-1]), + render_dict_to_var(return_value), conditionals, ) return conditionals - if "cond" in tag: + if "cond_state" in tag: return ternary_operation( - tag["cond"], + Var(tag["cond_state"]), render_dict_to_var(tag["true_value"]), render_dict_to_var(tag["false_value"]) if tag["false_value"] is not None @@ -2842,8 +2849,6 @@ def render_dict_to_var(tag: dict | Component | str) -> Var: props = Var("({" + ",".join(tag["props"]) + "})") - contents = tag["contents"] if tag["contents"] else None - raw_tag_name = tag.get("name") tag_name = Var(raw_tag_name or "Fragment") @@ -2852,7 +2857,6 @@ def render_dict_to_var(tag: dict | Component | str) -> Var: ).call( tag_name, props, - *([Var(contents)] if contents is not None else []), *[render_dict_to_var(child) for child in tag["children"]], ) diff --git a/reflex/components/core/cond.py b/reflex/components/core/cond.py index b2d7f226f01..e5a85b00622 100644 --- a/reflex/components/core/cond.py +++ b/reflex/components/core/cond.py @@ -61,7 +61,7 @@ def create( def _render(self) -> Tag: return CondTag( - cond=self.cond, + cond_state=str(self.cond), true_value=self.children[0].render(), false_value=self.children[1].render(), ) @@ -72,17 +72,11 @@ def render(self) -> dict: 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, - ), - cond_state=str(self.cond), - ) + return { + "cond_state": str(self.cond), + "true_value": self.children[0].render(), + "false_value": self.children[1].render(), + } def add_imports(self) -> ImportDict: """Add imports for the Cond component. diff --git a/reflex/components/core/foreach.py b/reflex/components/core/foreach.py index a6bea168fc0..3ec03473d77 100644 --- a/reflex/components/core/foreach.py +++ b/reflex/components/core/foreach.py @@ -181,7 +181,7 @@ def render(self): tag, iterable_state=str(tag.iterable), arg_name=tag.arg_var_name, - arg_index=tag.get_index_var_arg(), + arg_index=tag.index_var_name, ) diff --git a/reflex/components/core/match.py b/reflex/components/core/match.py index aab748ddb7a..00523cc00bb 100644 --- a/reflex/components/core/match.py +++ b/reflex/components/core/match.py @@ -1,11 +1,12 @@ """rx.match.""" import textwrap -from typing import Any +from typing import Any, cast from reflex.components.base import Fragment from reflex.components.component import BaseComponent, Component, MemoizationLeaf, field -from reflex.components.tags import MatchTag, Tag +from reflex.components.tags import Tag +from reflex.components.tags.match_tag import MatchTag from reflex.style import Style from reflex.utils import format from reflex.utils.exceptions import MatchTypeError @@ -21,10 +22,14 @@ class Match(MemoizationLeaf): cond: Var[Any] # The list of match cases to be matched. - match_cases: list[Any] = field(default_factory=list, is_javascript_property=False) + match_cases: list[tuple[list[Var], BaseComponent]] = field( + default_factory=list, is_javascript_property=False + ) # The catchall case to match. - default: Any = field(default=None, is_javascript_property=False) + default: BaseComponent = field( + default_factory=Fragment.create, is_javascript_property=False + ) @classmethod def create(cls, cond: Any, *cases) -> Component | Var: @@ -44,9 +49,9 @@ def create(cls, cond: Any, *cases) -> Component | Var: cases, default = cls._process_cases(list(cases)) match_cases = cls._process_match_cases(cases) - cls._validate_return_types(match_cases) + match_cases = cls._validate_return_types(match_cases) - if default is None and isinstance(match_cases[0][-1], Var): + if default is None and isinstance(match_cases[0][1], Var): msg = "For cases with return types as Vars, a default case must be provided" raise ValueError(msg) @@ -75,7 +80,9 @@ def _create_condition_var(cls, cond: Any) -> Var: return match_cond_var @classmethod - def _process_cases(cls, cases: list) -> tuple[list, Var | BaseComponent | None]: + def _process_cases( + cls, cases: list + ) -> tuple[list[tuple], Var | BaseComponent | None]: """Process the list of match cases and the catchall default case. Args: @@ -87,26 +94,29 @@ def _process_cases(cls, cases: list) -> tuple[list, Var | BaseComponent | None]: Raises: ValueError: If there are multiple default cases. """ - default = None - - if len([case for case in cases if not isinstance(case, tuple)]) > 1: - msg = "rx.match can only have one default case." - raise ValueError(msg) - if not cases: msg = "rx.match should have at least one case." raise ValueError(msg) - # Get the default case which should be the last non-tuple arg if not isinstance(cases[-1], tuple): - default = cases.pop() - default = ( - cls._create_case_var_with_var_data(default) - if not isinstance(default, BaseComponent) - else default + *cases, default_return = cases + default_return = ( + cls._create_case_var_with_var_data(default_return) + if not isinstance(default_return, BaseComponent) + else default_return ) + else: + default_return = None - return cases, default + if any(case for case in cases if not isinstance(case, tuple)): + msg = "rx.match should have tuples of cases and one default case as the last argument." + raise ValueError(msg) + + if not cases: + msg = "rx.match should have at least one case." + raise ValueError(msg) + + return cases, default_return @classmethod def _create_case_var_with_var_data(cls, case_element: Any) -> Var: @@ -124,7 +134,9 @@ def _create_case_var_with_var_data(cls, case_element: Any) -> Var: return LiteralVar.create(case_element, _var_data=_var_data) @classmethod - def _process_match_cases(cls, cases: list) -> list[list[Var]]: + def _process_match_cases( + cls, cases: list[tuple] + ) -> list[tuple[list[Var], BaseComponent | Var]]: """Process the individual match cases. Args: @@ -136,40 +148,48 @@ def _process_match_cases(cls, cases: list) -> list[list[Var]]: Raises: ValueError: If the default case is not the last case or the tuple elements are less than 2. """ - match_cases = [] - for case in cases: - if not isinstance(case, tuple): - msg = "rx.match should have tuples of cases and a default case as the last argument." - raise ValueError(msg) + match_cases: list[tuple[list[Var], BaseComponent | Var]] = [] + for case_index, case in enumerate(cases): # There should be at least two elements in a case tuple(a condition and return value) if len(case) < 2: msg = "A case tuple should have at least a match case element and a return value." raise ValueError(msg) - case_list = [] - for element in case: - # convert all non component element to vars. - el = ( - cls._create_case_var_with_var_data(element) - if not isinstance(element, BaseComponent) - else element - ) - if not isinstance(el, (Var, BaseComponent)): - msg = "Case element must be a var or component" + *conditions, return_value = case + + conditions_vars: list[Var] = [] + for condition_index, condition in enumerate(conditions): + if isinstance(condition, BaseComponent): + msg = f"Match condition {condition_index} of case {case_index} cannot be a component." raise ValueError(msg) - case_list.append(el) + conditions_vars.append(cls._create_case_var_with_var_data(condition)) + + return_value = ( + cls._create_case_var_with_var_data(return_value) + if not isinstance(return_value, BaseComponent) + else return_value + ) + + if not isinstance(return_value, (Var, BaseComponent)): + msg = "Return value must be a var or component" + raise ValueError(msg) - match_cases.append(case_list) + match_cases.append((conditions_vars, return_value)) return match_cases @classmethod - def _validate_return_types(cls, match_cases: list[list[Var]]) -> None: + def _validate_return_types( + cls, match_cases: list[tuple[list[Var], BaseComponent | Var]] + ) -> list[tuple[list[Var], Var]] | list[tuple[list[Var], BaseComponent]]: """Validate that match cases have the same return types. Args: match_cases: The match cases. + Returns: + The validated match cases. + Raises: MatchTypeError: If the return types of cases are different. """ @@ -181,20 +201,25 @@ def _validate_return_types(cls, match_cases: list[list[Var]]) -> None: elif isinstance(first_case_return, Var): return_type = Var + cases = [] for index, case in enumerate(match_cases): - if not isinstance(case[-1], return_type): + conditions, return_value = case + if not isinstance(return_value, return_type): msg = ( f"Match cases should have the same return types. Case {index} with return " - f"value `{case[-1]._js_expr if isinstance(case[-1], Var) else textwrap.shorten(str(case[-1]), width=250)}`" - f" of type {type(case[-1])!r} is not {return_type}" + f"value `{return_value._js_expr if isinstance(return_value, Var) else textwrap.shorten(str(return_value), width=250)}`" + f" of type {type(return_value)!r} is not {return_type}" ) raise MatchTypeError(msg) + cases.append((conditions, return_value)) + return cases @classmethod def _create_match_cond_var_or_component( cls, match_cond_var: Var, - match_cases: list[list[Var]], + match_cases: list[tuple[list[Var], BaseComponent]] + | list[tuple[list[Var], Var]], default: Var | BaseComponent | None, ) -> Component | Var: """Create and return the match condition var or component. @@ -206,29 +231,22 @@ def _create_match_cond_var_or_component( Returns: The match component wrapped in a fragment or the match var. - - Raises: - ValueError: If the return types are not vars when creating a match var for Var types. """ - if default is None and isinstance(match_cases[0][-1], BaseComponent): - default = Fragment.create() + if isinstance(match_cases[0][1], BaseComponent): + if default is None: + default = Fragment.create() - if isinstance(match_cases[0][-1], BaseComponent): return Fragment.create( cls._create( cond=match_cond_var, match_cases=match_cases, default=default, - children=[case[-1] for case in match_cases] + [default], # pyright: ignore [reportArgumentType] + children=[case[1] for case in match_cases] + [default], # pyright: ignore [reportArgumentType] ) ) - # Validate the match cases (as well as the default case) to have Var return types. - if any( - case for case in match_cases if not isinstance(case[-1], Var) - ) or not isinstance(default, Var): - msg = "Return types of match cases should be Vars." - raise ValueError(msg) + match_cases = cast("list[tuple[list[Var], Var]]", match_cases) + default = cast("Var", default) return Var( _js_expr=format.format_match( @@ -239,14 +257,20 @@ def _create_match_cond_var_or_component( _var_type=default._var_type, _var_data=VarData.merge( match_cond_var._get_all_var_data(), - *[el._get_all_var_data() for case in match_cases for el in case], + *[el._get_all_var_data() for case in match_cases for el in case[0]], + *[case[1]._get_all_var_data() for case in match_cases], default._get_all_var_data(), ), ) def _render(self) -> Tag: return MatchTag( - cond=self.cond, match_cases=self.match_cases, default=self.default + cond=str(self.cond), + match_cases=[ + ([str(cond) for cond in conditions], return_value.render()) + for conditions, return_value in self.match_cases + ], + default=self.default.render(), ) def render(self) -> dict: @@ -255,8 +279,7 @@ def render(self) -> dict: Returns: The dictionary for template of component. """ - tag = self._render() - return dict(tag.set(name="match")) + return dict(self._render()) def add_imports(self) -> ImportDict: """Add imports for the Match component. diff --git a/reflex/components/dynamic.py b/reflex/components/dynamic.py index da090748f40..6ad9d0fe7de 100644 --- a/reflex/components/dynamic.py +++ b/reflex/components/dynamic.py @@ -86,7 +86,7 @@ def make_component(component: Component) -> str: ) rendered_components[ - templates.STATEFUL_COMPONENT.render( + templates.stateful_component_template( tag_name="MySSRComponent", memo_trigger_hooks=[], component=component, @@ -111,10 +111,10 @@ def make_component(component: Component) -> str: else: imports[lib] = names - module_code_lines = templates.STATEFUL_COMPONENTS.render( + module_code_lines = templates.stateful_components_template( imports=utils.compile_imports(imports), memoized_code="\n".join(rendered_components), - ).splitlines()[1:] + ).splitlines() # Rewrite imports from `/` to destructure from window for ix, line in enumerate(module_code_lines[:]): diff --git a/reflex/components/el/elements/forms.py b/reflex/components/el/elements/forms.py index c9a7c6ac298..a96e0670010 100644 --- a/reflex/components/el/elements/forms.py +++ b/reflex/components/el/elements/forms.py @@ -6,8 +6,6 @@ from hashlib import md5 from typing import Any, ClassVar, Literal -from jinja2 import Environment - from reflex.components.el.element import Element from reflex.components.tags.tag import Tag from reflex.constants import Dirs, EventTriggers @@ -31,21 +29,40 @@ from .base import BaseHTML -HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string( + +def _handle_submit_js_template( + handle_submit_unique_name: str, + form_data: str, + field_ref_mapping: str, + on_submit_event_chain: str, + reset_on_submit: str, +) -> str: + """Generate handle submit JS using f-string formatting. + + Args: + handle_submit_unique_name: Unique name for the handle submit function. + form_data: Name of the form data variable. + field_ref_mapping: JSON string of field reference mappings. + on_submit_event_chain: Event chain for the submit handler. + reset_on_submit: Boolean string indicating if form should reset after submit. + + Returns: + JavaScript code for the form submit handler. """ - const handleSubmit_{{ handle_submit_unique_name }} = useCallback((ev) => { + return f""" + const handleSubmit_{handle_submit_unique_name} = useCallback((ev) => {{ const $form = ev.target ev.preventDefault() - const {{ form_data }} = {...Object.fromEntries(new FormData($form).entries()), ...{{ field_ref_mapping }}}; + const {form_data} = {{...Object.fromEntries(new FormData($form).entries()), ...{field_ref_mapping}}}; - ({{ on_submit_event_chain }}(ev)); + ({on_submit_event_chain}(ev)); - if ({{ reset_on_submit }}) { + if ({reset_on_submit}) {{ $form.reset() - } - }) + }} + }}) """ -) + ButtonType = Literal["submit", "reset", "button"] @@ -198,14 +215,14 @@ def add_hooks(self) -> list[str]: if EventTriggers.ON_SUBMIT not in self.event_triggers: return [] return [ - HANDLE_SUBMIT_JS_JINJA2.render( - handle_submit_unique_name=self.handle_submit_unique_name, - form_data=FORM_DATA, + _handle_submit_js_template( + handle_submit_unique_name=str(self.handle_submit_unique_name), + form_data=str(FORM_DATA), field_ref_mapping=str(LiteralVar.create(self._get_form_refs())), on_submit_event_chain=str( LiteralVar.create(self.event_triggers[EventTriggers.ON_SUBMIT]) ), - reset_on_submit=self.reset_on_submit, + reset_on_submit=str(self.reset_on_submit).lower(), ) ] diff --git a/reflex/components/markdown/markdown.py b/reflex/components/markdown/markdown.py index 25acf5e7ce2..0b7ac34ddfb 100644 --- a/reflex/components/markdown/markdown.py +++ b/reflex/components/markdown/markdown.py @@ -422,12 +422,12 @@ def _get_component_map_name(self) -> str: def _get_custom_code(self) -> str | None: hooks = {} - from reflex.compiler.templates import MACROS + from reflex.compiler.templates import _render_hooks for _component in self.component_map.values(): comp = _component(_MOCK_ARG) hooks.update(comp._get_all_hooks()) - formatted_hooks = MACROS.module.renderHooks(hooks) # pyright: ignore [reportAttributeAccessIssue] + formatted_hooks = _render_hooks(hooks) return f""" function {self._get_component_map_name()} () {{ {formatted_hooks} diff --git a/reflex/components/tags/cond_tag.py b/reflex/components/tags/cond_tag.py index 7120b4852d6..297f1c0c461 100644 --- a/reflex/components/tags/cond_tag.py +++ b/reflex/components/tags/cond_tag.py @@ -1,22 +1,31 @@ """Tag to conditionally render components.""" import dataclasses -from collections.abc import Mapping +from collections.abc import Iterator, Mapping from typing import Any from reflex.components.tags.tag import Tag -from reflex.vars.base import Var -@dataclasses.dataclass(frozen=True) +@dataclasses.dataclass(frozen=True, kw_only=True) class CondTag(Tag): """A conditional tag.""" # The condition to determine which component to render. - cond: Var[Any] = dataclasses.field(default_factory=lambda: Var.create(True)) + cond_state: str # The code to render if the condition is true. - true_value: Mapping = dataclasses.field(default_factory=dict) + true_value: Mapping # The code to render if the condition is false. false_value: Mapping | None = None + + def __iter__(self) -> Iterator[tuple[str, Any]]: + """Iterate over the tag's attributes. + + Yields: + An iterator over the tag's attributes. + """ + yield ("cond_state", self.cond_state) + yield ("true_value", self.true_value) + yield ("false_value", self.false_value) diff --git a/reflex/components/tags/iter_tag.py b/reflex/components/tags/iter_tag.py index 46bcce5545d..970cd606fce 100644 --- a/reflex/components/tags/iter_tag.py +++ b/reflex/components/tags/iter_tag.py @@ -68,32 +68,6 @@ def get_arg_var(self) -> Var: _var_type=self.get_iterable_var_type(), ).guess_type() - def get_index_var_arg(self) -> Var: - """Get the index var for the tag (without curly braces). - - This is used to render the index var in the .map() function. - - Returns: - The index var. - """ - return Var( - _js_expr=self.index_var_name, - _var_type=int, - ).guess_type() - - def get_arg_var_arg(self) -> Var: - """Get the arg var for the tag (without curly braces). - - This is used to render the arg var in the .map() function. - - Returns: - The arg var. - """ - return Var( - _js_expr=self.arg_var_name, - _var_type=self.get_iterable_var_type(), - ).guess_type() - def render_component(self) -> Component: """Render the component. diff --git a/reflex/components/tags/match_tag.py b/reflex/components/tags/match_tag.py index 3b986b78443..723654fd487 100644 --- a/reflex/components/tags/match_tag.py +++ b/reflex/components/tags/match_tag.py @@ -1,22 +1,31 @@ """Tag to conditionally match cases.""" import dataclasses -from collections.abc import Sequence +from collections.abc import Iterator, Mapping, Sequence from typing import Any from reflex.components.tags.tag import Tag -from reflex.vars.base import Var -@dataclasses.dataclass(frozen=True) +@dataclasses.dataclass(frozen=True, kw_only=True) class MatchTag(Tag): """A match tag.""" # The condition to determine which case to match. - cond: Var[Any] = dataclasses.field(default_factory=lambda: Var.create(True)) + cond: str # The list of match cases to be matched. - match_cases: Sequence[Any] = dataclasses.field(default_factory=list) + match_cases: Sequence[tuple[Sequence[str], Mapping]] # The catchall case to match. - default: Any = dataclasses.field(default=Var.create(None)) + default: Any + + def __iter__(self) -> Iterator[tuple[str, Any]]: + """Iterate over the tag's attributes. + + Yields: + An iterator over the tag's attributes. + """ + yield ("cond", self.cond) + yield ("match_cases", self.match_cases) + yield ("default", self.default) diff --git a/reflex/components/tags/tag.py b/reflex/components/tags/tag.py index da0464aa9e0..f73de5860cf 100644 --- a/reflex/components/tags/tag.py +++ b/reflex/components/tags/tag.py @@ -3,7 +3,7 @@ from __future__ import annotations import dataclasses -from collections.abc import Mapping, Sequence +from collections.abc import Iterator, Mapping, Sequence from typing import Any from reflex.event import EventChain @@ -41,16 +41,13 @@ class Tag: # The props of the tag. props: Mapping[str, Any] = dataclasses.field(default_factory=dict) - # The inner contents of the tag. - contents: str = "" - # Special props that aren't key value pairs. special_props: Sequence[Var] = dataclasses.field(default_factory=list) # The children components. children: Sequence[Any] = dataclasses.field(default_factory=list) - def format_props(self) -> list: + def format_props(self) -> list[str]: """Format the tag's props. Returns: @@ -69,7 +66,7 @@ def set(self, **kwargs: Any): """ return dataclasses.replace(self, **kwargs) - def __iter__(self): + def __iter__(self) -> Iterator[tuple[str, Any]]: """Iterate over the tag's fields. Yields: diff --git a/reflex/components/tags/tagless.py b/reflex/components/tags/tagless.py index 1288e61cbe0..3d8db827d88 100644 --- a/reflex/components/tags/tagless.py +++ b/reflex/components/tags/tagless.py @@ -1,12 +1,18 @@ """A tag with no tag.""" +import dataclasses + from reflex.components.tags import Tag from reflex.utils import format +@dataclasses.dataclass(frozen=True, kw_only=True) class Tagless(Tag): """A tag with no tag.""" + # The inner contents of the tag. + contents: str + def __str__(self) -> str: """Return the string representation of the tag. @@ -20,3 +26,11 @@ def __str__(self) -> str: if len(self.contents) > 0 and self.contents[-1] == " ": out = out + space return out + + def __iter__(self): + """Iterate over the tag's fields. + + Yields: + tuple[str, Any]: The field name and value. + """ + yield "contents", self.contents diff --git a/reflex/constants/base.py b/reflex/constants/base.py index c6359f1c1b2..e33eb30352f 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -141,8 +141,6 @@ class Dirs(SimpleNamespace): BASE = Reflex.ROOT_DIR / Reflex.MODULE_NAME / ".templates" # The web subdirectory of the template directory. WEB_TEMPLATE = BASE / "web" - # The jinja template directory. - JINJA_TEMPLATE = BASE / "jinja" # Where the code for the templates is stored. CODE = "code" diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index 17e14db15cb..c6672bd7c0e 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -18,6 +18,202 @@ from reflex.utils import console, frontend_skeleton +def _pyproject_toml_template( + package_name: str, module_name: str, reflex_version: str +) -> str: + """Template for custom components pyproject.toml. + + Args: + package_name: The name of the package. + module_name: The name of the module. + reflex_version: The version of Reflex. + + Returns: + Rendered pyproject.toml content as string. + """ + return f"""[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "{package_name}" +version = "0.0.1" +description = "Reflex custom component {module_name}" +readme = "README.md" +license = {{ text = "Apache-2.0" }} +requires-python = ">=3.10" +authors = [{{ name = "", email = "YOUREMAIL@domain.com" }}] +keywords = ["reflex","reflex-custom-components"] + +dependencies = ["reflex>={reflex_version}"] + +classifiers = ["Development Status :: 4 - Beta"] + +[project.urls] + +[project.optional-dependencies] +dev = ["build", "twine"] + +[tool.setuptools.packages.find] +where = ["custom_components"] +""" + + +def _readme_template(module_name: str, package_name: str) -> str: + """Template for custom components README. + + Args: + module_name: The name of the module. + package_name: The name of the package. + + Returns: + Rendered README.md content as string. + """ + return f"""# {module_name} + +A Reflex custom component {module_name}. + +## Installation + +```bash +pip install {package_name} +``` +""" + + +def _source_template(component_class_name: str, module_name: str) -> str: + """Template for custom components source. + + Args: + component_class_name: The name of the component class. + module_name: The name of the module. + + Returns: + Rendered custom component source code as string. + """ + return rf''' +"""Reflex custom component {component_class_name}.""" + +# For wrapping react guide, visit https://reflex.dev/docs/wrapping-react/overview/ + +import reflex as rx + +# Some libraries you want to wrap may require dynamic imports. +# This is because they they may not be compatible with Server-Side Rendering (SSR). +# To handle this in Reflex, all you need to do is subclass `NoSSRComponent` instead. +# For example: +# from reflex.components.component import NoSSRComponent +# class {component_class_name}(NoSSRComponent): +# pass + + +class {component_class_name}(rx.Component): + """{component_class_name} component.""" + + # The React library to wrap. + library = "Fill-Me" + + # The React component tag. + tag = "Fill-Me" + + # If the tag is the default export from the module, you must set is_default = True. + # This is normally used when components don't have curly braces around them when importing. + # is_default = True + + # If you are wrapping another components with the same tag as a component in your project + # you can use aliases to differentiate between them and avoid naming conflicts. + # alias = "Other{component_class_name}" + + # The props of the React component. + # Note: when Reflex compiles the component to Javascript, + # `snake_case` property names are automatically formatted as `camelCase`. + # The prop names may be defined in `camelCase` as well. + # some_prop: rx.Var[str] = "some default value" + # some_other_prop: rx.Var[int] = 1 + + # By default Reflex will install the library you have specified in the library property. + # However, sometimes you may need to install other libraries to use a component. + # In this case you can use the lib_dependencies property to specify other libraries to install. + # lib_dependencies: list[str] = [] + + # Event triggers declaration if any. + # Below is equivalent to merging `{{ "on_change": lambda e: [e] }}` + # onto the default event triggers of parent/base Component. + # The function defined for the `on_change` trigger maps event for the javascript + # trigger to what will be passed to the backend event handler function. + # on_change: rx.EventHandler[lambda e: [e]] + + # To add custom code to your component + # def _get_custom_code(self) -> str: + # return "const customCode = 'customCode';" + + +{module_name} = {component_class_name}.create +''' + + +def _init_template(module_name: str) -> str: + """Template for custom components __init__.py. + + Args: + module_name: The name of the module. + + Returns: + Rendered __init__.py content as string. + """ + return f"from .{module_name} import *" + + +def _demo_app_template(custom_component_module_dir: str, module_name: str) -> str: + """Template for custom components demo app. + + Args: + custom_component_module_dir: The directory of the custom component module. + module_name: The name of the module. + + Returns: + Rendered demo app source code as string. + """ + return rf''' +"""Welcome to Reflex! This file showcases the custom component in a basic app.""" + +from rxconfig import config + +import reflex as rx + +from {custom_component_module_dir} import {module_name} + +filename = f"{{config.app_name}}/{{config.app_name}}.py" + + +class State(rx.State): + """The app state.""" + pass + +def index() -> rx.Component: + return rx.center( + rx.theme_panel(), + rx.vstack( + rx.heading("Welcome to Reflex!", size="9"), + rx.text( + "Test your custom component by editing ", + rx.code(filename), + font_size="2em", + ), + {module_name}(), + align="center", + spacing="7", + ), + height="100vh", + ) + + +# Add state and page to the app. +app = rx.App() +app.add_page(index) +''' + + def set_loglevel(ctx: Any, self: Any, value: str | None): """Set the log level. @@ -77,11 +273,9 @@ def _create_package_config(module_name: str, package_name: str): module_name: The name of the module. package_name: The name of the package typically constructed with `reflex-` prefix and a meaningful library name. """ - from reflex.compiler import templates - pyproject = Path(CustomComponents.PYPROJECT_TOML) pyproject.write_text( - templates.CUSTOM_COMPONENTS_PYPROJECT_TOML.render( + _pyproject_toml_template( module_name=module_name, package_name=package_name, reflex_version=constants.Reflex.VERSION, @@ -96,11 +290,9 @@ def _create_readme(module_name: str, package_name: str): module_name: The name of the module. package_name: The name of the python package to be published. """ - from reflex.compiler import templates - readme = Path(CustomComponents.PACKAGE_README) readme.write_text( - templates.CUSTOM_COMPONENTS_README.render( + _readme_template( module_name=module_name, package_name=package_name, ) @@ -119,19 +311,15 @@ def _write_source_and_init_py( component_class_name: The name of the component class. module_name: The name of the module. """ - from reflex.compiler import templates - module_path = custom_component_src_dir / f"{module_name}.py" module_path.write_text( - templates.CUSTOM_COMPONENTS_SOURCE.render( + _source_template( component_class_name=component_class_name, module_name=module_name ) ) init_path = custom_component_src_dir / CustomComponents.INIT_FILE - init_path.write_text( - templates.CUSTOM_COMPONENTS_INIT_FILE.render(module_name=module_name) - ) + init_path.write_text(_init_template(module_name=module_name)) def _populate_demo_app(name_variants: NameVariants): @@ -141,7 +329,6 @@ def _populate_demo_app(name_variants: NameVariants): name_variants: the tuple including various names such as package name, class name needed for the project. """ from reflex import constants - from reflex.compiler import templates from reflex.reflex import _init demo_app_dir = Path(name_variants.demo_app_dir) @@ -155,10 +342,10 @@ def _populate_demo_app(name_variants: NameVariants): # We start with the blank template as basis. _init(name=demo_app_name, template=constants.Templates.DEFAULT) # Then overwrite the app source file with the one we want for testing custom components. - # This source file is rendered using jinja template file. + # This source file is rendered using template file. demo_file = Path(f"{demo_app_name}/{demo_app_name}.py") demo_file.write_text( - templates.CUSTOM_COMPONENTS_DEMO_APP.render( + _demo_app_template( custom_component_module_dir=name_variants.custom_component_module_dir, module_name=name_variants.module_name, ) diff --git a/reflex/experimental/client_state.py b/reflex/experimental/client_state.py index 631b2025607..e2f2111d060 100644 --- a/reflex/experimental/client_state.py +++ b/reflex/experimental/client_state.py @@ -167,7 +167,7 @@ def create( ] = None imports.update(_refs_import) return cls( - _js_expr="", + _js_expr="null", _setter_name=setter_name, _getter_name=var_name, _id_name=id_name, diff --git a/reflex/plugins/shared_tailwind.py b/reflex/plugins/shared_tailwind.py index 3a292c6a169..c9dc7af6a74 100644 --- a/reflex/plugins/shared_tailwind.py +++ b/reflex/plugins/shared_tailwind.py @@ -1,12 +1,11 @@ """Tailwind CSS configuration types for Reflex plugins.""" import dataclasses +from collections.abc import Mapping from copy import deepcopy from typing import Any, Literal, TypedDict -from typing_extensions import NotRequired - -from reflex.utils.decorator import once +from typing_extensions import NotRequired, Unpack from .base import Plugin as PluginBase @@ -80,71 +79,97 @@ class TailwindConfig(TypedDict): plugins: NotRequired[list[TailwindPluginConfig]] -@once -def tailwind_config_js_template(): - """Get the Tailwind config template. +def tailwind_config_js_template( + *, default_content: list[str], **kwargs: Unpack[TailwindConfig] +): + """Generate a Tailwind CSS configuration file in JavaScript format. + + Args: + default_content: The default content to use if none is provided. + **kwargs: The template variables. Returns: The Tailwind config template. """ - from reflex.compiler.templates import from_string - - source = r""" -{# Extract destructured imports from plugin dicts only #} -{%- set imports = [] %} - -{%- for plugin in plugins if plugin is mapping and plugin.import is defined %} - {%- set _ = imports.append(plugin.import) %} -{%- endfor %} - -{%- for imp in imports %} -import { {{ imp.name }} } from {{ imp.from | tojson }}; -{%- endfor %} - -{%- for plugin in plugins %} -{% if plugin is mapping and plugin.call is not defined %} -import plugin{{ loop.index }} from {{ plugin.name | tojson }}; -{%- elif plugin is not mapping %} -import plugin{{ loop.index }} from {{ plugin | tojson }}; -{%- endif %} -{%- endfor %} - -{%- for preset in presets %} -import preset{{ loop.index }} from {{ preset | tojson }}; -{%- endfor %} - -export default { - content: {{ (content if content is defined else DEFAULT_CONTENT) | tojson }}, - {% if theme is defined %}theme: {{ theme | tojson }},{% else %}theme: {},{% endif %} - {% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %} - {% if corePlugins is defined %}corePlugins: {{ corePlugins | tojson }},{% endif %} - {% if important is defined %}important: {{ important | tojson }},{% endif %} - {% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %} - {% if separator is defined %}separator: {{ separator | tojson }},{% endif %} - {% if presets is defined %} - presets: [ - {% for preset in presets %} - preset{{ loop.index }}, - {% endfor %} - ], - {% endif %} - plugins: [ - {% for plugin in plugins %} - {% if plugin is mapping and plugin.call is defined %} - {{ plugin.call }}( - {%- if plugin.args is defined -%} - {{ plugin.args | tojson }} - {%- endif -%} - ), - {% else %} - plugin{{ loop.index }}, - {% endif %} - {% endfor %} + import json + + # Extract parameters + plugins = kwargs.get("plugins", []) + presets = kwargs.get("presets", []) + content = kwargs.get("content") + theme = kwargs.get("theme") + dark_mode = kwargs.get("darkMode") + core_plugins = kwargs.get("corePlugins") + important = kwargs.get("important") + prefix = kwargs.get("prefix") + separator = kwargs.get("separator") + + # Extract destructured imports from plugin dicts only + imports = [ + plugin["import"] + for plugin in plugins + if isinstance(plugin, Mapping) and "import" in plugin ] -}; -""" - return from_string(source) + # Generate import statements for destructured imports + import_lines = "\n".join( + [ + f"import {{ {imp['name']} }} from {json.dumps(imp['from'])};" + for imp in imports + ] + ) + + # Generate plugin imports + plugin_imports = [] + for i, plugin in enumerate(plugins, 1): + if isinstance(plugin, Mapping) and "call" not in plugin: + plugin_imports.append( + f"import plugin{i} from {json.dumps(plugin['name'])};" + ) + elif not isinstance(plugin, Mapping): + plugin_imports.append(f"import plugin{i} from {json.dumps(plugin)};") + + plugin_imports_lines = "\n".join(plugin_imports) + + presets_imports_lines = "\n".join( + [ + f"import preset{i} from {json.dumps(preset)};" + for i, preset in enumerate(presets, 1) + ] + ) + + # Generate plugin array + plugin_list = [] + for i, plugin in enumerate(plugins, 1): + if isinstance(plugin, Mapping) and "call" in plugin: + args_part = "" + if "args" in plugin: + args_part = json.dumps(plugin["args"]) + plugin_list.append(f"{plugin['call']}({args_part})") + else: + plugin_list.append(f"plugin{i}") + + plugin_use_str = ",".join(plugin_list) + + return rf""" +{import_lines} + +{plugin_imports_lines} + +{presets_imports_lines} + +export default {{ + content: {json.dumps(content if content else default_content)}, + theme: {json.dumps(theme if theme else {})}, + {f"darkMode: {json.dumps(dark_mode)}," if dark_mode is not None else ""} + {f"corePlugins: {json.dumps(core_plugins)}," if core_plugins is not None else ""} + {f"importants: {json.dumps(important)}," if important is not None else ""} + {f"prefix: {json.dumps(prefix)}," if prefix is not None else ""} + {f"separator: {json.dumps(separator)}," if separator is not None else ""} + {f"presets: [{', '.join(f'preset{i}' for i in range(1, len(presets) + 1))}]," if presets else ""} + plugins: [{plugin_use_str}] +}}; +""" @dataclasses.dataclass diff --git a/reflex/plugins/tailwind_v3.py b/reflex/plugins/tailwind_v3.py index 80e383e609c..1d1398f71df 100644 --- a/reflex/plugins/tailwind_v3.py +++ b/reflex/plugins/tailwind_v3.py @@ -48,9 +48,9 @@ def compile_config(config: TailwindConfig): Returns: The compiled Tailwind config. """ - return Constants.CONFIG, tailwind_config_js_template().render( + return Constants.CONFIG, tailwind_config_js_template( **config, - DEFAULT_CONTENT=Constants.CONTENT, + default_content=Constants.CONTENT, ) diff --git a/reflex/plugins/tailwind_v4.py b/reflex/plugins/tailwind_v4.py index d3e3eb61046..ae73980f327 100644 --- a/reflex/plugins/tailwind_v4.py +++ b/reflex/plugins/tailwind_v4.py @@ -47,9 +47,9 @@ def compile_config(config: TailwindConfig): Returns: The compiled Tailwind config. """ - return Constants.CONFIG, tailwind_config_js_template().render( + return Constants.CONFIG, tailwind_config_js_template( **config, - DEFAULT_CONTENT=Constants.CONTENT, + default_content=Constants.CONTENT, ) diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 80c7f810aa4..613fdb93e2f 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -330,7 +330,7 @@ def format_route(route: str) -> str: def format_match( cond: str | Var, - match_cases: list[list[Var]], + match_cases: list[tuple[list[Var], Var]], default: Var, ) -> str: """Format a match expression whose return type is a Var. @@ -347,8 +347,7 @@ def format_match( switch_code = f"(() => {{ switch (JSON.stringify({cond})) {{" for case in match_cases: - conditions = case[:-1] - return_value = case[-1] + conditions, return_value = case case_conditions = " ".join( [f"case JSON.stringify({condition!s}):" for condition in conditions] diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index d3e9f69256b..a6317ddad68 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -169,7 +169,7 @@ def _update_react_router_config(config: Config, prerender_routes: bool = False): def _compile_package_json(): - return templates.PACKAGE_JSON.render( + return templates.package_json_template( scripts={ "dev": constants.PackageJson.Commands.DEV, "export": constants.PackageJson.Commands.EXPORT, @@ -192,7 +192,7 @@ def _compile_vite_config(config: Config): base = "/" if frontend_path := config.frontend_path.strip("/"): base += frontend_path + "/" - return templates.VITE_CONFIG.render(base=base) + return templates.vite_config_template(base=base) def initialize_vite_config(): diff --git a/reflex/utils/imports.py b/reflex/utils/imports.py index bc85b62c45f..884947c50d7 100644 --- a/reflex/utils/imports.py +++ b/reflex/utils/imports.py @@ -7,6 +7,24 @@ from collections.abc import Mapping, Sequence +def merge_parsed_imports( + *imports: ImmutableParsedImportDict, +) -> ParsedImportDict: + """Merge multiple parsed import dicts together. + + Args: + *imports: The list of import dicts to merge. + + Returns: + The merged import dicts. + """ + all_imports: defaultdict[str, list[ImportVar]] = defaultdict(list) + for import_dict in imports: + for lib, fields in import_dict.items(): + all_imports[lib].extend(fields) + return all_imports + + def merge_imports( *imports: ImportDict | ParsedImportDict | ParsedImportTuple, ) -> ParsedImportDict: diff --git a/reflex/utils/templates.py b/reflex/utils/templates.py index b452c0361c5..431d7c022b1 100644 --- a/reflex/utils/templates.py +++ b/reflex/utils/templates.py @@ -1,7 +1,6 @@ """This module provides utilities for managing Reflex app templates.""" import dataclasses -import re import shutil import tempfile import zipfile @@ -33,12 +32,8 @@ def create_config(app_name: str): # Import here to avoid circular imports. from reflex.compiler import templates - config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config" - console.debug(f"Creating {constants.Config.FILE}") - constants.Config.FILE.write_text( - templates.RXCONFIG.render(app_name=app_name, config_name=config_name) - ) + constants.Config.FILE.write_text(templates.rxconfig_template(app_name=app_name)) def initialize_app_directory( diff --git a/tests/integration/test_large_state.py b/tests/integration/test_large_state.py index a176fa964c4..b41c046e1ed 100644 --- a/tests/integration/test_large_state.py +++ b/tests/integration/test_large_state.py @@ -2,20 +2,22 @@ import time -import jinja2 import pytest from selenium.webdriver.common.by import By from reflex.testing import AppHarness, WebDriver -LARGE_STATE_APP_TEMPLATE = """ + +def _large_state_app_template(var_count: int) -> str: + var_part = "\n".join( + f' var{i}: str = "{i}" * 10000' for i in range(1, var_count) + ) + return f""" import reflex as rx class State(rx.State): var0: int = 0 - {% for i in range(1, var_count) %} - var{{ i }}: str = "{{ i }}" * 10000 - {% endfor %} + {var_part} def increment_var0(self): self.var0 += 1 @@ -54,8 +56,7 @@ def test_large_state(var_count: int, tmp_path_factory, benchmark): Raises: TimeoutError: if the state doesn't update within 30 seconds """ - template = jinja2.Template(LARGE_STATE_APP_TEMPLATE) - large_state_rendered = template.render(var_count=var_count) + large_state_rendered = _large_state_app_template(var_count) with AppHarness.create( root=tmp_path_factory.mktemp("large_state"), diff --git a/tests/units/compiler/test_compiler.py b/tests/units/compiler/test_compiler.py index 18324e610a2..920249d216b 100644 --- a/tests/units/compiler/test_compiler.py +++ b/tests/units/compiler/test_compiler.py @@ -107,12 +107,12 @@ def test_compile_imports(import_dict: ParsedImportDict, test_dicts: list[dict]): test_dicts: The expected output. """ imports = utils.compile_imports(import_dict) - for import_dict, test_dict in zip(imports, test_dicts, strict=True): - assert import_dict["lib"] == test_dict["lib"] - assert import_dict["default"] == test_dict["default"] + for one_import_dict, test_dict in zip(imports, test_dicts, strict=True): + assert one_import_dict["lib"] == test_dict["lib"] + assert one_import_dict["default"] == test_dict["default"] assert ( sorted( - import_dict["rest"], + one_import_dict["rest"], key=lambda i: i if isinstance(i, str) else (i.tag or ""), ) == test_dict["rest"] @@ -159,12 +159,12 @@ def test_compile_stylesheets(tmp_path: Path, mocker: MockerFixture): / (PageNames.STYLESHEET_ROOT + ".css") ), "@layer __reflex_base;\n" - "@import url('./__reflex_style_reset.css'); \n" - "@import url('@radix-ui/themes/styles.css'); \n" - "@import url('https://fonts.googleapis.com/css?family=Sofia&effect=neon|outline|emboss|shadow-multiple'); \n" - "@import url('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css'); \n" - "@import url('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css'); \n" - "@import url('./style.css'); \n", + "@import url('./__reflex_style_reset.css');\n" + "@import url('@radix-ui/themes/styles.css');\n" + "@import url('https://fonts.googleapis.com/css?family=Sofia&effect=neon|outline|emboss|shadow-multiple');\n" + "@import url('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css');\n" + "@import url('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css');\n" + "@import url('./style.css');", ) assert (project / constants.Dirs.WEB / "styles" / "style.css").read_text() == ( @@ -221,11 +221,11 @@ def test_compile_stylesheets_scss_sass(tmp_path: Path, mocker: MockerFixture): / (PageNames.STYLESHEET_ROOT + ".css") ), "@layer __reflex_base;\n" - "@import url('./__reflex_style_reset.css'); \n" - "@import url('@radix-ui/themes/styles.css'); \n" - "@import url('./style.css'); \n" - f"@import url('./{Path('preprocess') / Path('styles_a.css')!s}'); \n" - f"@import url('./{Path('preprocess') / Path('styles_b.css')!s}'); \n", + "@import url('./__reflex_style_reset.css');\n" + "@import url('@radix-ui/themes/styles.css');\n" + "@import url('./style.css');\n" + f"@import url('./{Path('preprocess') / Path('styles_a.css')!s}');\n" + f"@import url('./{Path('preprocess') / Path('styles_b.css')!s}');", ) stylesheets = [ @@ -241,11 +241,11 @@ def test_compile_stylesheets_scss_sass(tmp_path: Path, mocker: MockerFixture): / (PageNames.STYLESHEET_ROOT + ".css") ), "@layer __reflex_base;\n" - "@import url('./__reflex_style_reset.css'); \n" - "@import url('@radix-ui/themes/styles.css'); \n" - "@import url('./style.css'); \n" - f"@import url('./{Path('preprocess') / Path('styles_a.css')!s}'); \n" - f"@import url('./{Path('preprocess') / Path('styles_b.css')!s}'); \n", + "@import url('./__reflex_style_reset.css');\n" + "@import url('@radix-ui/themes/styles.css');\n" + "@import url('./style.css');\n" + f"@import url('./{Path('preprocess') / Path('styles_a.css')!s}');\n" + f"@import url('./{Path('preprocess') / Path('styles_b.css')!s}');", ) assert (project / constants.Dirs.WEB / "styles" / "style.css").read_text() == ( @@ -288,7 +288,7 @@ def test_compile_stylesheets_exclude_tailwind(tmp_path, mocker: MockerFixture): assert compiler.compile_root_stylesheet(stylesheets) == ( str(Path(".web") / "styles" / (PageNames.STYLESHEET_ROOT + ".css")), - "@layer __reflex_base;\n@import url('./__reflex_style_reset.css'); \n@import url('@radix-ui/themes/styles.css'); \n@import url('./style.css'); \n", + "@layer __reflex_base;\n@import url('./__reflex_style_reset.css');\n@import url('@radix-ui/themes/styles.css');\n@import url('./style.css');", ) @@ -327,7 +327,7 @@ def test_compile_stylesheets_no_reset(tmp_path: Path, mocker: MockerFixture): / "styles" / (PageNames.STYLESHEET_ROOT + ".css") ), - "@layer __reflex_base;\n@import url('@radix-ui/themes/styles.css'); \n@import url('./style.css'); \n", + "@layer __reflex_base;\n@import url('@radix-ui/themes/styles.css');\n@import url('./style.css');", ) diff --git a/tests/units/components/base/test_script.py b/tests/units/components/base/test_script.py index d9b1dea23ba..a279dfefc1e 100644 --- a/tests/units/components/base/test_script.py +++ b/tests/units/components/base/test_script.py @@ -10,7 +10,6 @@ def test_script_inline(): component = Script.create("let x = 42") render_dict = component.render()["children"][0] assert render_dict["name"] == '"script"' - assert not render_dict["contents"] assert len(render_dict["children"]) == 1 assert render_dict["children"][0]["contents"] == '"let x = 42"' @@ -20,7 +19,6 @@ def test_script_src(): component = Script.create(src="foo.js") render_dict = component.render()["children"][0] assert render_dict["name"] == '"script"' - assert not render_dict["contents"] assert not render_dict["children"] assert 'src:"foo.js"' in render_dict["props"] diff --git a/tests/units/components/core/test_debounce.py b/tests/units/components/core/test_debounce.py index fa63787e41e..43cca255c70 100644 --- a/tests/units/components/core/test_debounce.py +++ b/tests/units/components/core/test_debounce.py @@ -65,7 +65,6 @@ def test_render_child_props(): ) assert len(tag.props["onChange"].events) == 1 assert tag.props["onChange"].events[0].handler == S.on_change - assert tag.contents == "" def test_render_with_class_name(): @@ -161,7 +160,6 @@ def test_render_child_props_recursive(): assert tag.props["debounceTimeout"]._js_expr == "42" assert len(tag.props["onChange"].events) == 1 assert tag.props["onChange"].events[0].handler == S.on_change - assert tag.contents == "" def test_full_control_implicit_debounce(): @@ -173,7 +171,6 @@ def test_full_control_implicit_debounce(): assert tag.props["debounceTimeout"]._js_expr == str(DEFAULT_DEBOUNCE_TIMEOUT) assert len(tag.props["onChange"].events) == 1 assert tag.props["onChange"].events[0].handler == S.on_change - assert tag.contents == "" def test_full_control_implicit_debounce_text_area(): @@ -185,4 +182,3 @@ def test_full_control_implicit_debounce_text_area(): assert tag.props["debounceTimeout"]._js_expr == str(DEFAULT_DEBOUNCE_TIMEOUT) assert len(tag.props["onChange"].events) == 1 assert tag.props["onChange"].events[0].handler == S.on_change - assert tag.contents == "" diff --git a/tests/units/components/core/test_foreach.py b/tests/units/components/core/test_foreach.py index 1239285440e..19cc3ad4594 100644 --- a/tests/units/components/core/test_foreach.py +++ b/tests/units/components/core/test_foreach.py @@ -15,7 +15,6 @@ from reflex.components.radix.themes.typography.text import text from reflex.constants.state import FIELD_MARKER from reflex.state import BaseState, ComponentState -from reflex.vars.base import Var from reflex.vars.number import NumberVar from reflex.vars.sequence import ArrayVar @@ -141,9 +140,6 @@ def display_color_index_tuple(color): return box(text(color)) -seen_index_vars = set() - - @pytest.mark.parametrize( ("state_var", "render_fn", "render_dict"), [ @@ -248,10 +244,7 @@ def test_foreach_render(state_var, render_fn, render_dict): # Make sure the index vars are unique. arg_index = rend["arg_index"] - assert isinstance(arg_index, Var) - assert arg_index._js_expr not in seen_index_vars - assert arg_index._var_type is int - seen_index_vars.add(arg_index._js_expr) + assert isinstance(arg_index, str) def test_foreach_bad_annotations(): diff --git a/tests/units/components/core/test_html.py b/tests/units/components/core/test_html.py index 611753fca1d..8ab466a4e33 100644 --- a/tests/units/components/core/test_html.py +++ b/tests/units/components/core/test_html.py @@ -19,7 +19,7 @@ def test_html_create(): assert str(html.dangerouslySetInnerHTML) == '({ ["__html"] : "

Hello !

" })' # pyright: ignore [reportAttributeAccessIssue] assert ( str(html) - == 'jsx("div",{className:"rx-Html",dangerouslySetInnerHTML:({ ["__html"] : "

Hello !

" })},)\n' + == 'jsx("div",{className:"rx-Html",dangerouslySetInnerHTML:({ ["__html"] : "

Hello !

" })},)' ) @@ -39,5 +39,5 @@ class TestState(State): ) assert ( str(html) - == f'jsx("div",{{className:"rx-Html",dangerouslySetInnerHTML:{html_dangerouslySetInnerHTML!s}}},)\n' + == f'jsx("div",{{className:"rx-Html",dangerouslySetInnerHTML:{html_dangerouslySetInnerHTML!s}}},)' ) diff --git a/tests/units/components/core/test_match.py b/tests/units/components/core/test_match.py index eb870577880..52f5620a593 100644 --- a/tests/units/components/core/test_match.py +++ b/tests/units/components/core/test_match.py @@ -1,5 +1,4 @@ import re -from collections.abc import Mapping, Sequence import pytest @@ -31,7 +30,7 @@ def test_match_components(): (MatchState.num + 1, rx.text("sixth value")), rx.text("default value"), ) - match_comp = Match.create(MatchState.value, *match_case_tuples) + match_comp = rx.match(MatchState.value, *match_case_tuples) assert isinstance(match_comp, Component) match_dict = match_comp.render() @@ -39,48 +38,36 @@ def test_match_components(): [match_child] = match_dict["children"] - assert match_child["name"] == "match" assert str(match_child["cond"]) == f"{MatchState.get_name()}.value" + FIELD_MARKER match_cases = match_child["match_cases"] assert len(match_cases) == 6 - - assert match_cases[0][0]._js_expr == "1" - assert match_cases[0][0]._var_type is int + assert match_cases[0][0] == ["1"] first_return_value_render = match_cases[0][1] assert first_return_value_render["name"] == "RadixThemesText" assert first_return_value_render["children"][0]["contents"] == '"first value"' - assert match_cases[1][0]._js_expr == "2" - assert match_cases[1][0]._var_type is int - assert match_cases[1][1]._js_expr == "3" - assert match_cases[1][1]._var_type is int - second_return_value_render = match_cases[1][2] + assert match_cases[1][0] == ["2", "3"] + second_return_value_render = match_cases[1][1] assert second_return_value_render["name"] == "RadixThemesText" assert second_return_value_render["children"][0]["contents"] == '"second value"' - assert match_cases[2][0]._js_expr == "[1, 2]" - assert match_cases[2][0]._var_type == Sequence[int] + assert match_cases[2][0] == ["[1, 2]"] third_return_value_render = match_cases[2][1] assert third_return_value_render["name"] == "RadixThemesText" assert third_return_value_render["children"][0]["contents"] == '"third value"' - assert match_cases[3][0]._js_expr == '"random"' - assert match_cases[3][0]._var_type is str + assert match_cases[3][0] == ['"random"'] fourth_return_value_render = match_cases[3][1] assert fourth_return_value_render["name"] == "RadixThemesText" assert fourth_return_value_render["children"][0]["contents"] == '"fourth value"' - assert match_cases[4][0]._js_expr == '({ ["foo"] : "bar" })' - assert match_cases[4][0]._var_type == Mapping[str, str] + assert match_cases[4][0] == ['({ ["foo"] : "bar" })'] fifth_return_value_render = match_cases[4][1] assert fifth_return_value_render["name"] == "RadixThemesText" assert fifth_return_value_render["children"][0]["contents"] == '"fifth value"' - assert ( - match_cases[5][0]._js_expr == f"({MatchState.get_name()}.num{FIELD_MARKER} + 1)" - ) - assert match_cases[5][0]._var_type is int + assert match_cases[5][0] == [f"({MatchState.get_name()}.num{FIELD_MARKER} + 1)"] fifth_return_value_render = match_cases[5][1] assert fifth_return_value_render["name"] == "RadixThemesText" assert fifth_return_value_render["children"][0]["contents"] == '"sixth value"' @@ -157,7 +144,7 @@ def test_match_on_component_without_default(): (2, 3, rx.text("second value")), ) - match_comp = Match.create(MatchState.value, *match_case_tuples) + match_comp = rx.match(MatchState.value, *match_case_tuples) assert isinstance(match_comp, Component) default = match_comp.render()["children"][0]["default"] @@ -208,7 +195,7 @@ def test_match_default_not_last_arg(match_case): """ with pytest.raises( ValueError, - match="rx.match should have tuples of cases and a default case as the last argument.", + match="rx.match should have tuples of cases and one default case as the last argument.", ): Match.create(MatchState.value, *match_case) @@ -269,7 +256,7 @@ def test_match_case_tuple_elements(match_case): ([1, 2], rx.text("third value")), rx.text("default value"), ), - 'Match cases should have the same return types. Case 3 with return value `jsx( RadixThemesText, {as:"p"}, "first value" ,)` ' + 'Match cases should have the same return types. Case 3 with return value `jsx(RadixThemesText,{as:"p"},"first value")` ' "of type is not ", ), ], @@ -313,7 +300,10 @@ def test_match_multiple_default_cases(match_case): Args: match_case: the cases to match. """ - with pytest.raises(ValueError, match="rx.match can only have one default case."): + with pytest.raises( + ValueError, + match="rx.match should have tuples of cases and one default case as the last argument.", + ): Match.create(MatchState.value, *match_case) diff --git a/tests/units/components/markdown/test_markdown.py b/tests/units/components/markdown/test_markdown.py index 36a06f3995e..6d4052114e0 100644 --- a/tests/units/components/markdown/test_markdown.py +++ b/tests/units/components/markdown/test_markdown.py @@ -146,7 +146,7 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe ( "code", {}, - r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; ; return inline ? ( jsx(RadixThemesCode,{...props},children,) ) : ( jsx(SyntaxHighlighter,{children:((Array.isArray(children)) ? children.join("\n") : children),css:({ ["marginTop"] : "1em", ["marginBottom"] : "1em" }),language:_language,style:((resolvedColorMode === "light") ? oneLight : oneDark),wrapLongLines:true,...props},) ); })""", + r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; ; return inline ? ( jsx(RadixThemesCode,{...props},children) ) : ( jsx(SyntaxHighlighter,{children:((Array.isArray(children)) ? children.join("\n") : children),css:({ ["marginTop"] : "1em", ["marginBottom"] : "1em" }),language:_language,style:((resolvedColorMode === "light") ? oneLight : oneDark),wrapLongLines:true,...props},) ); })""", ), ( "code", @@ -155,7 +155,7 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe value, **props ) }, - r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; ; return inline ? ( jsx(RadixThemesCode,{...props},children,) ) : ( jsx(RadixThemesBox,{css:({ ["pre"] : ({ ["margin"] : "0", ["padding"] : "24px", ["background"] : "transparent", ["overflowX"] : "auto", ["borderRadius"] : "6px" }) }),...props},jsx(ShikiCode,{code:((Array.isArray(children)) ? children.join("\n") : children),decorations:[],language:_language,theme:((resolvedColorMode === "light") ? "one-light" : "one-dark-pro"),transformers:[]},),) ); })""", + r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; ; return inline ? ( jsx(RadixThemesCode,{...props},children) ) : ( jsx(RadixThemesBox,{css:({ ["pre"] : ({ ["margin"] : "0", ["padding"] : "24px", ["background"] : "transparent", ["overflowX"] : "auto", ["borderRadius"] : "6px" }) }),...props},jsx(ShikiCode,{code:((Array.isArray(children)) ? children.join("\n") : children),decorations:[],language:_language,theme:((resolvedColorMode === "light") ? "one-light" : "one-dark-pro"),transformers:[]},)) ); })""", ), ( "h1", @@ -164,12 +164,12 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe Heading.create(value, as_="h1", size="6", margin_y="0.5em") ) }, - """(({custom_node, custom_children, custom_props}) => (jsx(CustomMarkdownComponent,{...props},jsx(RadixThemesHeading,{as:"h1",css:({ ["marginTop"] : "0.5em", ["marginBottom"] : "0.5em" }),size:"6"},children,),)))""", + """(({custom_node, custom_children, custom_props}) => (jsx(CustomMarkdownComponent,{...props},jsx(RadixThemesHeading,{as:"h1",css:({ ["marginTop"] : "0.5em", ["marginBottom"] : "0.5em" }),size:"6"},children))))""", ), ( "code", {"codeblock": syntax_highlighter_memoized_component(CodeBlock)}, - r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; ; return inline ? ( jsx(RadixThemesCode,{...props},children,) ) : ( jsx(CodeBlock,{codeRxMemo:((Array.isArray(children)) ? children.join("\n") : children),languageRxMemo:_language,...props},) ); })""", + r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; ; return inline ? ( jsx(RadixThemesCode,{...props},children) ) : ( jsx(CodeBlock,{codeRxMemo:((Array.isArray(children)) ? children.join("\n") : children),languageRxMemo:_language,...props},) ); })""", ), ( "code", @@ -178,7 +178,7 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe ShikiHighLevelCodeBlock ) }, - r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; ; return inline ? ( jsx(RadixThemesCode,{...props},children,) ) : ( jsx(CodeBlock,{codeRxMemo:((Array.isArray(children)) ? children.join("\n") : children),languageRxMemo:_language,...props},) ); })""", + r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; ; return inline ? ( jsx(RadixThemesCode,{...props},children) ) : ( jsx(CodeBlock,{codeRxMemo:((Array.isArray(children)) ? children.join("\n") : children),languageRxMemo:_language,...props},) ); })""", ), ], ) diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index 0802e45da4b..8d236d0f17a 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -684,18 +684,13 @@ def test_component_create_unallowed_types(children, test_component): { "name": "Fragment", "props": [], - "contents": "", "children": [ { "name": "RadixThemesText", "props": ['as:"p"'], - "contents": "", "children": [ { - "name": "", - "props": [], "contents": '"first_text"', - "children": [], } ], } @@ -709,31 +704,22 @@ def test_component_create_unallowed_types(children, test_component): { "children": [ { - "children": [], "contents": '"first_text"', - "name": "", - "props": [], } ], - "contents": "", "name": "RadixThemesText", "props": ['as:"p"'], }, { "children": [ { - "children": [], "contents": '"second_text"', - "name": "", - "props": [], } ], - "contents": "", "name": "RadixThemesText", "props": ['as:"p"'], }, ], - "contents": "", "name": "Fragment", "props": [], }, @@ -745,13 +731,9 @@ def test_component_create_unallowed_types(children, test_component): { "children": [ { - "children": [], "contents": '"first_text"', - "name": "", - "props": [], } ], - "contents": "", "name": "RadixThemesText", "props": ['as:"p"'], }, @@ -762,28 +744,21 @@ def test_component_create_unallowed_types(children, test_component): { "children": [ { - "children": [], "contents": '"second_text"', - "name": "", - "props": [], } ], - "contents": "", "name": "RadixThemesText", "props": ['as:"p"'], } ], - "contents": "", "name": "Fragment", "props": [], } ], - "contents": "", "name": "RadixThemesBox", "props": [], }, ], - "contents": "", "name": "Fragment", "props": [], }, @@ -1152,10 +1127,10 @@ def test_component_with_only_valid_children(fixture, request): @pytest.mark.parametrize( ("component", "rendered"), [ - (rx.text("hi"), 'jsx(\nRadixThemesText,\n{as:"p"},\n"hi"\n,)'), + (rx.text("hi"), 'jsx(RadixThemesText,{as:"p"},"hi")'), ( rx.box(rx.heading("test", size="3")), - 'jsx(\nRadixThemesBox,\n{},\njsx(\nRadixThemesHeading,\n{size:"3"},\n"test"\n,),)', + 'jsx(RadixThemesBox,{},jsx(RadixThemesHeading,{size:"3"},"test"))', ), ], ) @@ -1592,12 +1567,12 @@ def test_validate_valid_children(): ), ( "fifth", + "sixth", rx.match( "nested_condition", ("nested_first", valid_component2()), rx.fragment(valid_component2()), ), - valid_component2(), ), ) ) @@ -1654,12 +1629,12 @@ def test_validate_valid_parents(): ), ( "fifth", + "sixth", rx.match( "nested_condition", ("nested_first", valid_component3()), rx.fragment(valid_component3()), ), - valid_component3(), ), ) ) diff --git a/tests/units/components/test_tag.py b/tests/units/components/test_tag.py index d799b511de1..7245c9e0ba6 100644 --- a/tests/units/components/test_tag.py +++ b/tests/units/components/test_tag.py @@ -65,7 +65,6 @@ def test_add_props(): { "name": "", "children": [], - "contents": "", "props": [], }, ), @@ -74,26 +73,13 @@ def test_add_props(): { "name": "br", "children": [], - "contents": "", "props": [], }, ), ( - Tag(contents="hello"), + tagless.Tagless(contents="hello"), { - "name": "", - "children": [], "contents": "hello", - "props": [], - }, - ), - ( - Tag(name="h1", contents="hello"), - { - "name": "h1", - "children": [], - "contents": "hello", - "props": [], }, ), ( @@ -101,20 +87,6 @@ def test_add_props(): { "name": "box", "children": [], - "contents": "", - "props": ['color:"red"', 'textAlign:"center"'], - }, - ), - ( - Tag( - name="box", - props={"color": "red", "textAlign": "center"}, - contents="text", - ), - { - "name": "box", - "children": [], - "contents": "text", "props": ['color:"red"', 'textAlign:"center"'], }, ), @@ -134,23 +106,20 @@ def test_format_tag(tag: Tag, expected: dict): def test_format_cond_tag(): """Test that the cond tag dict is correct.""" tag = CondTag( - true_value=dict(Tag(name="h1", contents="True content")), - false_value=dict(Tag(name="h2", contents="False content")), - cond=Var("logged_in", _var_type=bool), + cond_state="logged_in", + true_value=dict(tagless.Tagless(contents="True content")), + false_value=dict(tagless.Tagless(contents="False content")), ) tag_dict = dict(tag) - cond, true_value, false_value = ( - tag_dict["cond"], + cond_state, true_value, false_value = ( + tag_dict["cond_state"], tag_dict["true_value"], tag_dict["false_value"], ) - assert cond._js_expr == "logged_in" - assert cond._var_type is bool + assert cond_state == "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/units/test_app.py b/tests/units/test_app.py index 64fd149b123..0c89c3746e1 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -1362,17 +1362,18 @@ def test_app_wrap_compile_theme( app_js_contents = ( web_dir / constants.Dirs.PAGES / constants.PageNames.APP_ROOT ).read_text() - app_js_lines = [ - line.strip() for line in app_js_contents.splitlines() if line.strip() - ] - lines = "".join(app_js_lines) + function_app_definition = app_js_contents[ + app_js_contents.index("function AppWrap") : app_js_contents.index( + "export function Layout" + ) + ].strip() expected = ( - "function AppWrap({children}) {" - "const [addEvents, connectErrors] = useContext(EventLoopContext);" + "function AppWrap({children}) {\n" + "const [addEvents, connectErrors] = useContext(EventLoopContext);\n\n\n\n" "return (" + ("jsx(StrictMode,{}," if react_strict_mode else "") + "jsx(ErrorBoundary,{" - """fallbackRender:((event_args) => (jsx("div", ({css:({ ["height"] : "100%", ["width"] : "100%", ["position"] : "absolute", ["backgroundColor"] : "#fff", ["color"] : "#000", ["display"] : "flex", ["alignItems"] : "center", ["justifyContent"] : "center" })}), (jsx("div", ({css:({ ["display"] : "flex", ["flexDirection"] : "column", ["gap"] : "1rem" })}), (jsx("div", ({css:({ ["display"] : "flex", ["flexDirection"] : "column", ["gap"] : "1rem", ["maxWidth"] : "50ch", ["border"] : "1px solid #888888", ["borderRadius"] : "0.25rem", ["padding"] : "1rem" })}), (jsx("h2", ({css:({ ["fontSize"] : "1.25rem", ["fontWeight"] : "bold" })}), (jsx(Fragment, ({}), "An error occurred while rendering this page.")))), (jsx("p", ({css:({ ["opacity"] : "0.75" })}), (jsx(Fragment, ({}), "This is an error with the application itself.")))), (jsx("details", ({}), (jsx("summary", ({css:({ ["padding"] : "0.5rem" })}), (jsx(Fragment, ({}), "Error message")))), (jsx("div", ({css:({ ["width"] : "100%", ["maxHeight"] : "50vh", ["overflow"] : "auto", ["background"] : "#000", ["color"] : "#fff", ["borderRadius"] : "0.25rem" })}), (jsx("div", ({css:({ ["padding"] : "0.5rem", ["width"] : "fit-content" })}), (jsx("pre", ({}), (jsx(Fragment, ({}), event_args.error.name + \': \' + event_args.error.message + \'\\n\' + event_args.error.stack)))))))), (jsx("button", ({css:({ ["padding"] : "0.35rem 0.75rem", ["margin"] : "0.5rem", ["background"] : "#fff", ["color"] : "#000", ["border"] : "1px solid #000", ["borderRadius"] : "0.25rem", ["fontWeight"] : "bold" }),onClick:((_e) => (addEvents([(Event("_call_function", ({ ["function"] : (() => (navigator["clipboard"]["writeText"](event_args.error.name + \': \' + event_args.error.message + \'\\n\' + event_args.error.stack))), ["callback"] : null }), ({ })))], [_e], ({ }))))}), (jsx(Fragment, ({}), "Copy")))))))), (jsx("hr", ({css:({ ["borderColor"] : "currentColor", ["opacity"] : "0.25" })}))), (jsx(ReactRouterLink, ({to:"https://reflex.dev"}), (jsx("div", ({css:({ ["display"] : "flex", ["alignItems"] : "baseline", ["justifyContent"] : "center", ["fontFamily"] : "monospace", ["--default-font-family"] : "monospace", ["gap"] : "0.5rem" })}), (jsx(Fragment, ({}), "Built with ")), (jsx("svg", ({"aria-label":"Reflex",css:({ ["fill"] : "currentColor" }),height:"12",role:"img",width:"56",xmlns:"http://www.w3.org/2000/svg"}), (jsx("path", ({d:"M0 11.5999V0.399902H8.96V4.8799H6.72V2.6399H2.24V4.8799H6.72V7.1199H2.24V11.5999H0ZM6.72 11.5999V7.1199H8.96V11.5999H6.72Z"}))), (jsx("path", ({d:"M11.2 11.5999V0.399902H17.92V2.6399H13.44V4.8799H17.92V7.1199H13.44V9.3599H17.92V11.5999H11.2Z"}))), (jsx("path", ({d:"M20.16 11.5999V0.399902H26.88V2.6399H22.4V4.8799H26.88V7.1199H22.4V11.5999H20.16Z"}))), (jsx("path", ({d:"M29.12 11.5999V0.399902H31.36V9.3599H35.84V11.5999H29.12Z"}))), (jsx("path", ({d:"M38.08 11.5999V0.399902H44.8V2.6399H40.32V4.8799H44.8V7.1199H40.32V9.3599H44.8V11.5999H38.08Z"}))), (jsx("path", ({d:"M47.04 4.8799V0.399902H49.28V4.8799H47.04ZM53.76 4.8799V0.399902H56V4.8799H53.76ZM49.28 7.1199V4.8799H53.76V7.1199H49.28ZM47.04 11.5999V7.1199H49.28V11.5999H47.04ZM53.76 11.5999V7.1199H56V11.5999H53.76Z"}))), (jsx("title", ({}), (jsx(Fragment, ({}), "Reflex"))))))))))))))),""" + """fallbackRender:((event_args) => (jsx("div", ({css:({ ["height"] : "100%", ["width"] : "100%", ["position"] : "absolute", ["backgroundColor"] : "#fff", ["color"] : "#000", ["display"] : "flex", ["alignItems"] : "center", ["justifyContent"] : "center" })}), (jsx("div", ({css:({ ["display"] : "flex", ["flexDirection"] : "column", ["gap"] : "1rem" })}), (jsx("div", ({css:({ ["display"] : "flex", ["flexDirection"] : "column", ["gap"] : "1rem", ["maxWidth"] : "50ch", ["border"] : "1px solid #888888", ["borderRadius"] : "0.25rem", ["padding"] : "1rem" })}), (jsx("h2", ({css:({ ["fontSize"] : "1.25rem", ["fontWeight"] : "bold" })}), "An error occurred while rendering this page.")), (jsx("p", ({css:({ ["opacity"] : "0.75" })}), "This is an error with the application itself.")), (jsx("details", ({}), (jsx("summary", ({css:({ ["padding"] : "0.5rem" })}), "Error message")), (jsx("div", ({css:({ ["width"] : "100%", ["maxHeight"] : "50vh", ["overflow"] : "auto", ["background"] : "#000", ["color"] : "#fff", ["borderRadius"] : "0.25rem" })}), (jsx("div", ({css:({ ["padding"] : "0.5rem", ["width"] : "fit-content" })}), (jsx("pre", ({}), event_args.error.name + \': \' + event_args.error.message + \'\\n\' + event_args.error.stack)))))), (jsx("button", ({css:({ ["padding"] : "0.35rem 0.75rem", ["margin"] : "0.5rem", ["background"] : "#fff", ["color"] : "#000", ["border"] : "1px solid #000", ["borderRadius"] : "0.25rem", ["fontWeight"] : "bold" }),onClick:((_e) => (addEvents([(Event("_call_function", ({ ["function"] : (() => (navigator["clipboard"]["writeText"](event_args.error.name + \': \' + event_args.error.message + \'\\n\' + event_args.error.stack))), ["callback"] : null }), ({ })))], [_e], ({ }))))}), "Copy")))))), (jsx("hr", ({css:({ ["borderColor"] : "currentColor", ["opacity"] : "0.25" })}))), (jsx(ReactRouterLink, ({to:"https://reflex.dev"}), (jsx("div", ({css:({ ["display"] : "flex", ["alignItems"] : "baseline", ["justifyContent"] : "center", ["fontFamily"] : "monospace", ["--default-font-family"] : "monospace", ["gap"] : "0.5rem" })}), "Built with ", (jsx("svg", ({"aria-label":"Reflex",css:({ ["fill"] : "currentColor" }),height:"12",role:"img",width:"56",xmlns:"http://www.w3.org/2000/svg"}), (jsx("path", ({d:"M0 11.5999V0.399902H8.96V4.8799H6.72V2.6399H2.24V4.8799H6.72V7.1199H2.24V11.5999H0ZM6.72 11.5999V7.1199H8.96V11.5999H6.72Z"}))), (jsx("path", ({d:"M11.2 11.5999V0.399902H17.92V2.6399H13.44V4.8799H17.92V7.1199H13.44V9.3599H17.92V11.5999H11.2Z"}))), (jsx("path", ({d:"M20.16 11.5999V0.399902H26.88V2.6399H22.4V4.8799H26.88V7.1199H22.4V11.5999H20.16Z"}))), (jsx("path", ({d:"M29.12 11.5999V0.399902H31.36V9.3599H35.84V11.5999H29.12Z"}))), (jsx("path", ({d:"M38.08 11.5999V0.399902H44.8V2.6399H40.32V4.8799H44.8V7.1199H40.32V9.3599H44.8V11.5999H38.08Z"}))), (jsx("path", ({d:"M47.04 4.8799V0.399902H49.28V4.8799H47.04ZM53.76 4.8799V0.399902H56V4.8799H53.76ZM49.28 7.1199V4.8799H53.76V7.1199H49.28ZM47.04 11.5999V7.1199H49.28V11.5999H47.04ZM53.76 11.5999V7.1199H56V11.5999H53.76Z"}))), (jsx("title", ({}), "Reflex"))))))))))))),""" """onError:((_error, _info) => (addEvents([(Event("reflex___state____state.reflex___state____frontend_event_exception_state.handle_frontend_exception", ({ ["info"] : ((((_error["name"]+": ")+_error["message"])+"\\n")+_error["stack"]), ["component_stack"] : _info["componentStack"] }), ({ })))], [_error, _info], ({ }))))""" "}," "jsx(RadixThemesColorModeProvider,{}," @@ -1382,16 +1383,11 @@ def test_app_wrap_compile_theme( "jsx(Fragment,{}," "jsx(MemoizedToastProvider,{},)," "jsx(Fragment,{}," - "children," - ")," - ")," - ")," - ")," - ")," - ")" + (",)" if react_strict_mode else "") + ")" - "}" + "children" + "))))))" + (")" if react_strict_mode else "") + ")" + "\n}" ) - assert expected in lines + assert expected.split(",") == function_app_definition.split(",") @pytest.mark.parametrize( @@ -1439,18 +1435,19 @@ def page(): app_js_contents = ( web_dir / constants.Dirs.PAGES / constants.PageNames.APP_ROOT ).read_text() - app_js_lines = [ - line.strip() for line in app_js_contents.splitlines() if line.strip() - ] - lines = "".join(app_js_lines) + function_app_definition = app_js_contents[ + app_js_contents.index("function AppWrap") : app_js_contents.index( + "export function Layout" + ) + ].strip() expected = ( - "function AppWrap({children}) {" - "const [addEvents, connectErrors] = useContext(EventLoopContext);" + "function AppWrap({children}) {\n" + "const [addEvents, connectErrors] = useContext(EventLoopContext);\n\n\n\n" "return (" + ("jsx(StrictMode,{}," if react_strict_mode else "") + "jsx(RadixThemesBox,{}," "jsx(ErrorBoundary,{" - """fallbackRender:((event_args) => (jsx("div", ({css:({ ["height"] : "100%", ["width"] : "100%", ["position"] : "absolute", ["backgroundColor"] : "#fff", ["color"] : "#000", ["display"] : "flex", ["alignItems"] : "center", ["justifyContent"] : "center" })}), (jsx("div", ({css:({ ["display"] : "flex", ["flexDirection"] : "column", ["gap"] : "1rem" })}), (jsx("div", ({css:({ ["display"] : "flex", ["flexDirection"] : "column", ["gap"] : "1rem", ["maxWidth"] : "50ch", ["border"] : "1px solid #888888", ["borderRadius"] : "0.25rem", ["padding"] : "1rem" })}), (jsx("h2", ({css:({ ["fontSize"] : "1.25rem", ["fontWeight"] : "bold" })}), (jsx(Fragment, ({}), "An error occurred while rendering this page.")))), (jsx("p", ({css:({ ["opacity"] : "0.75" })}), (jsx(Fragment, ({}), "This is an error with the application itself.")))), (jsx("details", ({}), (jsx("summary", ({css:({ ["padding"] : "0.5rem" })}), (jsx(Fragment, ({}), "Error message")))), (jsx("div", ({css:({ ["width"] : "100%", ["maxHeight"] : "50vh", ["overflow"] : "auto", ["background"] : "#000", ["color"] : "#fff", ["borderRadius"] : "0.25rem" })}), (jsx("div", ({css:({ ["padding"] : "0.5rem", ["width"] : "fit-content" })}), (jsx("pre", ({}), (jsx(Fragment, ({}), event_args.error.name + \': \' + event_args.error.message + \'\\n\' + event_args.error.stack)))))))), (jsx("button", ({css:({ ["padding"] : "0.35rem 0.75rem", ["margin"] : "0.5rem", ["background"] : "#fff", ["color"] : "#000", ["border"] : "1px solid #000", ["borderRadius"] : "0.25rem", ["fontWeight"] : "bold" }),onClick:((_e) => (addEvents([(Event("_call_function", ({ ["function"] : (() => (navigator["clipboard"]["writeText"](event_args.error.name + \': \' + event_args.error.message + \'\\n\' + event_args.error.stack))), ["callback"] : null }), ({ })))], [_e], ({ }))))}), (jsx(Fragment, ({}), "Copy")))))))), (jsx("hr", ({css:({ ["borderColor"] : "currentColor", ["opacity"] : "0.25" })}))), (jsx(ReactRouterLink, ({to:"https://reflex.dev"}), (jsx("div", ({css:({ ["display"] : "flex", ["alignItems"] : "baseline", ["justifyContent"] : "center", ["fontFamily"] : "monospace", ["--default-font-family"] : "monospace", ["gap"] : "0.5rem" })}), (jsx(Fragment, ({}), "Built with ")), (jsx("svg", ({"aria-label":"Reflex",css:({ ["fill"] : "currentColor" }),height:"12",role:"img",width:"56",xmlns:"http://www.w3.org/2000/svg"}), (jsx("path", ({d:"M0 11.5999V0.399902H8.96V4.8799H6.72V2.6399H2.24V4.8799H6.72V7.1199H2.24V11.5999H0ZM6.72 11.5999V7.1199H8.96V11.5999H6.72Z"}))), (jsx("path", ({d:"M11.2 11.5999V0.399902H17.92V2.6399H13.44V4.8799H17.92V7.1199H13.44V9.3599H17.92V11.5999H11.2Z"}))), (jsx("path", ({d:"M20.16 11.5999V0.399902H26.88V2.6399H22.4V4.8799H26.88V7.1199H22.4V11.5999H20.16Z"}))), (jsx("path", ({d:"M29.12 11.5999V0.399902H31.36V9.3599H35.84V11.5999H29.12Z"}))), (jsx("path", ({d:"M38.08 11.5999V0.399902H44.8V2.6399H40.32V4.8799H44.8V7.1199H40.32V9.3599H44.8V11.5999H38.08Z"}))), (jsx("path", ({d:"M47.04 4.8799V0.399902H49.28V4.8799H47.04ZM53.76 4.8799V0.399902H56V4.8799H53.76ZM49.28 7.1199V4.8799H53.76V7.1199H49.28ZM47.04 11.5999V7.1199H49.28V11.5999H47.04ZM53.76 11.5999V7.1199H56V11.5999H53.76Z"}))), (jsx("title", ({}), (jsx(Fragment, ({}), "Reflex"))))))))))))))),""" + """fallbackRender:((event_args) => (jsx("div", ({css:({ ["height"] : "100%", ["width"] : "100%", ["position"] : "absolute", ["backgroundColor"] : "#fff", ["color"] : "#000", ["display"] : "flex", ["alignItems"] : "center", ["justifyContent"] : "center" })}), (jsx("div", ({css:({ ["display"] : "flex", ["flexDirection"] : "column", ["gap"] : "1rem" })}), (jsx("div", ({css:({ ["display"] : "flex", ["flexDirection"] : "column", ["gap"] : "1rem", ["maxWidth"] : "50ch", ["border"] : "1px solid #888888", ["borderRadius"] : "0.25rem", ["padding"] : "1rem" })}), (jsx("h2", ({css:({ ["fontSize"] : "1.25rem", ["fontWeight"] : "bold" })}), "An error occurred while rendering this page.")), (jsx("p", ({css:({ ["opacity"] : "0.75" })}), "This is an error with the application itself.")), (jsx("details", ({}), (jsx("summary", ({css:({ ["padding"] : "0.5rem" })}), "Error message")), (jsx("div", ({css:({ ["width"] : "100%", ["maxHeight"] : "50vh", ["overflow"] : "auto", ["background"] : "#000", ["color"] : "#fff", ["borderRadius"] : "0.25rem" })}), (jsx("div", ({css:({ ["padding"] : "0.5rem", ["width"] : "fit-content" })}), (jsx("pre", ({}), event_args.error.name + \': \' + event_args.error.message + \'\\n\' + event_args.error.stack)))))), (jsx("button", ({css:({ ["padding"] : "0.35rem 0.75rem", ["margin"] : "0.5rem", ["background"] : "#fff", ["color"] : "#000", ["border"] : "1px solid #000", ["borderRadius"] : "0.25rem", ["fontWeight"] : "bold" }),onClick:((_e) => (addEvents([(Event("_call_function", ({ ["function"] : (() => (navigator["clipboard"]["writeText"](event_args.error.name + \': \' + event_args.error.message + \'\\n\' + event_args.error.stack))), ["callback"] : null }), ({ })))], [_e], ({ }))))}), "Copy")))))), (jsx("hr", ({css:({ ["borderColor"] : "currentColor", ["opacity"] : "0.25" })}))), (jsx(ReactRouterLink, ({to:"https://reflex.dev"}), (jsx("div", ({css:({ ["display"] : "flex", ["alignItems"] : "baseline", ["justifyContent"] : "center", ["fontFamily"] : "monospace", ["--default-font-family"] : "monospace", ["gap"] : "0.5rem" })}), "Built with ", (jsx("svg", ({"aria-label":"Reflex",css:({ ["fill"] : "currentColor" }),height:"12",role:"img",width:"56",xmlns:"http://www.w3.org/2000/svg"}), (jsx("path", ({d:"M0 11.5999V0.399902H8.96V4.8799H6.72V2.6399H2.24V4.8799H6.72V7.1199H2.24V11.5999H0ZM6.72 11.5999V7.1199H8.96V11.5999H6.72Z"}))), (jsx("path", ({d:"M11.2 11.5999V0.399902H17.92V2.6399H13.44V4.8799H17.92V7.1199H13.44V9.3599H17.92V11.5999H11.2Z"}))), (jsx("path", ({d:"M20.16 11.5999V0.399902H26.88V2.6399H22.4V4.8799H26.88V7.1199H22.4V11.5999H20.16Z"}))), (jsx("path", ({d:"M29.12 11.5999V0.399902H31.36V9.3599H35.84V11.5999H29.12Z"}))), (jsx("path", ({d:"M38.08 11.5999V0.399902H44.8V2.6399H40.32V4.8799H44.8V7.1199H40.32V9.3599H44.8V11.5999H38.08Z"}))), (jsx("path", ({d:"M47.04 4.8799V0.399902H49.28V4.8799H47.04ZM53.76 4.8799V0.399902H56V4.8799H53.76ZM49.28 7.1199V4.8799H53.76V7.1199H49.28ZM47.04 11.5999V7.1199H49.28V11.5999H47.04ZM53.76 11.5999V7.1199H56V11.5999H53.76Z"}))), (jsx("title", ({}), "Reflex"))))))))))))),""" """onError:((_error, _info) => (addEvents([(Event("reflex___state____state.reflex___state____frontend_event_exception_state.handle_frontend_exception", ({ ["info"] : ((((_error["name"]+": ")+_error["message"])+"\\n")+_error["stack"]), ["component_stack"] : _info["componentStack"] }), ({ })))], [_error, _info], ({ }))))""" "}," 'jsx(RadixThemesText,{as:"p"},' @@ -1462,9 +1459,9 @@ def page(): "jsx(MemoizedToastProvider,{},)," "jsx(Fragment,{}," "children" - ",),),),),),),)" + (",)" if react_strict_mode else "") + ")))))))" + (")" if react_strict_mode else "") + "))\n}" ) - assert expected in lines + assert expected.split(",") == function_app_definition.split(",") def test_app_state_determination(): diff --git a/tests/units/utils/test_format.py b/tests/units/utils/test_format.py index d6a4c40eaf9..e377420449d 100644 --- a/tests/units/utils/test_format.py +++ b/tests/units/utils/test_format.py @@ -313,13 +313,16 @@ def test_format_route(route: str, expected: str): ( "state__state.value", [ - [LiteralVar.create(1), LiteralVar.create("red")], - [LiteralVar.create(2), LiteralVar.create(3), LiteralVar.create("blue")], - [TestState.mapping, TestState.num1], - [ - LiteralVar.create(f"{TestState.map_key}-key"), + ([LiteralVar.create(1)], LiteralVar.create("red")), + ( + [LiteralVar.create(2), LiteralVar.create(3)], + LiteralVar.create("blue"), + ), + ([TestState.mapping], TestState.num1), + ( + [LiteralVar.create(f"{TestState.map_key}-key")], LiteralVar.create("return-key"), - ], + ), ], LiteralVar.create("yellow"), '(() => { switch (JSON.stringify(state__state.value)) {case JSON.stringify(1): return ("red"); break;case JSON.stringify(2): case JSON.stringify(3): ' @@ -331,7 +334,7 @@ def test_format_route(route: str, expected: str): ) def test_format_match( condition: str, - match_cases: list[list[Var]], + match_cases: list[tuple[list[Var], Var]], default: Var, expected: str, ): diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index 85ebdacb201..fdfba6e42ae 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -316,7 +316,7 @@ def test_unsupported_literals(cls: type): ], ) def test_create_config(app_name: str, expected_config_name: str, mocker: MockerFixture): - """Test templates.RXCONFIG is formatted with correct app name and config class name. + """Test templates.rxconfig_template is formatted with correct app name and config class name. Args: app_name: App name. @@ -324,11 +324,9 @@ def test_create_config(app_name: str, expected_config_name: str, mocker: MockerF mocker: Mocker object. """ mocker.patch("pathlib.Path.write_text") - tmpl_mock = mocker.patch("reflex.compiler.templates.RXCONFIG") + tmpl_mock = mocker.patch("reflex.compiler.templates.rxconfig_template") templates.create_config(app_name) - tmpl_mock.render.assert_called_with( - app_name=app_name, config_name=expected_config_name - ) + tmpl_mock.assert_called_with(app_name=app_name) @pytest.fixture diff --git a/uv.lock b/uv.lock index 7a78aa3d892..282a87c6942 100644 --- a/uv.lock +++ b/uv.lock @@ -1707,7 +1707,6 @@ dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, { name = "httpx" }, - { name = "jinja2" }, { name = "packaging" }, { name = "platformdirs" }, { name = "psutil", marker = "sys_platform == 'win32'" }, @@ -1770,7 +1769,6 @@ requires-dist = [ { name = "click", specifier = ">=8.2" }, { name = "granian", extras = ["reload"], specifier = ">=2.4.0" }, { name = "httpx", specifier = ">=0.23.3,<1.0" }, - { name = "jinja2", specifier = ">=3.1.2,<4.0" }, { name = "packaging", specifier = ">=24.2,<26" }, { name = "platformdirs", specifier = ">=4.3.7,<5.0" }, { name = "psutil", marker = "sys_platform == 'win32'", specifier = ">=7.0.0,<8.0" },