Compare commits

...

40 Commits

Author SHA1 Message Date
Masen Furer
c17c973d7f
implement get_config_safe to get config via subprocess
It's honking SLOW
2024-12-11 10:14:29 -08:00
Masen Furer
5ae1541aa9
Set REFLEX_ENV_MODE=prod for exports
Ensure that exported frontends use minified names

Set REFLEX_ENV_MODE earlier
2024-12-10 17:12:33 -08:00
Benedikt Bartscher
5c9839e0fe
Merge remote-tracking branch 'upstream/main' into minify-state-names-v2 2024-11-25 00:43:29 +01:00
Benedikt Bartscher
0262143a4d
prevent env api breaking change 2024-11-22 21:21:57 +01:00
Benedikt Bartscher
d8def13530
Merge remote-tracking branch 'upstream/main' into minify-state-names-v2 2024-11-22 21:09:29 +01:00
Benedikt Bartscher
0be2c3e810
fix: properly cleanup env vars with pytest fixtures 2024-11-21 02:13:41 +01:00
Benedikt Bartscher
5840c0a24b
Merge remote-tracking branch 'upstream/main' into minify-state-names-v2 2024-11-21 00:47:37 +01:00
Alek Petuskey
1e45a8e140
Update bug_report.md (#4382) 2024-11-14 21:24:51 +01:00
Elijah Ahianyo
a154a7d920
Add template name to reflex init success msg (#4349)
* Add template name to reflex init success msg

* fix pyright message
2024-11-14 21:24:51 +01:00
Khaleel Al-Adhami
a639f526da
add typing to function vars (#4372)
* add typing to function vars

* import ParamSpec from typing_extensions

* remove ellipsis as they are not supported in 3.9

* try importing everything from extensions

* special case 3.9

* don't use Any from extensions

* get typevar from extensions
2024-11-14 21:24:51 +01:00
Benedikt Bartscher
3dd6e9c8ad
Merge remote-tracking branch 'upstream/main' into minify-state-names-v2 2024-11-12 23:23:17 +01:00
Benedikt Bartscher
1ef54ecb93
Merge remote-tracking branch 'upstream/main' into minify-state-names-v2 2024-11-08 22:35:29 +01:00
Benedikt Bartscher
2e12feb120
darglinting it 2024-11-08 22:34:47 +01:00
Benedikt Bartscher
74c336f294
forgot ruffing 2024-11-07 01:06:10 +01:00
Benedikt Bartscher
6ce9471e6f
set env mode before importing modules that contain rx.State subclasses 2024-11-07 00:03:04 +01:00
Benedikt Bartscher
f94328f74f
delete unused function, add cleanup fixture for test_is_prod_mode 2024-11-06 23:58:01 +01:00
Benedikt Bartscher
46fefcf58c
Merge remote-tracking branch 'upstream/main' into minify-state-names-v2 2024-11-06 23:11:41 +01:00
Benedikt Bartscher
24caf5fef1
cleanup unnecessary or False 2024-11-06 01:32:12 +01:00
Benedikt Bartscher
bcd0cf824d
Merge remote-tracking branch 'upstream/main' into minify-state-names-v2 2024-11-06 00:46:11 +01:00
Benedikt Bartscher
84c14a189f
cleanup conflicts 2024-11-05 22:35:47 +01:00
Benedikt Bartscher
4116ab9f66
do not set appharness env twice, already done in AppHarnessProd 2024-11-05 22:34:59 +01:00
Benedikt Bartscher
d32604713c
implement default_factory for EnvVar, improve env_var typing
also migrate environment to be a class singleton to prevent unintended chaos with default factories
2024-11-05 12:53:17 -08:00
Masen Furer
db2b5b0320
AppHarness: handle get_state_name for minified names
Also clear the StatefulComponent cache between AppInstance runs
2024-11-05 12:53:17 -08:00
Masen Furer
6eb808ca2c
state.js: more reliable isStateful detection 2024-11-05 12:53:17 -08:00
Masen Furer
4bbd988411
test_minified_states: remove skip -- things seem to be working as-is 2024-11-05 12:53:16 -08:00
Benedikt Bartscher
f163d41563
pass constants via js consts to state.js 2024-11-05 12:53:16 -08:00
Benedikt Bartscher
c26d626bf2
Revert "move state.js to jinja, related to #3738"
This reverts commit 4d2a72c4e86be3c8466cc701daf173ea70cba90d.
2024-11-05 12:53:16 -08:00
Benedikt Bartscher
79fc10957d
move state.js to jinja, related to #3738 2024-11-05 12:53:16 -08:00
Benedikt Bartscher
4e76e4d6ac
fix type ignore comment 2024-11-05 12:53:16 -08:00
Benedikt Bartscher
dadfb5663a
wip: more dynamic jinja contexts, tests for minification 2024-11-05 12:53:16 -08:00
Benedikt Bartscher
215a8343f4
wip minified state integration test 2024-11-05 12:53:15 -08:00
Benedikt Bartscher
7287c3a167
fix typo 2024-11-05 12:53:15 -08:00
Benedikt Bartscher
e9cedd2a92
fix default state names 2024-11-05 12:53:15 -08:00
Benedikt Bartscher
7bf15b4f44
add simple test that minified state names are unique 2024-11-05 12:53:15 -08:00
Benedikt Bartscher
51aef9fd22
enable minified state names by default in prod 2024-11-05 12:53:15 -08:00
Benedikt Bartscher
8fc8fd9ec7
fix state name init for substates, thanks @masenf 2024-11-05 12:53:14 -08:00
Benedikt Bartscher
bae98e80ed
fix hardcoded event handlers and states 2024-11-05 12:53:14 -08:00
Benedikt Bartscher
1b0577a7e1
all _state_names should be classvars 2024-11-05 12:53:14 -08:00
Benedikt Bartscher
0c406f4bac
cleanup 2024-11-05 12:53:14 -08:00
Benedikt Bartscher
673c488dc0
wip minified state names 2024-11-05 12:53:13 -08:00
22 changed files with 631 additions and 154 deletions

View File

@ -23,6 +23,8 @@ export const clientStorage = {{ client_storage|json_dumps }}
export const clientStorage = {}
{% endif %}
export const main_state_name = "{{const.main_state_name}}"
export const update_vars_internal = "{{const.update_vars_internal}}"
{% if state_name %}
export const state_name = "{{state_name}}"

View File

@ -12,6 +12,8 @@ import {
onLoadInternalEvent,
state_name,
exception_state_name,
main_state_name,
update_vars_internal,
} from "$/utils/context.js";
import debounce from "$/utils/helpers/debounce";
import throttle from "$/utils/helpers/throttle";
@ -117,7 +119,7 @@ export const isStateful = () => {
if (event_queue.length === 0) {
return false;
}
return event_queue.some((event) => event.name.startsWith("reflex___state"));
return event_queue.some(event => event.name.startsWith(main_state_name));
};
/**
@ -822,7 +824,7 @@ export const useEventLoop = (
const vars = {};
vars[storage_to_state_map[e.key]] = e.newValue;
const event = Event(
`${state_name}.reflex___state____update_vars_internal_state.update_vars_internal`,
`${state_name}.${update_vars_internal}`,
{ vars: vars }
);
addEvents([event], e);
@ -836,7 +838,7 @@ export const useEventLoop = (
// Route after the initial page hydration.
useEffect(() => {
const change_start = () => {
const main_state_dispatch = dispatch["reflex___state____state"];
const main_state_dispatch = dispatch[main_state_name];
if (main_state_dispatch !== undefined) {
main_state_dispatch({ is_hydrated: false });
}

View File

@ -34,7 +34,7 @@ def _compile_document_root(root: Component) -> str:
Returns:
The compiled document root.
"""
return templates.DOCUMENT_ROOT.render(
return templates.document_root().render(
imports=utils.compile_imports(root._get_all_imports()),
document=root.render(),
)
@ -72,7 +72,7 @@ def _compile_app(app_root: Component) -> str:
("utils_state", f"$/{constants.Dirs.UTILS}/state"),
]
return templates.APP_ROOT.render(
return templates.app_root().render(
imports=utils.compile_imports(app_root._get_all_imports()),
custom_codes=app_root._get_all_custom_code(),
hooks={**app_root._get_all_hooks_internal(), **app_root._get_all_hooks()},
@ -90,7 +90,7 @@ def _compile_theme(theme: str) -> str:
Returns:
The compiled theme.
"""
return templates.THEME.render(theme=theme)
return templates.theme().render(theme=theme)
def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None) -> str:
@ -109,7 +109,7 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None)
last_compiled_time = str(datetime.now())
return (
templates.CONTEXT.render(
templates.context().render(
initial_state=utils.compile_state(state),
state_name=state.get_name(),
client_storage=utils.compile_client_storage(state),
@ -118,7 +118,7 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None)
default_color_mode=appearance,
)
if state
else templates.CONTEXT.render(
else templates.context().render(
is_dev_mode=not is_prod_mode(),
default_color_mode=appearance,
last_compiled_time=last_compiled_time,
@ -145,7 +145,7 @@ def _compile_page(
# Compile the code to render the component.
kwargs = {"state_name": state.get_name()} if state is not None else {}
return templates.PAGE.render(
return templates.page().render(
imports=imports,
dynamic_imports=component._get_all_dynamic_imports(),
custom_codes=component._get_all_custom_code(),
@ -201,7 +201,7 @@ def _compile_root_stylesheet(stylesheets: list[str]) -> str:
)
stylesheet = f"../{constants.Dirs.PUBLIC}/{stylesheet.strip('/')}"
sheets.append(stylesheet) if stylesheet not in sheets else None
return templates.STYLE.render(stylesheets=sheets)
return templates.style().render(stylesheets=sheets)
def _compile_component(component: Component | StatefulComponent) -> str:
@ -213,7 +213,7 @@ def _compile_component(component: Component | StatefulComponent) -> str:
Returns:
The compiled component.
"""
return templates.COMPONENT.render(component=component)
return templates.component().render(component=component)
def _compile_components(
@ -241,7 +241,7 @@ def _compile_components(
# Compile the components page.
return (
templates.COMPONENTS.render(
templates.components().render(
imports=utils.compile_imports(imports),
components=component_renders,
),
@ -319,7 +319,7 @@ def _compile_stateful_components(
f"$/{constants.Dirs.UTILS}/{constants.PageNames.STATEFUL_COMPONENTS}", None
)
return templates.STATEFUL_COMPONENTS.render(
return templates.stateful_components().render(
imports=utils.compile_imports(all_imports),
memoized_code="\n".join(rendered_components),
)
@ -336,7 +336,7 @@ def _compile_tailwind(
Returns:
The compiled Tailwind config.
"""
return templates.TAILWIND_CONFIG.render(
return templates.tailwind_config().render(
**config,
)

View File

@ -11,6 +11,13 @@ class ReflexJinjaEnvironment(Environment):
def __init__(self) -> None:
"""Set default environment."""
from reflex.state import (
FrontendEventExceptionState,
OnLoadInternalState,
State,
UpdateVarsInternalState,
)
extensions = ["jinja2.ext.debug"]
super().__init__(
extensions=extensions,
@ -42,9 +49,10 @@ class ReflexJinjaEnvironment(Environment):
"set_color_mode": constants.ColorMode.SET,
"use_color_mode": constants.ColorMode.USE,
"hydrate": constants.CompileVars.HYDRATE,
"on_load_internal": constants.CompileVars.ON_LOAD_INTERNAL,
"update_vars_internal": constants.CompileVars.UPDATE_VARS_INTERNAL,
"frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL,
"main_state_name": State.get_name(),
"on_load_internal": f"{OnLoadInternalState.get_name()}.on_load_internal",
"update_vars_internal": f"{UpdateVarsInternalState.get_name()}.update_vars_internal",
"frontend_exception_state": FrontendEventExceptionState.get_full_name(),
}
@ -60,61 +68,172 @@ def get_template(name: str) -> Template:
return ReflexJinjaEnvironment().get_template(name=name)
# Template for the Reflex config file.
RXCONFIG = get_template("app/rxconfig.py.jinja2")
def rxconfig():
"""Template for the Reflex config file.
# Code to render a NextJS Document root.
DOCUMENT_ROOT = get_template("web/pages/_document.js.jinja2")
Returns:
Template: The template for the Reflex config file.
"""
return get_template("app/rxconfig.py.jinja2")
# Code to render NextJS App root.
APP_ROOT = get_template("web/pages/_app.js.jinja2")
# Template for the theme file.
THEME = get_template("web/utils/theme.js.jinja2")
def document_root():
"""Code to render a NextJS Document root.
# Template for the context file.
CONTEXT = get_template("web/utils/context.js.jinja2")
Returns:
Template: The template for the NextJS Document root.
"""
return get_template("web/pages/_document.js.jinja2")
# Template for Tailwind config.
TAILWIND_CONFIG = get_template("web/tailwind.config.js.jinja2")
# Template to render a component tag.
COMPONENT = get_template("web/pages/component.js.jinja2")
def app_root():
"""Code to render NextJS App root.
# Code to render a single NextJS page.
PAGE = get_template("web/pages/index.js.jinja2")
Returns:
Template: The template for the NextJS App root.
"""
return get_template("web/pages/_app.js.jinja2")
# Code to render the custom components page.
COMPONENTS = get_template("web/pages/custom_component.js.jinja2")
# Code to render Component instances as part of StatefulComponent
STATEFUL_COMPONENT = get_template("web/pages/stateful_component.js.jinja2")
def theme():
"""Template for the theme file.
# Code to render StatefulComponent to an external file to be shared
STATEFUL_COMPONENTS = get_template("web/pages/stateful_components.js.jinja2")
Returns:
Template: The template for the theme file.
"""
return get_template("web/utils/theme.js.jinja2")
# Sitemap config file.
SITEMAP_CONFIG = "module.exports = {config}".format
# Code to render the root stylesheet.
STYLE = get_template("web/styles/styles.css.jinja2")
def context():
"""Template for the context file.
# Code that generate the package json file
PACKAGE_JSON = get_template("web/package.json.jinja2")
Returns:
Template: The template for the context file.
"""
return get_template("web/utils/context.js.jinja2")
# Code that generate the pyproject.toml file for custom components.
CUSTOM_COMPONENTS_PYPROJECT_TOML = get_template(
"custom_components/pyproject.toml.jinja2"
)
# Code that generates the README file for custom components.
CUSTOM_COMPONENTS_README = get_template("custom_components/README.md.jinja2")
def tailwind_config():
"""Template for Tailwind config.
# Code that generates the source file for custom components.
CUSTOM_COMPONENTS_SOURCE = get_template("custom_components/src.py.jinja2")
Returns:
Template: The template for the Tailwind config
"""
return get_template("web/tailwind.config.js.jinja2")
# Code that generates the init file for custom components.
CUSTOM_COMPONENTS_INIT_FILE = get_template("custom_components/__init__.py.jinja2")
# Code that generates the demo app main py file for testing custom components.
CUSTOM_COMPONENTS_DEMO_APP = get_template("custom_components/demo_app.py.jinja2")
def component():
"""Template to render a component tag.
Returns:
Template: The template for the component tag.
"""
return get_template("web/pages/component.js.jinja2")
def page():
"""Code to render a single NextJS page.
Returns:
Template: The template for the NextJS page.
"""
return get_template("web/pages/index.js.jinja2")
def components():
"""Code to render the custom components page.
Returns:
Template: The template for the custom components page.
"""
return get_template("web/pages/custom_component.js.jinja2")
def stateful_component():
"""Code to render Component instances as part of StatefulComponent.
Returns:
Template: The template for the StatefulComponent.
"""
return get_template("web/pages/stateful_component.js.jinja2")
def stateful_components():
"""Code to render StatefulComponent to an external file to be shared.
Returns:
Template: The template for the StatefulComponent.
"""
return get_template("web/pages/stateful_components.js.jinja2")
def sitemap_config():
"""Sitemap config file.
Returns:
Template: The template for the sitemap config file.
"""
return "module.exports = {config}".format
def style():
"""Code to render the root stylesheet.
Returns:
Template: The template for the root stylesheet
"""
return get_template("web/styles/styles.css.jinja2")
def package_json():
"""Code that generate the package json file.
Returns:
Template: The template for the package json file
"""
return get_template("web/package.json.jinja2")
def custom_components_pyproject_toml():
"""Code that generate the pyproject.toml file for custom components.
Returns:
Template: The template for the pyproject.toml file
"""
return get_template("custom_components/pyproject.toml.jinja2")
def custom_components_readme():
"""Code that generates the README file for custom components.
Returns:
Template: The template for the README file
"""
return get_template("custom_components/README.md.jinja2")
def custom_components_source():
"""Code that generates the source file for custom components.
Returns:
Template: The template for the source file
"""
return get_template("custom_components/src.py.jinja2")
def custom_components_init():
"""Code that generates the init file for custom components.
Returns:
Template: The template for the init file
"""
return get_template("custom_components/__init__.py.jinja2")
def custom_components_demo_app():
"""Code that generates the demo app main py file for testing custom components.
Returns:
Template: The template for the demo app main py file
"""
return get_template("custom_components/demo_app.py.jinja2")

View File

@ -25,7 +25,7 @@ from typing import (
import reflex.state
from reflex.base import Base
from reflex.compiler.templates import STATEFUL_COMPONENT
from reflex.compiler.templates import stateful_component
from reflex.components.core.breakpoints import Breakpoints
from reflex.components.dynamic import load_dynamic_serializer
from reflex.components.tags import Tag
@ -2162,7 +2162,7 @@ class StatefulComponent(BaseComponent):
component.event_triggers[event_trigger] = memo_trigger
# Render the code for this component and hooks.
return STATEFUL_COMPONENT.render(
return stateful_component().render(
tag_name=tag_name,
memo_trigger_hooks=memo_trigger_hooks,
component=component,

View File

@ -80,7 +80,7 @@ def load_dynamic_serializer():
)
rendered_components[
templates.STATEFUL_COMPONENT.render(
templates.stateful_component().render(
tag_name="MySSRComponent",
memo_trigger_hooks=[],
component=component,
@ -101,10 +101,14 @@ def load_dynamic_serializer():
else:
imports[lib] = names
module_code_lines = templates.STATEFUL_COMPONENTS.render(
imports=utils.compile_imports(imports),
memoized_code="\n".join(rendered_components),
).splitlines()[1:]
module_code_lines = (
templates.stateful_components()
.render(
imports=utils.compile_imports(imports),
memoized_code="\n".join(rendered_components),
)
.splitlines()[1:]
)
# Rewrite imports from `/` to destructure from window
for ix, line in enumerate(module_code_lines[:]):

View File

@ -2,19 +2,22 @@
from __future__ import annotations
import dataclasses
import enum
import importlib
import inspect
import os
import subprocess
import sys
import tempfile
import threading
import urllib.parse
from functools import lru_cache
from importlib.util import find_spec
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Generic,
List,
@ -149,28 +152,6 @@ class DBConfig(Base):
return f"{self.engine}://{path}/{self.database}"
def get_default_value_for_field(field: dataclasses.Field) -> Any:
"""Get the default value for a field.
Args:
field: The field.
Returns:
The default value.
Raises:
ValueError: If no default value is found.
"""
if field.default != dataclasses.MISSING:
return field.default
elif field.default_factory != dataclasses.MISSING:
return field.default_factory()
else:
raise ValueError(
f"Missing value for environment variable {field.name} and no default value found"
)
# TODO: Change all interpret_.* signatures to value: str, field: dataclasses.Field once we migrate rx.Config to dataclasses
def interpret_boolean_env(value: str, field_name: str) -> bool:
"""Interpret a boolean environment variable value.
@ -314,26 +295,47 @@ def interpret_env_var_value(
T = TypeVar("T")
ENV_VAR_DEFAULT_FACTORY = Callable[[], T]
class EnvVar(Generic[T]):
"""Environment variable."""
name: str
default: Any
default_factory: Optional[ENV_VAR_DEFAULT_FACTORY]
type_: T
def __init__(self, name: str, default: Any, type_: T) -> None:
def __init__(
self,
name: str,
default: Any,
default_factory: Optional[ENV_VAR_DEFAULT_FACTORY],
type_: T,
) -> None:
"""Initialize the environment variable.
Args:
name: The environment variable name.
default: The default value.
default_factory: The default factory.
type_: The type of the value.
"""
self.name = name
self.default = default
self.default_factory = default_factory
self.type_ = type_
def get_default(self) -> T:
"""Get the default value.
Returns:
The default value.
"""
if self.default_factory is not None:
return self.default_factory()
return self.default
def interpret(self, value: str) -> T:
"""Interpret the environment variable value.
@ -373,7 +375,7 @@ class EnvVar(Generic[T]):
env_value = self.getenv()
if env_value is not None:
return env_value
return self.default
return self.get_default()
def set(self, value: T | None) -> None:
"""Set the environment variable. None unsets the variable.
@ -394,16 +396,24 @@ class env_var: # type: ignore
name: str
default: Any
default_factory: Optional[ENV_VAR_DEFAULT_FACTORY]
internal: bool = False
def __init__(self, default: Any, internal: bool = False) -> None:
def __init__(
self,
default: Any = None,
default_factory: Optional[ENV_VAR_DEFAULT_FACTORY] = None,
internal: bool = False,
) -> None:
"""Initialize the descriptor.
Args:
default: The default value.
default_factory: The default factory.
internal: Whether the environment variable is reflex internal.
"""
self.default = default
self.default_factory = default_factory
self.internal = internal
def __set_name__(self, owner, name):
@ -429,22 +439,30 @@ class env_var: # type: ignore
env_name = self.name
if self.internal:
env_name = f"__{env_name}"
return EnvVar(name=env_name, default=self.default, type_=type_)
return EnvVar(
name=env_name,
default=self.default,
type_=type_,
default_factory=self.default_factory,
)
if TYPE_CHECKING:
if TYPE_CHECKING:
def __new__(
cls,
default: Optional[T] = None,
default_factory: Optional[ENV_VAR_DEFAULT_FACTORY[T]] = None,
internal: bool = False,
) -> EnvVar[T]:
"""Create a new EnvVar instance.
def env_var(default, internal=False) -> EnvVar:
"""Typing helper for the env_var descriptor.
Args:
default: The default value.
internal: Whether the environment variable is reflex internal.
Returns:
The EnvVar instance.
"""
return default
Args:
cls: The class.
default: The default value.
default_factory: The default factory.
internal: Whether the environment variable is reflex internal.
"""
...
class PathExistsFlag:
@ -465,6 +483,16 @@ class PerformanceMode(enum.Enum):
class EnvironmentVariables:
"""Environment variables class to instantiate environment variables."""
def __init__(self):
"""Initialize the environment variables.
Raises:
NotImplementedError: Always.
"""
raise NotImplementedError(
f"{type(self).__name__} is a class singleton and not meant to be instantiated."
)
# Whether to use npm over bun to install frontend packages.
REFLEX_USE_NPM: EnvVar[bool] = env_var(False)
@ -564,8 +592,13 @@ class EnvironmentVariables:
# The maximum size of the reflex state in kilobytes.
REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000)
# Whether to minify state names. Default to true in prod mode and false otherwise.
REFLEX_MINIFY_STATES: EnvVar[Optional[bool]] = env_var(
default_factory=lambda: environment.REFLEX_ENV_MODE.get() == constants.Env.PROD
)
environment = EnvironmentVariables()
environment = EnvironmentVariables
class Config(Base):
@ -870,3 +903,28 @@ def get_config(reload: bool = False) -> Config:
# Restore the original sys.path.
sys.path.clear()
sys.path.extend(sys_path)
@lru_cache
def get_config_safe() -> Config:
"""Get the app config without introducing import side-effects.
Returns:
The app config.
"""
with (
tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as script,
tempfile.NamedTemporaryFile() as config_json,
):
script.write(f"""
from pathlib import Path
from reflex.config import get_config
Path({config_json.name!r}).write_text(get_config().json())
""")
script.flush()
subprocess.run(
[sys.executable, script.name],
)
return Config.parse_file(config_json.name)

View File

@ -61,18 +61,6 @@ class CompileVars(SimpleNamespace):
CONNECT_ERROR = "connectErrors"
# The name of the function for converting a dict to an event.
TO_EVENT = "Event"
# The name of the internal on_load event.
ON_LOAD_INTERNAL = "reflex___state____on_load_internal_state.on_load_internal"
# The name of the internal event to update generic state vars.
UPDATE_VARS_INTERNAL = (
"reflex___state____update_vars_internal_state.update_vars_internal"
)
# The name of the frontend event exception state
FRONTEND_EXCEPTION_STATE = "reflex___state____frontend_event_exception_state"
# The full name of the frontend exception state
FRONTEND_EXCEPTION_STATE_FULL = (
f"reflex___state____state.{FRONTEND_EXCEPTION_STATE}"
)
class PageNames(SimpleNamespace):

View File

@ -17,11 +17,11 @@ import typer
from tomlkit.exceptions import TOMLKitError
from reflex import constants
from reflex.config import environment, get_config
from reflex.config import environment, get_config_safe
from reflex.constants import CustomComponents
from reflex.utils import console
config = get_config()
config = get_config_safe()
custom_components_cli = typer.Typer()
POST_CUSTOM_COMPONENTS_GALLERY_ENDPOINT = (
@ -65,7 +65,7 @@ def _create_package_config(module_name: str, package_name: str):
pyproject = Path(CustomComponents.PYPROJECT_TOML)
pyproject.write_text(
templates.CUSTOM_COMPONENTS_PYPROJECT_TOML.render(
templates.custom_components_pyproject_toml().render(
module_name=module_name,
package_name=package_name,
reflex_version=constants.Reflex.VERSION,
@ -106,7 +106,7 @@ def _create_readme(module_name: str, package_name: str):
readme = Path(CustomComponents.PACKAGE_README)
readme.write_text(
templates.CUSTOM_COMPONENTS_README.render(
templates.custom_components_readme().render(
module_name=module_name,
package_name=package_name,
)
@ -129,14 +129,14 @@ def _write_source_and_init_py(
module_path = custom_component_src_dir / f"{module_name}.py"
module_path.write_text(
templates.CUSTOM_COMPONENTS_SOURCE.render(
templates.custom_components_source().render(
component_class_name=component_class_name, module_name=module_name
)
)
init_path = custom_component_src_dir / CustomComponents.INIT_FILE
init_path.write_text(
templates.CUSTOM_COMPONENTS_INIT_FILE.render(module_name=module_name)
templates.custom_components_init.render(module_name=module_name)
)
@ -164,7 +164,7 @@ def _populate_demo_app(name_variants: NameVariants):
# This source file is rendered using jinja template file.
with open(f"{demo_app_name}/{demo_app_name}.py", "w") as f:
f.write(
templates.CUSTOM_COMPONENTS_DEMO_APP.render(
templates.custom_components_demo_app().render(
custom_component_module_dir=name_variants.custom_component_module_dir,
module_name=name_variants.module_name,
)

View File

@ -2,6 +2,7 @@
from __future__ import annotations
# WARNING: do not import any modules that contain rx.State subclasses here
import atexit
import os
from pathlib import Path
@ -14,9 +15,8 @@ from reflex_cli.utils import dependency
from reflex_cli.v2.deployments import check_version, hosting_cli
from reflex import constants
from reflex.config import environment, get_config
from reflex.config import environment, get_config, get_config_safe
from reflex.custom_components.custom_components import custom_components_cli
from reflex.state import reset_disk_state_manager
from reflex.utils import console, telemetry
# Disable typer+rich integration for help panels
@ -29,8 +29,8 @@ except TypeError:
# Fallback for older typer versions.
cli = typer.Typer(add_completion=False)
# Get the config.
config = get_config()
# Get the config via subprocess without triggering import side-effects.
config = get_config_safe()
def version(value: bool):
@ -134,14 +134,16 @@ def _run(
loglevel: constants.LogLevel = config.loglevel,
):
"""Run the app in the given directory."""
# Set env mode in the environment
# This must be set before importing modules that contain rx.State subclasses
environment.REFLEX_ENV_MODE.set(env)
from reflex.state import reset_disk_state_manager
from reflex.utils import build, exec, prerequisites, processes
# Set the log level.
console.set_log_level(loglevel)
# Set env mode in the environment
environment.REFLEX_ENV_MODE.set(env)
# Show system info
exec.output_system_info()
@ -314,6 +316,10 @@ def export(
),
):
"""Export the app to a zip file."""
# Set env mode in the environment
# This must be set before importing modules that contain rx.State subclasses
environment.REFLEX_ENV_MODE.set(constants.Env.PROD)
from reflex.utils import export as export_utils
from reflex.utils import prerequisites
@ -660,6 +666,10 @@ def deployv2(
),
):
"""Deploy the app to the Reflex hosting service."""
# Set env mode in the environment
# This must be set before importing modules that contain rx.State subclasses
environment.REFLEX_ENV_MODE.set(constants.Env.PROD)
from reflex_cli.v2 import cli as hosting_cli
from reflex_cli.v2.utils import dependency

View File

@ -301,6 +301,61 @@ def get_var_for_field(cls: Type[BaseState], f: ModelField):
)
# Keep track of all state instances to calculate minified state names
state_count: int = 0
minified_state_names: Dict[str, str] = {}
def next_minified_state_name() -> str:
"""Get the next minified state name.
Returns:
The next minified state name.
"""
global state_count
num = state_count
# All possible chars for minified state name
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_"
base = len(chars)
state_name = ""
if num == 0:
state_name = chars[0]
while num > 0:
state_name = chars[num % base] + state_name
num = num // base
state_count += 1
return state_name
def get_minified_state_name(state_name: str) -> str:
"""Generate a minified state name.
Args:
state_name: The state name to minify.
Returns:
The minified state name.
Raises:
ValueError: If no more minified state names are available
"""
if state_name in minified_state_names:
return minified_state_names[state_name]
while name := next_minified_state_name():
if name in minified_state_names.values():
continue
minified_state_names[state_name] = name
return name
raise ValueError("No more minified state names available")
class BaseState(Base, ABC, extra=pydantic.Extra.allow):
"""The state of the app."""
@ -896,7 +951,10 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
The name of the state.
"""
module = cls.__module__.replace(".", "___")
return format.to_snake_case(f"{module}___{cls.__name__}")
state_name = format.to_snake_case(f"{module}___{cls.__name__}")
if environment.REFLEX_MINIFY_STATES.get():
return get_minified_state_name(state_name)
return state_name
@classmethod
@functools.lru_cache()

View File

@ -46,12 +46,15 @@ import reflex.utils.processes
from reflex.config import environment
from reflex.state import (
BaseState,
State,
StateManager,
StateManagerDisk,
StateManagerMemory,
StateManagerRedis,
minified_state_names,
reload_state_module,
)
from reflex.utils.types import override
try:
from selenium import webdriver # pyright: ignore [reportMissingImports]
@ -141,7 +144,7 @@ class AppHarness:
Callable[[], None] | types.ModuleType | str | functools.partial[Any]
] = None,
app_name: Optional[str] = None,
) -> "AppHarness":
) -> AppHarness:
"""Create an AppHarness instance at root.
Args:
@ -192,9 +195,12 @@ class AppHarness:
Returns:
The state name
"""
return reflex.utils.format.to_snake_case(
state_name = reflex.utils.format.to_snake_case(
f"{self.app_name}___{self.app_name}___" + state_cls_name
)
if environment.REFLEX_MINIFY_STATES.get():
return minified_state_names.get(state_name, state_name)
return state_name
def get_full_state_name(self, path: List[str]) -> str:
"""Get the full state name for the given state class name.
@ -207,7 +213,7 @@ class AppHarness:
"""
# NOTE: using State.get_name() somehow causes trouble here
# path = [State.get_name()] + [self.get_state_name(p) for p in path]
path = ["reflex___state____state"] + [self.get_state_name(p) for p in path]
path = [State.get_name()] + [self.get_state_name(p) for p in path]
return ".".join(path)
def _get_globals_from_signature(self, func: Any) -> dict[str, Any]:
@ -412,7 +418,7 @@ class AppHarness:
self.frontend_output_thread = threading.Thread(target=consume_frontend_output)
self.frontend_output_thread.start()
def start(self) -> "AppHarness":
def start(self) -> AppHarness:
"""Start the backend in a new thread and dev frontend as a separate process.
Returns:
@ -442,7 +448,7 @@ class AppHarness:
return f"{key} = {value!r}"
return inspect.getsource(value)
def __enter__(self) -> "AppHarness":
def __enter__(self) -> AppHarness:
"""Contextmanager protocol for `start()`.
Returns:
@ -921,6 +927,7 @@ class AppHarnessProd(AppHarness):
)
self.frontend_server.serve_forever()
@override
def _start_frontend(self):
# Set up the frontend.
with chdir(self.app_path):
@ -932,17 +939,19 @@ class AppHarnessProd(AppHarness):
zipping=False,
frontend=True,
backend=False,
loglevel=reflex.constants.LogLevel.INFO,
loglevel=reflex.constants.base.LogLevel.INFO,
)
self.frontend_thread = threading.Thread(target=self._run_frontend)
self.frontend_thread.start()
@override
def _wait_frontend(self):
self._poll_for(lambda: self.frontend_server is not None)
_ = self._poll_for(lambda: self.frontend_server is not None)
if self.frontend_server is None or not self.frontend_server.socket.fileno():
raise RuntimeError("Frontend did not start")
@override
def _start_backend(self):
if self.app_instance is None:
raise RuntimeError("App was not initialized.")
@ -959,12 +968,14 @@ class AppHarnessProd(AppHarness):
self.backend_thread = threading.Thread(target=self.backend.run)
self.backend_thread.start()
@override
def _poll_for_servers(self, timeout: TimeoutType = None) -> socket.socket:
try:
return super()._poll_for_servers(timeout)
finally:
environment.REFLEX_SKIP_COMPILE.set(None)
@override
def stop(self):
"""Stop the frontend python webserver."""
super().stop()

View File

@ -44,7 +44,7 @@ def generate_sitemap_config(deploy_url: str, export=False):
config = json.dumps(config)
sitemap = prerequisites.get_web_dir() / constants.Next.SITEMAP_CONFIG_FILE
sitemap.write_text(templates.SITEMAP_CONFIG(config=config))
sitemap.write_text(templates.sitemap_config()(config=config))
def _zip(

View File

@ -441,7 +441,7 @@ def create_config(app_name: str):
config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config"
with open(constants.Config.FILE, "w") as f:
console.debug(f"Creating {constants.Config.FILE}")
f.write(templates.RXCONFIG.render(app_name=app_name, config_name=config_name))
f.write(templates.rxconfig().render(app_name=app_name, config_name=config_name))
def initialize_gitignore(
@ -611,7 +611,7 @@ def initialize_web_directory():
def _compile_package_json():
return templates.PACKAGE_JSON.render(
return templates.package_json().render(
scripts={
"dev": constants.PackageJson.Commands.DEV,
"export": constants.PackageJson.Commands.EXPORT,

View File

@ -3,10 +3,13 @@
import os
import re
from pathlib import Path
from typing import Generator, Type
import pytest
import reflex.constants
from reflex.config import environment
from reflex.constants.base import Env
from reflex.testing import AppHarness, AppHarnessProd
DISPLAY = None
@ -64,15 +67,30 @@ def pytest_exception_interact(node, call, report):
@pytest.fixture(
scope="session", params=[AppHarness, AppHarnessProd], ids=["dev", "prod"]
scope="session",
params=[
AppHarness,
AppHarnessProd,
],
ids=[
reflex.constants.Env.DEV.value,
reflex.constants.Env.PROD.value,
],
)
def app_harness_env(request):
def app_harness_env(
request: pytest.FixtureRequest,
) -> Generator[Type[AppHarness], None, None]:
"""Parametrize the AppHarness class to use for the test, either dev or prod.
Args:
request: The pytest fixture request object.
Returns:
Yields:
The AppHarness class to use for the test.
"""
return request.param
harness: Type[AppHarness] = request.param
if issubclass(harness, AppHarnessProd):
environment.REFLEX_ENV_MODE.set(Env.PROD)
yield harness
if isinstance(harness, AppHarnessProd):
environment.REFLEX_ENV_MODE.set(None)

View File

@ -106,7 +106,6 @@ def ComputedVars():
),
)
# raise Exception(State.count3._deps(objclass=State))
app = rx.App()
app.add_page(index)

View File

@ -0,0 +1,161 @@
"""Integration tests for minified state names."""
from __future__ import annotations
from functools import partial
from typing import Generator, Optional, Type
import pytest
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from reflex.config import environment
from reflex.testing import AppHarness, AppHarnessProd
def MinifiedStatesApp(minify: bool | None) -> None:
"""A test app for minified state names.
Args:
minify: whether to minify state names
"""
import reflex as rx
class MinifiedState(rx.State):
"""State for the MinifiedStatesApp app."""
pass
app = rx.App()
def index():
return rx.vstack(
rx.input(
value=MinifiedState.router.session.client_token,
is_read_only=True,
id="token",
),
rx.text(f"minify: {minify}", id="minify"),
rx.text(MinifiedState.get_name(), id="state_name"),
rx.text(MinifiedState.get_full_name(), id="state_full_name"),
)
app.add_page(index)
@pytest.fixture(
params=[
pytest.param(False),
pytest.param(True),
pytest.param(None),
],
)
def minify_state_env(
request: pytest.FixtureRequest,
) -> Generator[Optional[bool], None, None]:
"""Set the environment variable to minify state names.
Args:
request: pytest fixture request
Yields:
minify_states: whether to minify state names
"""
minify_states: Optional[bool] = request.param
environment.REFLEX_MINIFY_STATES.set(minify_states)
yield minify_states
environment.REFLEX_MINIFY_STATES.set(None)
@pytest.fixture
def test_app(
app_harness_env: Type[AppHarness],
tmp_path_factory: pytest.TempPathFactory,
minify_state_env: Optional[bool],
) -> Generator[AppHarness, None, None]:
"""Start MinifiedStatesApp app at tmp_path via AppHarness.
Args:
app_harness_env: either AppHarness (dev) or AppHarnessProd (prod)
tmp_path_factory: pytest tmp_path_factory fixture
minify_state_env: need to request this fixture to set env before the app starts
Yields:
running AppHarness instance
"""
name = f"testminifiedstates_{app_harness_env.__name__.lower()}"
with app_harness_env.create(
root=tmp_path_factory.mktemp(name),
app_name=name,
app_source=partial(MinifiedStatesApp, minify=minify_state_env), # type: ignore
) as harness:
yield harness
@pytest.fixture
def driver(test_app: AppHarness) -> Generator[WebDriver, None, None]:
"""Get an instance of the browser open to the test_app app.
Args:
test_app: harness for MinifiedStatesApp app
Yields:
WebDriver instance.
"""
assert test_app.app_instance is not None, "app is not running"
driver = test_app.frontend()
try:
yield driver
finally:
driver.quit()
def test_minified_states(
test_app: AppHarness,
driver: WebDriver,
minify_state_env: Optional[bool],
) -> None:
"""Test minified state names.
Args:
test_app: harness for MinifiedStatesApp
driver: WebDriver instance.
minify_state_env: whether state minification is enabled by env var.
"""
assert test_app.app_instance is not None, "app is not running"
is_prod = isinstance(test_app, AppHarnessProd)
# default to minifying in production
should_minify: bool = is_prod
# env overrides default
if minify_state_env is not None:
should_minify = minify_state_env
# get a reference to the connected client
token_input = driver.find_element(By.ID, "token")
assert token_input
# wait for the backend connection to send the token
token = test_app.poll_for_value(token_input)
assert token
state_name_text = driver.find_element(By.ID, "state_name")
assert state_name_text
state_name = state_name_text.text
state_full_name_text = driver.find_element(By.ID, "state_full_name")
assert state_full_name_text
_ = state_full_name_text.text
assert test_app.app_module
module_state_prefix = test_app.app_module.__name__.replace(".", "___")
if should_minify:
assert len(state_name) == 1
else:
assert state_name == f"{module_state_prefix}____minified_state"

View File

@ -0,0 +1,14 @@
from typing import Set
from reflex.state import next_minified_state_name
def test_next_minified_state_name():
"""Test that the next_minified_state_name function returns unique state names."""
state_names: Set[str] = set()
gen = 10000
for _ in range(gen):
state_name = next_minified_state_name()
assert state_name not in state_names
state_names.add(state_name)
assert len(state_names) == gen

View File

@ -1032,7 +1032,7 @@ async def test_dynamic_route_var_route_change_completed_on_load(
prev_exp_val = ""
for exp_index, exp_val in enumerate(exp_vals):
on_load_internal = _event(
name=f"{state.get_full_name()}.{constants.CompileVars.ON_LOAD_INTERNAL.rpartition('.')[2]}",
name=f"{state.get_full_name()}.on_load_internal",
val=exp_val,
)
exp_router_data = {

View File

@ -253,6 +253,11 @@ def test_env_var():
INTERNAL: EnvVar[str] = env_var("default", internal=True)
BOOLEAN: EnvVar[bool] = env_var(False)
# default_factory with other env_var as fallback
BLUBB_OR_BLA: EnvVar[str] = env_var(
default_factory=lambda: TestEnv.BLUBB.getenv() or "bla"
)
assert TestEnv.BLUBB.get() == "default"
assert TestEnv.BLUBB.name == "BLUBB"
TestEnv.BLUBB.set("new")
@ -280,3 +285,15 @@ def test_env_var():
assert TestEnv.BOOLEAN.get() is False
TestEnv.BOOLEAN.set(None)
assert "BOOLEAN" not in os.environ
assert TestEnv.BLUBB_OR_BLA.get() == "bla"
TestEnv.BLUBB.set("new")
assert TestEnv.BLUBB_OR_BLA.get() == "new"
TestEnv.BLUBB.set(None)
assert TestEnv.BLUBB_OR_BLA.get() == "bla"
TestEnv.BLUBB_OR_BLA.set("test")
assert TestEnv.BLUBB_OR_BLA.get() == "test"
TestEnv.BLUBB.set("other")
assert TestEnv.BLUBB_OR_BLA.get() == "test"
TestEnv.BLUBB_OR_BLA.set(None)
TestEnv.BLUBB.set(None)

View File

@ -66,6 +66,7 @@ CI = bool(os.environ.get("CI", False))
LOCK_EXPIRATION = 2000 if CI else 300
LOCK_EXPIRE_SLEEP = 2.5 if CI else 0.4
ON_LOAD_INTERNAL = f"{OnLoadInternalState.get_name()}.on_load_internal"
formatted_router = {
"session": {"client_token": "", "client_ip": "", "session_id": ""},
@ -2818,7 +2819,7 @@ async def test_preprocess(app_module_mock, token, test_state, expected, mocker):
app=app,
event=Event(
token=token,
name=f"{state.get_name()}.{CompileVars.ON_LOAD_INTERNAL}",
name=f"{state.get_name()}.{ON_LOAD_INTERNAL}",
router_data={RouteVar.PATH: "/", RouteVar.ORIGIN: "/", RouteVar.QUERY: {}},
),
sid="sid",
@ -2865,7 +2866,7 @@ async def test_preprocess_multiple_load_events(app_module_mock, token, mocker):
app=app,
event=Event(
token=token,
name=f"{state.get_full_name()}.{CompileVars.ON_LOAD_INTERNAL}",
name=f"{state.get_full_name()}.{ON_LOAD_INTERNAL}",
router_data={RouteVar.PATH: "/", RouteVar.ORIGIN: "/", RouteVar.QUERY: {}},
),
sid="sid",

View File

@ -271,7 +271,7 @@ def test_unsupported_literals(cls: type):
],
)
def test_create_config(app_name, expected_config_name, mocker):
"""Test templates.RXCONFIG is formatted with correct app name and config class name.
"""Test templates.rxconfig is formatted with correct app name and config class name.
Args:
app_name: App name.
@ -279,9 +279,9 @@ def test_create_config(app_name, expected_config_name, mocker):
mocker: Mocker object.
"""
mocker.patch("builtins.open")
tmpl_mock = mocker.patch("reflex.compiler.templates.RXCONFIG")
tmpl_mock = mocker.patch("reflex.compiler.templates.rxconfig")
prerequisites.create_config(app_name)
tmpl_mock.render.assert_called_with(
tmpl_mock().render.assert_called_with(
app_name=app_name, config_name=expected_config_name
)
@ -592,8 +592,23 @@ def test_style_prop_with_event_handler_value(callable):
)
def test_is_prod_mode() -> None:
"""Test that the prod mode is correctly determined."""
@pytest.fixture
def cleanup_reflex_env_mode():
"""Cleanup the reflex env mode.
Yields:
None
"""
yield
environment.REFLEX_ENV_MODE.set(None)
def test_is_prod_mode(cleanup_reflex_env_mode: None) -> None:
"""Test that the prod mode is correctly determined.
Args:
cleanup_reflex_env_mode: Fixture to cleanup the reflex env mode.
"""
environment.REFLEX_ENV_MODE.set(constants.Env.PROD)
assert utils_exec.is_prod_mode()
environment.REFLEX_ENV_MODE.set(None)