-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Minify state names #3728
base: main
Are you sure you want to change the base?
Minify state names #3728
Conversation
Actually, it's not a problem with the env vars not getting passed. Tests are failing due to |
b9c852e
to
bf6ec96
Compare
@masenf do you have any idea how to solve this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as for the _var_set_state
, we probably could just save a reference to the state class and defer formatting of the state name until it's actually needed (_var_full_name_needs_state_prefix)
Thanks, i was thinking about that. Do you want me to migrate to state refs in this PR or in a followup? |
i think you can do the refs in this PR, as long as it's not too invasive. We have a big Var refactor coming in the next week or so, that might conflict, but we can fix that up |
Merged |
Thank you very much @masenf! Regarding your commit msg "test_minified_states: remove skip -- things seem to be working as-is" - that might be due to the var refactor from @adhami3310 especially I think this should be ready for review ✔️ |
Do we still want to get this in? I will rebase it if there is intention to review it |
yes, we want to get it 🤞 thanks @benedikt-bartscher |
Marking this as Draft until the conflict with main are solved. |
Thanks for your work on this. |
5e7c38c
to
7652fa1
Compare
@masenf thanks for testing it again
|
* 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
* Add template name to reflex init success msg * fix pyright message
@masenf I fixed the ci |
Fixed conflicts and ci failure due to recent reflex-web changes as well |
Note to myself: consider porting EnvVar default_factory support to a separate PR for #4430 |
If there is smth missing here please let me know |
Just a friendly remainder, i am still interested in getting this one in. Please let me know if anything is missing or not working properly. I could also split it up into multiple PRs if wanted |
I'm testing this with
run a caddy webserver over
The first thing I noticed is that If I set So I tried to update the code to set |
okay, i probed a little further... and it turns out that |
i tried out this patch diff --git a/reflex/config.py b/reflex/config.py
index a882ce9d2..29ab353d9 100644
--- a/reflex/config.py
+++ b/reflex/config.py
@@ -3,10 +3,13 @@
from __future__ import annotations
import enum
+from functools import lru_cache
import importlib
import inspect
import os
+import subprocess
import sys
+import tempfile
import threading
import urllib.parse
from importlib.util import find_spec
@@ -900,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)
diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py
index 1406e37bc..92ee1a576 100644
--- a/reflex/custom_components/custom_components.py
+++ b/reflex/custom_components/custom_components.py
@@ -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 = (
diff --git a/reflex/reflex.py b/reflex/reflex.py
index 7b74071f3..3b5c561a5 100644
--- a/reflex/reflex.py
+++ b/reflex/reflex.py
@@ -15,7 +15,7 @@ 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.utils import console, telemetry
@@ -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): And while it does work for avoiding import side-effects before we have a chance to set the I don't think we can take that much of a slowdown on startup, there must be another way to implement this feature. Particularly, we need something that is more deterministic and doesn't depend on the import order, because I'm sure some innocuous looking change somewhere will regress this current approach in the future, and it doesn't seem like our current tests are able to effectively catch that. We need an approach where the minified name is not tied so strongly to the initialization of the state class but can be turned on before compilation and still work. I think it would also be nice if either regular or minified name could be used against the same backend. |
i tried adding code i tried diff --git a/reflex/reflex.py b/reflex/reflex.py
index 7b74071f3..1e4d57f28 100644
--- a/reflex/reflex.py
+++ b/reflex/reflex.py
@@ -18,6 +18,7 @@ from reflex import constants
from reflex.config import environment, get_config
from reflex.custom_components.custom_components import custom_components_cli
from reflex.utils import console, telemetry
+from reflex.utils.exec import set_env_mode
# Disable typer+rich integration for help panels
typer.core.rich = None # type: ignore
@@ -136,7 +137,7 @@ def _run(
"""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)
+ set_env_mode(env)
from reflex.state import reset_disk_state_manager
from reflex.utils import build, exec, prerequisites, processes
@@ -318,7 +319,7 @@ 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)
+ set_env_mode(constants.Env.PROD)
from reflex.utils import export as export_utils
from reflex.utils import prerequisites
@@ -668,7 +669,7 @@ 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)
+ set_env_mode(constants.Env.PROD)
from reflex_cli.v2 import cli as hosting_cli
from reflex_cli.v2.utils import dependency
diff --git a/reflex/state.py b/reflex/state.py
index c1e423f9e..c25ff745f 100644
--- a/reflex/state.py
+++ b/reflex/state.py
@@ -554,18 +554,23 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if parent_state is not None:
cls.inherited_vars = parent_state.vars
cls.inherited_backend_vars = parent_state.backend_vars
+ else:
+ # Track class_subclasses of the BaseState
+ parent_state = BaseState
- # Check if another substate class with the same name has already been defined.
- if cls.get_name() in set(
- c.get_name() for c in parent_state.class_subclasses
- ):
- # This should not happen, since we have added module prefix to state names in #3214
- raise StateValueError(
- f"The substate class '{cls.get_name()}' has been defined multiple times. "
- "Shadowing substate classes is not allowed."
- )
- # Track this new subclass in the parent state's subclasses set.
- parent_state.class_subclasses.add(cls)
+ # Check if another substate class with the same name has already been defined.
+ #if not _reflex_internal_reinit and cls.get_name() in set(
+ if cls.get_name() in set(
+ c.get_name() for c in parent_state.class_subclasses
+ ):
+ breakpoint()
+ # This should not happen, since we have added module prefix to state names in #3214
+ raise StateValueError(
+ f"The substate class '{cls.get_name()}' has been defined multiple times. "
+ "Shadowing substate classes is not allowed."
+ )
+ # Track this new subclass in the parent state's subclasses set.
+ parent_state.class_subclasses.add(cls)
# Get computed vars.
computed_vars = cls._get_computed_vars()
@@ -970,6 +975,21 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
name = ".".join((parent_state.get_full_name(), name))
return name
+ @classmethod
+ def _reset_name_cache(cls):
+ """Reset `get_name` and `get_full_name` cache to use minified state names."""
+ subclasses = sorted(cls.class_subclasses, key=lambda x: x.__name__)
+ cls.get_name.cache_clear()
+ cls.get_full_name.cache_clear()
+ if cls is BaseState:
+ cls.class_subclasses.clear()
+ else:
+ cls.__init_subclass__()
+ print(f"REINIT {cls.__name__} ({cls.get_full_name()})")
+ print(minified_state_names)
+ for subcls in subclasses:
+ subcls._reset_name_cache()
+
@classmethod
@functools.lru_cache()
def get_class_substate(cls, path: Sequence[str] | str) -> Type[BaseState]:
@@ -1658,6 +1678,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
raise ValueError(
"The value of state cannot be None when processing an event."
)
+ if name not in substate.event_handlers:
+ breakpoint()
handler = substate.event_handlers[name]
# For background tasks, proxy the state
diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py
index 5291de095..56c16f712 100644
--- a/reflex/utils/exec.py
+++ b/reflex/utils/exec.py
@@ -497,6 +497,19 @@ def is_prod_mode() -> bool:
return current_mode == constants.Env.PROD
+def set_env_mode(mode: constants.Env):
+ """Mark the app as running in dev or prod mode.
+
+ Args:
+ mode: The mode to set the app to.
+ """
+ environment.REFLEX_ENV_MODE.set(mode)
+ from reflex.state import BaseState
+
+ # Ensure that minified names are or are not used based on the mode.
+ BaseState._reset_name_cache()
+
+
def is_frontend_only() -> bool:
"""Check if the app is running in frontend-only mode. I think a proper solution for minifying the state names needs a more deterministic approach. Either
Basically the following needs to work for reflex export --frontend-only --no-zip
python3.12 -m http.server -d .web/_static 3000 &
reflex run --env prod --backend-only So unfortunately this PR won't make it into our last release of 2024, but we do still want it, and think it's helpful for reducing network payload. |
Thanks for all the details and ideas @masenf! I will work through your comments in the next days and try to find a better approach. Will keep you posted.
Absolutely no problem, we obviously need to properly address all issues first. |
Alternative to #3701