code cleanup (split constants into a folder) (#1866)

This commit is contained in:
Thomas Brandého 2023-09-29 02:27:20 +02:00 committed by GitHub
parent 8326abf5d5
commit dcb17103bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1524 additions and 1051 deletions

View File

@ -183,8 +183,8 @@ class App(Base):
else config.cors_allowed_origins,
cors_credentials=True,
max_http_buffer_size=constants.POLLING_MAX_HTTP_BUFFER_SIZE,
ping_interval=constants.PING_INTERVAL,
ping_timeout=constants.PING_TIMEOUT,
ping_interval=constants.Ping.INTERVAL,
ping_timeout=constants.Ping.TIMEOUT,
)
# Create the socket app. Note event endpoint constant replaces the default 'socket.io' path.
@ -340,14 +340,14 @@ class App(Base):
self,
component: Component | ComponentCallable,
route: str | None = None,
title: str = constants.DEFAULT_TITLE,
description: str = constants.DEFAULT_DESCRIPTION,
image=constants.DEFAULT_IMAGE,
title: str = constants.DefaultPage.TITLE,
description: str = constants.DefaultPage.DESCRIPTION,
image: str = constants.DefaultPage.IMAGE,
on_load: EventHandler
| EventSpec
| list[EventHandler | EventSpec]
| None = None,
meta: list[dict[str, str]] = constants.DEFAULT_META_LIST,
meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
script_tags: list[Component] | None = None,
):
"""Add a page to the app.
@ -433,7 +433,7 @@ class App(Base):
"""
route = route.lstrip("/")
if route == "":
route = constants.INDEX_ROUTE
route = constants.PageNames.INDEX_ROUTE
return self.load_events.get(route, [])
def _check_routes_conflict(self, new_route: str):
@ -472,14 +472,14 @@ class App(Base):
def add_custom_404_page(
self,
component: Component | ComponentCallable | None = None,
title: str = constants.TITLE_404,
image: str = constants.FAVICON_404,
description: str = constants.DESCRIPTION_404,
title: str = constants.Page404.TITLE,
image: str = constants.Page404.IMAGE,
description: str = constants.Page404.DESCRIPTION,
on_load: EventHandler
| EventSpec
| list[EventHandler | EventSpec]
| None = None,
meta: list[dict[str, str]] = constants.DEFAULT_META_LIST,
meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
):
"""Define a custom 404 page for any url having no match.
@ -498,10 +498,10 @@ class App(Base):
component = Default404Page.create()
self.add_page(
component=wait_for_client_redirect(self._generate_component(component)),
route=constants.SLUG_404,
title=title or constants.TITLE_404,
image=image or constants.FAVICON_404,
description=description or constants.DESCRIPTION_404,
route=constants.Page404.SLUG,
title=title or constants.Page404.TITLE,
image=image or constants.Page404.IMAGE,
description=description or constants.Page404.DESCRIPTION,
on_load=on_load,
meta=meta,
)
@ -541,11 +541,13 @@ class App(Base):
page_imports = {
i
for i, tags in imports.items()
if i not in compiler.DEFAULT_IMPORTS.keys()
and i != "focus-visible/dist/focus-visible"
and "next" not in i
and not i.startswith("/")
and not i.startswith(".")
if i
not in [
*compiler.DEFAULT_IMPORTS.keys(),
*constants.PackageJson.DEPENDENCIES.keys(),
*constants.PackageJson.DEV_DEPENDENCIES.keys(),
]
and not any(i.startswith(prefix) for prefix in ["/", ".", "next/"])
and i != ""
and any(tag.install for tag in tags)
}
@ -581,7 +583,7 @@ class App(Base):
self.add_page(render, **kwargs)
# Render a default 404 page if the user didn't supply one
if constants.SLUG_404 not in self.pages:
if constants.Page404.SLUG not in self.pages:
self.add_custom_404_page()
task = progress.add_task("Compiling: ", total=len(self.pages))
@ -649,7 +651,7 @@ class App(Base):
# Compile the Tailwind config.
if config.tailwind is not None:
config.tailwind["content"] = config.tailwind.get(
"content", constants.TAILWIND_CONTENT
"content", constants.Tailwind.CONTENT
)
compile_results.append(compiler.compile_tailwind(config.tailwind))

View File

@ -23,7 +23,7 @@ DEFAULT_IMPORTS: imports.ImportDict = {
ImportVar(tag="useContext"),
},
"next/router": {ImportVar(tag="useRouter")},
f"/{constants.STATE_PATH}": {
f"/{constants.Dirs.STATE_PATH}": {
ImportVar(tag="uploadFiles"),
ImportVar(tag="Event"),
ImportVar(tag="isTrue"),
@ -40,9 +40,9 @@ DEFAULT_IMPORTS: imports.ImportDict = {
ImportVar(tag="initialEvents"),
ImportVar(tag="StateContext"),
},
"": {ImportVar(tag="focus-visible/dist/focus-visible")},
"": {ImportVar(tag="focus-visible/dist/focus-visible", install=False)},
"@chakra-ui/react": {
ImportVar(tag=constants.USE_COLOR_MODE),
ImportVar(tag=constants.ColorMode.USE),
ImportVar(tag="Box"),
ImportVar(tag="Text"),
},
@ -151,7 +151,7 @@ def _compile_root_stylesheet(stylesheets: list[str]) -> str:
"""
# Add tailwind css if enabled.
sheets = (
[constants.TAILWIND_ROOT_STYLE_PATH]
[constants.Tailwind.ROOT_STYLE_PATH]
if get_config().tailwind is not None
else []
)
@ -159,7 +159,7 @@ def _compile_root_stylesheet(stylesheets: list[str]) -> str:
if not utils.is_valid_url(stylesheet):
# check if stylesheet provided exists.
stylesheet_full_path = (
Path.cwd() / constants.APP_ASSETS_DIR / stylesheet.strip("/")
Path.cwd() / constants.Dirs.APP_ASSETS / stylesheet.strip("/")
)
if not os.path.exists(stylesheet_full_path):
raise FileNotFoundError(
@ -193,7 +193,7 @@ def _compile_components(components: set[CustomComponent]) -> str:
"""
imports = {
"react": {ImportVar(tag="memo")},
f"/{constants.STATE_PATH}": {ImportVar(tag="E"), ImportVar(tag="isTrue")},
f"/{constants.Dirs.STATE_PATH}": {ImportVar(tag="E"), ImportVar(tag="isTrue")},
}
component_renders = []
@ -236,7 +236,7 @@ def compile_document_root(head_components: list[Component]) -> tuple[str, str]:
The path and code of the compiled document root.
"""
# Get the path for the output file.
output_path = utils.get_page_path(constants.DOCUMENT_ROOT)
output_path = utils.get_page_path(constants.PageNames.DOCUMENT_ROOT)
# Create the document root.
document_root = utils.create_document_root(head_components)
@ -330,7 +330,7 @@ def compile_tailwind(
The compiled Tailwind config.
"""
# Get the path for the output file.
output_path = constants.TAILWIND_CONFIG
output_path = constants.Tailwind.CONFIG
# Compile the config.
code = _compile_tailwind(config)
@ -339,4 +339,4 @@ def compile_tailwind(
def purge_web_pages_dir():
"""Empty out .web directory."""
utils.empty_dir(constants.WEB_PAGES_DIR, keep_files=["_app.js"])
utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"])

View File

@ -19,26 +19,26 @@ class ReflexJinjaEnvironment(Environment):
)
self.filters["json_dumps"] = json_dumps
self.filters["react_setter"] = lambda state: f"set{state.capitalize()}"
self.loader = FileSystemLoader(constants.JINJA_TEMPLATE_DIR)
self.loader = FileSystemLoader(constants.Templates.Dirs.JINJA_TEMPLATE)
self.globals["const"] = {
"socket": constants.SOCKET,
"result": constants.RESULT,
"router": constants.ROUTER,
"socket": constants.CompileVars.SOCKET,
"result": constants.CompileVars.RESULT,
"router": constants.CompileVars.ROUTER,
"event_endpoint": constants.Endpoint.EVENT.name,
"events": constants.EVENTS,
"state": constants.STATE,
"final": constants.FINAL,
"processing": constants.PROCESSING,
"events": constants.CompileVars.EVENTS,
"state": constants.CompileVars.STATE,
"final": constants.CompileVars.FINAL,
"processing": constants.CompileVars.PROCESSING,
"initial_result": {
constants.STATE: None,
constants.EVENTS: [],
constants.FINAL: True,
constants.PROCESSING: False,
constants.CompileVars.STATE: None,
constants.CompileVars.EVENTS: [],
constants.CompileVars.FINAL: True,
constants.CompileVars.PROCESSING: False,
},
"color_mode": constants.COLOR_MODE,
"toggle_color_mode": constants.TOGGLE_COLOR_MODE,
"use_color_mode": constants.USE_COLOR_MODE,
"hydrate": constants.HYDRATE,
"color_mode": constants.ColorMode.NAME,
"toggle_color_mode": constants.ColorMode.TOGGLE,
"use_color_mode": constants.ColorMode.USE,
"hydrate": constants.CompileVars.HYDRATE,
}

View File

@ -93,7 +93,7 @@ def compile_imports(imports: imports.ImportDict) -> list[dict]:
default, rest = compile_import_statement(fields)
# prevent lib from being rendered on the page if all imports are non rendered kind
if all({not f.render for f in fields}): # type: ignore
if not any({f.render for f in fields}): # type: ignore
continue
if not lib:
@ -104,9 +104,7 @@ def compile_imports(imports: imports.ImportDict) -> list[dict]:
continue
# remove the version before rendering the package imports
lib, at, version = lib.rpartition("@")
if not lib:
lib = at + version
lib = format.format_library_name(lib)
import_dicts.append(get_import_dict(lib, default, rest))
return import_dicts
@ -313,7 +311,7 @@ def get_page_path(path: str) -> str:
Returns:
The path of the compiled JS file.
"""
return os.path.join(constants.WEB_PAGES_DIR, path + constants.JS_EXT)
return os.path.join(constants.Dirs.WEB_PAGES, path + constants.Ext.JS)
def get_theme_path() -> str:
@ -322,7 +320,9 @@ def get_theme_path() -> str:
Returns:
The path of the theme style.
"""
return os.path.join(constants.WEB_UTILS_DIR, constants.THEME + constants.JS_EXT)
return os.path.join(
constants.Dirs.WEB_UTILS, constants.PageNames.THEME + constants.Ext.JS
)
def get_root_stylesheet_path() -> str:
@ -332,7 +332,7 @@ def get_root_stylesheet_path() -> str:
The path of the app root file.
"""
return os.path.join(
constants.STYLES_DIR, constants.STYLESHEET_ROOT + constants.CSS_EXT
constants.STYLES_DIR, constants.PageNames.STYLESHEET_ROOT + constants.Ext.CSS
)
@ -342,7 +342,7 @@ def get_context_path() -> str:
Returns:
The path of the context module.
"""
return os.path.join(constants.WEB_UTILS_DIR, "context" + constants.JS_EXT)
return os.path.join(constants.Dirs.WEB_UTILS, "context" + constants.Ext.JS)
def get_components_path() -> str:
@ -351,7 +351,7 @@ def get_components_path() -> str:
Returns:
The path of the compiled components.
"""
return os.path.join(constants.WEB_UTILS_DIR, "components" + constants.JS_EXT)
return os.path.join(constants.Dirs.WEB_UTILS, "components" + constants.Ext.JS)
def get_asset_path(filename: str | None = None) -> str:
@ -364,9 +364,9 @@ def get_asset_path(filename: str | None = None) -> str:
The path of the asset.
"""
if filename is None:
return constants.WEB_ASSETS_DIR
return constants.Dirs.WEB_ASSETS
else:
return os.path.join(constants.WEB_ASSETS_DIR, filename)
return os.path.join(constants.Dirs.WEB_ASSETS, filename)
def add_meta(

View File

@ -7,10 +7,9 @@ from abc import ABC
from functools import wraps
from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
from reflex import constants
from reflex.base import Base
from reflex.components.tags import Tag
from reflex.constants import EventTriggers
from reflex.constants import Dirs, EventTriggers
from reflex.event import (
EventChain,
EventHandler,
@ -762,7 +761,7 @@ class CustomComponent(Component):
"""A custom user-defined component."""
# Use the components library.
library = f"/{constants.COMPONENTS_PATH}"
library = f"/{Dirs.COMPONENTS_PATH}"
# The function that creates the component.
component_fn: Callable[..., Component] = Component.create

View File

@ -163,7 +163,7 @@ class Config(Base):
telemetry_enabled: bool = True
# The bun path
bun_path: str = constants.DEFAULT_BUN_PATH
bun_path: str = constants.Bun.DEFAULT_PATH
# List of origins that are allowed to connect to the backend API.
cors_allowed_origins: List[str] = ["*"]
@ -284,11 +284,9 @@ def get_config(reload: bool = False) -> Config:
Returns:
The app config.
"""
from reflex.config import Config
sys.path.insert(0, os.getcwd())
try:
rxconfig = __import__(constants.CONFIG_MODULE)
rxconfig = __import__(constants.Config.MODULE)
if reload:
importlib.reload(rxconfig)
return rxconfig.config

View File

@ -1,444 +0,0 @@
"""Constants used throughout the package."""
from __future__ import annotations
import os
import platform
import re
from enum import Enum
from types import SimpleNamespace
from platformdirs import PlatformDirs
# importlib is only available for Python 3.8+ so we need the backport for Python 3.7
try:
from importlib import metadata
except ImportError:
import importlib_metadata as metadata # pyright: ignore[reportMissingImports]
IS_WINDOWS = platform.system() == "Windows"
def get_fnm_name() -> str | None:
"""Get the appropriate fnm executable name based on the current platform.
Returns:
The fnm executable name for the current platform.
"""
platform_os = platform.system()
if platform_os == "Windows":
return "fnm-windows"
elif platform_os == "Darwin":
return "fnm-macos"
elif platform_os == "Linux":
machine = platform.machine()
if machine == "arm" or machine.startswith("armv7"):
return "fnm-arm32"
elif machine.startswith("aarch") or machine.startswith("armv8"):
return "fnm-arm64"
return "fnm-linux"
return None
# App names and versions.
# The name of the Reflex package.
MODULE_NAME = "reflex"
# The current version of Reflex.
VERSION = metadata.version(MODULE_NAME)
# Files and directories used to init a new project.
# The directory to store reflex dependencies.
REFLEX_DIR = (
# 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.
PlatformDirs(MODULE_NAME, False).user_data_dir
)
# The root directory of the reflex library.
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# The name of the assets directory.
APP_ASSETS_DIR = "assets"
# The template directory used during reflex init.
TEMPLATE_DIR = os.path.join(ROOT_DIR, MODULE_NAME, ".templates")
# The web subdirectory of the template directory.
WEB_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, "web")
# The assets subdirectory of the template directory.
ASSETS_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, APP_ASSETS_DIR)
# The jinja template directory.
JINJA_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, "jinja")
# Bun config.
# The Bun version.
BUN_VERSION = "0.7.3"
# Min Bun Version
MIN_BUN_VERSION = "0.7.0"
# The directory to store the bun.
BUN_ROOT_PATH = os.path.join(REFLEX_DIR, "bun")
# Default bun path.
DEFAULT_BUN_PATH = os.path.join(BUN_ROOT_PATH, "bin", "bun")
# URL to bun install script.
BUN_INSTALL_URL = "https://bun.sh/install"
# FNM / Node config.
# The FNM version.
FNM_VERSION = "1.35.1"
# The Node version.
NODE_VERSION = "18.17.0"
# The minimum required node version.
NODE_VERSION_MIN = "16.8.0"
# The directory to store fnm.
FNM_DIR = os.path.join(REFLEX_DIR, "fnm")
FNM_FILENAME = get_fnm_name()
# The fnm executable binary.
FNM_EXE = os.path.join(FNM_DIR, "fnm.exe" if IS_WINDOWS else "fnm")
# The node bin path.
NODE_BIN_PATH = os.path.join(
FNM_DIR,
"node-versions",
f"v{NODE_VERSION}",
"installation",
"bin" if not IS_WINDOWS else "",
)
# The default path where node is installed.
NODE_PATH = os.path.join(NODE_BIN_PATH, "node.exe" if IS_WINDOWS else "node")
# The default path where npm is installed.
NPM_PATH = os.path.join(NODE_BIN_PATH, "npm")
# The URL to the fnm release binary
FNM_INSTALL_URL = (
f"https://github.com/Schniz/fnm/releases/download/v{FNM_VERSION}/{FNM_FILENAME}.zip"
)
# The frontend directories in a project.
# The web folder where the NextJS app is compiled to.
WEB_DIR = ".web"
# The name of the utils file.
UTILS_DIR = "utils"
# The name of the output static directory.
STATIC_DIR = "_static"
# The name of the state file.
STATE_PATH = "/".join([UTILS_DIR, "state"])
# The name of the components file.
COMPONENTS_PATH = "/".join([UTILS_DIR, "components"])
# The directory where the app pages are compiled to.
WEB_PAGES_DIR = os.path.join(WEB_DIR, "pages")
# The directory where the static build is located.
WEB_STATIC_DIR = os.path.join(WEB_DIR, STATIC_DIR)
# The directory where the utils file is located.
WEB_UTILS_DIR = os.path.join(WEB_DIR, UTILS_DIR)
# The directory where the assets are located.
WEB_ASSETS_DIR = os.path.join(WEB_DIR, "public")
# The directory where styles are located.
STYLES_DIR = os.path.join(WEB_DIR, "styles")
# The Tailwind config.
TAILWIND_CONFIG = os.path.join(WEB_DIR, "tailwind.config.js")
# Default Tailwind content paths
TAILWIND_CONTENT = ["./pages/**/*.{js,ts,jsx,tsx}"]
# Relative tailwind style path to root stylesheet in STYLES_DIR.
TAILWIND_ROOT_STYLE_PATH = "./tailwind.css"
# The Tailwindcss version
TAILWIND_VERSION = "tailwindcss@^3.3.2"
# The package json file
PACKAGE_JSON_PATH = os.path.join(WEB_DIR, "package.json")
# The NextJS config file
NEXT_CONFIG_FILE = "next.config.js"
# The sitemap config file.
SITEMAP_CONFIG_FILE = os.path.join(WEB_DIR, "next-sitemap.config.js")
# The node modules directory.
NODE_MODULES = "node_modules"
# The package lock file.
PACKAGE_LOCK = "package-lock.json"
# The reflex json file.
REFLEX_JSON = os.path.join(WEB_DIR, "reflex.json")
# The env json file.
ENV_JSON = os.path.join(WEB_DIR, "env.json")
# Compiler variables.
# The extension for compiled Javascript files.
JS_EXT = ".js"
# The extension for python files.
PY_EXT = ".py"
# The extension for css files.
CSS_EXT = ".css"
# The expected variable name where the rx.App is stored.
APP_VAR = "app"
# The expected variable name where the API object is stored for deployment.
API_VAR = "api"
# The name of the router variable.
ROUTER = "router"
# The name of the socket variable.
SOCKET = "socket"
# The name of the variable to hold API results.
RESULT = "result"
# The name of the final variable.
FINAL = "final"
# The name of the process variable.
PROCESSING = "processing"
# The name of the state variable.
STATE = "state"
# The name of the events variable.
EVENTS = "events"
# The name of the initial hydrate event.
HYDRATE = "hydrate"
# The name of the is_hydrated variable.
IS_HYDRATED = "is_hydrated"
# The name of the index page.
INDEX_ROUTE = "index"
# The name of the app root page.
APP_ROOT = "_app"
# The root stylesheet filename.
STYLESHEET_ROOT = "styles"
# The name of the document root page.
DOCUMENT_ROOT = "_document"
# The name of the theme page.
THEME = "theme"
# The prefix used to create setters for state vars.
SETTER_PREFIX = "set_"
# The name of the frontend zip during deployment.
FRONTEND_ZIP = "frontend.zip"
# The name of the backend zip during deployment.
BACKEND_ZIP = "backend.zip"
# The default title to show for Reflex apps.
DEFAULT_TITLE = "Reflex App"
# The default description to show for Reflex apps.
DEFAULT_DESCRIPTION = "A Reflex app."
# The default image to show for Reflex apps.
DEFAULT_IMAGE = "favicon.ico"
# The default meta list to show for Reflex apps.
DEFAULT_META_LIST = []
# The gitignore file.
GITIGNORE_FILE = ".gitignore"
# Files to gitignore.
DEFAULT_GITIGNORE = {WEB_DIR, "*.db", "__pycache__/", "*.py[cod]"}
# The name of the reflex config module.
CONFIG_MODULE = "rxconfig"
# The python config file.
CONFIG_FILE = f"{CONFIG_MODULE}{PY_EXT}"
# The previous config file.
OLD_CONFIG_FILE = f"pcconfig{PY_EXT}"
# The deployment URL.
PRODUCTION_BACKEND_URL = "https://{username}-{app_name}.api.pynecone.app"
# Token expiration time in seconds.
TOKEN_EXPIRATION = 60 * 60
# Maximum time in milliseconds that a state can be locked for exclusive access.
LOCK_EXPIRATION = 10000
# Testing variables.
# Testing os env set by pytest when running a test case.
PYTEST_CURRENT_TEST = "PYTEST_CURRENT_TEST"
# Env modes
class Env(str, Enum):
"""The environment modes."""
DEV = "dev"
PROD = "prod"
# Log levels
class LogLevel(str, Enum):
"""The log levels."""
DEBUG = "debug"
INFO = "info"
WARNING = "warning"
ERROR = "error"
CRITICAL = "critical"
def __le__(self, other: LogLevel) -> bool:
"""Compare log levels.
Args:
other: The other log level.
Returns:
True if the log level is less than or equal to the other log level.
"""
levels = list(LogLevel)
return levels.index(self) <= levels.index(other)
# Templates
class Template(str, Enum):
"""The templates to use for the app."""
DEFAULT = "default"
COUNTER = "counter"
class Endpoint(Enum):
"""Endpoints for the reflex backend API."""
PING = "ping"
EVENT = "_event"
UPLOAD = "_upload"
def __str__(self) -> str:
"""Get the string representation of the endpoint.
Returns:
The path for the endpoint.
"""
return f"/{self.value}"
def get_url(self) -> str:
"""Get the URL for the endpoint.
Returns:
The full URL for the endpoint.
"""
# Import here to avoid circular imports.
from reflex.config import get_config
# Get the API URL from the config.
config = get_config()
url = "".join([config.api_url, str(self)])
# The event endpoint is a websocket.
if self == Endpoint.EVENT:
# Replace the protocol with ws.
url = url.replace("https://", "wss://").replace("http://", "ws://")
# Return the url.
return url
class SocketEvent(Enum):
"""Socket events sent by the reflex backend API."""
PING = "ping"
EVENT = "event"
def __str__(self) -> str:
"""Get the string representation of the event name.
Returns:
The event name string.
"""
return str(self.value)
class RouteArgType(SimpleNamespace):
"""Type of dynamic route arg extracted from URI route."""
# Typecast to str is needed for Enum to work.
SINGLE = str("arg_single")
LIST = str("arg_list")
# the name of the backend var containing path and client information
ROUTER_DATA = "router_data"
class RouteVar(SimpleNamespace):
"""Names of variables used in the router_data dict stored in State."""
CLIENT_IP = "ip"
CLIENT_TOKEN = "token"
HEADERS = "headers"
PATH = "pathname"
ORIGIN = "asPath"
SESSION_ID = "sid"
QUERY = "query"
COOKIE = "cookie"
class RouteRegex(SimpleNamespace):
"""Regex used for extracting route args in route."""
ARG = re.compile(r"\[(?!\.)([^\[\]]+)\]")
# group return the catchall pattern (i.e. "[[..slug]]")
CATCHALL = re.compile(r"(\[?\[\.{3}(?![0-9]).*\]?\])")
# group return the arg name (i.e. "slug")
STRICT_CATCHALL = re.compile(r"\[\.{3}([a-zA-Z_][\w]*)\]")
# group return the arg name (i.e. "slug")
OPT_CATCHALL = re.compile(r"\[\[\.{3}([a-zA-Z_][\w]*)\]\]")
class PackageJsonCommands(SimpleNamespace):
"""Commands used in package.json file."""
DEV = "next dev"
EXPORT = "next build && next export -o _static"
EXPORT_SITEMAP = "next build && next-sitemap && next export -o _static"
PROD = "next start"
PACKAGE_DEPENDENCIES = {
"@chakra-ui/react": "^2.6.0",
"@chakra-ui/system": "^2.5.6",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"axios": "^1.4.0",
"chakra-react-select": "^4.6.0",
"focus-visible": "^5.2.0",
"json5": "^2.2.3",
"next": "^13.3.1",
"next-sitemap": "^4.1.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"socket.io-client": "^4.6.1",
"universal-cookie": "^4.0.4",
}
PACKAGE_DEV_DEPENDENCIES = {
"autoprefixer": "^10.4.14",
"postcss": "^8.4.24",
}
# 404 variables
SLUG_404 = "404"
TITLE_404 = "404 - Not Found"
FAVICON_404 = "favicon.ico"
DESCRIPTION_404 = "The page was not found"
ROUTE_NOT_FOUND = "routeNotFound"
# Color mode variables
USE_COLOR_MODE = "useColorMode"
COLOR_MODE = "colorMode"
TOGGLE_COLOR_MODE = "toggleColorMode"
# Server socket configuration variables
POLLING_MAX_HTTP_BUFFER_SIZE = 1000 * 1000
PING_INTERVAL = 25
PING_TIMEOUT = 120
# Alembic migrations
ALEMBIC_CONFIG = os.environ.get("ALEMBIC_CONFIG", "alembic.ini")
# Keys in the client_side_storage dict
COOKIES = "cookies"
LOCAL_STORAGE = "local_storage"
class EventTriggers(SimpleNamespace):
"""All trigger names used in Reflex."""
ON_FOCUS = "on_focus"
ON_BLUR = "on_blur"
ON_CANCEL = "on_cancel"
ON_CLICK = "on_click"
ON_CHANGE = "on_change"
ON_CHANGE_END = "on_change_end"
ON_CHANGE_START = "on_change_start"
ON_COMPLETE = "on_complete"
ON_CONTEXT_MENU = "on_context_menu"
ON_DOUBLE_CLICK = "on_double_click"
ON_DROP = "on_drop"
ON_EDIT = "on_edit"
ON_KEY_DOWN = "on_key_down"
ON_KEY_UP = "on_key_up"
ON_MOUSE_DOWN = "on_mouse_down"
ON_MOUSE_ENTER = "on_mouse_enter"
ON_MOUSE_LEAVE = "on_mouse_leave"
ON_MOUSE_MOVE = "on_mouse_move"
ON_MOUSE_OUT = "on_mouse_out"
ON_MOUSE_OVER = "on_mouse_over"
ON_MOUSE_UP = "on_mouse_up"
ON_SCROLL = "on_scroll"
ON_SUBMIT = "on_submit"
ON_MOUNT = "on_mount"
ON_UNMOUNT = "on_unmount"
# If this env var is set to "yes", App.compile will be a no-op
SKIP_COMPILE_ENV_VAR = "__REFLEX_SKIP_COMPILE"

View File

@ -0,0 +1,92 @@
"""The constants package."""
from .base import (
COOKIES,
IS_WINDOWS,
LOCAL_STORAGE,
POLLING_MAX_HTTP_BUFFER_SIZE,
PYTEST_CURRENT_TEST,
SKIP_COMPILE_ENV_VAR,
ColorMode,
Dirs,
Env,
LogLevel,
Next,
Ping,
Reflex,
Templates,
)
from .compiler import (
SETTER_PREFIX,
CompileVars,
ComponentName,
Ext,
PageNames,
)
from .config import (
ALEMBIC_CONFIG,
PRODUCTION_BACKEND_URL,
Config,
Expiration,
GitIgnore,
)
from .event import Endpoint, EventTriggers, SocketEvent
from .installer import (
Bun,
Fnm,
Node,
PackageJson,
)
from .route import (
ROUTE_NOT_FOUND,
ROUTER_DATA,
DefaultPage,
Page404,
RouteArgType,
RouteRegex,
RouteVar,
)
from .style import STYLES_DIR, Tailwind
__ALL__ = [
ALEMBIC_CONFIG,
Bun,
ColorMode,
Config,
COOKIES,
ComponentName,
DefaultPage,
Dirs,
Endpoint,
Env,
EventTriggers,
Expiration,
Ext,
Fnm,
GitIgnore,
IS_WINDOWS,
LOCAL_STORAGE,
LogLevel,
Next,
Node,
PackageJson,
PageNames,
Page404,
Ping,
POLLING_MAX_HTTP_BUFFER_SIZE,
PYTEST_CURRENT_TEST,
PRODUCTION_BACKEND_URL,
Reflex,
RouteVar,
RouteRegex,
RouteArgType,
ROUTER_DATA,
ROUTE_NOT_FOUND,
SETTER_PREFIX,
SKIP_COMPILE_ENV_VAR,
SocketEvent,
STYLES_DIR,
Tailwind,
Templates,
CompileVars,
]

174
reflex/constants/base.py Normal file
View File

@ -0,0 +1,174 @@
"""Base file for constants that don't fit any other categories."""
from __future__ import annotations
import os
import platform
from enum import Enum
from types import SimpleNamespace
from platformdirs import PlatformDirs
# importlib is only available for Python 3.8+ so we need the backport for Python 3.7
try:
from importlib import metadata
except ImportError:
import importlib_metadata as metadata # pyright: ignore[reportMissingImports]
IS_WINDOWS = platform.system() == "Windows"
class Dirs(SimpleNamespace):
"""Various directories/paths used by Reflex."""
# The frontend directories in a project.
# The web folder where the NextJS app is compiled to.
WEB = ".web"
# The name of the assets directory.
APP_ASSETS = "assets"
# The name of the utils file.
UTILS = "utils"
# The name of the output static directory.
STATIC = "_static"
# The name of the state file.
STATE_PATH = "/".join([UTILS, "state"])
# The name of the components file.
COMPONENTS_PATH = "/".join([UTILS, "components"])
# The directory where the app pages are compiled to.
WEB_PAGES = os.path.join(WEB, "pages")
# The directory where the static build is located.
WEB_STATIC = os.path.join(WEB, STATIC)
# The directory where the utils file is located.
WEB_UTILS = os.path.join(WEB, UTILS)
# The directory where the assets are located.
WEB_ASSETS = os.path.join(WEB, "public")
# The env json file.
ENV_JSON = os.path.join(WEB, "env.json")
class Reflex(SimpleNamespace):
"""Base constants concerning Reflex."""
# App names and versions.
# The name of the Reflex package.
MODULE_NAME = "reflex"
# The current version of Reflex.
VERSION = metadata.version(MODULE_NAME)
# The reflex json file.
JSON = os.path.join(Dirs.WEB, "reflex.json")
# Files and directories used to init a new project.
# The directory to store reflex dependencies.
DIR = (
# 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.
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__)))
)
class Templates(SimpleNamespace):
"""Constants related to Templates."""
class Kind(str, Enum):
"""The templates to use for the app."""
DEFAULT = "default"
COUNTER = "counter"
class Dirs(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")
# The web subdirectory of the template directory.
WEB_TEMPLATE = os.path.join(BASE, "web")
# The assets subdirectory of the template directory.
ASSETS_TEMPLATE = os.path.join(BASE, Dirs.APP_ASSETS)
# The jinja template directory.
JINJA_TEMPLATE = os.path.join(BASE, "jinja")
class Next(SimpleNamespace):
"""Constants related to NextJS."""
# The NextJS config file
CONFIG_FILE = "next.config.js"
# The sitemap config file.
SITEMAP_CONFIG_FILE = os.path.join(Dirs.WEB, "next-sitemap.config.js")
# The node modules directory.
NODE_MODULES = "node_modules"
# The package lock file.
PACKAGE_LOCK = "package-lock.json"
# Color mode variables
class ColorMode(SimpleNamespace):
"""Constants related to ColorMode."""
NAME = "colorMode"
USE = "useColorMode"
TOGGLE = "toggleColorMode"
# Env modes
class Env(str, Enum):
"""The environment modes."""
DEV = "dev"
PROD = "prod"
# Log levels
class LogLevel(str, Enum):
"""The log levels."""
DEBUG = "debug"
INFO = "info"
WARNING = "warning"
ERROR = "error"
CRITICAL = "critical"
def __le__(self, other: LogLevel) -> bool:
"""Compare log levels.
Args:
other: The other log level.
Returns:
True if the log level is less than or equal to the other log level.
"""
levels = list(LogLevel)
return levels.index(self) <= levels.index(other)
# Server socket configuration variables
POLLING_MAX_HTTP_BUFFER_SIZE = 1000 * 1000
class Ping(SimpleNamespace):
"""PING constants."""
# The 'ping' interval
INTERVAL = 25
# The 'ping' timeout
TIMEOUT = 120
# Keys in the client_side_storage dict
COOKIES = "cookies"
LOCAL_STORAGE = "local_storage"
# If this env var is set to "yes", App.compile will be a no-op
SKIP_COMPILE_ENV_VAR = "__REFLEX_SKIP_COMPILE"
# Testing variables.
# Testing os env set by pytest when running a test case.
PYTEST_CURRENT_TEST = "PYTEST_CURRENT_TEST"

View File

@ -0,0 +1,76 @@
"""Compiler variables."""
from enum import Enum
from types import SimpleNamespace
# The prefix used to create setters for state vars.
SETTER_PREFIX = "set_"
class Ext(SimpleNamespace):
"""Extension used in Reflex."""
# The extension for JS files.
JS = ".js"
# The extension for python files.
PY = ".py"
# The extension for css files.
CSS = ".css"
# The extension for zip files.
ZIP = ".zip"
class CompileVars(SimpleNamespace):
"""The variables used during compilation."""
# The expected variable name where the rx.App is stored.
APP = "app"
# The expected variable name where the API object is stored for deployment.
API = "api"
# The name of the router variable.
ROUTER = "router"
# The name of the socket variable.
SOCKET = "socket"
# The name of the variable to hold API results.
RESULT = "result"
# The name of the final variable.
FINAL = "final"
# The name of the process variable.
PROCESSING = "processing"
# The name of the state variable.
STATE = "state"
# The name of the events variable.
EVENTS = "events"
# The name of the initial hydrate event.
HYDRATE = "hydrate"
# The name of the is_hydrated variable.
IS_HYDRATED = "is_hydrated"
class PageNames(SimpleNamespace):
"""The name of basic pages deployed in NextJS."""
# The name of the index page.
INDEX_ROUTE = "index"
# The name of the app root page.
APP_ROOT = "_app"
# The root stylesheet filename.
STYLESHEET_ROOT = "styles"
# The name of the document root page.
DOCUMENT_ROOT = "_document"
# The name of the theme page.
THEME = "theme"
class ComponentName(Enum):
"""Component names."""
BACKEND = "Backend"
FRONTEND = "Frontend"
def zip(self):
"""Give the zip filename for the component.
Returns:
The lower-case filename with zip extension.
"""
return self.value.lower() + Ext.ZIP

View File

@ -0,0 +1,45 @@
"""Config constants."""
import os
from types import SimpleNamespace
from reflex.constants.base import Dirs
from .compiler import Ext
# Alembic migrations
ALEMBIC_CONFIG = os.environ.get("ALEMBIC_CONFIG", "alembic.ini")
class Config(SimpleNamespace):
"""Config constants."""
# 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}"
class Expiration(SimpleNamespace):
"""Expiration constants."""
# Token expiration time in seconds
TOKEN = 60 * 60
# Maximum time in milliseconds that a state can be locked for exclusive access.
LOCK = 10000
# The PING timeout
PING = 120
class GitIgnore(SimpleNamespace):
"""Gitignore constants."""
# The gitignore file.
FILE = ".gitignore"
# Files to gitignore.
DEFAULTS = {Dirs.WEB, "*.db", "__pycache__/", "*.py[cod]"}
# The deployment URL.
PRODUCTION_BACKEND_URL = "https://{username}-{app_name}.api.pynecone.app"

85
reflex/constants/event.py Normal file
View File

@ -0,0 +1,85 @@
"""Event-related constants."""
from enum import Enum
from types import SimpleNamespace
class Endpoint(Enum):
"""Endpoints for the reflex backend API."""
PING = "ping"
EVENT = "_event"
UPLOAD = "_upload"
def __str__(self) -> str:
"""Get the string representation of the endpoint.
Returns:
The path for the endpoint.
"""
return f"/{self.value}"
def get_url(self) -> str:
"""Get the URL for the endpoint.
Returns:
The full URL for the endpoint.
"""
# Import here to avoid circular imports.
from reflex.config import get_config
# Get the API URL from the config.
config = get_config()
url = "".join([config.api_url, str(self)])
# The event endpoint is a websocket.
if self == Endpoint.EVENT:
# Replace the protocol with ws.
url = url.replace("https://", "wss://").replace("http://", "ws://")
# Return the url.
return url
class SocketEvent(SimpleNamespace):
"""Socket events sent by the reflex backend API."""
PING = "ping"
EVENT = "event"
def __str__(self) -> str:
"""Get the string representation of the event name.
Returns:
The event name string.
"""
return str(self.value)
class EventTriggers(SimpleNamespace):
"""All trigger names used in Reflex."""
ON_FOCUS = "on_focus"
ON_BLUR = "on_blur"
ON_CANCEL = "on_cancel"
ON_CLICK = "on_click"
ON_CHANGE = "on_change"
ON_CHANGE_END = "on_change_end"
ON_CHANGE_START = "on_change_start"
ON_COMPLETE = "on_complete"
ON_CONTEXT_MENU = "on_context_menu"
ON_DOUBLE_CLICK = "on_double_click"
ON_DROP = "on_drop"
ON_EDIT = "on_edit"
ON_KEY_DOWN = "on_key_down"
ON_KEY_UP = "on_key_up"
ON_MOUSE_DOWN = "on_mouse_down"
ON_MOUSE_ENTER = "on_mouse_enter"
ON_MOUSE_LEAVE = "on_mouse_leave"
ON_MOUSE_MOVE = "on_mouse_move"
ON_MOUSE_OUT = "on_mouse_out"
ON_MOUSE_OVER = "on_mouse_over"
ON_MOUSE_UP = "on_mouse_up"
ON_SCROLL = "on_scroll"
ON_SUBMIT = "on_submit"
ON_MOUNT = "on_mount"
ON_UNMOUNT = "on_unmount"

View File

@ -0,0 +1,123 @@
"""File for constants related to the installation process. (Bun/FNM/Node)."""
from __future__ import annotations
import os
import platform
from types import SimpleNamespace
from .base import IS_WINDOWS, Dirs, Reflex
def get_fnm_name() -> str | None:
"""Get the appropriate fnm executable name based on the current platform.
Returns:
The fnm executable name for the current platform.
"""
platform_os = platform.system()
if platform_os == "Windows":
return "fnm-windows"
elif platform_os == "Darwin":
return "fnm-macos"
elif platform_os == "Linux":
machine = platform.machine()
if machine == "arm" or machine.startswith("armv7"):
return "fnm-arm32"
elif machine.startswith("aarch") or machine.startswith("armv8"):
return "fnm-arm64"
return "fnm-linux"
return None
# Bun config.
class Bun(SimpleNamespace):
"""Bun constants."""
# The Bun version.
VERSION = "0.7.3"
# Min Bun Version
MIN_VERSION = "0.7.0"
# The directory to store the bun.
ROOT_PATH = os.path.join(Reflex.DIR, "bun")
# Default bun path.
DEFAULT_PATH = os.path.join(ROOT_PATH, "bin", "bun")
# URL to bun install script.
INSTALL_URL = "https://bun.sh/install"
# FNM config.
class Fnm(SimpleNamespace):
"""FNM constants."""
# The FNM version.
VERSION = "1.35.1"
# The directory to store fnm.
DIR = os.path.join(Reflex.DIR, "fnm")
FILENAME = get_fnm_name()
# The fnm executable binary.
EXE = os.path.join(DIR, "fnm.exe" if IS_WINDOWS else "fnm")
# The URL to the fnm release binary
INSTALL_URL = (
f"https://github.com/Schniz/fnm/releases/download/v{VERSION}/{FILENAME}.zip"
)
# Node / NPM config
class Node(SimpleNamespace):
"""Node/ NPM constants."""
# The Node version.
VERSION = "18.17.0"
# The minimum required node version.
MIN_VERSION = "16.8.0"
# The node bin path.
BIN_PATH = os.path.join(
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")
# The default path where npm is installed.
NPM_PATH = os.path.join(BIN_PATH, "npm")
class PackageJson(SimpleNamespace):
"""Constants used to build the package.json file."""
class Commands(SimpleNamespace):
"""The commands to define in package.json."""
DEV = "next dev"
EXPORT = "next build && next export -o _static"
EXPORT_SITEMAP = "next build && next-sitemap && next export -o _static"
PROD = "next start"
PATH = os.path.join(Dirs.WEB, "package.json")
DEPENDENCIES = {
"@chakra-ui/react": "^2.6.0",
"@chakra-ui/system": "^2.5.6",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"axios": "^1.4.0",
"chakra-react-select": "^4.6.0",
"focus-visible": "^5.2.0",
"json5": "^2.2.3",
"next": "^13.3.1",
"next-sitemap": "^4.1.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"socket.io-client": "^4.6.1",
"universal-cookie": "^4.0.4",
}
DEV_DEPENDENCIES = {
"autoprefixer": "^10.4.14",
"postcss": "^8.4.24",
}

67
reflex/constants/route.py Normal file
View File

@ -0,0 +1,67 @@
"""Route constants."""
import re
from types import SimpleNamespace
class RouteArgType(SimpleNamespace):
"""Type of dynamic route arg extracted from URI route."""
# Typecast to str is needed for Enum to work.
SINGLE = str("arg_single")
LIST = str("arg_list")
# the name of the backend var containing path and client information
ROUTER_DATA = "router_data"
class RouteVar(SimpleNamespace):
"""Names of variables used in the router_data dict stored in State."""
CLIENT_IP = "ip"
CLIENT_TOKEN = "token"
HEADERS = "headers"
PATH = "pathname"
ORIGIN = "asPath"
SESSION_ID = "sid"
QUERY = "query"
COOKIE = "cookie"
class RouteRegex(SimpleNamespace):
"""Regex used for extracting route args in route."""
ARG = re.compile(r"\[(?!\.)([^\[\]]+)\]")
# group return the catchall pattern (i.e. "[[..slug]]")
CATCHALL = re.compile(r"(\[?\[\.{3}(?![0-9]).*\]?\])")
# group return the arg name (i.e. "slug")
STRICT_CATCHALL = re.compile(r"\[\.{3}([a-zA-Z_][\w]*)\]")
# group return the arg name (i.e. "slug") (optional arg can be empty)
OPT_CATCHALL = re.compile(r"\[\[\.{3}([a-zA-Z_][\w]*)\]\]")
class DefaultPage(SimpleNamespace):
"""Default page constants."""
# The default title to show for Reflex apps.
TITLE = "Reflex App"
# The default description to show for Reflex apps.
DESCRIPTION = "A Reflex app."
# The default image to show for Reflex apps.
IMAGE = "favicon.ico"
# The default meta list to show for Reflex apps.
META_LIST = []
# 404 variables
class Page404(SimpleNamespace):
"""Page 404 constants."""
SLUG = "404"
TITLE = "404 - Not Found"
IMAGE = "favicon.ico"
DESCRIPTION = "The page was not found"
ROUTE_NOT_FOUND = "routeNotFound"

22
reflex/constants/style.py Normal file
View File

@ -0,0 +1,22 @@
"""Style constants."""
import os
from types import SimpleNamespace
from reflex.constants.base import Dirs
# The directory where styles are located.
STYLES_DIR = os.path.join(Dirs.WEB, "styles")
class Tailwind(SimpleNamespace):
"""Tailwind constants."""
# The Tailwindcss version
VERSION = "tailwindcss@^3.3.2"
# The Tailwind config.
CONFIG = os.path.join(Dirs.WEB, "tailwind.config.js")
# Default Tailwind content paths
CONTENT = ["./pages/**/*.{js,ts,jsx,tsx}"]
# Relative tailwind style path to root stylesheet in STYLES_DIR.
ROOT_STYLE_PATH = "./tailwind.css"

View File

@ -33,7 +33,7 @@ def attr_to_prop(attr_name: str) -> str:
# Names of HTML attributes that are provided by Reflex out of the box.
PYNECONE_PROVIDED_ATTRS = {"class", "id", "style"}
REFLEX_PROVIDED_ATTRS = {"class", "id", "style"}
# ATTR_TO_ELEMENTS contains HTML attribute names, which might be invalid as
# Reflex prop names. PROP_TO_ELEMENTS contains the corresponding Reflex
@ -41,7 +41,7 @@ PYNECONE_PROVIDED_ATTRS = {"class", "id", "style"}
PROP_TO_ELEMENTS = {
attr_to_prop(attr_name): elements
for attr_name, elements in ATTR_TO_ELEMENTS.items()
if attr_name not in PYNECONE_PROVIDED_ATTRS
if attr_name not in REFLEX_PROVIDED_ATTRS
}
# Invert PROP_TO_ELEMENTS to enable easier lookup.

View File

@ -192,7 +192,7 @@ class EventChain(Base):
events: List[EventSpec]
args_spec: Optional[ArgsSpec]
args_spec: Optional[Callable]
class Target(Base):
@ -481,7 +481,7 @@ def get_hydrate_event(state) -> str:
Returns:
The name of the hydrate event.
"""
return get_event(state, constants.HYDRATE)
return get_event(state, constants.CompileVars.HYDRATE)
def call_event_handler(
@ -507,12 +507,12 @@ def call_event_handler(
# handle new API using lambda to define triggers
if isinstance(arg_spec, ArgsSpec):
parsed_args = parse_args_spec(arg_spec)
parsed_args = parse_args_spec(arg_spec) # type: ignore
if len(args) == len(["self", *parsed_args]):
return event_handler(*parsed_args) # type: ignore
else:
source = inspect.getsource(arg_spec)
source = inspect.getsource(arg_spec) # type: ignore
raise ValueError(
f"number of arguments in {event_handler.fn.__name__} "
f"doesn't match the definition '{source.strip().strip(',')}'"
@ -524,12 +524,12 @@ def call_event_handler(
deprecation_version="0.2.8",
removal_version="0.2.9",
)
if len(args) == 1:
return event_handler()
assert (
len(args) == 2
), f"Event handler {event_handler.fn} must have 1 or 2 arguments."
return event_handler(arg_spec)
if len(args) == 1:
return event_handler()
assert (
len(args) == 2
), f"Event handler {event_handler.fn} must have 1 or 2 arguments."
return event_handler(arg_spec) # type: ignore
def parse_args_spec(arg_spec: ArgsSpec):
@ -578,7 +578,7 @@ def call_event_fn(fn: Callable, arg: Union[Var, ArgsSpec]) -> list[EventSpec]:
args = inspect.getfullargspec(fn).args
if isinstance(arg, ArgsSpec):
out = fn(*parse_args_spec(arg))
out = fn(*parse_args_spec(arg)) # type: ignore
else:
# Call the lambda.
if len(args) == 0:

View File

@ -13,7 +13,7 @@ if TYPE_CHECKING:
from reflex.app import App
State.add_var(constants.IS_HYDRATED, type_=bool, default_value=False)
State.add_var(constants.CompileVars.IS_HYDRATED, type_=bool, default_value=False)
class HydrateMiddleware(Middleware):
@ -40,7 +40,7 @@ class HydrateMiddleware(Middleware):
state._reset_client_storage()
# Mark state as not hydrated (until on_loads are complete)
setattr(state, constants.IS_HYDRATED, False)
setattr(state, constants.CompileVars.IS_HYDRATED, False)
# Apply client side storage values to state
for storage_type in (constants.COOKIES, constants.LOCAL_STORAGE):

View File

@ -29,7 +29,7 @@ def version(value: bool):
typer.Exit: If the version flag was passed.
"""
if value:
console.print(constants.VERSION)
console.print(constants.Reflex.VERSION)
raise typer.Exit()
@ -53,8 +53,9 @@ def init(
name: str = typer.Option(
None, metavar="APP_NAME", help="The name of the app to initialize."
),
template: constants.Template = typer.Option(
constants.Template.DEFAULT, help="The template to initialize the app with."
template: constants.Templates.Kind = typer.Option(
constants.Templates.Kind.DEFAULT,
help="The template to initialize the app with.",
),
loglevel: constants.LogLevel = typer.Option(
config.loglevel, help="The log level to use."
@ -78,7 +79,7 @@ def init(
prerequisites.migrate_to_reflex()
# Set up the app directory, only if the config doesn't exist.
if not os.path.exists(constants.CONFIG_FILE):
if not os.path.exists(constants.Config.FILE):
prerequisites.create_config(app_name)
prerequisites.initialize_app_directory(app_name, template)
telemetry.send("init")
@ -193,7 +194,7 @@ def run(
def deploy(
dry_run: bool = typer.Option(False, help="Whether to run a dry run."),
loglevel: constants.LogLevel = typer.Option(
console.LOG_LEVEL, help="The log level to use."
console._LOG_LEVEL, help="The log level to use."
),
):
"""Deploy the app to the Reflex hosting service."""
@ -223,10 +224,10 @@ def deploy(
backend = response["backend_resources_url"]
# Upload the frontend and backend.
with open(constants.FRONTEND_ZIP, "rb") as f:
with open(constants.ComponentName.FRONTEND.zip(), "rb") as f:
httpx.put(frontend, data=f) # type: ignore
with open(constants.BACKEND_ZIP, "rb") as f:
with open(constants.ComponentName.BACKEND.zip(), "rb") as f:
httpx.put(backend, data=f) # type: ignore
@ -242,7 +243,7 @@ def export(
True, "--frontend-only", help="Export only frontend.", show_default=False
),
loglevel: constants.LogLevel = typer.Option(
console.LOG_LEVEL, help="The log level to use."
console._LOG_LEVEL, help="The log level to use."
),
):
"""Export the app to a zip file."""

View File

@ -1102,7 +1102,7 @@ class StateProxy(wrapt.ObjectProxy):
state_instance: The state instance to proxy.
"""
super().__init__(state_instance)
self._self_app = getattr(prerequisites.get_app(), constants.APP_VAR)
self._self_app = getattr(prerequisites.get_app(), constants.CompileVars.APP)
self._self_substate_path = state_instance.get_full_name().split(".")
self._self_actx = None
self._self_mutable = False
@ -1355,10 +1355,10 @@ class StateManagerRedis(StateManager):
redis: Redis
# The token expiration time (s).
token_expiration: int = constants.TOKEN_EXPIRATION
token_expiration: int = constants.Expiration.TOKEN
# The maximum time to hold a lock (ms).
lock_expiration: int = constants.LOCK_EXPIRATION
lock_expiration: int = constants.Expiration.LOCK
# The keyspace subscription string when redis is waiting for lock to be released
_redis_notify_keyspace_events: str = (

View File

@ -7,8 +7,8 @@ from reflex.event import EventChain
from reflex.utils import format
from reflex.vars import BaseVar, Var
color_mode = BaseVar(name=constants.COLOR_MODE, type_="str")
toggle_color_mode = BaseVar(name=constants.TOGGLE_COLOR_MODE, type_=EventChain)
color_mode = BaseVar(name=constants.ColorMode.NAME, type_="str")
toggle_color_mode = BaseVar(name=constants.ColorMode.TOGGLE, type_=EventChain)
def convert(style_dict):

View File

@ -154,7 +154,7 @@ class AppHarness:
with chdir(self.app_path):
reflex.reflex.init(
name=self.app_name,
template=reflex.constants.Template.DEFAULT,
template=reflex.constants.Templates.Kind.DEFAULT,
loglevel=reflex.constants.LogLevel.INFO,
)
self.app_module_path.write_text(source_code)
@ -211,7 +211,7 @@ class AppHarness:
# Start the frontend.
self.frontend_process = reflex.utils.processes.new_process(
[reflex.utils.prerequisites.get_package_manager(), "run", "dev"],
cwd=self.app_path / reflex.constants.WEB_DIR,
cwd=self.app_path / reflex.constants.Dirs.WEB,
env={"PORT": "0"},
**FRONTEND_POPEN_ARGS,
)
@ -647,7 +647,7 @@ class AppHarnessProd(AppHarness):
frontend_server: Optional[Subdir404TCPServer] = None
def _run_frontend(self):
web_root = self.app_path / reflex.constants.WEB_DIR / "_static"
web_root = self.app_path / reflex.constants.Dirs.WEB / "_static"
error_page_map = {
404: web_root / "404.html",
}

View File

@ -6,7 +6,6 @@ import json
import os
import subprocess
import zipfile
from enum import Enum
from pathlib import Path
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
@ -19,7 +18,7 @@ from reflex.utils import console, path_ops, prerequisites, processes
def set_env_json():
"""Write the upload url to a REFLEX_JSON."""
path_ops.update_json_file(
constants.ENV_JSON,
constants.Dirs.ENV_JSON,
{endpoint.name: endpoint.get_url() for endpoint in constants.Endpoint},
)
@ -52,17 +51,12 @@ def generate_sitemap_config(deploy_url: str):
}
)
with open(constants.SITEMAP_CONFIG_FILE, "w") as f:
with open(constants.Next.SITEMAP_CONFIG_FILE, "w") as f:
f.write(templates.SITEMAP_CONFIG(config=config))
class _ComponentName(Enum):
BACKEND = "Backend"
FRONTEND = "Frontend"
def _zip(
component_name: _ComponentName,
component_name: constants.ComponentName,
target: str,
root_dir: str,
dirs_to_exclude: set[str] | None = None,
@ -130,7 +124,7 @@ def export(
deploy_url: The URL of the deployed app.
"""
# Remove the static folder.
path_ops.rm(constants.WEB_STATIC_DIR)
path_ops.rm(constants.Dirs.WEB_STATIC)
# The export command to run.
command = "export"
@ -155,25 +149,28 @@ def export(
# Start the subprocess with the progress bar.
process = processes.new_process(
[prerequisites.get_package_manager(), "run", command],
cwd=constants.WEB_DIR,
cwd=constants.Dirs.WEB,
shell=constants.IS_WINDOWS,
)
processes.show_progress("Creating Production Build", process, checkpoints)
# Zip up the app.
if zip:
files_to_exclude = {constants.FRONTEND_ZIP, constants.BACKEND_ZIP}
files_to_exclude = {
constants.ComponentName.FRONTEND.zip(),
constants.ComponentName.BACKEND.zip(),
}
if frontend:
_zip(
component_name=_ComponentName.FRONTEND,
target=constants.FRONTEND_ZIP,
component_name=constants.ComponentName.FRONTEND,
target=constants.ComponentName.FRONTEND.zip(),
root_dir=".web/_static",
files_to_exclude=files_to_exclude,
)
if backend:
_zip(
component_name=_ComponentName.BACKEND,
target=constants.BACKEND_ZIP,
component_name=constants.ComponentName.BACKEND,
target=constants.ComponentName.BACKEND.zip(),
root_dir=".",
dirs_to_exclude={"assets", "__pycache__"},
files_to_exclude=files_to_exclude,
@ -192,8 +189,8 @@ def setup_frontend(
"""
# Copy asset files to public folder.
path_ops.cp(
src=str(root / constants.APP_ASSETS_DIR),
dest=str(root / constants.WEB_ASSETS_DIR),
src=str(root / constants.Dirs.APP_ASSETS),
dest=str(root / constants.Dirs.WEB_ASSETS),
)
# Set the environment variables in client (env.json).
@ -209,7 +206,7 @@ def setup_frontend(
"telemetry",
"disable",
],
cwd=constants.WEB_DIR,
cwd=constants.Dirs.WEB,
stdout=subprocess.DEVNULL,
shell=constants.IS_WINDOWS,
)

View File

@ -14,7 +14,7 @@ from reflex.constants import LogLevel
_console = Console()
# The current log level.
LOG_LEVEL = LogLevel.INFO
_LOG_LEVEL = LogLevel.INFO
def set_log_level(log_level: LogLevel):
@ -23,8 +23,8 @@ def set_log_level(log_level: LogLevel):
Args:
log_level: The log level to set.
"""
global LOG_LEVEL
LOG_LEVEL = log_level
global _LOG_LEVEL
_LOG_LEVEL = log_level
def print(msg: str, **kwargs):
@ -44,7 +44,7 @@ def debug(msg: str, **kwargs):
msg: The debug message.
kwargs: Keyword arguments to pass to the print function.
"""
if LOG_LEVEL <= LogLevel.DEBUG:
if _LOG_LEVEL <= LogLevel.DEBUG:
print(f"[blue]Debug: {msg}[/blue]", **kwargs)
@ -55,7 +55,7 @@ def info(msg: str, **kwargs):
msg: The info message.
kwargs: Keyword arguments to pass to the print function.
"""
if LOG_LEVEL <= LogLevel.INFO:
if _LOG_LEVEL <= LogLevel.INFO:
print(f"[cyan]Info: {msg}[/cyan]", **kwargs)
@ -66,7 +66,7 @@ def success(msg: str, **kwargs):
msg: The success message.
kwargs: Keyword arguments to pass to the print function.
"""
if LOG_LEVEL <= LogLevel.INFO:
if _LOG_LEVEL <= LogLevel.INFO:
print(f"[green]Success: {msg}[/green]", **kwargs)
@ -77,7 +77,7 @@ def log(msg: str, **kwargs):
msg: The message to log.
kwargs: Keyword arguments to pass to the print function.
"""
if LOG_LEVEL <= LogLevel.INFO:
if _LOG_LEVEL <= LogLevel.INFO:
_console.log(msg, **kwargs)
@ -98,7 +98,7 @@ def warn(msg: str, **kwargs):
msg: The warning message.
kwargs: Keyword arguments to pass to the print function.
"""
if LOG_LEVEL <= LogLevel.WARNING:
if _LOG_LEVEL <= LogLevel.WARNING:
print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
@ -123,7 +123,7 @@ def deprecate(
f"{feature_name} has been deprecated in version {deprecation_version} {reason}. It will be completely "
f"removed in {removal_version}"
)
if LOG_LEVEL <= LogLevel.WARNING:
if _LOG_LEVEL <= LogLevel.WARNING:
print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs)
@ -134,7 +134,7 @@ def error(msg: str, **kwargs):
msg: The error message.
kwargs: Keyword arguments to pass to the print function.
"""
if LOG_LEVEL <= LogLevel.ERROR:
if _LOG_LEVEL <= LogLevel.ERROR:
print(f"[red]{msg}[/red]", **kwargs)

View File

@ -73,7 +73,7 @@ def run_process_and_launch_url(run_command: list[str]):
Args:
run_command: The command to run.
"""
json_file_path = os.path.join(constants.WEB_DIR, "package.json")
json_file_path = os.path.join(constants.Dirs.WEB, "package.json")
last_hash = detect_package_change(json_file_path)
process = None
first_run = True
@ -81,7 +81,7 @@ def run_process_and_launch_url(run_command: list[str]):
while True:
if process is None:
process = processes.new_process(
run_command, cwd=constants.WEB_DIR, shell=constants.IS_WINDOWS
run_command, cwd=constants.Dirs.WEB, shell=constants.IS_WINDOWS
)
if process.stdout:
for line in processes.stream_logs("Starting frontend", process):
@ -153,9 +153,9 @@ def run_backend(
loglevel: The log level.
"""
config = get_config()
app_module = f"{config.app_name}.{config.app_name}:{constants.APP_VAR}"
app_module = f"{config.app_name}.{config.app_name}:{constants.CompileVars.APP}"
uvicorn.run(
app=f"{app_module}.{constants.API_VAR}",
app=f"{app_module}.{constants.CompileVars.API}",
host=host,
port=port,
log_level=loglevel.value,
@ -180,7 +180,7 @@ def run_backend_prod(
config = get_config()
RUN_BACKEND_PROD = f"gunicorn --worker-class uvicorn.workers.UvicornH11Worker --preload --timeout {config.timeout} --log-level critical".split()
RUN_BACKEND_PROD_WINDOWS = f"uvicorn --timeout-keep-alive {config.timeout}".split()
app_module = f"{config.app_name}.{config.app_name}:{constants.APP_VAR}"
app_module = f"{config.app_name}.{config.app_name}:{constants.CompileVars.APP}"
command = (
[
*RUN_BACKEND_PROD_WINDOWS,
@ -217,7 +217,7 @@ def run_backend_prod(
def output_system_info():
"""Show system information if the loglevel is in DEBUG."""
if console.LOG_LEVEL > constants.LogLevel.DEBUG:
if console._LOG_LEVEL > constants.LogLevel.DEBUG:
return
config = get_config()
@ -231,8 +231,8 @@ def output_system_info():
console.debug(f"Config: {config}")
dependencies = [
f"[Reflex {constants.VERSION} with Python {platform.python_version()} (PATH: {sys.executable})]",
f"[Node {prerequisites.get_node_version()} (Expected: {constants.NODE_VERSION}) (PATH:{path_ops.get_node_path()})]",
f"[Reflex {constants.Reflex.VERSION} with Python {platform.python_version()} (PATH: {sys.executable})]",
f"[Node {prerequisites.get_node_version()} (Expected: {constants.Node.VERSION}) (PATH:{path_ops.get_node_path()})]",
]
system = platform.system()
@ -240,13 +240,13 @@ def output_system_info():
if system != "Windows":
dependencies.extend(
[
f"[FNM {constants.FNM_VERSION} (Expected: {constants.FNM_VERSION}) (PATH: {constants.FNM_EXE})]",
f"[Bun {prerequisites.get_bun_version()} (Expected: {constants.BUN_VERSION}) (PATH: {config.bun_path})]",
f"[FNM {constants.Fnm.VERSION} (Expected: {constants.Fnm.VERSION}) (PATH: {constants.Fnm.EXE})]",
f"[Bun {prerequisites.get_bun_version()} (Expected: {constants.Bun.VERSION}) (PATH: {config.bun_path})]",
],
)
else:
dependencies.append(
f"[FNM {constants.FNM_VERSION} (Expected: {constants.FNM_VERSION}) (PATH: {constants.FNM_EXE})]",
f"[FNM {constants.Fnm.VERSION} (Expected: {constants.Fnm.VERSION}) (PATH: {constants.Fnm.EXE})]",
)
if system == "Linux":

View File

@ -5,7 +5,6 @@ from __future__ import annotations
import inspect
import json
import os
import os.path as op
import re
import sys
from typing import TYPE_CHECKING, Any, Union
@ -230,7 +229,7 @@ def format_route(route: str, format_case=True) -> str:
# If the route is empty, return the index route.
if route == "":
return constants.INDEX_ROUTE
return constants.PageNames.INDEX_ROUTE
return route
@ -559,11 +558,27 @@ def format_breadcrumbs(route: str) -> list[tuple[str, str]]:
# create and return breadcrumbs
return [
(part, op.join("/", *route_parts[: i + 1]))
(part, "/".join(["", *route_parts[: i + 1]]))
for i, part in enumerate(route_parts)
]
def format_library_name(library_fullname: str):
"""Format the name of a library.
Args:
library_fullname: The fullname of the library.
Returns:
The name without the @version if it was part of the name
"""
lib, at, version = library_fullname.rpartition("@")
if not lib:
lib = at + version
return lib
def json_dumps(obj: Any) -> str:
"""Takes an object and returns a jsonified string.

View File

@ -118,10 +118,10 @@ def get_node_bin_path() -> str | None:
Returns:
The path to the node bin folder.
"""
if not os.path.exists(constants.NODE_BIN_PATH):
if not os.path.exists(constants.Node.BIN_PATH):
str_path = which("node")
return str(Path(str_path).parent) if str_path else str_path
return constants.NODE_BIN_PATH
return constants.Node.BIN_PATH
def get_node_path() -> str | None:
@ -130,9 +130,9 @@ def get_node_path() -> str | None:
Returns:
The path to the node binary file.
"""
if not os.path.exists(constants.NODE_PATH):
if not os.path.exists(constants.Node.PATH):
return which("node")
return constants.NODE_PATH
return constants.Node.PATH
def get_npm_path() -> str | None:
@ -141,9 +141,9 @@ def get_npm_path() -> str | None:
Returns:
The path to the npm binary file.
"""
if not os.path.exists(constants.NODE_PATH):
if not os.path.exists(constants.Node.PATH):
return which("npm")
return constants.NPM_PATH
return constants.Node.NPM_PATH
def update_json_file(file_path: str, update_dict: dict[str, int | str]):

View File

@ -39,9 +39,9 @@ def check_node_version() -> bool:
if current_version:
# Compare the version numbers
return (
current_version >= version.parse(constants.NODE_VERSION_MIN)
current_version >= version.parse(constants.Node.MIN_VERSION)
if constants.IS_WINDOWS
else current_version == version.parse(constants.NODE_VERSION)
else current_version == version.parse(constants.Node.VERSION)
)
return False
@ -111,7 +111,7 @@ def get_app(reload: bool = False) -> ModuleType:
config = get_config()
module = ".".join([config.app_name, config.app_name])
sys.path.insert(0, os.getcwd())
app = __import__(module, fromlist=(constants.APP_VAR,))
app = __import__(module, fromlist=(constants.CompileVars.APP,))
if reload:
importlib.reload(app)
return app
@ -160,9 +160,9 @@ def get_default_app_name() -> str:
app_name = os.getcwd().split(os.path.sep)[-1].replace("-", "_")
# Make sure the app is not named "reflex".
if app_name == constants.MODULE_NAME:
if app_name == constants.Reflex.MODULE_NAME:
console.error(
f"The app directory cannot be named [bold]{constants.MODULE_NAME}[/bold]."
f"The app directory cannot be named [bold]{constants.Reflex.MODULE_NAME}[/bold]."
)
raise typer.Exit(1)
@ -179,28 +179,28 @@ def create_config(app_name: str):
from reflex.compiler import templates
config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config"
with open(constants.CONFIG_FILE, "w") as f:
console.debug(f"Creating {constants.CONFIG_FILE}")
with open(constants.Config.FILE, "w") as f:
console.debug(f"Creating {constants.Config.FILE}")
f.write(templates.RXCONFIG.render(app_name=app_name, config_name=config_name))
def initialize_gitignore():
"""Initialize the template .gitignore file."""
# The files to add to the .gitignore file.
files = constants.DEFAULT_GITIGNORE
files = constants.GitIgnore.DEFAULTS
# Subtract current ignored files.
if os.path.exists(constants.GITIGNORE_FILE):
with open(constants.GITIGNORE_FILE, "r") as f:
if os.path.exists(constants.GitIgnore.FILE):
with open(constants.GitIgnore.FILE, "r") as f:
files |= set([line.strip() for line in f.readlines()])
# Write files to the .gitignore file.
with open(constants.GITIGNORE_FILE, "w") as f:
console.debug(f"Creating {constants.GITIGNORE_FILE}")
with open(constants.GitIgnore.FILE, "w") as f:
console.debug(f"Creating {constants.GitIgnore.FILE}")
f.write(f"{(path_ops.join(sorted(files))).lstrip()}")
def initialize_app_directory(app_name: str, template: constants.Template):
def initialize_app_directory(app_name: str, template: constants.Templates.Kind):
"""Initialize the app directory on reflex init.
Args:
@ -208,26 +208,28 @@ def initialize_app_directory(app_name: str, template: constants.Template):
template: The template to use.
"""
console.log("Initializing the app directory.")
path_ops.cp(os.path.join(constants.TEMPLATE_DIR, "apps", template.value), app_name)
path_ops.cp(
os.path.join(constants.Templates.Dirs.BASE, "apps", template.value), app_name
)
path_ops.mv(
os.path.join(app_name, template.value + ".py"),
os.path.join(app_name, app_name + constants.PY_EXT),
os.path.join(app_name, app_name + constants.Ext.PY),
)
path_ops.cp(constants.ASSETS_TEMPLATE_DIR, constants.APP_ASSETS_DIR)
path_ops.cp(constants.Templates.Dirs.ASSETS_TEMPLATE, constants.Dirs.APP_ASSETS)
def initialize_web_directory():
"""Initialize the web directory on reflex init."""
console.log("Initializing the web directory.")
path_ops.cp(constants.WEB_TEMPLATE_DIR, constants.WEB_DIR)
path_ops.cp(constants.Templates.Dirs.WEB_TEMPLATE, constants.Dirs.WEB)
initialize_package_json()
path_ops.mkdir(constants.WEB_ASSETS_DIR)
path_ops.mkdir(constants.Dirs.WEB_ASSETS)
# update nextJS config based on rxConfig
next_config_file = os.path.join(constants.WEB_DIR, constants.NEXT_CONFIG_FILE)
next_config_file = os.path.join(constants.Dirs.WEB, constants.Next.CONFIG_FILE)
with open(next_config_file, "r") as file:
next_config = file.read()
@ -243,19 +245,19 @@ def initialize_web_directory():
def _compile_package_json():
return templates.PACKAGE_JSON.render(
scripts={
"dev": constants.PackageJsonCommands.DEV,
"export": constants.PackageJsonCommands.EXPORT,
"export_sitemap": constants.PackageJsonCommands.EXPORT_SITEMAP,
"prod": constants.PackageJsonCommands.PROD,
"dev": constants.PackageJson.Commands.DEV,
"export": constants.PackageJson.Commands.EXPORT,
"export_sitemap": constants.PackageJson.Commands.EXPORT_SITEMAP,
"prod": constants.PackageJson.Commands.PROD,
},
dependencies=constants.PACKAGE_DEPENDENCIES,
dev_dependencies=constants.PACKAGE_DEV_DEPENDENCIES,
dependencies=constants.PackageJson.DEPENDENCIES,
dev_dependencies=constants.PackageJson.DEV_DEPENDENCIES,
)
def initialize_package_json():
"""Render and write in .web the package.json file."""
output_path = constants.PACKAGE_JSON_PATH
output_path = constants.PackageJson.PATH
code = _compile_package_json()
with open(output_path, "w") as file:
file.write(code)
@ -269,10 +271,10 @@ def init_reflex_json():
# Write the hash and version to the reflex json file.
reflex_json = {
"version": constants.VERSION,
"version": constants.Reflex.VERSION,
"project_hash": project_hash,
}
path_ops.update_json_file(constants.REFLEX_JSON, reflex_json)
path_ops.update_json_file(constants.Reflex.JSON, reflex_json)
def update_next_config(next_config: str, config: Config) -> str:
@ -302,7 +304,7 @@ def remove_existing_bun_installation():
"""Remove existing bun installation."""
console.debug("Removing existing bun installation.")
if os.path.exists(get_config().bun_path):
path_ops.rm(constants.BUN_ROOT_PATH)
path_ops.rm(constants.Bun.ROOT_PATH)
def download_and_run(url: str, *args, show_status: bool = False, **env):
@ -339,9 +341,9 @@ def download_and_extract_fnm_zip():
Exit: If an error occurs while downloading or extracting the FNM zip.
"""
# Download the zip file
url = constants.FNM_INSTALL_URL
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 = os.path.join(constants.Fnm.DIR, f"{constants.Fnm.FILENAME}.zip")
# Function to download and extract the FNM zip release.
try:
# Download the FNM zip release.
@ -354,7 +356,7 @@ def download_and_extract_fnm_zip():
# Extract the downloaded zip file.
with zipfile.ZipFile(fnm_zip_file, "r") as zip_ref:
zip_ref.extractall(constants.FNM_DIR)
zip_ref.extractall(constants.Fnm.DIR)
console.debug("FNM package downloaded and extracted successfully.")
except Exception as e:
@ -369,13 +371,13 @@ def install_node():
"""Install fnm and nodejs for use by Reflex.
Independent of any existing system installations.
"""
if not constants.FNM_FILENAME:
if not constants.Fnm.FILENAME:
# fnm only support Linux, macOS and Windows distros.
console.debug("")
return
path_ops.mkdir(constants.FNM_DIR)
if not os.path.exists(constants.FNM_EXE):
path_ops.mkdir(constants.Fnm.DIR)
if not os.path.exists(constants.Fnm.EXE):
download_and_extract_fnm_zip()
if constants.IS_WINDOWS:
@ -384,13 +386,13 @@ def install_node():
[
"powershell",
"-Command",
f'& "{constants.FNM_EXE}" install {constants.NODE_VERSION} --fnm-dir "{constants.FNM_DIR}"',
f'& "{constants.Fnm.EXE}" install {constants.Node.VERSION} --fnm-dir "{constants.Fnm.DIR}"',
],
)
else: # All other platforms (Linux, MacOS).
# TODO we can skip installation if check_node_version() checks out
# Add execute permissions to fnm executable.
os.chmod(constants.FNM_EXE, stat.S_IXUSR)
os.chmod(constants.Fnm.EXE, stat.S_IXUSR)
# Install node.
# Specify arm64 arch explicitly for M1s and M2s.
architecture_arg = (
@ -401,12 +403,12 @@ def install_node():
process = processes.new_process(
[
constants.FNM_EXE,
constants.Fnm.EXE,
"install",
*architecture_arg,
constants.NODE_VERSION,
constants.Node.VERSION,
"--fnm-dir",
constants.FNM_DIR,
constants.Fnm.DIR,
],
)
processes.show_status("Installing node", process)
@ -435,9 +437,9 @@ def install_bun():
# Run the bun install script.
download_and_run(
constants.BUN_INSTALL_URL,
f"bun-v{constants.BUN_VERSION}",
BUN_INSTALL=constants.BUN_ROOT_PATH,
constants.Bun.INSTALL_URL,
f"bun-v{constants.Bun.VERSION}",
BUN_INSTALL=constants.Bun.ROOT_PATH,
)
@ -453,7 +455,7 @@ def install_frontend_packages(packages: set[str]):
# Install the base packages.
process = processes.new_process(
[get_install_package_manager(), "install", "--loglevel", "silly"],
cwd=constants.WEB_DIR,
cwd=constants.Dirs.WEB,
shell=constants.IS_WINDOWS,
)
@ -467,10 +469,10 @@ def install_frontend_packages(packages: set[str]):
get_install_package_manager(),
"add",
"-d",
constants.TAILWIND_VERSION,
constants.Tailwind.VERSION,
*((config.tailwind or {}).get("plugins", [])),
],
cwd=constants.WEB_DIR,
cwd=constants.Dirs.WEB,
shell=constants.IS_WINDOWS,
)
processes.show_status("Installing tailwind", process)
@ -479,7 +481,7 @@ def install_frontend_packages(packages: set[str]):
if len(packages) > 0:
process = processes.new_process(
[get_install_package_manager(), "add", *packages],
cwd=constants.WEB_DIR,
cwd=constants.Dirs.WEB,
shell=constants.IS_WINDOWS,
)
processes.show_status(
@ -496,14 +498,14 @@ def check_initialized(frontend: bool = True):
Raises:
Exit: If the app is not initialized.
"""
has_config = os.path.exists(constants.CONFIG_FILE)
has_reflex_dir = not frontend or os.path.exists(constants.REFLEX_DIR)
has_web_dir = not frontend or os.path.exists(constants.WEB_DIR)
has_config = os.path.exists(constants.Config.FILE)
has_reflex_dir = not frontend or os.path.exists(constants.Reflex.DIR)
has_web_dir = not frontend or os.path.exists(constants.Dirs.WEB)
# Check if the app is initialized.
if not (has_config and has_reflex_dir and has_web_dir):
console.error(
f"The app is not initialized. Run [bold]{constants.MODULE_NAME} init[/bold] first."
f"The app is not initialized. Run [bold]{constants.Reflex.MODULE_NAME} init[/bold] first."
)
raise typer.Exit(1)
@ -527,11 +529,11 @@ def is_latest_template() -> bool:
Returns:
Whether the app is using the latest template.
"""
if not os.path.exists(constants.REFLEX_JSON):
if not os.path.exists(constants.Reflex.JSON):
return False
with open(constants.REFLEX_JSON) as f: # type: ignore
with open(constants.Reflex.JSON) as f: # type: ignore
app_version = json.load(f)["version"]
return app_version == constants.VERSION
return app_version == constants.Reflex.VERSION
def validate_bun():
@ -543,16 +545,16 @@ def validate_bun():
# if a custom bun path is provided, make sure its valid
# This is specific to non-FHS OS
bun_path = get_config().bun_path
if bun_path != constants.DEFAULT_BUN_PATH:
if bun_path != constants.Bun.DEFAULT_PATH:
bun_version = get_bun_version()
if not bun_version:
console.error(
"Failed to obtain bun version. Make sure the specified bun path in your config is correct."
)
raise typer.Exit(1)
elif bun_version < version.parse(constants.MIN_BUN_VERSION):
elif bun_version < version.parse(constants.Bun.MIN_VERSION):
console.error(
f"Reflex requires bun version {constants.BUN_VERSION} or higher to run, but the detected version is "
f"Reflex requires bun version {constants.Bun.VERSION} or higher to run, but the detected version is "
f"{bun_version}. If you have specified a custom bun path in your config, make sure to provide one "
f"that satisfies the minimum version requirement."
)
@ -582,7 +584,7 @@ def validate_frontend_dependencies(init=True):
if not check_node_version():
node_version = get_node_version()
console.error(
f"Reflex requires node version {constants.NODE_VERSION_MIN} or higher to run, but the detected version is {node_version}",
f"Reflex requires node version {constants.Node.MIN_VERSION} or higher to run, but the detected version is {node_version}",
)
raise typer.Exit(1)
@ -597,7 +599,7 @@ def validate_frontend_dependencies(init=True):
def initialize_frontend_dependencies():
"""Initialize all the frontend dependencies."""
# Create the reflex directory.
path_ops.mkdir(constants.REFLEX_DIR)
path_ops.mkdir(constants.Reflex.DIR)
# validate dependencies before install
validate_frontend_dependencies()
# Install the frontend dependencies.
@ -644,7 +646,7 @@ def check_schema_up_to_date():
def migrate_to_reflex():
"""Migration from Pynecone to Reflex."""
# Check if the old config file exists.
if not os.path.exists(constants.OLD_CONFIG_FILE):
if not os.path.exists(constants.Config.PREVIOUS_FILE):
return
# Ask the user if they want to migrate.
@ -657,16 +659,16 @@ def migrate_to_reflex():
# Rename pcconfig to rxconfig.
console.log(
f"[bold]Renaming {constants.OLD_CONFIG_FILE} to {constants.CONFIG_FILE}"
f"[bold]Renaming {constants.Config.PREVIOUS_FILE} to {constants.Config.FILE}"
)
os.rename(constants.OLD_CONFIG_FILE, 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)
file_list.append(constants.Config.FILE)
# Migrate all files.
updates = {

View File

@ -39,7 +39,7 @@ def get_reflex_version() -> str:
Returns:
The Reflex version.
"""
return constants.VERSION
return constants.Reflex.VERSION
def get_cpu_count() -> int:

View File

@ -4,7 +4,6 @@ from __future__ import annotations
import contextlib
import typing
from types import LambdaType
from typing import Any, Callable, Type, Union, _GenericAlias # type: ignore
from reflex.base import Base
@ -18,7 +17,8 @@ PrimitiveType = Union[int, float, bool, str, list, dict, set, tuple]
StateVar = Union[PrimitiveType, Base, None]
StateIterVar = Union[list, set, tuple]
ArgsSpec = LambdaType
# ArgsSpec = Callable[[Var], list[Var]]
ArgsSpec = Callable
def get_args(alias: _GenericAlias) -> tuple[Type, ...]:

View File

@ -8,7 +8,7 @@ import time
from watchdog.events import FileSystemEvent, FileSystemEventHandler
from watchdog.observers import Observer
from reflex.constants import APP_ASSETS_DIR, WEB_ASSETS_DIR
from reflex.constants import Dirs
class AssetFolderWatch:
@ -20,7 +20,7 @@ class AssetFolderWatch:
Args:
root: root path of the public.
"""
self.path = str(root / APP_ASSETS_DIR)
self.path = str(root / Dirs.APP_ASSETS)
self.event_handler = AssetFolderHandler(root)
def start(self):
@ -90,5 +90,5 @@ class AssetFolderHandler(FileSystemEventHandler):
The public file path.
"""
return src_path.replace(
str(self.root / APP_ASSETS_DIR), str(self.root / WEB_ASSETS_DIR)
str(self.root / Dirs.APP_ASSETS), str(self.root / Dirs.WEB_ASSETS)
)

View File

@ -27,8 +27,7 @@ from pydantic.fields import ModelField
from reflex import constants
from reflex.base import Base
from reflex.utils import console, format, types
from reflex.utils.serializers import serialize
from reflex.utils import console, format, serializers, types
if TYPE_CHECKING:
from reflex.state import State
@ -124,7 +123,7 @@ class Var(ABC):
# Try to serialize the value.
type_ = type(value)
name = serialize(value)
name = serializers.serialize(value)
if name is None:
raise TypeError(
f"No JSON serializer found for var {value} of type {type_}."

View File

@ -3,7 +3,7 @@ from typing import Any, Dict
import pytest
from reflex.app import App
from reflex.constants import IS_HYDRATED
from reflex.constants import CompileVars
from reflex.middleware.hydrate_middleware import HydrateMiddleware
from reflex.state import State, StateUpdate
@ -17,7 +17,7 @@ def exp_is_hydrated(state: State) -> Dict[str, Any]:
Returns:
dict similar to that returned by `State.get_delta` with IS_HYDRATED: True
"""
return {state.get_name(): {IS_HYDRATED: True}}
return {state.get_name(): {CompileVars.IS_HYDRATED: True}}
class TestState(State):

View File

@ -923,7 +923,7 @@ async def test_dynamic_route_var_route_change_completed_on_load(
state.get_name(): {
arg_name: exp_val,
f"comp_{arg_name}": exp_val,
constants.IS_HYDRATED: False,
constants.CompileVars.IS_HYDRATED: False,
"loaded": exp_index,
"counter": exp_index,
# "side_effect_counter": exp_index,

View File

@ -15,7 +15,7 @@ from plotly.graph_objects import Figure
import reflex as rx
from reflex.base import Base
from reflex.constants import APP_VAR, IS_HYDRATED, RouteVar, SocketEvent
from reflex.constants import CompileVars, RouteVar, SocketEvent
from reflex.event import Event, EventHandler
from reflex.state import (
ImmutableStateError,
@ -28,7 +28,7 @@ from reflex.state import (
StateProxy,
StateUpdate,
)
from reflex.utils import format, prerequisites
from reflex.utils import prerequisites
from reflex.vars import BaseVar, ComputedVar
from .states import GenState
@ -118,6 +118,15 @@ class GrandchildState(ChildState):
pass
class DateTimeState(State):
"""A State with some datetime fields."""
d: datetime.date = datetime.date.fromisoformat("1989-11-09")
dt: datetime.datetime = datetime.datetime.fromisoformat("1989-11-09T18:53:00+01:00")
t: datetime.time = datetime.time.fromisoformat("18:53:00+01:00")
td: datetime.timedelta = datetime.timedelta(days=11, minutes=11)
@pytest.fixture
def test_state() -> TestState:
"""A state.
@ -214,7 +223,7 @@ def test_class_vars(test_state):
"""
cls = type(test_state)
assert set(cls.vars.keys()) == {
IS_HYDRATED, # added by hydrate_middleware to all State
CompileVars.IS_HYDRATED, # added by hydrate_middleware to all State
"num1",
"num2",
"key",
@ -292,58 +301,6 @@ def test_dict(test_state):
)
def test_format_state(test_state):
"""Test that the format state is correct.
Args:
test_state: A state.
"""
formatted_state = format.format_state(test_state.dict())
exp_formatted_state = {
"array": [1, 2, 3.14],
"child_state": {"count": 23, "grandchild_state": {"value2": ""}, "value": ""},
"child_state2": {"value": ""},
"complex": {
1: {"prop1": 42, "prop2": "hello"},
2: {"prop1": 42, "prop2": "hello"},
},
"dt": "1989-11-09 18:53:00+01:00",
"fig": [],
"is_hydrated": False,
"key": "",
"map_key": "a",
"mapping": {"a": [1, 2, 3], "b": [4, 5, 6]},
"num1": 0,
"num2": 3.14,
"obj": {"prop1": 42, "prop2": "hello"},
"sum": 3.14,
"upper": "",
}
assert formatted_state == exp_formatted_state
def test_format_state_datetime():
"""Test that the format state is correct for datetime classes."""
class DateTimeState(State):
d: datetime.date = datetime.date.fromisoformat("1989-11-09")
dt: datetime.datetime = datetime.datetime.fromisoformat(
"1989-11-09T18:53:00+01:00"
)
t: datetime.time = datetime.time.fromisoformat("18:53:00+01:00")
td: datetime.timedelta = datetime.timedelta(days=11, minutes=11)
formatted_state = format.format_state(DateTimeState().dict())
exp_formatted_state = {
"d": "1989-11-09",
"dt": "1989-11-09 18:53:00+01:00",
"is_hydrated": False,
"t": "18:53:00+01:00",
"td": "11 days, 0:11:00",
}
assert formatted_state == exp_formatted_state
def test_default_setters(test_state):
"""Test that we can set default values.
@ -750,21 +707,6 @@ async def test_process_event_generator():
assert count == 6
def test_format_event_handler():
"""Test formatting an event handler."""
assert (
format.format_event_handler(TestState.do_something) == "test_state.do_something" # type: ignore
)
assert (
format.format_event_handler(ChildState.change_both) # type: ignore
== "test_state.child_state.change_both"
)
assert (
format.format_event_handler(GrandchildState.do_nothing) # type: ignore
== "test_state.child_state.grandchild_state.do_nothing"
)
def test_get_token(test_state, mocker, router_data):
"""Test that the token obtained from the router_data is correct.
@ -1184,17 +1126,17 @@ def test_computed_var_depends_on_parent_non_cached():
assert ps.dict() == {
cs.get_name(): {"dep_v": 2},
"no_cache_v": 1,
IS_HYDRATED: False,
CompileVars.IS_HYDRATED: False,
}
assert ps.dict() == {
cs.get_name(): {"dep_v": 4},
"no_cache_v": 3,
IS_HYDRATED: False,
CompileVars.IS_HYDRATED: False,
}
assert ps.dict() == {
cs.get_name(): {"dep_v": 6},
"no_cache_v": 5,
IS_HYDRATED: False,
CompileVars.IS_HYDRATED: False,
}
assert counter == 6
@ -1595,7 +1537,7 @@ def mock_app(monkeypatch, app: rx.App, state_manager: StateManager) -> rx.App:
The app, after mocking out prerequisites.get_app()
"""
app_module = Mock()
setattr(app_module, APP_VAR, app)
setattr(app_module, CompileVars.APP, app)
app.state = TestState
app.state_manager = state_manager
assert app.event_namespace is not None

578
tests/utils/test_format.py Normal file
View File

@ -0,0 +1,578 @@
from typing import Any
import pytest
from reflex.components.tags.tag import Tag
from reflex.event import EVENT_ARG, EventChain, EventHandler, EventSpec
from reflex.style import Style
from reflex.utils import format
from reflex.vars import BaseVar, Var
from tests.test_state import ChildState, DateTimeState, GrandchildState, TestState
def mock_event(arg):
pass
@pytest.mark.parametrize(
"input,output",
[
("{", "}"),
("(", ")"),
("[", "]"),
("<", ">"),
('"', '"'),
("'", "'"),
],
)
def test_get_close_char(input: str, output: str):
"""Test getting the close character for a given open character.
Args:
input: The open character.
output: The expected close character.
"""
assert format.get_close_char(input) == output
@pytest.mark.parametrize(
"text,open,expected",
[
("", "{", False),
("{wrap}", "{", True),
("{wrap", "{", False),
("{wrap}", "(", False),
("(wrap)", "(", True),
],
)
def test_is_wrapped(text: str, open: str, expected: bool):
"""Test checking if a string is wrapped in the given open and close characters.
Args:
text: The text to check.
open: The open character.
expected: Whether the text is wrapped.
"""
assert format.is_wrapped(text, open) == expected
@pytest.mark.parametrize(
"text,open,check_first,num,expected",
[
("", "{", True, 1, "{}"),
("wrap", "{", True, 1, "{wrap}"),
("wrap", "(", True, 1, "(wrap)"),
("wrap", "(", True, 2, "((wrap))"),
("(wrap)", "(", True, 1, "(wrap)"),
("{wrap}", "{", True, 2, "{wrap}"),
("(wrap)", "{", True, 1, "{(wrap)}"),
("(wrap)", "(", False, 1, "((wrap))"),
],
)
def test_wrap(text: str, open: str, expected: str, check_first: bool, num: int):
"""Test wrapping a string.
Args:
text: The text to wrap.
open: The open character.
expected: The expected output string.
check_first: Whether to check if the text is already wrapped.
num: The number of times to wrap the text.
"""
assert format.wrap(text, open, check_first=check_first, num=num) == expected
@pytest.mark.parametrize(
"text,indent_level,expected",
[
("", 2, ""),
("hello", 2, "hello"),
("hello\nworld", 2, " hello\n world\n"),
("hello\nworld", 4, " hello\n world\n"),
(" hello\n world", 2, " hello\n world\n"),
],
)
def test_indent(text: str, indent_level: int, expected: str, windows_platform: bool):
"""Test indenting a string.
Args:
text: The text to indent.
indent_level: The number of spaces to indent by.
expected: The expected output string.
windows_platform: Whether the system is windows.
"""
assert format.indent(text, indent_level) == (
expected.replace("\n", "\r\n") if windows_platform else expected
)
@pytest.mark.parametrize(
"input,output",
[
("", ""),
("hello", "hello"),
("Hello", "hello"),
("camelCase", "camel_case"),
("camelTwoHumps", "camel_two_humps"),
("_start_with_underscore", "_start_with_underscore"),
("__start_with_double_underscore", "__start_with_double_underscore"),
],
)
def test_to_snake_case(input: str, output: str):
"""Test converting strings to snake case.
Args:
input: The input string.
output: The expected output string.
"""
assert format.to_snake_case(input) == output
@pytest.mark.parametrize(
"input,output",
[
("", ""),
("hello", "hello"),
("Hello", "Hello"),
("snake_case", "snakeCase"),
("snake_case_two", "snakeCaseTwo"),
],
)
def test_to_camel_case(input: str, output: str):
"""Test converting strings to camel case.
Args:
input: The input string.
output: The expected output string.
"""
assert format.to_camel_case(input) == output
@pytest.mark.parametrize(
"input,output",
[
("", ""),
("hello", "Hello"),
("Hello", "Hello"),
("snake_case", "SnakeCase"),
("snake_case_two", "SnakeCaseTwo"),
],
)
def test_to_title_case(input: str, output: str):
"""Test converting strings to title case.
Args:
input: The input string.
output: The expected output string.
"""
assert format.to_title_case(input) == output
@pytest.mark.parametrize(
"input,output",
[
("", ""),
("hello", "hello"),
("Hello", "hello"),
("snake_case", "snake-case"),
("snake_case_two", "snake-case-two"),
],
)
def test_to_kebab_case(input: str, output: str):
"""Test converting strings to kebab case.
Args:
input: the input string.
output: the output string.
"""
assert format.to_kebab_case(input) == output
@pytest.mark.parametrize(
"input,output",
[
("", "{``}"),
("hello", "{`hello`}"),
("hello world", "{`hello world`}"),
("hello=`world`", "{`hello=\\`world\\``}"),
],
)
def test_format_string(input: str, output: str):
"""Test formating the input as JS string literal.
Args:
input: the input string.
output: the output string.
"""
assert format.format_string(input) == output
@pytest.mark.parametrize(
"input,output",
[
(Var.create(value="test"), "{`test`}"),
(Var.create(value="test", is_local=True), "{`test`}"),
(Var.create(value="test", is_local=False), "{test}"),
(Var.create(value="test", is_string=True), "{`test`}"),
(Var.create(value="test", is_string=False), "{`test`}"),
(Var.create(value="test", is_local=False, is_string=False), "{test}"),
],
)
def test_format_var(input: Var, output: str):
assert format.format_var(input) == output
@pytest.mark.parametrize(
"route,format_case,expected",
[
("", True, "index"),
("/", True, "index"),
("custom-route", True, "custom-route"),
("custom-route", False, "custom-route"),
("custom-route/", True, "custom-route"),
("custom-route/", False, "custom-route"),
("/custom-route", True, "custom-route"),
("/custom-route", False, "custom-route"),
("/custom_route", True, "custom-route"),
("/custom_route", False, "custom_route"),
("/CUSTOM_route", True, "custom-route"),
("/CUSTOM_route", False, "CUSTOM_route"),
],
)
def test_format_route(route: str, format_case: bool, expected: bool):
"""Test formatting a route.
Args:
route: The route to format.
format_case: Whether to change casing to snake_case.
expected: The expected formatted route.
"""
assert format.format_route(route, format_case=format_case) == expected
@pytest.mark.parametrize(
"condition,true_value,false_value,expected",
[
("cond", "<C1>", '""', '{isTrue(cond) ? <C1> : ""}'),
("cond", "<C1>", "<C2>", "{isTrue(cond) ? <C1> : <C2>}"),
],
)
def test_format_cond(condition: str, true_value: str, false_value: str, expected: str):
"""Test formatting a cond.
Args:
condition: The condition to check.
true_value: The value to return if the condition is true.
false_value: The value to return if the condition is false.
expected: The expected output string.
"""
assert format.format_cond(condition, true_value, false_value) == expected
@pytest.mark.parametrize(
"prop,formatted",
[
("string", '"string"'),
("{wrapped_string}", "{wrapped_string}"),
(True, "{true}"),
(False, "{false}"),
(123, "{123}"),
(3.14, "{3.14}"),
([1, 2, 3], "{[1, 2, 3]}"),
(["a", "b", "c"], '{["a", "b", "c"]}'),
({"a": 1, "b": 2, "c": 3}, '{{"a": 1, "b": 2, "c": 3}}'),
({"a": 'foo "bar" baz'}, r'{{"a": "foo \"bar\" baz"}}'),
(
{
"a": 'foo "{ "bar" }" baz',
"b": BaseVar(name="val", type_="str"),
},
r'{{"a": "foo \"{ \"bar\" }\" baz", "b": val}}',
),
(
EventChain(
events=[EventSpec(handler=EventHandler(fn=mock_event))], args_spec=None
),
'{_e => addEvents([Event("mock_event", {})], _e)}',
),
(
EventChain(
events=[
EventSpec(
handler=EventHandler(fn=mock_event),
args=((Var.create_safe("arg"), EVENT_ARG.target.value),),
)
],
args_spec=None,
),
'{_e => addEvents([Event("mock_event", {arg:_e.target.value})], _e)}',
),
({"a": "red", "b": "blue"}, '{{"a": "red", "b": "blue"}}'),
(BaseVar(name="var", type_="int"), "{var}"),
(
BaseVar(
name="_",
type_=Any,
state="",
is_local=True,
is_string=False,
),
"{_}",
),
(BaseVar(name='state.colors["a"]', type_="str"), '{state.colors["a"]}'),
({"a": BaseVar(name="val", type_="str")}, '{{"a": val}}'),
({"a": BaseVar(name='"val"', type_="str")}, '{{"a": "val"}}'),
(
{"a": BaseVar(name='state.colors["val"]', type_="str")},
'{{"a": state.colors["val"]}}',
),
# tricky real-world case from markdown component
(
{
"h1": f"{{({{node, ...props}}) => <Heading {{...props}} {''.join(Tag(name='', props=Style({'as_': 'h1'})).format_props())} />}}"
},
'{{"h1": ({node, ...props}) => <Heading {...props} as={`h1`} />}}',
),
],
)
def test_format_prop(prop: Var, formatted: str):
"""Test that the formatted value of an prop is correct.
Args:
prop: The prop to test.
formatted: The expected formatted value.
"""
assert format.format_prop(prop) == formatted
@pytest.mark.parametrize(
"single_props,key_value_props,output",
[
(["string"], {"key": 42}, ["key={42}", "string"]),
],
)
def test_format_props(single_props, key_value_props, output):
"""Test the result of formatting a set of props (both single and keyvalue).
Args:
single_props: the list of single props
key_value_props: the dict of key value props
output: the expected output
"""
assert format.format_props(*single_props, **key_value_props) == output
@pytest.mark.parametrize(
"input,output",
[
(EventHandler(fn=mock_event), ("", "mock_event")),
],
)
def test_get_handler_parts(input, output):
assert format.get_event_handler_parts(input) == output
@pytest.mark.parametrize(
"input,output",
[
(TestState.do_something, "test_state.do_something"),
(ChildState.change_both, "test_state.child_state.change_both"),
(
GrandchildState.do_nothing,
"test_state.child_state.grandchild_state.do_nothing",
),
],
)
def test_format_event_handler(input, output):
"""Test formatting an event handler.
Args:
input: The event handler input.
output: The expected output.
"""
assert format.format_event_handler(input) == output # type: ignore
@pytest.mark.parametrize(
"input,output",
[
(EventSpec(handler=EventHandler(fn=mock_event)), 'Event("mock_event", {})'),
],
)
def test_format_event(input, output):
assert format.format_event(input) == output
@pytest.mark.parametrize(
"input,output",
[
(
EventChain(
events=[
EventSpec(handler=EventHandler(fn=mock_event)),
EventSpec(handler=EventHandler(fn=mock_event)),
],
args_spec=None,
),
'addEvents([Event("mock_event", {}),Event("mock_event", {})])',
),
(
EventChain(
events=[
EventSpec(handler=EventHandler(fn=mock_event)),
EventSpec(handler=EventHandler(fn=mock_event)),
],
args_spec=lambda e0: [e0],
),
'addEvents([Event("mock_event", {}),Event("mock_event", {})])',
),
],
)
def test_format_event_chain(input, output):
assert format.format_event_chain(input) == output
@pytest.mark.parametrize(
"input,output",
[
({"query": {"k1": 1, "k2": 2}}, {"k1": 1, "k2": 2}),
({"query": {"k1": 1, "k-2": 2}}, {"k1": 1, "k_2": 2}),
],
)
def test_format_query_params(input, output):
assert format.format_query_params(input) == output
@pytest.mark.parametrize(
"input, output",
[
(
TestState().dict(), # type: ignore
{
"array": [1, 2, 3.14],
"child_state": {
"count": 23,
"grandchild_state": {"value2": ""},
"value": "",
},
"child_state2": {"value": ""},
"complex": {
1: {"prop1": 42, "prop2": "hello"},
2: {"prop1": 42, "prop2": "hello"},
},
"dt": "1989-11-09 18:53:00+01:00",
"fig": [],
"is_hydrated": False,
"key": "",
"map_key": "a",
"mapping": {"a": [1, 2, 3], "b": [4, 5, 6]},
"num1": 0,
"num2": 3.14,
"obj": {"prop1": 42, "prop2": "hello"},
"sum": 3.14,
"upper": "",
},
),
(
DateTimeState().dict(),
{
"d": "1989-11-09",
"dt": "1989-11-09 18:53:00+01:00",
"is_hydrated": False,
"t": "18:53:00+01:00",
"td": "11 days, 0:11:00",
},
),
],
)
def test_format_state(input, output):
"""Test that the format state is correct.
Args:
input: The state to format.
output: The expected formatted state.
"""
assert format.format_state(input) == output
@pytest.mark.parametrize(
"input,output",
[
("input1", "ref_input1"),
("input 1", "ref_input_1"),
("input-1", "ref_input_1"),
("input_1", "ref_input_1"),
("a long test?1! name", "ref_a_long_test_1_name"),
],
)
def test_format_ref(input, output):
"""Test formatting a ref.
Args:
input: The name to format.
output: The expected formatted name.
"""
assert format.format_ref(input) == output
@pytest.mark.parametrize(
"input,output",
[
(("my_array", None), "refs_my_array"),
(("my_array", Var.create(0)), "refs_my_array[0]"),
(("my_array", Var.create(1)), "refs_my_array[1]"),
],
)
def test_format_array_ref(input, output):
assert format.format_array_ref(input[0], input[1]) == output
@pytest.mark.parametrize(
"input,output",
[
("/foo", [("foo", "/foo")]),
("/foo/bar", [("foo", "/foo"), ("bar", "/foo/bar")]),
(
"/foo/bar/baz",
[("foo", "/foo"), ("bar", "/foo/bar"), ("baz", "/foo/bar/baz")],
),
],
)
def test_format_breadcrumbs(input, output):
assert format.format_breadcrumbs(input) == output
@pytest.mark.parametrize(
"input, output",
[
("library@^0.1.2", "library"),
("library", "library"),
("@library@^0.1.2", "@library"),
("@library", "@library"),
],
)
def test_format_library_name(input: str, output: str):
"""Test formating a library name to remove the @version part.
Args:
input: the input string.
output: the output string.
"""
assert format.format_library_name(input) == output
@pytest.mark.parametrize(
"input,output",
[
(None, "null"),
(True, "true"),
(1, "1"),
(1.0, "1.0"),
([], "[]"),
([1, 2, 3], "[1, 2, 3]"),
({}, "{}"),
({"k1": False, "k2": True}, '{"k1": false, "k2": true}'),
],
)
def test_json_dumps(input, output):
assert format.json_dumps(input) == output

View File

@ -9,20 +9,17 @@ from packaging import version
from reflex import constants
from reflex.base import Base
from reflex.components.tags import Tag
from reflex.event import EVENT_ARG, EventChain, EventHandler, EventSpec
from reflex.event import EventHandler
from reflex.state import State
from reflex.style import Style
from reflex.utils import (
build,
format,
imports,
prerequisites,
types,
)
from reflex.utils import exec as utils_exec
from reflex.utils.serializers import serialize
from reflex.vars import BaseVar, Var
from reflex.vars import Var
def mock_event(arg):
@ -36,7 +33,7 @@ def get_above_max_version():
max bun version plus one.
"""
semantic_version_list = constants.BUN_VERSION.split(".")
semantic_version_list = constants.Bun.VERSION.split(".")
semantic_version_list[-1] = str(int(semantic_version_list[-1]) + 1) # type: ignore
return ".".join(semantic_version_list)
@ -59,179 +56,6 @@ def test_func():
pass
@pytest.mark.parametrize(
"input,output",
[
("", ""),
("hello", "hello"),
("Hello", "hello"),
("camelCase", "camel_case"),
("camelTwoHumps", "camel_two_humps"),
("_start_with_underscore", "_start_with_underscore"),
("__start_with_double_underscore", "__start_with_double_underscore"),
],
)
def test_to_snake_case(input: str, output: str):
"""Test converting strings to snake case.
Args:
input: The input string.
output: The expected output string.
"""
assert format.to_snake_case(input) == output
@pytest.mark.parametrize(
"input,output",
[
("", ""),
("hello", "hello"),
("Hello", "Hello"),
("snake_case", "snakeCase"),
("snake_case_two", "snakeCaseTwo"),
],
)
def test_to_camel_case(input: str, output: str):
"""Test converting strings to camel case.
Args:
input: The input string.
output: The expected output string.
"""
assert format.to_camel_case(input) == output
@pytest.mark.parametrize(
"input,output",
[
("", ""),
("hello", "Hello"),
("Hello", "Hello"),
("snake_case", "SnakeCase"),
("snake_case_two", "SnakeCaseTwo"),
],
)
def test_to_title_case(input: str, output: str):
"""Test converting strings to title case.
Args:
input: The input string.
output: The expected output string.
"""
assert format.to_title_case(input) == output
@pytest.mark.parametrize(
"input,output",
[
("{", "}"),
("(", ")"),
("[", "]"),
("<", ">"),
('"', '"'),
("'", "'"),
],
)
def test_get_close_char(input: str, output: str):
"""Test getting the close character for a given open character.
Args:
input: The open character.
output: The expected close character.
"""
assert format.get_close_char(input) == output
@pytest.mark.parametrize(
"text,open,expected",
[
("", "{", False),
("{wrap}", "{", True),
("{wrap", "{", False),
("{wrap}", "(", False),
("(wrap)", "(", True),
],
)
def test_is_wrapped(text: str, open: str, expected: bool):
"""Test checking if a string is wrapped in the given open and close characters.
Args:
text: The text to check.
open: The open character.
expected: Whether the text is wrapped.
"""
assert format.is_wrapped(text, open) == expected
@pytest.mark.parametrize(
"text,open,check_first,num,expected",
[
("", "{", True, 1, "{}"),
("wrap", "{", True, 1, "{wrap}"),
("wrap", "(", True, 1, "(wrap)"),
("wrap", "(", True, 2, "((wrap))"),
("(wrap)", "(", True, 1, "(wrap)"),
("{wrap}", "{", True, 2, "{wrap}"),
("(wrap)", "{", True, 1, "{(wrap)}"),
("(wrap)", "(", False, 1, "((wrap))"),
],
)
def test_wrap(text: str, open: str, expected: str, check_first: bool, num: int):
"""Test wrapping a string.
Args:
text: The text to wrap.
open: The open character.
expected: The expected output string.
check_first: Whether to check if the text is already wrapped.
num: The number of times to wrap the text.
"""
assert format.wrap(text, open, check_first=check_first, num=num) == expected
@pytest.mark.parametrize(
"text,indent_level,expected",
[
("", 2, ""),
("hello", 2, "hello"),
("hello\nworld", 2, " hello\n world\n"),
("hello\nworld", 4, " hello\n world\n"),
(" hello\n world", 2, " hello\n world\n"),
],
)
def test_indent(text: str, indent_level: int, expected: str, windows_platform: bool):
"""Test indenting a string.
Args:
text: The text to indent.
indent_level: The number of spaces to indent by.
expected: The expected output string.
windows_platform: Whether the system is windows.
"""
assert format.indent(text, indent_level) == (
expected.replace("\n", "\r\n") if windows_platform else expected
)
@pytest.mark.parametrize(
"condition,true_value,false_value,expected",
[
("cond", "<C1>", '""', '{isTrue(cond) ? <C1> : ""}'),
("cond", "<C1>", "<C2>", "{isTrue(cond) ? <C1> : <C2>}"),
],
)
def test_format_cond(condition: str, true_value: str, false_value: str, expected: str):
"""Test formatting a cond.
Args:
condition: The condition to check.
true_value: The value to return if the condition is true.
false_value: The value to return if the condition is false.
expected: The expected output string.
"""
assert format.format_cond(condition, true_value, false_value) == expected
def test_merge_imports():
"""Test that imports are merged correctly."""
d1 = {"react": {"Component"}}
@ -263,110 +87,6 @@ def test_is_generic_alias(cls: type, expected: bool):
assert types.is_generic_alias(cls) == expected
@pytest.mark.parametrize(
"route,format_case,expected",
[
("", True, "index"),
("/", True, "index"),
("custom-route", True, "custom-route"),
("custom-route", False, "custom-route"),
("custom-route/", True, "custom-route"),
("custom-route/", False, "custom-route"),
("/custom-route", True, "custom-route"),
("/custom-route", False, "custom-route"),
("/custom_route", True, "custom-route"),
("/custom_route", False, "custom_route"),
("/CUSTOM_route", True, "custom-route"),
("/CUSTOM_route", False, "CUSTOM_route"),
],
)
def test_format_route(route: str, format_case: bool, expected: bool):
"""Test formatting a route.
Args:
route: The route to format.
format_case: Whether to change casing to snake_case.
expected: The expected formatted route.
"""
assert format.format_route(route, format_case=format_case) == expected
@pytest.mark.parametrize(
"prop,formatted",
[
("string", '"string"'),
("{wrapped_string}", "{wrapped_string}"),
(True, "{true}"),
(False, "{false}"),
(123, "{123}"),
(3.14, "{3.14}"),
([1, 2, 3], "{[1, 2, 3]}"),
(["a", "b", "c"], '{["a", "b", "c"]}'),
({"a": 1, "b": 2, "c": 3}, '{{"a": 1, "b": 2, "c": 3}}'),
({"a": 'foo "bar" baz'}, r'{{"a": "foo \"bar\" baz"}}'),
(
{
"a": 'foo "{ "bar" }" baz',
"b": BaseVar(name="val", type_="str"),
},
r'{{"a": "foo \"{ \"bar\" }\" baz", "b": val}}',
),
(
EventChain(
events=[EventSpec(handler=EventHandler(fn=mock_event))], args_spec=None
),
'{_e => addEvents([Event("mock_event", {})], _e)}',
),
(
EventChain(
events=[
EventSpec(
handler=EventHandler(fn=mock_event),
args=((Var.create_safe("arg"), EVENT_ARG.target.value),),
)
],
args_spec=None,
),
'{_e => addEvents([Event("mock_event", {arg:_e.target.value})], _e)}',
),
({"a": "red", "b": "blue"}, '{{"a": "red", "b": "blue"}}'),
(BaseVar(name="var", type_="int"), "{var}"),
(
BaseVar(
name="_",
type_=Any,
state="",
is_local=True,
is_string=False,
),
"{_}",
),
(BaseVar(name='state.colors["a"]', type_="str"), '{state.colors["a"]}'),
({"a": BaseVar(name="val", type_="str")}, '{{"a": val}}'),
({"a": BaseVar(name='"val"', type_="str")}, '{{"a": "val"}}'),
(
{"a": BaseVar(name='state.colors["val"]', type_="str")},
'{{"a": state.colors["val"]}}',
),
# tricky real-world case from markdown component
(
{
"h1": f"{{({{node, ...props}}) => <Heading {{...props}} {''.join(Tag(name='', props=Style({'as_': 'h1'})).format_props())} />}}"
},
'{{"h1": ({node, ...props}) => <Heading {...props} as={`h1`} />}}',
),
],
)
def test_format_prop(prop: Var, formatted: str):
"""Test that the formatted value of an prop is correct.
Args:
prop: The prop to test.
formatted: The expected formatted value.
"""
assert format.format_prop(prop) == formatted
def test_validate_invalid_bun_path(mocker):
"""Test that an error is thrown when a custom specified bun path is not valid
or does not exist.
@ -523,31 +243,11 @@ def test_create_config_e2e(tmp_working_dir):
app_name = "e2e"
prerequisites.create_config(app_name)
eval_globals = {}
exec((tmp_working_dir / constants.CONFIG_FILE).read_text(), eval_globals)
exec((tmp_working_dir / constants.Config.FILE).read_text(), eval_globals)
config = eval_globals["config"]
assert config.app_name == app_name
@pytest.mark.parametrize(
"name,expected",
[
("input1", "ref_input1"),
("input 1", "ref_input_1"),
("input-1", "ref_input_1"),
("input_1", "ref_input_1"),
("a long test?1! name", "ref_a_long_test_1_name"),
],
)
def test_format_ref(name, expected):
"""Test formatting a ref.
Args:
name: The name to format.
expected: The expected formatted name.
"""
assert format.format_ref(name) == expected
class DataFrame:
"""A Fake pandas DataFrame class."""
@ -585,8 +285,8 @@ def test_initialize_non_existent_gitignore(tmp_path, mocker, gitignore_exists):
mocker: The mock object.
gitignore_exists: Whether a gitignore file exists in the root dir.
"""
expected = constants.DEFAULT_GITIGNORE.copy()
mocker.patch("reflex.constants.GITIGNORE_FILE", tmp_path / ".gitignore")
expected = constants.GitIgnore.DEFAULTS.copy()
mocker.patch("reflex.constants.GitIgnore.FILE", tmp_path / ".gitignore")
gitignore_file = tmp_path / ".gitignore"
@ -633,8 +333,8 @@ def test_node_install_windows(tmp_path, mocker):
fnm_root_path = tmp_path / "reflex" / "fnm"
fnm_exe = fnm_root_path / "fnm.exe"
mocker.patch("reflex.utils.prerequisites.constants.FNM_DIR", fnm_root_path)
mocker.patch("reflex.utils.prerequisites.constants.FNM_EXE", fnm_exe)
mocker.patch("reflex.utils.prerequisites.constants.Fnm.DIR", fnm_root_path)
mocker.patch("reflex.utils.prerequisites.constants.Fnm.EXE", fnm_exe)
mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", True)
mocker.patch("reflex.utils.processes.new_process")
mocker.patch("reflex.utils.processes.stream_logs")
@ -675,8 +375,8 @@ def test_node_install_unix(tmp_path, mocker, machine, system):
fnm_root_path = tmp_path / "reflex" / "fnm"
fnm_exe = fnm_root_path / "fnm"
mocker.patch("reflex.utils.prerequisites.constants.FNM_DIR", fnm_root_path)
mocker.patch("reflex.utils.prerequisites.constants.FNM_EXE", fnm_exe)
mocker.patch("reflex.utils.prerequisites.constants.Fnm.DIR", fnm_root_path)
mocker.patch("reflex.utils.prerequisites.constants.Fnm.EXE", fnm_exe)
mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", False)
mocker.patch("reflex.utils.prerequisites.platform.machine", return_value=machine)
mocker.patch("reflex.utils.prerequisites.platform.system", return_value=system)
@ -701,14 +401,14 @@ def test_node_install_unix(tmp_path, mocker, machine, system):
fnm_exe,
"install",
"--arch=arm64",
constants.NODE_VERSION,
constants.Node.VERSION,
"--fnm-dir",
fnm_root_path,
]
)
else:
process.assert_called_with(
[fnm_exe, "install", constants.NODE_VERSION, "--fnm-dir", fnm_root_path]
[fnm_exe, "install", constants.Node.VERSION, "--fnm-dir", fnm_root_path]
)
chmod.assert_called_once()
@ -759,7 +459,7 @@ def test_output_system_info(mocker):
This test makes no assertions about the output, other than it executes
without crashing.
"""
mocker.patch("reflex.utils.console.LOG_LEVEL", constants.LogLevel.DEBUG)
mocker.patch("reflex.utils.console._LOG_LEVEL", constants.LogLevel.DEBUG)
utils_exec.output_system_info()