This commit is contained in:
Khaleel Al-Adhami 2024-10-04 14:12:10 -07:00
commit 8085d7e203
42 changed files with 1094 additions and 603 deletions

View File

@ -3,8 +3,8 @@
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
from utils import send_data_to_posthog
@ -28,7 +28,7 @@ def insert_benchmarking_data(
send_data_to_posthog("lighthouse_benchmark", properties)
def get_lighthouse_scores(directory_path: str) -> dict:
def get_lighthouse_scores(directory_path: str | Path) -> dict:
"""Extracts the Lighthouse scores from the JSON files in the specified directory.
Args:
@ -38,24 +38,21 @@ def get_lighthouse_scores(directory_path: str) -> dict:
dict: The Lighthouse scores.
"""
scores = {}
directory_path = Path(directory_path)
try:
for filename in os.listdir(directory_path):
if filename.endswith(".json") and filename != "manifest.json":
file_path = os.path.join(directory_path, filename)
with open(file_path, "r") as file:
data = json.load(file)
# Extract scores and add them to the dictionary with the filename as key
scores[data["finalUrl"].replace("http://localhost:3000/", "/")] = {
"performance_score": data["categories"]["performance"]["score"],
"accessibility_score": data["categories"]["accessibility"][
"score"
],
"best_practices_score": data["categories"]["best-practices"][
"score"
],
"seo_score": data["categories"]["seo"]["score"],
}
for filename in directory_path.iterdir():
if filename.suffix == ".json" and filename.stem != "manifest":
file_path = directory_path / filename
data = json.loads(file_path.read_text())
# Extract scores and add them to the dictionary with the filename as key
scores[data["finalUrl"].replace("http://localhost:3000/", "/")] = {
"performance_score": data["categories"]["performance"]["score"],
"accessibility_score": data["categories"]["accessibility"]["score"],
"best_practices_score": data["categories"]["best-practices"][
"score"
],
"seo_score": data["categories"]["seo"]["score"],
}
except Exception as e:
return {"error": e}

View File

@ -2,11 +2,12 @@
import argparse
import os
from pathlib import Path
from utils import get_directory_size, get_python_version, send_data_to_posthog
def get_package_size(venv_path, os_name):
def get_package_size(venv_path: Path, os_name):
"""Get the size of a specified package.
Args:
@ -26,14 +27,12 @@ def get_package_size(venv_path, os_name):
is_windows = "windows" in os_name
full_path = (
["lib", f"python{python_version}", "site-packages"]
package_dir: Path = (
venv_path / "lib" / f"python{python_version}" / "site-packages"
if not is_windows
else ["Lib", "site-packages"]
else venv_path / "Lib" / "site-packages"
)
package_dir = os.path.join(venv_path, *full_path)
if not os.path.exists(package_dir):
if not package_dir.exists():
raise ValueError(
"Error: Virtual environment does not exist or is not activated."
)
@ -63,9 +62,9 @@ def insert_benchmarking_data(
path: The path to the dir or file to check size.
"""
if "./dist" in path:
size = get_directory_size(path)
size = get_directory_size(Path(path))
else:
size = get_package_size(path, os_type_version)
size = get_package_size(Path(path), os_type_version)
# Prepare the event data
properties = {

View File

@ -2,6 +2,7 @@
import argparse
import os
from pathlib import Path
from utils import get_directory_size, send_data_to_posthog
@ -28,7 +29,7 @@ def insert_benchmarking_data(
pr_id: The id of the PR.
path: The path to the dir or file to check size.
"""
size = get_directory_size(path)
size = get_directory_size(Path(path))
# Prepare the event data
properties = {

View File

@ -2,12 +2,13 @@
import os
import subprocess
from pathlib import Path
import httpx
from httpx import HTTPError
def get_python_version(venv_path, os_name):
def get_python_version(venv_path: Path, os_name):
"""Get the python version of python in a virtual env.
Args:
@ -18,13 +19,13 @@ def get_python_version(venv_path, os_name):
The python version.
"""
python_executable = (
os.path.join(venv_path, "bin", "python")
venv_path / "bin" / "python"
if "windows" not in os_name
else os.path.join(venv_path, "Scripts", "python.exe")
else venv_path / "Scripts" / "python.exe"
)
try:
output = subprocess.check_output(
[python_executable, "--version"], stderr=subprocess.STDOUT
[str(python_executable), "--version"], stderr=subprocess.STDOUT
)
python_version = output.decode("utf-8").strip().split()[1]
return ".".join(python_version.split(".")[:-1])
@ -32,7 +33,7 @@ def get_python_version(venv_path, os_name):
return None
def get_directory_size(directory):
def get_directory_size(directory: Path):
"""Get the size of a directory in bytes.
Args:
@ -44,8 +45,8 @@ def get_directory_size(directory):
total_size = 0
for dirpath, _, filenames in os.walk(directory):
for f in filenames:
fp = os.path.join(dirpath, f)
total_size += os.path.getsize(fp)
fp = Path(dirpath) / f
total_size += fp.stat().st_size
return total_size

96
poetry.lock generated
View File

@ -516,21 +516,6 @@ files = [
{file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"},
]
[[package]]
name = "dill"
version = "0.3.8"
description = "serialize all of Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"},
{file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"},
]
[package.extras]
graph = ["objgraph (>=1.7.2)"]
profile = ["gprof2dot (>=2022.7.29)"]
[[package]]
name = "distlib"
version = "0.3.8"
@ -719,13 +704,13 @@ files = [
[[package]]
name = "httpcore"
version = "1.0.5"
version = "1.0.6"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
files = [
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
{file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"},
{file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"},
]
[package.dependencies]
@ -736,7 +721,7 @@ h11 = ">=0.13,<0.15"
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<0.26.0)"]
trio = ["trio (>=0.22.0,<1.0)"]
[[package]]
name = "httpx"
@ -863,21 +848,25 @@ test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-c
[[package]]
name = "jaraco-functools"
version = "4.0.2"
version = "4.1.0"
description = "Functools like those found in stdlib"
optional = false
python-versions = ">=3.8"
files = [
{file = "jaraco.functools-4.0.2-py3-none-any.whl", hash = "sha256:c9d16a3ed4ccb5a889ad8e0b7a343401ee5b2a71cee6ed192d3f68bc351e94e3"},
{file = "jaraco_functools-4.0.2.tar.gz", hash = "sha256:3460c74cd0d32bf82b9576bbb3527c4364d5b27a21f5158a62aed6c4b42e23f5"},
{file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"},
{file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"},
]
[package.dependencies]
more-itertools = "*"
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
test = ["jaraco.classes", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"]
type = ["pytest-mypy"]
[[package]]
name = "jeepney"
@ -1788,13 +1777,13 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyproject-hooks"
version = "1.1.0"
version = "1.2.0"
description = "Wrappers to call pyproject.toml-based build backend hooks."
optional = false
python-versions = ">=3.7"
files = [
{file = "pyproject_hooks-1.1.0-py3-none-any.whl", hash = "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2"},
{file = "pyproject_hooks-1.1.0.tar.gz", hash = "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965"},
{file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"},
{file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"},
]
[[package]]
@ -1992,13 +1981,13 @@ docs = ["sphinx"]
[[package]]
name = "python-multipart"
version = "0.0.10"
version = "0.0.12"
description = "A streaming multipart parser for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "python_multipart-0.0.10-py3-none-any.whl", hash = "sha256:2b06ad9e8d50c7a8db80e3b56dab590137b323410605af2be20d62a5f1ba1dc8"},
{file = "python_multipart-0.0.10.tar.gz", hash = "sha256:46eb3c6ce6fdda5fb1a03c7e11d490e407c6930a2703fe7aef4da71c374688fa"},
{file = "python_multipart-0.0.12-py3-none-any.whl", hash = "sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf"},
{file = "python_multipart-0.0.12.tar.gz", hash = "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb"},
]
[[package]]
@ -2143,31 +2132,31 @@ md = ["cmarkgfm (>=0.8.0)"]
[[package]]
name = "redis"
version = "5.0.8"
version = "5.1.0"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"},
{file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"},
{file = "redis-5.1.0-py3-none-any.whl", hash = "sha256:fd4fccba0d7f6aa48c58a78d76ddb4afc698f5da4a2c1d03d916e4fd7ab88cdd"},
{file = "redis-5.1.0.tar.gz", hash = "sha256:b756df1e4a3858fcc0ef861f3fc53623a96c41e2b1f5304e09e0fe758d333d40"},
]
[package.dependencies]
async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""}
[package.extras]
hiredis = ["hiredis (>1.0.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
hiredis = ["hiredis (>=3.0.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"]
[[package]]
name = "reflex-chakra"
version = "0.6.0"
version = "0.6.1"
description = "reflex using chakra components"
optional = false
python-versions = "<4.0,>=3.8"
files = [
{file = "reflex_chakra-0.6.0-py3-none-any.whl", hash = "sha256:eca1593fca67289e05591dd21fbcc8632c119d64a08bdc41fd995055a114cc91"},
{file = "reflex_chakra-0.6.0.tar.gz", hash = "sha256:db1c7b48f1ba547bf91e5af103fce6fc7191d7225b414ebfbada7d983e33dd87"},
{file = "reflex_chakra-0.6.1-py3-none-any.whl", hash = "sha256:824d461264b6d2c836ba4a2a430e677a890b82e83da149672accfc58786442fa"},
{file = "reflex_chakra-0.6.1.tar.gz", hash = "sha256:4b9b3c8bada19cbb4d1b8d8bc4ab0460ec008a91f380010c34d416d5b613dc07"},
]
[package.dependencies]
@ -2247,18 +2236,19 @@ idna2008 = ["idna"]
[[package]]
name = "rich"
version = "13.8.1"
version = "13.9.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.7.0"
python-versions = ">=3.8.0"
files = [
{file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"},
{file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"},
{file = "rich-13.9.1-py3-none-any.whl", hash = "sha256:b340e739f30aa58921dc477b8adaa9ecdb7cecc217be01d93730ee1bc8aa83be"},
{file = "rich-13.9.1.tar.gz", hash = "sha256:097cffdf85db1babe30cc7deba5ab3a29e1b9885047dab24c57e9a7f8a9c1466"},
]
[package.dependencies]
markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0"
typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""}
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
@ -2595,13 +2585,13 @@ files = [
[[package]]
name = "tomli"
version = "2.0.1"
version = "2.0.2"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
{file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"},
{file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"},
]
[[package]]
@ -2734,13 +2724,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "uvicorn"
version = "0.30.6"
version = "0.31.0"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
files = [
{file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"},
{file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"},
{file = "uvicorn-0.31.0-py3-none-any.whl", hash = "sha256:cac7be4dd4d891c363cd942160a7b02e69150dcbc7a36be04d5f4af4b17c8ced"},
{file = "uvicorn-0.31.0.tar.gz", hash = "sha256:13bc21373d103859f68fe739608e2eb054a816dea79189bc3ca08ea89a275906"},
]
[package.dependencies]
@ -2753,13 +2743,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
[[package]]
name = "virtualenv"
version = "20.26.5"
version = "20.26.6"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"},
{file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"},
{file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"},
{file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"},
]
[package.dependencies]
@ -3011,4 +3001,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "adccd071775567aeefe219261aeb9e222906c865745f03edb1e770edc79c44ac"
content-hash = "e4b462ebfae90550ba7fa49b360d7110c0d344ee616c23989c22d866ef8f6f31"

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "reflex"
version = "0.6.2dev1"
version = "0.6.3dev1"
description = "Web apps in pure Python."
license = "Apache-2.0"
authors = [
@ -27,7 +27,6 @@ packages = [
[tool.poetry.dependencies]
python = "^3.9"
dill = ">=0.3.8,<0.4"
fastapi = ">=0.96.0,!=0.111.0,!=0.111.1"
gunicorn = ">=20.1.0,<24.0"
jinja2 = ">=3.1.2,<4.0"

View File

@ -7,10 +7,9 @@ import '/styles/styles.css'
{% block declaration %}
import { EventLoopProvider, StateProvider, defaultColorMode } from "/utils/context.js";
import { ThemeProvider } from 'next-themes'
import * as React from "react";
import * as utils_context from "/utils/context.js";
import * as utils_state from "/utils/state.js";
import * as radix from "@radix-ui/themes";
{% for library_alias, library_path in window_libraries %}
import * as {{library_alias}} from "{{library_path}}";
{% endfor %}
{% for custom_code in custom_codes %}
{{custom_code}}
@ -33,10 +32,9 @@ export default function MyApp({ Component, pageProps }) {
React.useEffect(() => {
// Make contexts and state objects available globally for dynamic eval'd components
let windowImports = {
"react": React,
"@radix-ui/themes": radix,
"/utils/context": utils_context,
"/utils/state": utils_state,
{% for library_alias, library_path in window_libraries %}
"{{library_path}}": {{library_alias}},
{% endfor %}
};
window["__reflex"] = windowImports;
}, []);

View File

@ -544,13 +544,19 @@ export const uploadFiles = async (
/**
* Create an event object.
* @param name The name of the event.
* @param payload The payload of the event.
* @param handler The client handler to process event.
* @param {string} name The name of the event.
* @param {Object.<string, Any>} payload The payload of the event.
* @param {Object.<string, (number|boolean)>} event_actions The actions to take on the event.
* @param {string} handler The client handler to process event.
* @returns The event object.
*/
export const Event = (name, payload = {}, handler = null) => {
return { name, payload, handler };
export const Event = (
name,
payload = {},
event_actions = {},
handler = null
) => {
return { name, payload, handler, event_actions };
};
/**
@ -676,6 +682,12 @@ export const useEventLoop = (
if (!(args instanceof Array)) {
args = [args];
}
event_actions = events.reduce(
(acc, e) => ({ ...acc, ...e.event_actions }),
event_actions ?? {}
);
const _e = args.filter((o) => o?.preventDefault !== undefined)[0];
if (event_actions?.preventDefault && _e?.preventDefault) {

View File

@ -458,25 +458,12 @@ class App(MiddlewareMixin, LifespanMixin, Base):
The generated component.
Raises:
VarOperationTypeError: When an invalid component var related function is passed.
TypeError: When an invalid component function is passed.
exceptions.MatchTypeError: If the return types of match cases in rx.match are different.
"""
from reflex.utils.exceptions import VarOperationTypeError
try:
return component if isinstance(component, Component) else component()
except exceptions.MatchTypeError:
raise
except TypeError as e:
message = str(e)
if "Var" in message:
raise VarOperationTypeError(
"You may be trying to use an invalid Python function on a state var. "
"When referencing a var inside your render code, only limited var operations are supported. "
"See the var operation docs here: https://reflex.dev/docs/vars/var-operations/"
) from e
raise e
def add_page(
self,
@ -1567,7 +1554,9 @@ class EventNamespace(AsyncNamespace):
"""
fields = json.loads(data)
# Get the event.
event = Event(**{k: v for k, v in fields.items() if k != "handler"})
event = Event(
**{k: v for k, v in fields.items() if k not in ("handler", "event_actions")}
)
self.token_to_sid[event.token] = sid
self.sid_to_token[sid] = event.token

View File

@ -41,6 +41,20 @@ def _compile_document_root(root: Component) -> str:
)
def _normalize_library_name(lib: str) -> str:
"""Normalize the library name.
Args:
lib: The library name to normalize.
Returns:
The normalized library name.
"""
if lib == "react":
return "React"
return lib.replace("@", "").replace("/", "_").replace("-", "_")
def _compile_app(app_root: Component) -> str:
"""Compile the app template component.
@ -50,10 +64,20 @@ def _compile_app(app_root: Component) -> str:
Returns:
The compiled app.
"""
from reflex.components.dynamic import bundled_libraries
window_libraries = [
(_normalize_library_name(name), name) for name in bundled_libraries
] + [
("utils_context", f"/{constants.Dirs.UTILS}/context"),
("utils_state", f"/{constants.Dirs.UTILS}/state"),
]
return templates.APP_ROOT.render(
imports=utils.compile_imports(app_root._get_all_imports()),
custom_codes=app_root._get_all_custom_code(),
hooks={**app_root._get_all_hooks_internal(), **app_root._get_all_hooks()},
window_libraries=window_libraries,
render=app_root.render(),
)
@ -172,7 +196,7 @@ def _compile_root_stylesheet(stylesheets: list[str]) -> str:
stylesheet_full_path = (
Path.cwd() / constants.Dirs.APP_ASSETS / stylesheet.strip("/")
)
if not os.path.exists(stylesheet_full_path):
if not stylesheet_full_path.exists():
raise FileNotFoundError(
f"The stylesheet file {stylesheet_full_path} does not exist."
)

View File

@ -2,7 +2,6 @@
from __future__ import annotations
import os
from pathlib import Path
from typing import Any, Callable, Dict, Optional, Type, Union
from urllib.parse import urlparse
@ -457,16 +456,16 @@ def add_meta(
return page
def write_page(path: str, code: str):
def write_page(path: str | Path, code: str):
"""Write the given code to the given path.
Args:
path: The path to write the code to.
code: The code to write.
"""
path_ops.mkdir(os.path.dirname(path))
with open(path, "w", encoding="utf-8") as f:
f.write(code)
path = Path(path)
path_ops.mkdir(path.parent)
path.write_text(code, encoding="utf-8")
def empty_dir(path: str | Path, keep_files: list[str] | None = None):

View File

@ -38,8 +38,10 @@ from reflex.constants import (
)
from reflex.event import (
EventChain,
EventChainVar,
EventHandler,
EventSpec,
EventVar,
call_event_fn,
call_event_handler,
get_handler_args,
@ -514,7 +516,7 @@ class Component(BaseComponent, ABC):
Var,
EventHandler,
EventSpec,
List[Union[EventHandler, EventSpec]],
List[Union[EventHandler, EventSpec, EventVar]],
Callable,
],
) -> Union[EventChain, Var]:
@ -532,11 +534,16 @@ class Component(BaseComponent, ABC):
"""
# If it's an event chain var, return it.
if isinstance(value, Var):
if value._var_type is not EventChain:
if isinstance(value, EventChainVar):
return value
elif isinstance(value, EventVar):
value = [value]
elif issubclass(value._var_type, (EventChain, EventSpec)):
return self._create_event_chain(args_spec, value.guess_type())
else:
raise ValueError(
f"Invalid event chain: {repr(value)} of type {type(value)}"
f"Invalid event chain: {str(value)} of type {value._var_type}"
)
return value
elif isinstance(value, EventChain):
# Trust that the caller knows what they're doing passing an EventChain directly
return value
@ -547,7 +554,7 @@ class Component(BaseComponent, ABC):
# If the input is a list of event handlers, create an event chain.
if isinstance(value, List):
events: list[EventSpec] = []
events: List[Union[EventSpec, EventVar]] = []
for v in value:
if isinstance(v, (EventHandler, EventSpec)):
# Call the event handler to get the event.
@ -561,6 +568,8 @@ class Component(BaseComponent, ABC):
"lambda inside an EventChain list."
)
events.extend(result)
elif isinstance(v, EventVar):
events.append(v)
else:
raise ValueError(f"Invalid event: {v}")
@ -570,32 +579,30 @@ class Component(BaseComponent, ABC):
if isinstance(result, Var):
# Recursively call this function if the lambda returned an EventChain Var.
return self._create_event_chain(args_spec, result)
events = result
events = [*result]
# Otherwise, raise an error.
else:
raise ValueError(f"Invalid event chain: {value}")
# Add args to the event specs if necessary.
events = [e.with_args(get_handler_args(e)) for e in events]
# Collect event_actions from each spec
event_actions = {}
for e in events:
event_actions.update(e.event_actions)
events = [
(e.with_args(get_handler_args(e)) if isinstance(e, EventSpec) else e)
for e in events
]
# Return the event chain.
if isinstance(args_spec, Var):
return EventChain(
events=events,
args_spec=None,
event_actions=event_actions,
event_actions={},
)
else:
return EventChain(
events=events,
args_spec=args_spec,
event_actions=event_actions,
event_actions={},
)
def get_event_triggers(self) -> Dict[str, Any]:
@ -1030,8 +1037,11 @@ class Component(BaseComponent, ABC):
elif isinstance(event, EventChain):
event_args = []
for spec in event.events:
for args in spec.args:
event_args.extend(args)
if isinstance(spec, EventSpec):
for args in spec.args:
event_args.extend(args)
else:
event_args.append(spec)
yield event_trigger, event_args
def _get_vars(self, include_children: bool = False) -> list[Var]:
@ -1105,8 +1115,12 @@ class Component(BaseComponent, ABC):
for trigger in self.event_triggers.values():
if isinstance(trigger, EventChain):
for event in trigger.events:
if event.handler.state_full_name:
return True
if isinstance(event, EventSpec):
if event.handler.state_full_name:
return True
else:
if event._var_state:
return True
elif isinstance(trigger, Var) and trigger._var_state:
return True
return False

View File

@ -1,12 +1,18 @@
"""Components that are dynamically generated on the backend."""
from typing import TYPE_CHECKING
from reflex import constants
from reflex.utils import imports
from reflex.utils.exceptions import DynamicComponentMissingLibrary
from reflex.utils.format import format_library_name
from reflex.utils.serializers import serializer
from reflex.vars import Var, get_unique_variable_name
from reflex.vars.base import VarData, transform
if TYPE_CHECKING:
from reflex.components.component import Component
def get_cdn_url(lib: str) -> str:
"""Get the CDN URL for a library.
@ -20,6 +26,27 @@ def get_cdn_url(lib: str) -> str:
return f"https://cdn.jsdelivr.net/npm/{lib}" + "/+esm"
bundled_libraries = {
"react",
"@radix-ui/themes",
"@emotion/react",
}
def bundle_library(component: "Component"):
"""Bundle a library with the component.
Args:
component: The component to bundle the library with.
Raises:
DynamicComponentMissingLibrary: Raised when a dynamic component is missing a library.
"""
if component.library is None:
raise DynamicComponentMissingLibrary("Component must have a library to bundle.")
bundled_libraries.add(format_library_name(component.library))
def load_dynamic_serializer():
"""Load the serializer for dynamic components."""
# Causes a circular import, so we import here.
@ -58,10 +85,7 @@ def load_dynamic_serializer():
)
] = None
libs_in_window = [
"react",
"@radix-ui/themes",
]
libs_in_window = bundled_libraries
imports = {}
for lib, names in component._get_all_imports().items():
@ -69,10 +93,7 @@ def load_dynamic_serializer():
if (
not lib.startswith((".", "/"))
and not lib.startswith("http")
and all(
formatted_lib_name != lib_in_window
for lib_in_window in libs_in_window
)
and formatted_lib_name not in libs_in_window
):
imports[get_cdn_url(lib)] = names
else:
@ -110,7 +131,14 @@ def load_dynamic_serializer():
module_code_lines.insert(0, "const React = window.__reflex.react;")
return "//__reflex_evaluate\n" + "\n".join(module_code_lines)
return "\n".join(
[
"//__reflex_evaluate",
"/** @jsx jsx */",
"const { jsx } = window.__reflex['@emotion/react']",
*module_code_lines,
]
)
@transform
def evaluate_component(js_string: Var[str]) -> Var[Component]:

View File

@ -6,7 +6,8 @@ import importlib
import os
import sys
import urllib.parse
from typing import Any, Dict, List, Optional, Set
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Union
try:
import pydantic.v1 as pydantic
@ -188,7 +189,7 @@ class Config(Base):
telemetry_enabled: bool = True
# The bun path
bun_path: str = constants.Bun.DEFAULT_PATH
bun_path: Union[str, Path] = constants.Bun.DEFAULT_PATH
# List of origins that are allowed to connect to the backend API.
cors_allowed_origins: List[str] = ["*"]

View File

@ -6,6 +6,7 @@ import os
import platform
from enum import Enum
from importlib import metadata
from pathlib import Path
from types import SimpleNamespace
from platformdirs import PlatformDirs
@ -66,18 +67,19 @@ class Reflex(SimpleNamespace):
# Get directory value from enviroment variables if it exists.
_dir = os.environ.get("REFLEX_DIR", "")
DIR = _dir or (
# on windows, we use C:/Users/<username>/AppData/Local/reflex.
# on macOS, we use ~/Library/Application Support/reflex.
# on linux, we use ~/.local/share/reflex.
# If user sets REFLEX_DIR envroment variable use that instead.
PlatformDirs(MODULE_NAME, False).user_data_dir
DIR = Path(
_dir
or (
# on windows, we use C:/Users/<username>/AppData/Local/reflex.
# on macOS, we use ~/Library/Application Support/reflex.
# on linux, we use ~/.local/share/reflex.
# If user sets REFLEX_DIR envroment variable use that instead.
PlatformDirs(MODULE_NAME, False).user_data_dir
)
)
# The root directory of the reflex library.
ROOT_DIR = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
ROOT_DIR = Path(__file__).parents[2]
RELEASES_URL = f"https://api.github.com/repos/reflex-dev/templates/releases"
@ -125,11 +127,11 @@ class Templates(SimpleNamespace):
"""Folders used by the template system of Reflex."""
# The template directory used during reflex init.
BASE = os.path.join(Reflex.ROOT_DIR, Reflex.MODULE_NAME, ".templates")
BASE = Reflex.ROOT_DIR / Reflex.MODULE_NAME / ".templates"
# The web subdirectory of the template directory.
WEB_TEMPLATE = os.path.join(BASE, "web")
WEB_TEMPLATE = BASE / "web"
# The jinja template directory.
JINJA_TEMPLATE = os.path.join(BASE, "jinja")
JINJA_TEMPLATE = BASE / "jinja"
# Where the code for the templates is stored.
CODE = "code"
@ -191,6 +193,14 @@ class LogLevel(str, Enum):
levels = list(LogLevel)
return levels.index(self) <= levels.index(other)
def subprocess_level(self):
"""Return the log level for the subprocess.
Returns:
The log level for the subprocess
"""
return self if self != LogLevel.DEFAULT else LogLevel.WARNING
# Server socket configuration variables
POLLING_MAX_HTTP_BUFFER_SIZE = 1000 * 1000

View File

@ -1,6 +1,7 @@
"""Config constants."""
import os
from pathlib import Path
from types import SimpleNamespace
from reflex.constants.base import Dirs, Reflex
@ -17,9 +18,7 @@ class Config(SimpleNamespace):
# The name of the reflex config module.
MODULE = "rxconfig"
# The python config file.
FILE = f"{MODULE}{Ext.PY}"
# The previous config file.
PREVIOUS_FILE = f"pcconfig{Ext.PY}"
FILE = Path(f"{MODULE}{Ext.PY}")
class Expiration(SimpleNamespace):
@ -37,7 +36,7 @@ class GitIgnore(SimpleNamespace):
"""Gitignore constants."""
# The gitignore file.
FILE = ".gitignore"
FILE = Path(".gitignore")
# Files to gitignore.
DEFAULTS = {Dirs.WEB, "*.db", "__pycache__/", "*.py[cod]", "assets/external/"}

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
@ -11,9 +12,9 @@ class CustomComponents(SimpleNamespace):
# The name of the custom components source directory.
SRC_DIR = "custom_components"
# The name of the custom components pyproject.toml file.
PYPROJECT_TOML = "pyproject.toml"
PYPROJECT_TOML = Path("pyproject.toml")
# The name of the custom components package README file.
PACKAGE_README = "README.md"
PACKAGE_README = Path("README.md")
# The name of the custom components package .gitignore file.
PACKAGE_GITIGNORE = ".gitignore"
# The name of the distribution directory as result of a build.
@ -29,6 +30,6 @@ class CustomComponents(SimpleNamespace):
"testpypi": "https://test.pypi.org/legacy/",
}
# The .gitignore file for the custom component project.
FILE = ".gitignore"
FILE = Path(".gitignore")
# Files to gitignore.
DEFAULTS = {"__pycache__/", "*.py[cod]", "*.egg-info/", "dist/"}

View File

@ -2,7 +2,6 @@
from __future__ import annotations
import os
import platform
from types import SimpleNamespace
@ -40,11 +39,10 @@ class Bun(SimpleNamespace):
# Min Bun Version
MIN_VERSION = "0.7.0"
# The directory to store the bun.
ROOT_PATH = os.path.join(Reflex.DIR, "bun")
ROOT_PATH = Reflex.DIR / "bun"
# Default bun path.
DEFAULT_PATH = os.path.join(
ROOT_PATH, "bin", "bun" if not IS_WINDOWS else "bun.exe"
)
DEFAULT_PATH = ROOT_PATH / "bin" / ("bun" if not IS_WINDOWS else "bun.exe")
# URL to bun install script.
INSTALL_URL = "https://bun.sh/install"
# URL to windows install script.
@ -65,10 +63,10 @@ class Fnm(SimpleNamespace):
# The FNM version.
VERSION = "1.35.1"
# The directory to store fnm.
DIR = os.path.join(Reflex.DIR, "fnm")
DIR = Reflex.DIR / "fnm"
FILENAME = get_fnm_name()
# The fnm executable binary.
EXE = os.path.join(DIR, "fnm.exe" if IS_WINDOWS else "fnm")
EXE = DIR / ("fnm.exe" if IS_WINDOWS else "fnm")
# The URL to the fnm release binary
INSTALL_URL = (
@ -86,18 +84,19 @@ class Node(SimpleNamespace):
MIN_VERSION = "18.17.0"
# The node bin path.
BIN_PATH = os.path.join(
Fnm.DIR,
"node-versions",
f"v{VERSION}",
"installation",
"bin" if not IS_WINDOWS else "",
BIN_PATH = (
Fnm.DIR
/ "node-versions"
/ f"v{VERSION}"
/ "installation"
/ ("bin" if not IS_WINDOWS else "")
)
# The default path where node is installed.
PATH = os.path.join(BIN_PATH, "node.exe" if IS_WINDOWS else "node")
PATH = BIN_PATH / ("node.exe" if IS_WINDOWS else "node")
# The default path where npm is installed.
NPM_PATH = os.path.join(BIN_PATH, "npm")
NPM_PATH = BIN_PATH / "npm"
# The environment variable to use the system installed node.
USE_SYSTEM_VAR = "REFLEX_USE_SYSTEM_NODE"

View File

@ -36,7 +36,7 @@ POST_CUSTOM_COMPONENTS_GALLERY_TIMEOUT = 15
@contextmanager
def set_directory(working_directory: str):
def set_directory(working_directory: str | Path):
"""Context manager that sets the working directory.
Args:
@ -45,7 +45,8 @@ def set_directory(working_directory: str):
Yields:
Yield to the caller to perform operations in the working directory.
"""
current_directory = os.getcwd()
current_directory = Path.cwd()
working_directory = Path(working_directory)
try:
os.chdir(working_directory)
yield
@ -62,14 +63,14 @@ def _create_package_config(module_name: str, package_name: str):
"""
from reflex.compiler import templates
with open(CustomComponents.PYPROJECT_TOML, "w") as f:
f.write(
templates.CUSTOM_COMPONENTS_PYPROJECT_TOML.render(
module_name=module_name,
package_name=package_name,
reflex_version=constants.Reflex.VERSION,
)
pyproject = Path(CustomComponents.PYPROJECT_TOML)
pyproject.write_text(
templates.CUSTOM_COMPONENTS_PYPROJECT_TOML.render(
module_name=module_name,
package_name=package_name,
reflex_version=constants.Reflex.VERSION,
)
)
def _get_package_config(exit_on_fail: bool = True) -> dict:
@ -84,11 +85,11 @@ def _get_package_config(exit_on_fail: bool = True) -> dict:
Raises:
Exit: If the pyproject.toml file is not found.
"""
pyproject = Path(CustomComponents.PYPROJECT_TOML)
try:
with open(CustomComponents.PYPROJECT_TOML, "rb") as f:
return dict(tomlkit.load(f))
return dict(tomlkit.loads(pyproject.read_bytes()))
except (OSError, TOMLKitError) as ex:
console.error(f"Unable to read from pyproject.toml due to {ex}")
console.error(f"Unable to read from {pyproject} due to {ex}")
if exit_on_fail:
raise typer.Exit(code=1) from ex
raise
@ -103,17 +104,17 @@ def _create_readme(module_name: str, package_name: str):
"""
from reflex.compiler import templates
with open(CustomComponents.PACKAGE_README, "w") as f:
f.write(
templates.CUSTOM_COMPONENTS_README.render(
module_name=module_name,
package_name=package_name,
)
readme = Path(CustomComponents.PACKAGE_README)
readme.write_text(
templates.CUSTOM_COMPONENTS_README.render(
module_name=module_name,
package_name=package_name,
)
)
def _write_source_and_init_py(
custom_component_src_dir: str,
custom_component_src_dir: Path,
component_class_name: str,
module_name: str,
):
@ -126,27 +127,17 @@ def _write_source_and_init_py(
"""
from reflex.compiler import templates
with open(
os.path.join(
custom_component_src_dir,
f"{module_name}.py",
),
"w",
) as f:
f.write(
templates.CUSTOM_COMPONENTS_SOURCE.render(
component_class_name=component_class_name, module_name=module_name
)
module_path = custom_component_src_dir / f"{module_name}.py"
module_path.write_text(
templates.CUSTOM_COMPONENTS_SOURCE.render(
component_class_name=component_class_name, module_name=module_name
)
)
with open(
os.path.join(
custom_component_src_dir,
CustomComponents.INIT_FILE,
),
"w",
) as f:
f.write(templates.CUSTOM_COMPONENTS_INIT_FILE.render(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)
)
def _populate_demo_app(name_variants: NameVariants):
@ -192,7 +183,7 @@ def _get_default_library_name_parts() -> list[str]:
Returns:
The parts of default library name.
"""
current_dir_name = os.getcwd().split(os.path.sep)[-1]
current_dir_name = Path.cwd().name
cleaned_dir_name = re.sub("[^0-9a-zA-Z-_]+", "", current_dir_name).lower()
parts = [part for part in re.split("-|_", cleaned_dir_name) if part]
@ -345,7 +336,7 @@ def init(
console.set_log_level(loglevel)
if os.path.exists(CustomComponents.PYPROJECT_TOML):
if CustomComponents.PYPROJECT_TOML.exists():
console.error(f"A {CustomComponents.PYPROJECT_TOML} already exists. Aborting.")
typer.Exit(code=1)

View File

@ -4,16 +4,19 @@ from __future__ import annotations
import dataclasses
import inspect
import sys
import types
import urllib.parse
from base64 import b64encode
from typing import (
Any,
Callable,
ClassVar,
Dict,
List,
Optional,
Tuple,
Type,
Union,
get_type_hints,
)
@ -25,8 +28,15 @@ from reflex.utils import format
from reflex.utils.exceptions import EventFnArgMismatch, EventHandlerArgMismatch
from reflex.utils.types import ArgsSpec, GenericType
from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var
from reflex.vars.function import FunctionStringVar, FunctionVar
from reflex.vars.base import (
CachedVarOperation,
LiteralNoneVar,
LiteralVar,
ToOperation,
Var,
cached_property_no_lock,
)
from reflex.vars.function import ArgsFunctionOperation, FunctionStringVar, FunctionVar
from reflex.vars.object import ObjectVar
try:
@ -375,7 +385,7 @@ class CallableEventSpec(EventSpec):
class EventChain(EventActionsMixin):
"""Container for a chain of events that will be executed in order."""
events: List[EventSpec] = dataclasses.field(default_factory=list)
events: List[Union[EventSpec, EventVar]] = dataclasses.field(default_factory=list)
args_spec: Optional[Callable] = dataclasses.field(default=None)
@ -478,7 +488,7 @@ class FileUpload:
if isinstance(events, Var):
raise ValueError(f"{on_upload_progress} cannot return a var {events}.")
on_upload_progress_chain = EventChain(
events=events,
events=[*events],
args_spec=self.on_upload_progress_args_spec,
)
formatted_chain = str(format.format_prop(on_upload_progress_chain))
@ -839,6 +849,16 @@ def call_script(
),
),
}
if isinstance(javascript_code, str):
# When there is VarData, include it and eval the JS code inline on the client.
javascript_code, original_code = (
LiteralVar.create(javascript_code),
javascript_code,
)
if not javascript_code._get_all_var_data():
# Without VarData, cast to string and eval the code in the event loop.
javascript_code = str(Var(_js_expr=original_code))
return server_side(
"_call_script",
get_fn_signature(call_script),
@ -1126,3 +1146,178 @@ def get_fn_signature(fn: Callable) -> inspect.Signature:
"state", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Any
)
return signature.replace(parameters=(new_param, *signature.parameters.values()))
class EventVar(ObjectVar):
"""Base class for event vars."""
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class LiteralEventVar(CachedVarOperation, LiteralVar, EventVar):
"""A literal event var."""
_var_value: EventSpec = dataclasses.field(default=None) # type: ignore
def __hash__(self) -> int:
"""Get the hash of the var.
Returns:
The hash of the var.
"""
return hash((self.__class__.__name__, self._js_expr))
@cached_property_no_lock
def _cached_var_name(self) -> str:
"""The name of the var.
Returns:
The name of the var.
"""
return str(
FunctionStringVar("Event").call(
# event handler name
".".join(
filter(
None,
format.get_event_handler_parts(self._var_value.handler),
)
),
# event handler args
{str(name): value for name, value in self._var_value.args},
# event actions
self._var_value.event_actions,
# client handler name
*(
[self._var_value.client_handler_name]
if self._var_value.client_handler_name
else []
),
)
)
@classmethod
def create(
cls,
value: EventSpec,
_var_data: VarData | None = None,
) -> LiteralEventVar:
"""Create a new LiteralEventVar instance.
Args:
value: The value of the var.
_var_data: The data of the var.
Returns:
The created LiteralEventVar instance.
"""
return cls(
_js_expr="",
_var_type=EventSpec,
_var_data=_var_data,
_var_value=value,
)
class EventChainVar(FunctionVar):
"""Base class for event chain vars."""
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class LiteralEventChainVar(CachedVarOperation, LiteralVar, EventChainVar):
"""A literal event chain var."""
_var_value: EventChain = dataclasses.field(default=None) # type: ignore
def __hash__(self) -> int:
"""Get the hash of the var.
Returns:
The hash of the var.
"""
return hash((self.__class__.__name__, self._js_expr))
@cached_property_no_lock
def _cached_var_name(self) -> str:
"""The name of the var.
Returns:
The name of the var.
"""
sig = inspect.signature(self._var_value.args_spec) # type: ignore
if sig.parameters:
arg_def = tuple((f"_{p}" for p in sig.parameters))
arg_def_expr = LiteralVar.create([Var(_js_expr=arg) for arg in arg_def])
else:
# add a default argument for addEvents if none were specified in value.args_spec
# used to trigger the preventDefault() on the event.
arg_def = ("...args",)
arg_def_expr = Var(_js_expr="args")
return str(
ArgsFunctionOperation.create(
arg_def,
FunctionStringVar.create("addEvents").call(
LiteralVar.create(
[LiteralVar.create(event) for event in self._var_value.events]
),
arg_def_expr,
self._var_value.event_actions,
),
)
)
@classmethod
def create(
cls,
value: EventChain,
_var_data: VarData | None = None,
) -> LiteralEventChainVar:
"""Create a new LiteralEventChainVar instance.
Args:
value: The value of the var.
_var_data: The data of the var.
Returns:
The created LiteralEventChainVar instance.
"""
return cls(
_js_expr="",
_var_type=EventChain,
_var_data=_var_data,
_var_value=value,
)
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class ToEventVarOperation(ToOperation, EventVar):
"""Result of a cast to an event var."""
_original: Var = dataclasses.field(default_factory=lambda: LiteralNoneVar.create())
_default_var_type: ClassVar[Type] = EventSpec
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class ToEventChainVarOperation(ToOperation, EventChainVar):
"""Result of a cast to an event chain var."""
_original: Var = dataclasses.field(default_factory=lambda: LiteralNoneVar.create())
_default_var_type: ClassVar[Type] = EventChain

126
reflex/istate/data.py Normal file
View File

@ -0,0 +1,126 @@
"""This module contains the dataclasses representing the router object."""
import dataclasses
from typing import Optional
from reflex import constants
from reflex.utils import format
@dataclasses.dataclass(frozen=True)
class HeaderData:
"""An object containing headers data."""
host: str = ""
origin: str = ""
upgrade: str = ""
connection: str = ""
cookie: str = ""
pragma: str = ""
cache_control: str = ""
user_agent: str = ""
sec_websocket_version: str = ""
sec_websocket_key: str = ""
sec_websocket_extensions: str = ""
accept_encoding: str = ""
accept_language: str = ""
def __init__(self, router_data: Optional[dict] = None):
"""Initalize the HeaderData object based on router_data.
Args:
router_data: the router_data dict.
"""
if router_data:
for k, v in router_data.get(constants.RouteVar.HEADERS, {}).items():
object.__setattr__(self, format.to_snake_case(k), v)
else:
for k in dataclasses.fields(self):
object.__setattr__(self, k.name, "")
@dataclasses.dataclass(frozen=True)
class PageData:
"""An object containing page data."""
host: str = "" # repeated with self.headers.origin (remove or keep the duplicate?)
path: str = ""
raw_path: str = ""
full_path: str = ""
full_raw_path: str = ""
params: dict = dataclasses.field(default_factory=dict)
def __init__(self, router_data: Optional[dict] = None):
"""Initalize the PageData object based on router_data.
Args:
router_data: the router_data dict.
"""
if router_data:
object.__setattr__(
self,
"host",
router_data.get(constants.RouteVar.HEADERS, {}).get("origin", ""),
)
object.__setattr__(
self, "path", router_data.get(constants.RouteVar.PATH, "")
)
object.__setattr__(
self, "raw_path", router_data.get(constants.RouteVar.ORIGIN, "")
)
object.__setattr__(self, "full_path", f"{self.host}{self.path}")
object.__setattr__(self, "full_raw_path", f"{self.host}{self.raw_path}")
object.__setattr__(
self, "params", router_data.get(constants.RouteVar.QUERY, {})
)
else:
object.__setattr__(self, "host", "")
object.__setattr__(self, "path", "")
object.__setattr__(self, "raw_path", "")
object.__setattr__(self, "full_path", "")
object.__setattr__(self, "full_raw_path", "")
object.__setattr__(self, "params", {})
@dataclasses.dataclass(frozen=True, init=False)
class SessionData:
"""An object containing session data."""
client_token: str = ""
client_ip: str = ""
session_id: str = ""
def __init__(self, router_data: Optional[dict] = None):
"""Initalize the SessionData object based on router_data.
Args:
router_data: the router_data dict.
"""
if router_data:
client_token = router_data.get(constants.RouteVar.CLIENT_TOKEN, "")
client_ip = router_data.get(constants.RouteVar.CLIENT_IP, "")
session_id = router_data.get(constants.RouteVar.SESSION_ID, "")
else:
client_token = client_ip = session_id = ""
object.__setattr__(self, "client_token", client_token)
object.__setattr__(self, "client_ip", client_ip)
object.__setattr__(self, "session_id", session_id)
@dataclasses.dataclass(frozen=True, init=False)
class RouterData:
"""An object containing RouterData."""
session: SessionData = dataclasses.field(default_factory=SessionData)
headers: HeaderData = dataclasses.field(default_factory=HeaderData)
page: PageData = dataclasses.field(default_factory=PageData)
def __init__(self, router_data: Optional[dict] = None):
"""Initialize the RouterData object.
Args:
router_data: the router_data dict.
"""
object.__setattr__(self, "session", SessionData(router_data))
object.__setattr__(self, "headers", HeaderData(router_data))
object.__setattr__(self, "page", PageData(router_data))

View File

@ -15,7 +15,6 @@ from reflex_cli.utils import dependency
from reflex import constants
from reflex.config import get_config
from reflex.constants.base import LogLevel
from reflex.custom_components.custom_components import custom_components_cli
from reflex.state import reset_disk_state_manager
from reflex.utils import console, redir, telemetry
@ -115,9 +114,6 @@ def _init(
app_name, generation_hash=generation_hash
)
# Migrate Pynecone projects to Reflex.
prerequisites.migrate_to_reflex()
# Initialize the .gitignore.
prerequisites.initialize_gitignore()
@ -247,11 +243,6 @@ def _run(
setup_frontend(Path.cwd())
commands.append((frontend_cmd, Path.cwd(), frontend_port, backend))
# If no loglevel is specified, set the subprocesses loglevel to WARNING.
subprocesses_loglevel = (
loglevel if loglevel != LogLevel.DEFAULT else LogLevel.WARNING
)
# In prod mode, run the backend on a separate thread.
if backend and env == constants.Env.PROD:
commands.append(
@ -259,7 +250,7 @@ def _run(
backend_cmd,
backend_host,
backend_port,
subprocesses_loglevel,
loglevel.subprocess_level(),
frontend,
)
)
@ -269,7 +260,7 @@ def _run(
# In dev mode, run the backend on the main thread.
if backend and env == constants.Env.DEV:
backend_cmd(
backend_host, int(backend_port), subprocesses_loglevel, frontend
backend_host, int(backend_port), loglevel.subprocess_level(), frontend
)
# The windows uvicorn bug workaround
# https://github.com/reflex-dev/reflex/issues/2335
@ -342,7 +333,7 @@ def export(
backend=backend,
zip_dest_dir=zip_dest_dir,
upload_db_file=upload_db_file,
loglevel=loglevel,
loglevel=loglevel.subprocess_level(),
)
@ -577,7 +568,7 @@ def deploy(
frontend=frontend,
backend=backend,
zipping=zipping,
loglevel=loglevel,
loglevel=loglevel.subprocess_level(),
upload_db_file=upload_db_file,
),
key=key,
@ -591,7 +582,7 @@ def deploy(
interactive=interactive,
with_metrics=with_metrics,
with_tracing=with_tracing,
loglevel=loglevel.value,
loglevel=loglevel.subprocess_level(),
)

View File

@ -9,6 +9,7 @@ import dataclasses
import functools
import inspect
import os
import pickle
import uuid
from abc import ABC, abstractmethod
from collections import defaultdict
@ -19,6 +20,7 @@ from typing import (
TYPE_CHECKING,
Any,
AsyncIterator,
BinaryIO,
Callable,
ClassVar,
Dict,
@ -33,11 +35,11 @@ from typing import (
get_type_hints,
)
import dill
from sqlalchemy.orm import DeclarativeBase
from typing_extensions import Self
from reflex.config import get_config
from reflex.istate.data import RouterData
from reflex.vars.base import (
ComputedVar,
DynamicRouteVar,
@ -74,6 +76,8 @@ from reflex.utils.exceptions import (
EventHandlerShadowsBuiltInStateMethod,
ImmutableStateError,
LockExpiredError,
SetUndefinedStateVarError,
StateSchemaMismatchError,
)
from reflex.utils.exec import is_testing_env
from reflex.utils.serializers import serializer
@ -92,125 +96,6 @@ var = computed_var
TOO_LARGE_SERIALIZED_STATE = 100 * 1024 # 100kb
@dataclasses.dataclass(frozen=True)
class HeaderData:
"""An object containing headers data."""
host: str = ""
origin: str = ""
upgrade: str = ""
connection: str = ""
cookie: str = ""
pragma: str = ""
cache_control: str = ""
user_agent: str = ""
sec_websocket_version: str = ""
sec_websocket_key: str = ""
sec_websocket_extensions: str = ""
accept_encoding: str = ""
accept_language: str = ""
def __init__(self, router_data: Optional[dict] = None):
"""Initalize the HeaderData object based on router_data.
Args:
router_data: the router_data dict.
"""
if router_data:
for k, v in router_data.get(constants.RouteVar.HEADERS, {}).items():
object.__setattr__(self, format.to_snake_case(k), v)
else:
for k in dataclasses.fields(self):
object.__setattr__(self, k.name, "")
@dataclasses.dataclass(frozen=True)
class PageData:
"""An object containing page data."""
host: str = "" # repeated with self.headers.origin (remove or keep the duplicate?)
path: str = ""
raw_path: str = ""
full_path: str = ""
full_raw_path: str = ""
params: dict = dataclasses.field(default_factory=dict)
def __init__(self, router_data: Optional[dict] = None):
"""Initalize the PageData object based on router_data.
Args:
router_data: the router_data dict.
"""
if router_data:
object.__setattr__(
self,
"host",
router_data.get(constants.RouteVar.HEADERS, {}).get("origin", ""),
)
object.__setattr__(
self, "path", router_data.get(constants.RouteVar.PATH, "")
)
object.__setattr__(
self, "raw_path", router_data.get(constants.RouteVar.ORIGIN, "")
)
object.__setattr__(self, "full_path", f"{self.host}{self.path}")
object.__setattr__(self, "full_raw_path", f"{self.host}{self.raw_path}")
object.__setattr__(
self, "params", router_data.get(constants.RouteVar.QUERY, {})
)
else:
object.__setattr__(self, "host", "")
object.__setattr__(self, "path", "")
object.__setattr__(self, "raw_path", "")
object.__setattr__(self, "full_path", "")
object.__setattr__(self, "full_raw_path", "")
object.__setattr__(self, "params", {})
@dataclasses.dataclass(frozen=True, init=False)
class SessionData:
"""An object containing session data."""
client_token: str = ""
client_ip: str = ""
session_id: str = ""
def __init__(self, router_data: Optional[dict] = None):
"""Initalize the SessionData object based on router_data.
Args:
router_data: the router_data dict.
"""
if router_data:
client_token = router_data.get(constants.RouteVar.CLIENT_TOKEN, "")
client_ip = router_data.get(constants.RouteVar.CLIENT_IP, "")
session_id = router_data.get(constants.RouteVar.SESSION_ID, "")
else:
client_token = client_ip = session_id = ""
object.__setattr__(self, "client_token", client_token)
object.__setattr__(self, "client_ip", client_ip)
object.__setattr__(self, "session_id", session_id)
@dataclasses.dataclass(frozen=True, init=False)
class RouterData:
"""An object containing RouterData."""
session: SessionData = dataclasses.field(default_factory=SessionData)
headers: HeaderData = dataclasses.field(default_factory=HeaderData)
page: PageData = dataclasses.field(default_factory=PageData)
def __init__(self, router_data: Optional[dict] = None):
"""Initialize the RouterData object.
Args:
router_data: the router_data dict.
"""
object.__setattr__(self, "session", SessionData(router_data))
object.__setattr__(self, "headers", HeaderData(router_data))
object.__setattr__(self, "page", PageData(router_data))
def _no_chain_background_task(
state_cls: Type["BaseState"], name: str, fn: Callable
) -> Callable:
@ -698,11 +583,14 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
)
@classmethod
def _evaluate(cls, f: Callable[[Self], Any]) -> Var:
def _evaluate(
cls, f: Callable[[Self], Any], of_type: Union[type, None] = None
) -> Var:
"""Evaluate a function to a ComputedVar. Experimental.
Args:
f: The function to evaluate.
of_type: The type of the ComputedVar. Defaults to Component.
Returns:
The ComputedVar.
@ -710,14 +598,23 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
console.warn(
"The _evaluate method is experimental and may be removed in future versions."
)
from reflex.components.base.fragment import fragment
from reflex.components.component import Component
of_type = of_type or Component
unique_var_name = get_unique_variable_name()
@computed_var(_js_expr=unique_var_name, return_type=Component)
@computed_var(_js_expr=unique_var_name, return_type=of_type)
def computed_var_func(state: Self):
return fragment(f(state))
result = f(state)
if not isinstance(result, of_type):
console.warn(
f"Inline ComputedVar {f} expected type {of_type}, got {type(result)}. "
"You can specify expected type with `of_type` argument."
)
return result
setattr(cls, unique_var_name, computed_var_func)
cls.computed_vars[unique_var_name] = computed_var_func
@ -1260,6 +1157,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
Args:
name: The name of the attribute.
value: The value of the attribute.
Raises:
SetUndefinedStateVarError: If a value of a var is set without first defining it.
"""
if isinstance(value, MutableProxy):
# unwrap proxy objects when assigning back to the state
@ -1277,6 +1177,17 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
self._mark_dirty()
return
if (
name not in self.vars
and name not in self.get_skip_vars()
and not name.startswith("__")
and not name.startswith(f"_{type(self).__name__}__")
):
raise SetUndefinedStateVarError(
f"The state variable '{name}' has not been defined in '{type(self).__name__}'. "
f"All state variables must be declared before they can be set."
)
# Set the attribute.
super().__setattr__(name, value)
@ -2005,7 +1916,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
def __getstate__(self):
"""Get the state for redis serialization.
This method is called by cloudpickle to serialize the object.
This method is called by pickle to serialize the object.
It explicitly removes parent_state and substates because those are serialized separately
by the StateManagerRedis to allow for better horizontal scaling as state size increases.
@ -2021,6 +1932,43 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
state["__dict__"].pop("_was_touched", None)
return state
def _serialize(self) -> bytes:
"""Serialize the state for redis.
Returns:
The serialized state.
"""
return pickle.dumps((state_to_schema(self), self))
@classmethod
def _deserialize(
cls, data: bytes | None = None, fp: BinaryIO | None = None
) -> BaseState:
"""Deserialize the state from redis/disk.
data and fp are mutually exclusive, but one must be provided.
Args:
data: The serialized state data.
fp: The file pointer to the serialized state data.
Returns:
The deserialized state.
Raises:
ValueError: If both data and fp are provided, or neither are provided.
StateSchemaMismatchError: If the state schema does not match the expected schema.
"""
if data is not None and fp is None:
(substate_schema, state) = pickle.loads(data)
elif fp is not None and data is None:
(substate_schema, state) = pickle.load(fp)
else:
raise ValueError("Only one of `data` or `fp` must be provided")
if substate_schema != state_to_schema(state):
raise StateSchemaMismatchError()
return state
class State(BaseState):
"""The app Base State."""
@ -2177,7 +2125,11 @@ class ComponentState(State, mixin=True):
"""
cls._per_component_state_instance_count += 1
state_cls_name = f"{cls.__name__}_n{cls._per_component_state_instance_count}"
component_state = type(state_cls_name, (cls, State), {}, mixin=False)
component_state = type(
state_cls_name, (cls, State), {"__module__": __name__}, mixin=False
)
# Save a reference to the dynamic state for pickle/unpickle.
globals()[state_cls_name] = component_state
component = component_state.get_component(*children, **props)
component.State = component_state
return component
@ -2643,7 +2595,7 @@ def is_serializable(value: Any) -> bool:
Whether the value is serializable.
"""
try:
return bool(dill.dumps(value))
return bool(pickle.dumps(value))
except Exception:
return False
@ -2779,8 +2731,7 @@ class StateManagerDisk(StateManager):
if token_path.exists():
try:
with token_path.open(mode="rb") as file:
(substate_schema, substate) = dill.load(file)
if substate_schema == state_to_schema(substate):
substate = BaseState._deserialize(fp=file)
await self.populate_substates(client_token, substate, root_state)
return substate
except Exception:
@ -2822,10 +2773,12 @@ class StateManagerDisk(StateManager):
client_token, substate_address = _split_substate_key(token)
root_state_token = _substate_key(client_token, substate_address.split(".")[0])
root_state = self.states.get(root_state_token)
if root_state is None:
# Create a new root state which will be persisted in the next set_state call.
root_state = self.state(_reflex_internal_init=True)
return await self.load_state(
root_state_token, self.state(_reflex_internal_init=True)
)
return await self.load_state(root_state_token, root_state)
async def set_state_for_substate(self, client_token: str, substate: BaseState):
"""Set the state for a substate.
@ -2838,7 +2791,7 @@ class StateManagerDisk(StateManager):
self.states[substate_token] = substate
state_dilled = dill.dumps((state_to_schema(substate), substate))
state_dilled = substate._serialize()
if not self.states_directory.exists():
self.states_directory.mkdir(parents=True, exist_ok=True)
self.token_path(substate_token).write_bytes(state_dilled)
@ -2881,25 +2834,6 @@ class StateManagerDisk(StateManager):
await self.set_state(token, state)
# Workaround https://github.com/cloudpipe/cloudpickle/issues/408 for dynamic pydantic classes
if not isinstance(State.validate.__func__, FunctionType):
cython_function_or_method = type(State.validate.__func__)
@dill.register(cython_function_or_method)
def _dill_reduce_cython_function_or_method(pickler, obj):
# Ignore cython function when pickling.
pass
@dill.register(type(State))
def _dill_reduce_state(pickler, obj):
if obj is not State and issubclass(obj, State):
# Avoid serializing subclasses of State, instead get them by reference from the State class.
pickler.save_reduce(State.get_class_substate, (obj.get_full_name(),), obj=obj)
else:
dill.Pickler.dispatch[type](pickler, obj)
def _default_lock_expiration() -> int:
"""Get the default lock expiration time.
@ -3039,7 +2973,7 @@ class StateManagerRedis(StateManager):
if redis_state is not None:
# Deserialize the substate.
state = dill.loads(redis_state)
state = BaseState._deserialize(data=redis_state)
# Populate parent state if missing and requested.
if parent_state is None:
@ -3151,7 +3085,7 @@ class StateManagerRedis(StateManager):
)
# Persist only the given state (parents or substates are excluded by BaseState.__getstate__).
if state._get_was_touched():
pickle_state = dill.dumps(state, byref=True)
pickle_state = state._serialize()
self._warn_if_too_large(state, len(pickle_state))
await self.redis.set(
_substate_key(client_token, state),

View File

@ -61,8 +61,8 @@ def generate_sitemap_config(deploy_url: str, export=False):
def _zip(
component_name: constants.ComponentName,
target: str,
root_dir: str,
target: str | Path,
root_dir: str | Path,
exclude_venv_dirs: bool,
upload_db_file: bool = False,
dirs_to_exclude: set[str] | None = None,
@ -82,22 +82,22 @@ def _zip(
top_level_dirs_to_exclude: The top level directory names immediately under root_dir to exclude. Do not exclude folders by these names further in the sub-directories.
"""
target = Path(target)
root_dir = Path(root_dir)
dirs_to_exclude = dirs_to_exclude or set()
files_to_exclude = files_to_exclude or set()
files_to_zip: list[str] = []
# Traverse the root directory in a top-down manner. In this traversal order,
# we can modify the dirs list in-place to remove directories we don't want to include.
for root, dirs, files in os.walk(root_dir, topdown=True):
root = Path(root)
# Modify the dirs in-place so excluded and hidden directories are skipped in next traversal.
dirs[:] = [
d
for d in dirs
if (basename := os.path.basename(os.path.normpath(d)))
not in dirs_to_exclude
if (basename := Path(d).resolve().name) not in dirs_to_exclude
and not basename.startswith(".")
and (
not exclude_venv_dirs or not _looks_like_venv_dir(os.path.join(root, d))
)
and (not exclude_venv_dirs or not _looks_like_venv_dir(root / d))
]
# If we are at the top level with root_dir, exclude the top level dirs.
if top_level_dirs_to_exclude and root == root_dir:
@ -109,7 +109,7 @@ def _zip(
if not f.startswith(".") and (upload_db_file or not f.endswith(".db"))
]
files_to_zip += [
os.path.join(root, file) for file in files if file not in files_to_exclude
str(root / file) for file in files if file not in files_to_exclude
]
# Create a progress bar for zipping the component.
@ -126,13 +126,13 @@ def _zip(
for file in files_to_zip:
console.debug(f"{target}: {file}", progress=progress)
progress.advance(task)
zipf.write(file, os.path.relpath(file, root_dir))
zipf.write(file, Path(file).relative_to(root_dir))
def zip_app(
frontend: bool = True,
backend: bool = True,
zip_dest_dir: str = os.getcwd(),
zip_dest_dir: str | Path = Path.cwd(),
upload_db_file: bool = False,
):
"""Zip up the app.
@ -143,6 +143,7 @@ def zip_app(
zip_dest_dir: The directory to export the zip file to.
upload_db_file: Whether to upload the database file.
"""
zip_dest_dir = Path(zip_dest_dir)
files_to_exclude = {
constants.ComponentName.FRONTEND.zip(),
constants.ComponentName.BACKEND.zip(),
@ -151,8 +152,8 @@ def zip_app(
if frontend:
_zip(
component_name=constants.ComponentName.FRONTEND,
target=os.path.join(zip_dest_dir, constants.ComponentName.FRONTEND.zip()),
root_dir=str(prerequisites.get_web_dir() / constants.Dirs.STATIC),
target=zip_dest_dir / constants.ComponentName.FRONTEND.zip(),
root_dir=prerequisites.get_web_dir() / constants.Dirs.STATIC,
files_to_exclude=files_to_exclude,
exclude_venv_dirs=False,
)
@ -160,8 +161,8 @@ def zip_app(
if backend:
_zip(
component_name=constants.ComponentName.BACKEND,
target=os.path.join(zip_dest_dir, constants.ComponentName.BACKEND.zip()),
root_dir=".",
target=zip_dest_dir / constants.ComponentName.BACKEND.zip(),
root_dir=Path("."),
dirs_to_exclude={"__pycache__"},
files_to_exclude=files_to_exclude,
top_level_dirs_to_exclude={"assets"},
@ -236,6 +237,9 @@ def setup_frontend(
# Set the environment variables in client (env.json).
set_env_json()
# update the last reflex run time.
prerequisites.set_last_reflex_run_time()
# Disable the Next telemetry.
if disable_telemetry:
processes.new_process(
@ -266,5 +270,6 @@ def setup_frontend_prod(
build(deploy_url=get_config().deploy_url)
def _looks_like_venv_dir(dir_to_check: str) -> bool:
return os.path.exists(os.path.join(dir_to_check, "pyvenv.cfg"))
def _looks_like_venv_dir(dir_to_check: str | Path) -> bool:
dir_to_check = Path(dir_to_check)
return (dir_to_check / "pyvenv.cfg").exists()

View File

@ -115,3 +115,15 @@ class PrimitiveUnserializableToJSON(ReflexError, ValueError):
class InvalidLifespanTaskType(ReflexError, TypeError):
"""Raised when an invalid task type is registered as a lifespan task."""
class DynamicComponentMissingLibrary(ReflexError, ValueError):
"""Raised when a dynamic component is missing a library."""
class SetUndefinedStateVarError(ReflexError, AttributeError):
"""Raised when setting the value of a var without first declaring it."""
class StateSchemaMismatchError(ReflexError, TypeError):
"""Raised when the serialized schema of a state class does not match the current schema."""

View File

@ -284,7 +284,7 @@ def run_granian_backend(host, port, loglevel: LogLevel):
).serve()
except ImportError:
console.error(
'InstallError: REFLEX_USE_GRANIAN is set but `granian` is not installed. (run `pip install "granian>=1.6.0"`)'
'InstallError: REFLEX_USE_GRANIAN is set but `granian` is not installed. (run `pip install "granian[reload]>=1.6.0"`)'
)
os._exit(1)
@ -410,7 +410,7 @@ def run_granian_backend_prod(host, port, loglevel):
)
except ImportError:
console.error(
'InstallError: REFLEX_USE_GRANIAN is set but `granian` is not installed. (run `pip install "granian>=1.6.0"`)'
'InstallError: REFLEX_USE_GRANIAN is set but `granian` is not installed. (run `pip install "granian[reload]>=1.6.0"`)'
)

View File

@ -359,19 +359,7 @@ def format_prop(
# Handle event props.
if isinstance(prop, EventChain):
sig = inspect.signature(prop.args_spec) # type: ignore
if sig.parameters:
arg_def = ",".join(f"_{p}" for p in sig.parameters)
arg_def_expr = f"[{arg_def}]"
else:
# add a default argument for addEvents if none were specified in prop.args_spec
# used to trigger the preventDefault() on the event.
arg_def = "...args"
arg_def_expr = "args"
chain = ",".join([format_event(event) for event in prop.events])
event = f"addEvents([{chain}], {arg_def_expr}, {json_dumps(prop.event_actions)})"
prop = f"({arg_def}) => {event}"
return str(Var.create(prop))
# Handle other types.
elif isinstance(prop, str):

View File

@ -164,7 +164,7 @@ def use_system_bun() -> bool:
return use_system_install(constants.Bun.USE_SYSTEM_VAR)
def get_node_bin_path() -> str | None:
def get_node_bin_path() -> Path | None:
"""Get the node binary dir path.
Returns:
@ -173,8 +173,8 @@ def get_node_bin_path() -> str | None:
bin_path = Path(constants.Node.BIN_PATH)
if not bin_path.exists():
str_path = which("node")
return str(Path(str_path).parent.resolve()) if str_path else str_path
return str(bin_path.resolve())
return Path(str_path).parent.resolve() if str_path else None
return bin_path.resolve()
def get_node_path() -> str | None:

View File

@ -2,9 +2,9 @@
from __future__ import annotations
import contextlib
import dataclasses
import functools
import glob
import importlib
import importlib.metadata
import json
@ -19,7 +19,6 @@ import tempfile
import time
import zipfile
from datetime import datetime
from fileinput import FileInput
from pathlib import Path
from types import ModuleType
from typing import Callable, List, Optional
@ -132,6 +131,14 @@ def get_or_set_last_reflex_version_check_datetime():
return last_version_check_datetime
def set_last_reflex_run_time():
"""Set the last Reflex run time."""
path_ops.update_json_file(
get_web_dir() / constants.Reflex.JSON,
{"last_reflex_run_datetime": str(datetime.now())},
)
def check_node_version() -> bool:
"""Check the version of Node.js.
@ -192,7 +199,7 @@ def get_bun_version() -> version.Version | None:
"""
try:
# Run the bun -v command and capture the output
result = processes.new_process([get_config().bun_path, "-v"], run=True)
result = processes.new_process([str(get_config().bun_path), "-v"], run=True)
return version.parse(result.stdout) # type: ignore
except FileNotFoundError:
return None
@ -217,7 +224,7 @@ def get_install_package_manager() -> str | None:
or windows_npm_escape_hatch()
):
return get_package_manager()
return get_config().bun_path
return str(get_config().bun_path)
def get_package_manager() -> str | None:
@ -394,9 +401,7 @@ def validate_app_name(app_name: str | None = None) -> str:
Raises:
Exit: if the app directory name is reflex or if the name is not standard for a python package name.
"""
app_name = (
app_name if app_name else os.getcwd().split(os.path.sep)[-1].replace("-", "_")
)
app_name = app_name if app_name else Path.cwd().name.replace("-", "_")
# Make sure the app is not named "reflex".
if app_name.lower() == constants.Reflex.MODULE_NAME:
console.error(
@ -430,7 +435,7 @@ def create_config(app_name: str):
def initialize_gitignore(
gitignore_file: str = constants.GitIgnore.FILE,
gitignore_file: Path = constants.GitIgnore.FILE,
files_to_ignore: set[str] = constants.GitIgnore.DEFAULTS,
):
"""Initialize the template .gitignore file.
@ -441,9 +446,10 @@ def initialize_gitignore(
"""
# Combine with the current ignored files.
current_ignore: set[str] = set()
if os.path.exists(gitignore_file):
with open(gitignore_file, "r") as f:
current_ignore |= set([line.strip() for line in f.readlines()])
if gitignore_file.exists():
current_ignore |= set(
line.strip() for line in gitignore_file.read_text().splitlines()
)
if files_to_ignore == current_ignore:
console.debug(f"{gitignore_file} already up to date.")
@ -451,9 +457,11 @@ def initialize_gitignore(
files_to_ignore |= current_ignore
# Write files to the .gitignore file.
with open(gitignore_file, "w", newline="\n") as f:
console.debug(f"Creating {gitignore_file}")
f.write(f"{(path_ops.join(sorted(files_to_ignore))).lstrip()}\n")
gitignore_file.touch(exist_ok=True)
console.debug(f"Creating {gitignore_file}")
gitignore_file.write_text(
"\n".join(sorted(files_to_ignore)) + "\n",
)
def initialize_requirements_txt():
@ -546,8 +554,8 @@ def initialize_app_directory(
# Rename the template app to the app name.
path_ops.mv(template_code_dir_name, app_name)
path_ops.mv(
os.path.join(app_name, template_name + constants.Ext.PY),
os.path.join(app_name, app_name + constants.Ext.PY),
Path(app_name) / (template_name + constants.Ext.PY),
Path(app_name) / (app_name + constants.Ext.PY),
)
# Fix up the imports.
@ -691,7 +699,7 @@ def _update_next_config(
def remove_existing_bun_installation():
"""Remove existing bun installation."""
console.debug("Removing existing bun installation.")
if os.path.exists(get_config().bun_path):
if Path(get_config().bun_path).exists():
path_ops.rm(constants.Bun.ROOT_PATH)
@ -731,7 +739,7 @@ def download_and_extract_fnm_zip():
# Download the zip file
url = constants.Fnm.INSTALL_URL
console.debug(f"Downloading {url}")
fnm_zip_file = os.path.join(constants.Fnm.DIR, f"{constants.Fnm.FILENAME}.zip")
fnm_zip_file = constants.Fnm.DIR / f"{constants.Fnm.FILENAME}.zip"
# Function to download and extract the FNM zip release.
try:
# Download the FNM zip release.
@ -770,7 +778,7 @@ def install_node():
return
path_ops.mkdir(constants.Fnm.DIR)
if not os.path.exists(constants.Fnm.EXE):
if not constants.Fnm.EXE.exists():
download_and_extract_fnm_zip()
if constants.IS_WINDOWS:
@ -827,7 +835,7 @@ def install_bun():
)
# Skip if bun is already installed.
if os.path.exists(get_config().bun_path) and get_bun_version() == version.parse(
if Path(get_config().bun_path).exists() and get_bun_version() == version.parse(
constants.Bun.VERSION
):
console.debug("Skipping bun installation as it is already installed.")
@ -842,7 +850,7 @@ def install_bun():
f"irm {constants.Bun.WINDOWS_INSTALL_URL}|iex",
],
env={
"BUN_INSTALL": constants.Bun.ROOT_PATH,
"BUN_INSTALL": str(constants.Bun.ROOT_PATH),
"BUN_VERSION": constants.Bun.VERSION,
},
shell=True,
@ -858,25 +866,26 @@ def install_bun():
download_and_run(
constants.Bun.INSTALL_URL,
f"bun-v{constants.Bun.VERSION}",
BUN_INSTALL=constants.Bun.ROOT_PATH,
BUN_INSTALL=str(constants.Bun.ROOT_PATH),
)
def _write_cached_procedure_file(payload: str, cache_file: str):
with open(cache_file, "w") as f:
f.write(payload)
def _write_cached_procedure_file(payload: str, cache_file: str | Path):
cache_file = Path(cache_file)
cache_file.write_text(payload)
def _read_cached_procedure_file(cache_file: str) -> str | None:
if os.path.exists(cache_file):
with open(cache_file, "r") as f:
return f.read()
def _read_cached_procedure_file(cache_file: str | Path) -> str | None:
cache_file = Path(cache_file)
if cache_file.exists():
return cache_file.read_text()
return None
def _clear_cached_procedure_file(cache_file: str):
if os.path.exists(cache_file):
os.remove(cache_file)
def _clear_cached_procedure_file(cache_file: str | Path):
cache_file = Path(cache_file)
if cache_file.exists():
cache_file.unlink()
def cached_procedure(cache_file: str, payload_fn: Callable[..., str]):
@ -977,7 +986,7 @@ def needs_reinit(frontend: bool = True) -> bool:
Raises:
Exit: If the app is not initialized.
"""
if not os.path.exists(constants.Config.FILE):
if not constants.Config.FILE.exists():
console.error(
f"[cyan]{constants.Config.FILE}[/cyan] not found. Move to the root folder of your project, or run [bold]{constants.Reflex.MODULE_NAME} init[/bold] to start a new project."
)
@ -988,7 +997,7 @@ def needs_reinit(frontend: bool = True) -> bool:
return False
# Make sure the .reflex directory exists.
if not os.path.exists(constants.Reflex.DIR):
if not constants.Reflex.DIR.exists():
return True
# Make sure the .web directory exists in frontend mode.
@ -1093,25 +1102,21 @@ def ensure_reflex_installation_id() -> Optional[int]:
"""
try:
initialize_reflex_user_directory()
installation_id_file = os.path.join(constants.Reflex.DIR, "installation_id")
installation_id_file = constants.Reflex.DIR / "installation_id"
installation_id = None
if os.path.exists(installation_id_file):
try:
with open(installation_id_file, "r") as f:
installation_id = int(f.read())
except Exception:
if installation_id_file.exists():
with contextlib.suppress(Exception):
installation_id = int(installation_id_file.read_text())
# If anything goes wrong at all... just regenerate.
# Like what? Examples:
# - file not exists
# - file not readable
# - content not parseable as an int
pass
if installation_id is None:
installation_id = random.getrandbits(128)
with open(installation_id_file, "w") as f:
f.write(str(installation_id))
installation_id_file.write_text(str(installation_id))
# If we get here, installation_id is definitely set
return installation_id
except Exception as e:
@ -1205,50 +1210,6 @@ def prompt_for_template(templates: list[Template]) -> str:
return templates[int(template)].name
def migrate_to_reflex():
"""Migration from Pynecone to Reflex."""
# Check if the old config file exists.
if not os.path.exists(constants.Config.PREVIOUS_FILE):
return
# Ask the user if they want to migrate.
action = console.ask(
"Pynecone project detected. Automatically upgrade to Reflex?",
choices=["y", "n"],
)
if action == "n":
return
# Rename pcconfig to rxconfig.
console.log(
f"[bold]Renaming {constants.Config.PREVIOUS_FILE} to {constants.Config.FILE}"
)
os.rename(constants.Config.PREVIOUS_FILE, constants.Config.FILE)
# Find all python files in the app directory.
file_pattern = os.path.join(get_config().app_name, "**/*.py")
file_list = glob.glob(file_pattern, recursive=True)
# Add the config file to the list of files to be migrated.
file_list.append(constants.Config.FILE)
# Migrate all files.
updates = {
"Pynecone": "Reflex",
"pynecone as pc": "reflex as rx",
"pynecone.io": "reflex.dev",
"pynecone": "reflex",
"pc.": "rx.",
"pcconfig": "rxconfig",
}
for file_path in file_list:
with FileInput(file_path, inplace=True) as file:
for line in file:
for old, new in updates.items():
line = line.replace(old, new)
print(line, end="")
def fetch_app_templates(version: str) -> dict[str, Template]:
"""Fetch a dict of templates from the templates repo using github API.
@ -1401,7 +1362,7 @@ def initialize_app(app_name: str, template: str | None = None):
from reflex.utils import telemetry
# Check if the app is already initialized.
if os.path.exists(constants.Config.FILE):
if constants.Config.FILE.exists():
telemetry.send("reinit")
return

View File

@ -156,7 +156,7 @@ def new_process(args, run: bool = False, show_logs: bool = False, **kwargs):
Raises:
Exit: When attempting to run a command with a None value.
"""
node_bin_path = path_ops.get_node_bin_path()
node_bin_path = str(path_ops.get_node_bin_path())
if not node_bin_path and not prerequisites.CURRENTLY_INSTALLING_NODE:
console.warn(
"The path to the Node binary could not be found. Please ensure that Node is properly "
@ -167,7 +167,7 @@ def new_process(args, run: bool = False, show_logs: bool = False, **kwargs):
console.error(f"Invalid command: {args}")
raise typer.Exit(1)
# Add the node bin path to the PATH environment variable.
env = {
env: dict[str, str] = {
**os.environ,
"PATH": os.pathsep.join(
[node_bin_path if node_bin_path else "", os.environ["PATH"]]

View File

@ -385,6 +385,15 @@ class Var(Generic[VAR_TYPE]):
Returns:
The converted var.
"""
from reflex.event import (
EventChain,
EventChainVar,
EventSpec,
EventVar,
ToEventChainVarOperation,
ToEventVarOperation,
)
from .function import FunctionVar, ToFunctionOperation
from .number import (
BooleanVar,
@ -416,6 +425,10 @@ class Var(Generic[VAR_TYPE]):
return self.to(BooleanVar, output)
if fixed_output_type is None:
return ToNoneOperation.create(self)
if fixed_output_type is EventSpec:
return self.to(EventVar, output)
if fixed_output_type is EventChain:
return self.to(EventChainVar, output)
if issubclass(fixed_output_type, Base):
return self.to(ObjectVar, output)
if dataclasses.is_dataclass(fixed_output_type) and not issubclass(
@ -453,10 +466,13 @@ class Var(Generic[VAR_TYPE]):
if issubclass(output, StringVar):
return ToStringOperation.create(self, var_type or str)
if issubclass(output, (ObjectVar, Base)):
return ToObjectOperation.create(self, var_type or dict)
if issubclass(output, EventVar):
return ToEventVarOperation.create(self, var_type or EventSpec)
if dataclasses.is_dataclass(output):
if issubclass(output, EventChainVar):
return ToEventChainVarOperation.create(self, var_type or EventChain)
if issubclass(output, (ObjectVar, Base)):
return ToObjectOperation.create(self, var_type or dict)
if issubclass(output, FunctionVar):
@ -469,6 +485,9 @@ class Var(Generic[VAR_TYPE]):
if issubclass(output, NoneVar):
return ToNoneOperation.create(self)
if dataclasses.is_dataclass(output):
return ToObjectOperation.create(self, var_type or dict)
# If we can't determine the first argument, we just replace the _var_type.
if not issubclass(output, Var) or var_type is None:
return dataclasses.replace(
@ -494,6 +513,8 @@ class Var(Generic[VAR_TYPE]):
Raises:
TypeError: If the type is not supported for guessing.
"""
from reflex.event import EventChain, EventChainVar, EventSpec, EventVar
from .number import BooleanVar, NumberVar
from .object import ObjectVar
from .sequence import ArrayVar, StringVar
@ -526,6 +547,10 @@ class Var(Generic[VAR_TYPE]):
return self
if fixed_type is Literal:
args = get_args(var_type)
fixed_type = unionize(*(type(arg) for arg in args))
if not inspect.isclass(fixed_type):
raise TypeError(f"Unsupported type {var_type} for guess_type.")
@ -539,6 +564,10 @@ class Var(Generic[VAR_TYPE]):
return self.to(ArrayVar, self._var_type)
if issubclass(fixed_type, str):
return self.to(StringVar, self._var_type)
if issubclass(fixed_type, EventSpec):
return self.to(EventVar, self._var_type)
if issubclass(fixed_type, EventChain):
return self.to(EventChainVar, self._var_type)
if issubclass(fixed_type, Base):
return self.to(ObjectVar, self._var_type)
if dataclasses.is_dataclass(fixed_type):
@ -1029,47 +1058,22 @@ class LiteralVar(Var):
if value is None:
return LiteralNoneVar.create(_var_data=_var_data)
from reflex.event import EventChain, EventHandler, EventSpec
from reflex.event import (
EventChain,
EventHandler,
EventSpec,
LiteralEventChainVar,
LiteralEventVar,
)
from reflex.utils.format import get_event_handler_parts
from .function import ArgsFunctionOperation, FunctionStringVar
from .object import LiteralObjectVar
if isinstance(value, EventSpec):
event_name = LiteralVar.create(
".".join(filter(None, get_event_handler_parts(value.handler)))
)
event_args = LiteralVar.create(
{str(name): value for name, value in value.args}
)
event_client_name = LiteralVar.create(value.client_handler_name)
return FunctionStringVar("Event").call(
event_name,
event_args,
*([event_client_name] if value.client_handler_name else []),
)
return LiteralEventVar.create(value, _var_data=_var_data)
if isinstance(value, EventChain):
sig = inspect.signature(value.args_spec) # type: ignore
if sig.parameters:
arg_def = tuple((f"_{p}" for p in sig.parameters))
arg_def_expr = LiteralVar.create([Var(_js_expr=arg) for arg in arg_def])
else:
# add a default argument for addEvents if none were specified in value.args_spec
# used to trigger the preventDefault() on the event.
arg_def = ("...args",)
arg_def_expr = Var(_js_expr="args")
return ArgsFunctionOperation.create(
arg_def,
FunctionStringVar.create("addEvents").call(
LiteralVar.create(
[LiteralVar.create(event) for event in value.events]
),
arg_def_expr,
LiteralVar.create(value.event_actions),
),
)
return LiteralEventChainVar.create(value, _var_data=_var_data)
if isinstance(value, EventHandler):
return Var(_js_expr=".".join(filter(None, get_event_handler_parts(value))))
@ -2126,9 +2130,16 @@ class NoneVar(Var[None]):
"""A var representing None."""
@dataclasses.dataclass(
eq=False,
frozen=True,
**{"slots": True} if sys.version_info >= (3, 10) else {},
)
class LiteralNoneVar(LiteralVar, NoneVar):
"""A var representing None."""
_var_value: None = None
def json(self) -> str:
"""Serialize the var to a JSON string.

View File

@ -46,6 +46,7 @@ def CallScript():
inline_counter: int = 0
external_counter: int = 0
value: str = "Initial"
last_result: str = ""
def call_script_callback(self, result):
self.results.append(result)
@ -137,6 +138,32 @@ def CallScript():
callback=CallScriptState.set_external_counter, # type: ignore
)
def call_with_var_f_string(self):
return rx.call_script(
f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}",
callback=CallScriptState.set_last_result, # type: ignore
)
def call_with_var_str_cast(self):
return rx.call_script(
f"{str(rx.Var('inline_counter'))} + {str(rx.Var('external_counter'))}",
callback=CallScriptState.set_last_result, # type: ignore
)
def call_with_var_f_string_wrapped(self):
return rx.call_script(
rx.Var(f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}"),
callback=CallScriptState.set_last_result, # type: ignore
)
def call_with_var_str_cast_wrapped(self):
return rx.call_script(
rx.Var(
f"{str(rx.Var('inline_counter'))} + {str(rx.Var('external_counter'))}"
),
callback=CallScriptState.set_last_result, # type: ignore
)
def reset_(self):
yield rx.call_script("inline_counter = 0; external_counter = 0")
self.reset()
@ -234,6 +261,68 @@ def CallScript():
id="update_value",
),
rx.button("Reset", id="reset", on_click=CallScriptState.reset_),
rx.input(
value=CallScriptState.last_result,
id="last_result",
read_only=True,
on_click=CallScriptState.set_last_result(""), # type: ignore
),
rx.button(
"call_with_var_f_string",
on_click=CallScriptState.call_with_var_f_string,
id="call_with_var_f_string",
),
rx.button(
"call_with_var_str_cast",
on_click=CallScriptState.call_with_var_str_cast,
id="call_with_var_str_cast",
),
rx.button(
"call_with_var_f_string_wrapped",
on_click=CallScriptState.call_with_var_f_string_wrapped,
id="call_with_var_f_string_wrapped",
),
rx.button(
"call_with_var_str_cast_wrapped",
on_click=CallScriptState.call_with_var_str_cast_wrapped,
id="call_with_var_str_cast_wrapped",
),
rx.button(
"call_with_var_f_string_inline",
on_click=rx.call_script(
f"{rx.Var('inline_counter')} + {CallScriptState.last_result}",
callback=CallScriptState.set_last_result, # type: ignore
),
id="call_with_var_f_string_inline",
),
rx.button(
"call_with_var_str_cast_inline",
on_click=rx.call_script(
f"{str(rx.Var('inline_counter'))} + {str(rx.Var('external_counter'))}",
callback=CallScriptState.set_last_result, # type: ignore
),
id="call_with_var_str_cast_inline",
),
rx.button(
"call_with_var_f_string_wrapped_inline",
on_click=rx.call_script(
rx.Var(
f"{rx.Var('inline_counter')} + {CallScriptState.last_result}"
),
callback=CallScriptState.set_last_result, # type: ignore
),
id="call_with_var_f_string_wrapped_inline",
),
rx.button(
"call_with_var_str_cast_wrapped_inline",
on_click=rx.call_script(
rx.Var(
f"{str(rx.Var('inline_counter'))} + {str(rx.Var('external_counter'))}"
),
callback=CallScriptState.set_last_result, # type: ignore
),
id="call_with_var_str_cast_wrapped_inline",
),
)
@ -363,3 +452,73 @@ def test_call_script(
call_script.poll_for_content(update_value_button, exp_not_equal="Initial")
== "updated"
)
def test_call_script_w_var(
call_script: AppHarness,
driver: WebDriver,
):
"""Test evaluating javascript expressions containing Vars.
Args:
call_script: harness for CallScript app.
driver: WebDriver instance.
"""
assert_token(driver)
last_result = driver.find_element(By.ID, "last_result")
assert last_result.get_attribute("value") == ""
inline_return_button = driver.find_element(By.ID, "inline_return")
call_with_var_f_string_button = driver.find_element(By.ID, "call_with_var_f_string")
call_with_var_str_cast_button = driver.find_element(By.ID, "call_with_var_str_cast")
call_with_var_f_string_wrapped_button = driver.find_element(
By.ID, "call_with_var_f_string_wrapped"
)
call_with_var_str_cast_wrapped_button = driver.find_element(
By.ID, "call_with_var_str_cast_wrapped"
)
call_with_var_f_string_inline_button = driver.find_element(
By.ID, "call_with_var_f_string_inline"
)
call_with_var_str_cast_inline_button = driver.find_element(
By.ID, "call_with_var_str_cast_inline"
)
call_with_var_f_string_wrapped_inline_button = driver.find_element(
By.ID, "call_with_var_f_string_wrapped_inline"
)
call_with_var_str_cast_wrapped_inline_button = driver.find_element(
By.ID, "call_with_var_str_cast_wrapped_inline"
)
inline_return_button.click()
call_with_var_f_string_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="") == "1"
inline_return_button.click()
call_with_var_str_cast_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="1") == "2"
inline_return_button.click()
call_with_var_f_string_wrapped_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="2") == "3"
inline_return_button.click()
call_with_var_str_cast_wrapped_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="3") == "4"
inline_return_button.click()
call_with_var_f_string_inline_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="4") == "9"
inline_return_button.click()
call_with_var_str_cast_inline_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="9") == "6"
inline_return_button.click()
call_with_var_f_string_wrapped_inline_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="6") == "13"
inline_return_button.click()
call_with_var_str_cast_wrapped_inline_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="13") == "8"

View File

@ -65,7 +65,9 @@ def DynamicComponents():
DynamicComponentsState.client_token_component,
DynamicComponentsState.button,
rx.text(
DynamicComponentsState._evaluate(lambda state: factorial(state.value)),
DynamicComponentsState._evaluate(
lambda state: factorial(state.value), of_type=int
),
id="factorial",
),
)

View File

@ -8,7 +8,7 @@ import pytest
import requests
def check_urls(repo_dir):
def check_urls(repo_dir: Path):
"""Check that all URLs in the repo are valid and secure.
Args:
@ -21,33 +21,33 @@ def check_urls(repo_dir):
errors = []
for root, _dirs, files in os.walk(repo_dir):
if "__pycache__" in root:
root = Path(root)
if root.stem == "__pycache__":
continue
for file_name in files:
if not file_name.endswith(".py") and not file_name.endswith(".md"):
continue
file_path = os.path.join(root, file_name)
file_path = root / file_name
try:
with open(file_path, "r", encoding="utf-8", errors="ignore") as file:
for line in file:
urls = url_pattern.findall(line)
for url in set(urls):
if url.startswith("http://"):
errors.append(
f"Found insecure HTTP URL: {url} in {file_path}"
)
url = url.strip('"\n')
try:
response = requests.head(
url, allow_redirects=True, timeout=5
)
response.raise_for_status()
except requests.RequestException as e:
errors.append(
f"Error accessing URL: {url} in {file_path} | Error: {e}, , Check your path ends with a /"
)
for line in file_path.read_text().splitlines():
urls = url_pattern.findall(line)
for url in set(urls):
if url.startswith("http://"):
errors.append(
f"Found insecure HTTP URL: {url} in {file_path}"
)
url = url.strip('"\n')
try:
response = requests.head(
url, allow_redirects=True, timeout=5
)
response.raise_for_status()
except requests.RequestException as e:
errors.append(
f"Error accessing URL: {url} in {file_path} | Error: {e}, , Check your path ends with a /"
)
except Exception as e:
errors.append(f"Error reading file: {file_path} | Error: {e}")
@ -58,7 +58,7 @@ def check_urls(repo_dir):
"repo_dir",
[Path(__file__).resolve().parent.parent / "reflex"],
)
def test_find_and_check_urls(repo_dir):
def test_find_and_check_urls(repo_dir: Path):
"""Test that all URLs in the repo are valid and secure.
Args:

View File

@ -1,4 +1,4 @@
import os
from pathlib import Path
from typing import List
import pytest
@ -130,7 +130,7 @@ def test_compile_stylesheets(tmp_path, mocker):
]
assert compiler.compile_root_stylesheet(stylesheets) == (
os.path.join(".web", "styles", "styles.css"),
str(Path(".web") / "styles" / "styles.css"),
f"@import url('./tailwind.css'); \n"
f"@import url('https://fonts.googleapis.com/css?family=Sofia&effect=neon|outline|emboss|shadow-multiple'); \n"
f"@import url('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css'); \n"
@ -164,7 +164,7 @@ def test_compile_stylesheets_exclude_tailwind(tmp_path, mocker):
]
assert compiler.compile_root_stylesheet(stylesheets) == (
os.path.join(".web", "styles", "styles.css"),
str(Path(".web") / "styles" / "styles.css"),
"@import url('../public/styles.css'); \n",
)

View File

@ -58,14 +58,14 @@ def test_script_event_handler():
)
render_dict = component.render()
assert (
f'onReady={{((...args) => ((addEvents([(Event("{EvState.get_full_name()}.on_ready", ({{ }})))], args, ({{ }})))))}}'
f'onReady={{((...args) => ((addEvents([(Event("{EvState.get_full_name()}.on_ready", ({{ }}), ({{ }})))], args, ({{ }})))))}}'
in render_dict["props"]
)
assert (
f'onLoad={{((...args) => ((addEvents([(Event("{EvState.get_full_name()}.on_load", ({{ }})))], args, ({{ }})))))}}'
f'onLoad={{((...args) => ((addEvents([(Event("{EvState.get_full_name()}.on_load", ({{ }}), ({{ }})))], args, ({{ }})))))}}'
in render_dict["props"]
)
assert (
f'onError={{((...args) => ((addEvents([(Event("{EvState.get_full_name()}.on_error", ({{ }})))], args, ({{ }})))))}}'
f'onError={{((...args) => ((addEvents([(Event("{EvState.get_full_name()}.on_error", ({{ }}), ({{ }})))], args, ({{ }})))))}}'
in render_dict["props"]
)

View File

@ -832,7 +832,7 @@ def test_component_event_trigger_arbitrary_args():
assert comp.render()["props"][0] == (
"onFoo={((__e, _alpha, _bravo, _charlie) => ((addEvents("
f'[(Event("{C1State.get_full_name()}.mock_handler", ({{ ["_e"] : __e["target"]["value"], ["_bravo"] : _bravo["nested"], ["_charlie"] : (_charlie["custom"] + 42) }})))], '
f'[(Event("{C1State.get_full_name()}.mock_handler", ({{ ["_e"] : __e["target"]["value"], ["_bravo"] : _bravo["nested"], ["_charlie"] : (_charlie["custom"] + 42) }}), ({{ }})))], '
"[__e, _alpha, _bravo, _charlie], ({ })))))}"
)
@ -1178,7 +1178,7 @@ TEST_VAR = LiteralVar.create("test")._replace(
)
FORMATTED_TEST_VAR = LiteralVar.create(f"foo{TEST_VAR}bar")
STYLE_VAR = TEST_VAR._replace(_js_expr="style")
EVENT_CHAIN_VAR = TEST_VAR._replace(_var_type=EventChain)
EVENT_CHAIN_VAR = TEST_VAR.to(EventChain)
ARG_VAR = Var(_js_expr="arg")
TEST_VAR_DICT_OF_DICT = LiteralVar.create({"a": {"b": "test"}})._replace(
@ -2159,7 +2159,7 @@ class TriggerState(rx.State):
rx.text("random text", on_click=TriggerState.do_something),
rx.text(
"random text",
on_click=Var(_js_expr="toggleColorMode", _var_type=EventChain),
on_click=Var(_js_expr="toggleColorMode").to(EventChain),
),
),
True,
@ -2169,7 +2169,7 @@ class TriggerState(rx.State):
rx.text("random text", on_click=rx.console_log("log")),
rx.text(
"random text",
on_click=Var(_js_expr="toggleColorMode", _var_type=EventChain),
on_click=Var(_js_expr="toggleColorMode").to(EventChain),
),
),
False,

View File

@ -192,4 +192,4 @@ def test_reflex_dir_env_var(monkeypatch, tmp_path):
mp_ctx = multiprocessing.get_context(method="spawn")
with mp_ctx.Pool(processes=1) as pool:
assert pool.apply(reflex_dir_constant) == str(tmp_path)
assert pool.apply(reflex_dir_constant) == tmp_path

View File

@ -41,6 +41,7 @@ from reflex.state import (
)
from reflex.testing import chdir
from reflex.utils import format, prerequisites, types
from reflex.utils.exceptions import SetUndefinedStateVarError
from reflex.utils.format import json_dumps
from reflex.vars.base import ComputedVar, Var
from tests.units.states.mutation import MutableSQLAModel, MutableTestState
@ -3262,3 +3263,45 @@ def test_child_mixin_state() -> None:
assert "computed" in ChildUsesMixinState.inherited_vars
assert "computed" not in ChildUsesMixinState.computed_vars
def test_assignment_to_undeclared_vars():
"""Test that an attribute error is thrown when undeclared vars are set."""
class State(BaseState):
val: str
_val: str
__val: str # type: ignore
def handle_supported_regular_vars(self):
self.val = "no underscore"
self._val = "single leading underscore"
self.__val = "double leading undercore"
def handle_regular_var(self):
self.num = 5
def handle_backend_var(self):
self._num = 5
def handle_non_var(self):
self.__num = 5
class Substate(State):
def handle_var(self):
self.value = 20
state = State() # type: ignore
sub_state = Substate() # type: ignore
with pytest.raises(SetUndefinedStateVarError):
state.handle_regular_var()
with pytest.raises(SetUndefinedStateVarError):
sub_state.handle_var()
with pytest.raises(SetUndefinedStateVarError):
state.handle_backend_var()
state.handle_supported_regular_vars()
state.handle_non_var()

View File

@ -52,4 +52,4 @@ def test_send(mocker, event):
telemetry._send(event, telemetry_enabled=True)
httpx_post_mock.assert_called_once()
pathlib_path_read_text_mock.assert_called_once()
assert pathlib_path_read_text_mock.call_count == 2

View File

@ -374,7 +374,7 @@ def test_format_match(
events=[EventSpec(handler=EventHandler(fn=mock_event))],
args_spec=lambda: [],
),
'((...args) => ((addEvents([(Event("mock_event", ({ })))], args, ({ })))))',
'((...args) => ((addEvents([(Event("mock_event", ({ }), ({ })))], args, ({ })))))',
),
(
EventChain(
@ -395,7 +395,7 @@ def test_format_match(
],
args_spec=lambda e: [e.target.value],
),
'((_e) => ((addEvents([(Event("mock_event", ({ ["arg"] : _e["target"]["value"] })))], [_e], ({ })))))',
'((_e) => ((addEvents([(Event("mock_event", ({ ["arg"] : _e["target"]["value"] }), ({ })))], [_e], ({ })))))',
),
(
EventChain(
@ -403,7 +403,19 @@ def test_format_match(
args_spec=lambda: [],
event_actions={"stopPropagation": True},
),
'((...args) => ((addEvents([(Event("mock_event", ({ })))], args, ({ ["stopPropagation"] : true })))))',
'((...args) => ((addEvents([(Event("mock_event", ({ }), ({ })))], args, ({ ["stopPropagation"] : true })))))',
),
(
EventChain(
events=[
EventSpec(
handler=EventHandler(fn=mock_event),
event_actions={"stopPropagation": True},
)
],
args_spec=lambda: [],
),
'((...args) => ((addEvents([(Event("mock_event", ({ }), ({ ["stopPropagation"] : true })))], args, ({ })))))',
),
(
EventChain(
@ -411,7 +423,7 @@ def test_format_match(
args_spec=lambda: [],
event_actions={"preventDefault": True},
),
'((...args) => ((addEvents([(Event("mock_event", ({ })))], args, ({ ["preventDefault"] : true })))))',
'((...args) => ((addEvents([(Event("mock_event", ({ }), ({ })))], args, ({ ["preventDefault"] : true })))))',
),
({"a": "red", "b": "blue"}, '({ ["a"] : "red", ["b"] : "blue" })'),
(Var(_js_expr="var", _var_type=int).guess_type(), "var"),
@ -519,7 +531,7 @@ def test_format_event_handler(input, output):
[
(
EventSpec(handler=EventHandler(fn=mock_event)),
'(Event("mock_event", ({ })))',
'(Event("mock_event", ({ }), ({ })))',
),
],
)

View File

@ -117,7 +117,7 @@ def test_remove_existing_bun_installation(mocker):
Args:
mocker: Pytest mocker.
"""
mocker.patch("reflex.utils.prerequisites.os.path.exists", return_value=True)
mocker.patch("reflex.utils.prerequisites.Path.exists", return_value=True)
rm = mocker.patch("reflex.utils.prerequisites.path_ops.rm", mocker.Mock())
prerequisites.remove_existing_bun_installation()
@ -458,7 +458,7 @@ def test_bun_install_without_unzip(mocker):
mocker: Pytest mocker object.
"""
mocker.patch("reflex.utils.path_ops.which", return_value=None)
mocker.patch("os.path.exists", return_value=False)
mocker.patch("pathlib.Path.exists", return_value=False)
mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", False)
with pytest.raises(FileNotFoundError):
@ -476,7 +476,7 @@ def test_bun_install_version(mocker, bun_version):
"""
mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", False)
mocker.patch("os.path.exists", return_value=True)
mocker.patch("pathlib.Path.exists", return_value=True)
mocker.patch(
"reflex.utils.prerequisites.get_bun_version",
return_value=version.parse(bun_version),