From dcb17103bbd4a80770eee079379a8ca2bd3646aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Fri, 29 Sep 2023 02:27:20 +0200 Subject: [PATCH] code cleanup (split constants into a folder) (#1866) --- reflex/app.py | 46 +- reflex/compiler/compiler.py | 18 +- reflex/compiler/templates.py | 32 +- reflex/compiler/utils.py | 22 +- reflex/components/component.py | 5 +- reflex/config.py | 6 +- reflex/constants.py | 444 --------------- reflex/constants/__init__.py | 92 ++++ reflex/constants/base.py | 174 ++++++ reflex/constants/compiler.py | 76 +++ reflex/constants/config.py | 45 ++ reflex/constants/event.py | 85 +++ reflex/constants/installer.py | 123 +++++ reflex/constants/route.py | 67 +++ reflex/constants/style.py | 22 + reflex/el/constants/reflex.py | 4 +- reflex/event.py | 22 +- reflex/middleware/hydrate_middleware.py | 4 +- reflex/reflex.py | 17 +- reflex/state.py | 6 +- reflex/style.py | 4 +- reflex/testing.py | 6 +- reflex/utils/build.py | 35 +- reflex/utils/console.py | 20 +- reflex/utils/exec.py | 22 +- reflex/utils/format.py | 21 +- reflex/utils/path_ops.py | 12 +- reflex/utils/prerequisites.py | 128 ++--- reflex/utils/telemetry.py | 2 +- reflex/utils/types.py | 4 +- reflex/utils/watch.py | 6 +- reflex/vars.py | 5 +- tests/middleware/test_hydrate_middleware.py | 4 +- tests/test_app.py | 2 +- tests/test_state.py | 90 +-- tests/utils/test_format.py | 578 ++++++++++++++++++++ tests/utils/test_utils.py | 326 +---------- 37 files changed, 1524 insertions(+), 1051 deletions(-) delete mode 100644 reflex/constants.py create mode 100644 reflex/constants/__init__.py create mode 100644 reflex/constants/base.py create mode 100644 reflex/constants/compiler.py create mode 100644 reflex/constants/config.py create mode 100644 reflex/constants/event.py create mode 100644 reflex/constants/installer.py create mode 100644 reflex/constants/route.py create mode 100644 reflex/constants/style.py create mode 100644 tests/utils/test_format.py diff --git a/reflex/app.py b/reflex/app.py index f7764829d9..4ef8c0ae71 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -183,8 +183,8 @@ def __init__(self, *args, **kwargs): 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 @@ def add_page( 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 @@ def get_load_events(self, route: str) -> list[EventHandler | EventSpec]: """ 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 @@ def _check_routes_conflict(self, new_route: str): 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 @@ def add_custom_404_page( 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 @@ def get_frontend_packages(self, imports: Dict[str, set[ImportVar]]): 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 @@ def compile(self): 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 @@ def compile(self): # 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)) diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 9a7c76a29b..2d35d31518 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -23,7 +23,7 @@ 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 @@ 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"]) diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index 0f845d07c6..6531dedf8a 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -19,26 +19,26 @@ def __init__(self) -> None: ) 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, } diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 617427312c..3b737f6ff6 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -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( diff --git a/reflex/components/component.py b/reflex/components/component.py index 9779ebc021..477d8a58b5 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -7,10 +7,9 @@ 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 diff --git a/reflex/config.py b/reflex/config.py index 232406c6c3..8caa297411 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -163,7 +163,7 @@ class Config: 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 diff --git a/reflex/constants.py b/reflex/constants.py deleted file mode 100644 index ae78b5428d..0000000000 --- a/reflex/constants.py +++ /dev/null @@ -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//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" diff --git a/reflex/constants/__init__.py b/reflex/constants/__init__.py new file mode 100644 index 0000000000..07adb9a8dc --- /dev/null +++ b/reflex/constants/__init__.py @@ -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, +] diff --git a/reflex/constants/base.py b/reflex/constants/base.py new file mode 100644 index 0000000000..9884f4372c --- /dev/null +++ b/reflex/constants/base.py @@ -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//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" diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py new file mode 100644 index 0000000000..70a4fc4992 --- /dev/null +++ b/reflex/constants/compiler.py @@ -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 diff --git a/reflex/constants/config.py b/reflex/constants/config.py new file mode 100644 index 0000000000..9828a0994a --- /dev/null +++ b/reflex/constants/config.py @@ -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" diff --git a/reflex/constants/event.py b/reflex/constants/event.py new file mode 100644 index 0000000000..c880c93ff9 --- /dev/null +++ b/reflex/constants/event.py @@ -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" diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py new file mode 100644 index 0000000000..e17c53ff28 --- /dev/null +++ b/reflex/constants/installer.py @@ -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", + } diff --git a/reflex/constants/route.py b/reflex/constants/route.py new file mode 100644 index 0000000000..c01b2e1b13 --- /dev/null +++ b/reflex/constants/route.py @@ -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" diff --git a/reflex/constants/style.py b/reflex/constants/style.py new file mode 100644 index 0000000000..0cba1305cf --- /dev/null +++ b/reflex/constants/style.py @@ -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" diff --git a/reflex/el/constants/reflex.py b/reflex/el/constants/reflex.py index ec9b21b307..05c2983258 100644 --- a/reflex/el/constants/reflex.py +++ b/reflex/el/constants/reflex.py @@ -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 @@ def attr_to_prop(attr_name: str) -> str: 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. diff --git a/reflex/event.py b/reflex/event.py index 9d960fa16d..be12f07456 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -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: diff --git a/reflex/middleware/hydrate_middleware.py b/reflex/middleware/hydrate_middleware.py index 77e0b33918..062ff33b3f 100644 --- a/reflex/middleware/hydrate_middleware.py +++ b/reflex/middleware/hydrate_middleware.py @@ -13,7 +13,7 @@ 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 @@ async def preprocess( 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): diff --git a/reflex/reflex.py b/reflex/reflex.py index e90d8b1ad4..0fb65f2338 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -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.""" diff --git a/reflex/state.py b/reflex/state.py index f6a11f1cf2..120a578817 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1102,7 +1102,7 @@ def __init__(self, state_instance): 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 = ( diff --git a/reflex/style.py b/reflex/style.py index 9a16467a82..cf11253509 100644 --- a/reflex/style.py +++ b/reflex/style.py @@ -7,8 +7,8 @@ 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): diff --git a/reflex/testing.py b/reflex/testing.py index 9486823f9b..e05d7c0996 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -154,7 +154,7 @@ def _initialize_app(self): 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 @@ def _start_frontend(self): # 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", } diff --git a/reflex/utils/build.py b/reflex/utils/build.py index ad5ab9a22c..9264b07355 100644 --- a/reflex/utils/build.py +++ b/reflex/utils/build.py @@ -6,7 +6,6 @@ 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 @@ 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, ) diff --git a/reflex/utils/console.py b/reflex/utils/console.py index 0374defaa8..ce426c9915 100644 --- a/reflex/utils/console.py +++ b/reflex/utils/console.py @@ -14,7 +14,7 @@ _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) diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 5fb1aa072b..2bed5cd3f2 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -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": diff --git a/reflex/utils/format.py b/reflex/utils/format.py index b53f9ef614..dd0948fdcc 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -5,7 +5,6 @@ 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. diff --git a/reflex/utils/path_ops.py b/reflex/utils/path_ops.py index db58c8f47a..9392492675 100644 --- a/reflex/utils/path_ops.py +++ b/reflex/utils/path_ops.py @@ -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]): diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 540955673b..fa78acfb8d 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -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 = { diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index 0d6dfe897b..d5ed2d6272 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -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: diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 169d0de8c5..7b6db4ea97 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -4,7 +4,6 @@ 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 @@ 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, ...]: diff --git a/reflex/utils/watch.py b/reflex/utils/watch.py index 923543ab92..a56635d6eb 100644 --- a/reflex/utils/watch.py +++ b/reflex/utils/watch.py @@ -8,7 +8,7 @@ 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 @@ def __init__(self, root): 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 @@ def get_dest_path(self, src_path: str) -> str: 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) ) diff --git a/reflex/vars.py b/reflex/vars.py index f0b4f563b0..6d805dbb65 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -27,8 +27,7 @@ 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 @@ def create( # 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_}." diff --git a/tests/middleware/test_hydrate_middleware.py b/tests/middleware/test_hydrate_middleware.py index 8c4e9688ab..150083bd59 100644 --- a/tests/middleware/test_hydrate_middleware.py +++ b/tests/middleware/test_hydrate_middleware.py @@ -3,7 +3,7 @@ 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): diff --git a/tests/test_app.py b/tests/test_app.py index 0044c2c328..4986205ef7 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -923,7 +923,7 @@ def _dynamic_state_event(name, val, **kwargs): 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, diff --git a/tests/test_state.py b/tests/test_state.py index a6ea8e4e03..af327f37fe 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -15,7 +15,7 @@ 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 @@ 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 @@ def do_nothing(self): 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 dep_v(self) -> int: 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 diff --git a/tests/utils/test_format.py b/tests/utils/test_format.py new file mode 100644 index 0000000000..61c7427eae --- /dev/null +++ b/tests/utils/test_format.py @@ -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", "", '""', '{isTrue(cond) ? : ""}'), + ("cond", "", "", "{isTrue(cond) ? : }"), + ], +) +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}}) => }}" + }, + '{{"h1": ({node, ...props}) => }}', + ), + ], +) +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 diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index e392cc09a4..23205296f8 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -9,20 +9,17 @@ 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", "", '""', '{isTrue(cond) ? : ""}'), - ("cond", "", "", "{isTrue(cond) ? : }"), - ], -) -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}}) => }}" - }, - '{{"h1": ({node, ...props}) => }}', - ), - ], -) -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 @@ class Resp(Base): 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()