Browse Source

implement default_factory for EnvVar, improve env_var typing
also migrate environment to be a class singleton to prevent unintended chaos with default factories

Benedikt Bartscher 7 months ago
parent
commit
d32604713c

+ 11 - 5
reflex/app.py

@@ -67,7 +67,7 @@ from reflex.components.core.client_side_routing import (
 )
 from reflex.components.core.upload import Upload, get_upload_dir
 from reflex.components.radix import themes
-from reflex.config import environment, get_config
+from reflex.config import EnvironmentVariables, get_config
 from reflex.event import (
     Event,
     EventHandler,
@@ -506,7 +506,10 @@ class App(MiddlewareMixin, LifespanMixin):
         # Check if the route given is valid
         verify_route_validity(route)
 
-        if route in self.unevaluated_pages and environment.RELOAD_CONFIG.is_set():
+        if (
+            route in self.unevaluated_pages
+            and EnvironmentVariables.RELOAD_CONFIG.is_set()
+        ):
             # when the app is reloaded(typically for app harness tests), we should maintain
             # the latest render function of a route.This applies typically to decorated pages
             # since they are only added when app._compile is called.
@@ -723,7 +726,7 @@ class App(MiddlewareMixin, LifespanMixin):
             Whether the app should be compiled.
         """
         # Check the environment variable.
-        if environment.REFLEX_SKIP_COMPILE.get():
+        if EnvironmentVariables.REFLEX_SKIP_COMPILE.get():
             return False
 
         nocompile = prerequisites.get_web_dir() / constants.NOCOMPILE_FILE
@@ -946,7 +949,10 @@ class App(MiddlewareMixin, LifespanMixin):
         executor = None
         if (
             platform.system() in ("Linux", "Darwin")
-            and (number_of_processes := environment.REFLEX_COMPILE_PROCESSES.get())
+            and (
+                number_of_processes
+                := EnvironmentVariables.REFLEX_COMPILE_PROCESSES.get()
+            )
             is not None
         ):
             executor = concurrent.futures.ProcessPoolExecutor(
@@ -955,7 +961,7 @@ class App(MiddlewareMixin, LifespanMixin):
             )
         else:
             executor = concurrent.futures.ThreadPoolExecutor(
-                max_workers=environment.REFLEX_COMPILE_THREADS.get()
+                max_workers=EnvironmentVariables.REFLEX_COMPILE_THREADS.get()
             )
 
         for route, component in zip(self.pages, page_components):

+ 2 - 2
reflex/compiler/compiler.py

@@ -16,7 +16,7 @@ from reflex.components.component import (
     CustomComponent,
     StatefulComponent,
 )
-from reflex.config import environment, get_config
+from reflex.config import EnvironmentVariables, get_config
 from reflex.state import BaseState
 from reflex.style import SYSTEM_COLOR_MODE
 from reflex.utils.exec import is_prod_mode
@@ -527,7 +527,7 @@ def remove_tailwind_from_postcss() -> tuple[str, str]:
 
 def purge_web_pages_dir():
     """Empty out .web/pages directory."""
-    if not is_prod_mode() and environment.REFLEX_PERSIST_WEB_DIR.get():
+    if not is_prod_mode() and EnvironmentVariables.REFLEX_PERSIST_WEB_DIR.get():
         # Skip purging the web directory in dev mode if REFLEX_PERSIST_WEB_DIR is set.
         return
 

+ 2 - 2
reflex/components/core/upload.py

@@ -13,7 +13,7 @@ from reflex.components.component import (
 )
 from reflex.components.el.elements.forms import Input
 from reflex.components.radix.themes.layout.box import Box
-from reflex.config import environment
+from reflex.config import EnvironmentVariables
 from reflex.constants import Dirs
 from reflex.constants.compiler import Hooks, Imports
 from reflex.event import (
@@ -132,7 +132,7 @@ def get_upload_dir() -> Path:
     """
     Upload.is_used = True
 
-    uploaded_files_dir = environment.REFLEX_UPLOADED_FILES_DIR.get()
+    uploaded_files_dir = EnvironmentVariables.REFLEX_UPLOADED_FILES_DIR.get()
     uploaded_files_dir.mkdir(parents=True, exist_ok=True)
     return uploaded_files_dir
 

+ 71 - 21
reflex/config.py

@@ -13,6 +13,7 @@ from pathlib import Path
 from typing import (
     TYPE_CHECKING,
     Any,
+    Callable,
     Dict,
     Generic,
     List,
@@ -312,26 +313,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.
 
@@ -371,7 +393,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.
@@ -392,16 +414,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):
@@ -427,22 +457,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:
@@ -455,6 +493,16 @@ ExistingPath = Annotated[Path, PathExistsFlag]
 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)
 
@@ -545,11 +593,13 @@ class EnvironmentVariables:
     # Where to save screenshots when tests fail.
     SCREENSHOT_DIR: EnvVar[Optional[Path]] = env_var(None)
 
-    # Whether to minify state names.
-    REFLEX_MINIFY_STATES: EnvVar[Optional[bool]] = env_var(False)
-
-
-environment = EnvironmentVariables()
+    # 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: (
+            EnvironmentVariables.REFLEX_ENV_MODE.get() == constants.Env.PROD
+        )
+        or False
+    )
 
 
 class Config(Base):

+ 9 - 6
reflex/constants/base.py

@@ -109,10 +109,10 @@ class Templates(SimpleNamespace):
         Returns:
             The URL to redirect to reflex.build.
         """
-        from reflex.config import environment
+        from reflex.config import EnvironmentVariables
 
         return (
-            environment.REFLEX_BUILD_FRONTEND.get()
+            EnvironmentVariables.REFLEX_BUILD_FRONTEND.get()
             + "/gen?reflex_init_token={reflex_init_token}"
         )
 
@@ -124,9 +124,12 @@ class Templates(SimpleNamespace):
         Returns:
             The URL to poll waiting for the user to select a generation.
         """
-        from reflex.config import environment
+        from reflex.config import EnvironmentVariables
 
-        return environment.REFLEX_BUILD_BACKEND.get() + "/api/init/{reflex_init_token}"
+        return (
+            EnvironmentVariables.REFLEX_BUILD_BACKEND.get()
+            + "/api/init/{reflex_init_token}"
+        )
 
     @classproperty
     @classmethod
@@ -136,10 +139,10 @@ class Templates(SimpleNamespace):
         Returns:
             The URL to fetch the generation's reflex code.
         """
-        from reflex.config import environment
+        from reflex.config import EnvironmentVariables
 
         return (
-            environment.REFLEX_BUILD_BACKEND.get()
+            EnvironmentVariables.REFLEX_BUILD_BACKEND.get()
             + "/api/gen/{generation_hash}/refactored"
         )
 

+ 1 - 29
reflex/constants/compiler.py

@@ -5,7 +5,7 @@ from enum import Enum
 from types import SimpleNamespace
 
 from reflex.base import Base
-from reflex.constants import Dirs, Env
+from reflex.constants import Dirs
 from reflex.utils.imports import ImportVar
 
 # The prefix used to create setters for state vars.
@@ -14,9 +14,6 @@ SETTER_PREFIX = "set_"
 # The file used to specify no compilation.
 NOCOMPILE_FILE = "nocompile"
 
-# The env var to toggle minification of states.
-ENV_MINIFY_STATES = "REFLEX_MINIFY_STATES"
-
 
 class Ext(SimpleNamespace):
     """Extension used in Reflex."""
@@ -33,22 +30,6 @@ class Ext(SimpleNamespace):
     EXE = ".exe"
 
 
-def minify_states() -> bool:
-    """Whether to minify states.
-
-    Returns:
-        True if states should be minified.
-    """
-    from reflex.config import environment
-
-    env = environment.REFLEX_MINIFY_STATES.get()
-    if env is not None:
-        return env
-
-    # minify states in prod by default
-    return environment.REFLEX_ENV_MODE.get() == Env.PROD
-
-
 class CompileVars(SimpleNamespace):
     """The variables used during compilation."""
 
@@ -81,15 +62,6 @@ class CompileVars(SimpleNamespace):
     # The name of the function for converting a dict to an event.
     TO_EVENT = "Event"
 
-    @classmethod
-    def MINIFY_STATES(cls) -> bool:
-        """Whether to minify states.
-
-        Returns:
-            True if states should be minified.
-        """
-        return minify_states()
-
 
 class PageNames(SimpleNamespace):
     """The name of basic pages deployed in NextJS."""

+ 4 - 4
reflex/constants/installer.py

@@ -61,9 +61,9 @@ class Bun(SimpleNamespace):
         Returns:
             The directory to store the bun.
         """
-        from reflex.config import environment
+        from reflex.config import EnvironmentVariables
 
-        return environment.REFLEX_DIR.get() / "bun"
+        return EnvironmentVariables.REFLEX_DIR.get() / "bun"
 
     @classproperty
     @classmethod
@@ -98,9 +98,9 @@ class Fnm(SimpleNamespace):
         Returns:
             The directory to store fnm.
         """
-        from reflex.config import environment
+        from reflex.config import EnvironmentVariables
 
-        return environment.REFLEX_DIR.get() / "fnm"
+        return EnvironmentVariables.REFLEX_DIR.get() / "fnm"
 
     @classproperty
     @classmethod

+ 3 - 3
reflex/custom_components/custom_components.py

@@ -17,7 +17,7 @@ import typer
 from tomlkit.exceptions import TOMLKitError
 
 from reflex import constants
-from reflex.config import environment, get_config
+from reflex.config import EnvironmentVariables, get_config
 from reflex.constants import CustomComponents
 from reflex.utils import console
 
@@ -609,14 +609,14 @@ def publish(
         help="The API token to use for authentication on python package repository. If token is provided, no username/password should be provided at the same time",
     ),
     username: Optional[str] = typer.Option(
-        environment.TWINE_USERNAME.get(),
+        EnvironmentVariables.TWINE_USERNAME.get(),
         "-u",
         "--username",
         show_default="TWINE_USERNAME environment variable value if set",
         help="The username to use for authentication on python package repository. Username and password must both be provided.",
     ),
     password: Optional[str] = typer.Option(
-        environment.TWINE_PASSWORD.get(),
+        EnvironmentVariables.TWINE_PASSWORD.get(),
         "-p",
         "--password",
         show_default="TWINE_PASSWORD environment variable value if set",

+ 8 - 8
reflex/model.py

@@ -17,7 +17,7 @@ import sqlalchemy.exc
 import sqlalchemy.orm
 
 from reflex.base import Base
-from reflex.config import environment, get_config
+from reflex.config import EnvironmentVariables, get_config
 from reflex.utils import console
 from reflex.utils.compat import sqlmodel, sqlmodel_field_has_primary_key
 
@@ -38,12 +38,12 @@ def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine:
     url = url or conf.db_url
     if url is None:
         raise ValueError("No database url configured")
-    if not environment.ALEMBIC_CONFIG.get().exists():
+    if not EnvironmentVariables.ALEMBIC_CONFIG.get().exists():
         console.warn(
             "Database is not initialized, run [bold]reflex db init[/bold] first."
         )
     # Print the SQL queries if the log level is INFO or lower.
-    echo_db_query = environment.SQLALCHEMY_ECHO.get()
+    echo_db_query = EnvironmentVariables.SQLALCHEMY_ECHO.get()
     # Needed for the admin dash on sqlite.
     connect_args = {"check_same_thread": False} if url.startswith("sqlite") else {}
     return sqlmodel.create_engine(url, echo=echo_db_query, connect_args=connect_args)
@@ -231,7 +231,7 @@ class Model(Base, sqlmodel.SQLModel):  # pyright: ignore [reportGeneralTypeIssue
         Returns:
             tuple of (config, script_directory)
         """
-        config = alembic.config.Config(environment.ALEMBIC_CONFIG.get())
+        config = alembic.config.Config(EnvironmentVariables.ALEMBIC_CONFIG.get())
         return config, alembic.script.ScriptDirectory(
             config.get_main_option("script_location", default="version"),
         )
@@ -266,8 +266,8 @@ class Model(Base, sqlmodel.SQLModel):  # pyright: ignore [reportGeneralTypeIssue
     def alembic_init(cls):
         """Initialize alembic for the project."""
         alembic.command.init(
-            config=alembic.config.Config(environment.ALEMBIC_CONFIG.get()),
-            directory=str(environment.ALEMBIC_CONFIG.get().parent / "alembic"),
+            config=alembic.config.Config(EnvironmentVariables.ALEMBIC_CONFIG.get()),
+            directory=str(EnvironmentVariables.ALEMBIC_CONFIG.get().parent / "alembic"),
         )
 
     @classmethod
@@ -287,7 +287,7 @@ class Model(Base, sqlmodel.SQLModel):  # pyright: ignore [reportGeneralTypeIssue
         Returns:
             True when changes have been detected.
         """
-        if not environment.ALEMBIC_CONFIG.get().exists():
+        if not EnvironmentVariables.ALEMBIC_CONFIG.get().exists():
             return False
 
         config, script_directory = cls._alembic_config()
@@ -388,7 +388,7 @@ class Model(Base, sqlmodel.SQLModel):  # pyright: ignore [reportGeneralTypeIssue
             True - indicating the process was successful.
             None - indicating the process was skipped.
         """
-        if not environment.ALEMBIC_CONFIG.get().exists():
+        if not EnvironmentVariables.ALEMBIC_CONFIG.get().exists():
             return
 
         with cls.get_db_engine().connect() as connection:

+ 8 - 8
reflex/reflex.py

@@ -13,7 +13,7 @@ from reflex_cli.deployments import deployments_cli
 from reflex_cli.utils import dependency
 
 from reflex import constants
-from reflex.config import environment, get_config
+from reflex.config import EnvironmentVariables, get_config
 from reflex.custom_components.custom_components import custom_components_cli
 from reflex.state import reset_disk_state_manager
 from reflex.utils import console, redir, telemetry
@@ -160,7 +160,7 @@ def _run(
     console.set_log_level(loglevel)
 
     # Set env mode in the environment
-    environment.REFLEX_ENV_MODE.set(env)
+    EnvironmentVariables.REFLEX_ENV_MODE.set(env)
 
     # Show system info
     exec.output_system_info()
@@ -277,13 +277,13 @@ def run(
         False,
         "--frontend-only",
         help="Execute only frontend.",
-        envvar=environment.REFLEX_FRONTEND_ONLY.name,
+        envvar=EnvironmentVariables.REFLEX_FRONTEND_ONLY.name,
     ),
     backend: bool = typer.Option(
         False,
         "--backend-only",
         help="Execute only backend.",
-        envvar=environment.REFLEX_BACKEND_ONLY.name,
+        envvar=EnvironmentVariables.REFLEX_BACKEND_ONLY.name,
     ),
     frontend_port: str = typer.Option(
         config.frontend_port, help="Specify a different frontend port."
@@ -302,8 +302,8 @@ def run(
     if frontend and backend:
         console.error("Cannot use both --frontend-only and --backend-only options.")
         raise typer.Exit(1)
-    environment.REFLEX_BACKEND_ONLY.set(backend)
-    environment.REFLEX_FRONTEND_ONLY.set(frontend)
+    EnvironmentVariables.REFLEX_BACKEND_ONLY.set(backend)
+    EnvironmentVariables.REFLEX_FRONTEND_ONLY.set(frontend)
 
     _run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel)
 
@@ -405,7 +405,7 @@ script_cli = typer.Typer()
 
 def _skip_compile():
     """Skip the compile step."""
-    environment.REFLEX_SKIP_COMPILE.set(True)
+    EnvironmentVariables.REFLEX_SKIP_COMPILE.set(True)
 
 
 @db_cli.command(name="init")
@@ -420,7 +420,7 @@ def db_init():
         return
 
     # Check the alembic config.
-    if environment.ALEMBIC_CONFIG.get().exists():
+    if EnvironmentVariables.ALEMBIC_CONFIG.get().exists():
         console.error(
             "Database is already initialized. Use "
             "[bold]reflex db makemigrations[/bold] to create schema change "

+ 3 - 3
reflex/state.py

@@ -69,7 +69,7 @@ from redis.exceptions import ResponseError
 import reflex.istate.dynamic
 from reflex import constants
 from reflex.base import Base
-from reflex.config import environment
+from reflex.config import EnvironmentVariables
 from reflex.event import (
     BACKGROUND_TASK_MARKER,
     Event,
@@ -932,7 +932,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
         """
         module = cls.__module__.replace(".", "___")
         state_name = format.to_snake_case(f"{module}___{cls.__name__}")
-        if constants.compiler.CompileVars.MINIFY_STATES():
+        if EnvironmentVariables.REFLEX_MINIFY_STATES.get():
             return get_minified_state_name(state_name)
         return state_name
 
@@ -3435,7 +3435,7 @@ class StateManagerRedis(StateManager):
             )
         except ResponseError:
             # Some redis servers only allow out-of-band configuration, so ignore errors here.
-            if not environment.REFLEX_IGNORE_REDIS_CONFIG_ERROR.get():
+            if not EnvironmentVariables.REFLEX_IGNORE_REDIS_CONFIG_ERROR.get():
                 raise
         async with self.redis.pubsub() as pubsub:
             await pubsub.psubscribe(lock_key_channel)

+ 9 - 9
reflex/testing.py

@@ -44,7 +44,7 @@ import reflex.utils.format
 import reflex.utils.prerequisites
 import reflex.utils.processes
 from reflex.components.component import StatefulComponent
-from reflex.config import environment
+from reflex.config import EnvironmentVariables
 from reflex.state import (
     BaseState,
     State,
@@ -199,7 +199,7 @@ class AppHarness:
         state_name = reflex.utils.format.to_snake_case(
             f"{self.app_name}___{self.app_name}___" + state_cls_name
         )
-        if reflex.constants.CompileVars.MINIFY_STATES():
+        if EnvironmentVariables.REFLEX_MINIFY_STATES.get():
             return minified_state_names.get(state_name, state_name)
         return state_name
 
@@ -626,10 +626,10 @@ class AppHarness:
         if self.frontend_url is None:
             raise RuntimeError("Frontend is not running.")
         want_headless = False
-        if environment.APP_HARNESS_HEADLESS.get():
+        if EnvironmentVariables.APP_HARNESS_HEADLESS.get():
             want_headless = True
         if driver_clz is None:
-            requested_driver = environment.APP_HARNESS_DRIVER.get()
+            requested_driver = EnvironmentVariables.APP_HARNESS_DRIVER.get()
             driver_clz = getattr(webdriver, requested_driver)
             if driver_options is None:
                 driver_options = getattr(webdriver, f"{requested_driver}Options")()
@@ -651,7 +651,7 @@ class AppHarness:
                 driver_options.add_argument("headless")
         if driver_options is None:
             raise RuntimeError(f"Could not determine options for {driver_clz}")
-        if args := environment.APP_HARNESS_DRIVER_ARGS.get():
+        if args := EnvironmentVariables.APP_HARNESS_DRIVER_ARGS.get():
             for arg in args.split(","):
                 driver_options.add_argument(arg)
         if driver_option_args is not None:
@@ -958,7 +958,7 @@ class AppHarnessProd(AppHarness):
     def _start_backend(self):
         if self.app_instance is None:
             raise RuntimeError("App was not initialized.")
-        environment.REFLEX_SKIP_COMPILE.set(True)
+        EnvironmentVariables.REFLEX_SKIP_COMPILE.set(True)
         self.backend = uvicorn.Server(
             uvicorn.Config(
                 app=self.app_instance,
@@ -976,7 +976,7 @@ class AppHarnessProd(AppHarness):
         try:
             return super()._poll_for_servers(timeout)
         finally:
-            environment.REFLEX_SKIP_COMPILE.set(None)
+            EnvironmentVariables.REFLEX_SKIP_COMPILE.set(None)
 
     @override
     def start(self) -> AppHarnessProd:
@@ -985,7 +985,7 @@ class AppHarnessProd(AppHarness):
         Returns:
             self
         """
-        environment.REFLEX_ENV_MODE.set(reflex.constants.base.Env.PROD)
+        EnvironmentVariables.REFLEX_ENV_MODE.set(reflex.constants.base.Env.PROD)
         _ = super().start()
         return self
 
@@ -997,4 +997,4 @@ class AppHarnessProd(AppHarness):
             self.frontend_server.shutdown()
         if self.frontend_thread is not None:
             self.frontend_thread.join()
-        environment.REFLEX_ENV_MODE.set(None)
+        EnvironmentVariables.REFLEX_ENV_MODE.set(None)

+ 8 - 8
reflex/utils/exec.py

@@ -15,7 +15,7 @@ from urllib.parse import urljoin
 import psutil
 
 from reflex import constants
-from reflex.config import environment, get_config
+from reflex.config import EnvironmentVariables, get_config
 from reflex.constants.base import LogLevel
 from reflex.utils import console, path_ops
 from reflex.utils.prerequisites import get_web_dir
@@ -184,7 +184,7 @@ def should_use_granian():
     Returns:
         True if Granian should be used.
     """
-    return environment.REFLEX_USE_GRANIAN.get()
+    return EnvironmentVariables.REFLEX_USE_GRANIAN.get()
 
 
 def get_app_module():
@@ -370,7 +370,7 @@ def run_uvicorn_backend_prod(host, port, loglevel):
         run=True,
         show_logs=True,
         env={
-            environment.REFLEX_SKIP_COMPILE.name: "true"
+            EnvironmentVariables.REFLEX_SKIP_COMPILE.name: "true"
         },  # skip compile for prod backend
     )
 
@@ -407,7 +407,7 @@ def run_granian_backend_prod(host, port, loglevel):
             run=True,
             show_logs=True,
             env={
-                environment.REFLEX_SKIP_COMPILE.name: "true"
+                EnvironmentVariables.REFLEX_SKIP_COMPILE.name: "true"
             },  # skip compile for prod backend
         )
     except ImportError:
@@ -493,7 +493,7 @@ def is_prod_mode() -> bool:
     Returns:
         True if the app is running in production mode or False if running in dev mode.
     """
-    current_mode = environment.REFLEX_ENV_MODE.get()
+    current_mode = EnvironmentVariables.REFLEX_ENV_MODE.get()
     return current_mode == constants.Env.PROD
 
 
@@ -509,7 +509,7 @@ def is_frontend_only() -> bool:
         deprecation_version="0.6.5",
         removal_version="0.7.0",
     )
-    return environment.REFLEX_FRONTEND_ONLY.get()
+    return EnvironmentVariables.REFLEX_FRONTEND_ONLY.get()
 
 
 def is_backend_only() -> bool:
@@ -524,7 +524,7 @@ def is_backend_only() -> bool:
         deprecation_version="0.6.5",
         removal_version="0.7.0",
     )
-    return environment.REFLEX_BACKEND_ONLY.get()
+    return EnvironmentVariables.REFLEX_BACKEND_ONLY.get()
 
 
 def should_skip_compile() -> bool:
@@ -539,4 +539,4 @@ def should_skip_compile() -> bool:
         deprecation_version="0.6.5",
         removal_version="0.7.0",
     )
-    return environment.REFLEX_SKIP_COMPILE.get()
+    return EnvironmentVariables.REFLEX_SKIP_COMPILE.get()

+ 2 - 2
reflex/utils/net.py

@@ -2,7 +2,7 @@
 
 import httpx
 
-from ..config import environment
+from ..config import EnvironmentVariables
 from . import console
 
 
@@ -12,7 +12,7 @@ def _httpx_verify_kwarg() -> bool:
     Returns:
         True if SSL verification is enabled, False otherwise
     """
-    return not environment.SSL_NO_VERIFY.get()
+    return not EnvironmentVariables.SSL_NO_VERIFY.get()
 
 
 def get(url: str, **kwargs) -> httpx.Response:

+ 3 - 3
reflex/utils/path_ops.py

@@ -9,7 +9,7 @@ import shutil
 from pathlib import Path
 
 from reflex import constants
-from reflex.config import environment
+from reflex.config import EnvironmentVariables
 
 # Shorthand for join.
 join = os.linesep.join
@@ -136,7 +136,7 @@ def use_system_node() -> bool:
     Returns:
         Whether the system node should be used.
     """
-    return environment.REFLEX_USE_SYSTEM_NODE.get()
+    return EnvironmentVariables.REFLEX_USE_SYSTEM_NODE.get()
 
 
 def use_system_bun() -> bool:
@@ -145,7 +145,7 @@ def use_system_bun() -> bool:
     Returns:
         Whether the system bun should be used.
     """
-    return environment.REFLEX_USE_SYSTEM_BUN.get()
+    return EnvironmentVariables.REFLEX_USE_SYSTEM_BUN.get()
 
 
 def get_node_bin_path() -> Path | None:

+ 12 - 9
reflex/utils/prerequisites.py

@@ -33,7 +33,7 @@ from redis.asyncio import Redis
 
 from reflex import constants, model
 from reflex.compiler import templates
-from reflex.config import Config, environment, get_config
+from reflex.config import Config, EnvironmentVariables, get_config
 from reflex.utils import console, net, path_ops, processes
 from reflex.utils.exceptions import GeneratedCodeHasNoFunctionDefs
 from reflex.utils.format import format_library_name
@@ -69,7 +69,7 @@ def get_web_dir() -> Path:
     Returns:
         The working directory.
     """
-    return environment.REFLEX_WEB_WORKDIR.get()
+    return EnvironmentVariables.REFLEX_WEB_WORKDIR.get()
 
 
 def _python_version_check():
@@ -260,7 +260,7 @@ def windows_npm_escape_hatch() -> bool:
     Returns:
         If the user has set REFLEX_USE_NPM.
     """
-    return environment.REFLEX_USE_NPM.get()
+    return EnvironmentVariables.REFLEX_USE_NPM.get()
 
 
 def get_app(reload: bool = False) -> ModuleType:
@@ -278,7 +278,7 @@ def get_app(reload: bool = False) -> ModuleType:
     from reflex.utils import telemetry
 
     try:
-        environment.RELOAD_CONFIG.set(reload)
+        EnvironmentVariables.RELOAD_CONFIG.set(reload)
         config = get_config()
         if not config.app_name:
             raise RuntimeError(
@@ -1019,7 +1019,7 @@ def needs_reinit(frontend: bool = True) -> bool:
         return False
 
     # Make sure the .reflex directory exists.
-    if not environment.REFLEX_DIR.get().exists():
+    if not EnvironmentVariables.REFLEX_DIR.get().exists():
         return True
 
     # Make sure the .web directory exists in frontend mode.
@@ -1124,7 +1124,7 @@ def ensure_reflex_installation_id() -> Optional[int]:
     """
     try:
         initialize_reflex_user_directory()
-        installation_id_file = environment.REFLEX_DIR.get() / "installation_id"
+        installation_id_file = EnvironmentVariables.REFLEX_DIR.get() / "installation_id"
 
         installation_id = None
         if installation_id_file.exists():
@@ -1149,7 +1149,7 @@ def ensure_reflex_installation_id() -> Optional[int]:
 def initialize_reflex_user_directory():
     """Initialize the reflex user directory."""
     # Create the reflex directory.
-    path_ops.mkdir(environment.REFLEX_DIR.get())
+    path_ops.mkdir(EnvironmentVariables.REFLEX_DIR.get())
 
 
 def initialize_frontend_dependencies():
@@ -1174,7 +1174,7 @@ def check_db_initialized() -> bool:
     """
     if (
         get_config().db_url is not None
-        and not environment.ALEMBIC_CONFIG.get().exists()
+        and not EnvironmentVariables.ALEMBIC_CONFIG.get().exists()
     ):
         console.error(
             "Database is not initialized. Run [bold]reflex db init[/bold] first."
@@ -1185,7 +1185,10 @@ def check_db_initialized() -> bool:
 
 def check_schema_up_to_date():
     """Check if the sqlmodel metadata matches the current database schema."""
-    if get_config().db_url is None or not environment.ALEMBIC_CONFIG.get().exists():
+    if (
+        get_config().db_url is None
+        or not EnvironmentVariables.ALEMBIC_CONFIG.get().exists()
+    ):
         return
     with model.Model.get_db_engine().connect() as connection:
         try:

+ 2 - 2
reflex/utils/registry.py

@@ -2,7 +2,7 @@
 
 import httpx
 
-from reflex.config import environment
+from reflex.config import EnvironmentVariables
 from reflex.utils import console, net
 
 
@@ -55,4 +55,4 @@ def _get_npm_registry() -> str:
     Returns:
         str:
     """
-    return environment.NPM_CONFIG_REGISTRY.get() or get_best_registry()
+    return EnvironmentVariables.NPM_CONFIG_REGISTRY.get() or get_best_registry()

+ 2 - 2
reflex/utils/telemetry.py

@@ -8,7 +8,7 @@ import multiprocessing
 import platform
 import warnings
 
-from reflex.config import environment
+from reflex.config import EnvironmentVariables
 
 try:
     from datetime import UTC, datetime
@@ -95,7 +95,7 @@ def _raise_on_missing_project_hash() -> bool:
         False when compilation should be skipped (i.e. no .web directory is required).
         Otherwise return True.
     """
-    return not environment.REFLEX_SKIP_COMPILE.get()
+    return not EnvironmentVariables.REFLEX_SKIP_COMPILE.get()
 
 
 def _prepare_event(event: str, **kwargs) -> dict:

+ 8 - 5
tests/integration/conftest.py

@@ -8,7 +8,7 @@ from typing import Generator, Type
 import pytest
 
 import reflex.constants
-from reflex.config import environment
+from reflex.config import EnvironmentVariables
 from reflex.testing import AppHarness, AppHarnessProd
 
 DISPLAY = None
@@ -24,7 +24,10 @@ def xvfb():
     Yields:
         the pyvirtualdisplay object that the browser will be open on
     """
-    if os.environ.get("GITHUB_ACTIONS") and not environment.APP_HARNESS_HEADLESS.get():
+    if (
+        os.environ.get("GITHUB_ACTIONS")
+        and not EnvironmentVariables.APP_HARNESS_HEADLESS.get()
+    ):
         from pyvirtualdisplay.smartdisplay import (  # pyright: ignore [reportMissingImports]
             SmartDisplay,
         )
@@ -45,7 +48,7 @@ def pytest_exception_interact(node, call, report):
         call: The pytest call describing when/where the test was invoked.
         report: The pytest log report object.
     """
-    screenshot_dir = environment.SCREENSHOT_DIR.get()
+    screenshot_dir = EnvironmentVariables.SCREENSHOT_DIR.get()
     if DISPLAY is None or screenshot_dir is None:
         return
 
@@ -89,7 +92,7 @@ def app_harness_env(
     """
     harness: Type[AppHarness] = request.param
     if issubclass(harness, AppHarnessProd):
-        environment.REFLEX_ENV_MODE.set(reflex.constants.Env.PROD)
+        EnvironmentVariables.REFLEX_ENV_MODE.set(reflex.constants.Env.PROD)
     yield harness
     if issubclass(harness, AppHarnessProd):
-        environment.REFLEX_ENV_MODE.set(None)
+        EnvironmentVariables.REFLEX_ENV_MODE.set(None)

+ 3 - 8
tests/integration/test_minified_states.py

@@ -2,7 +2,6 @@
 
 from __future__ import annotations
 
-import os
 from functools import partial
 from typing import Generator, Optional, Type
 
@@ -10,7 +9,7 @@ import pytest
 from selenium.webdriver.common.by import By
 from selenium.webdriver.remote.webdriver import WebDriver
 
-from reflex.constants.compiler import ENV_MINIFY_STATES
+from reflex.config import EnvironmentVariables
 from reflex.testing import AppHarness, AppHarnessProd
 
 
@@ -63,13 +62,9 @@ def minify_state_env(
         minify_states: whether to minify state names
     """
     minify_states: Optional[bool] = request.param
-    if minify_states is None:
-        _ = os.environ.pop(ENV_MINIFY_STATES, None)
-    else:
-        os.environ[ENV_MINIFY_STATES] = str(minify_states).lower()
+    EnvironmentVariables.REFLEX_MINIFY_STATES.set(minify_states)
     yield minify_states
-    if minify_states is not None:
-        os.environ.pop(ENV_MINIFY_STATES, None)
+    EnvironmentVariables.REFLEX_MINIFY_STATES.set(None)
 
 
 @pytest.fixture

+ 19 - 2
tests/units/test_config.py

@@ -8,9 +8,9 @@ import pytest
 import reflex as rx
 import reflex.config
 from reflex.config import (
+    EnvironmentVariables,
     EnvVar,
     env_var,
-    environment,
     interpret_boolean_env,
     interpret_enum_env,
     interpret_int_env,
@@ -216,7 +216,7 @@ def test_replace_defaults(
 
 
 def reflex_dir_constant() -> Path:
-    return environment.REFLEX_DIR.get()
+    return EnvironmentVariables.REFLEX_DIR.get()
 
 
 def test_reflex_dir_env_var(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
@@ -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)

+ 3 - 3
tests/units/utils/test_utils.py

@@ -10,7 +10,7 @@ from packaging import version
 
 from reflex import constants
 from reflex.base import Base
-from reflex.config import environment
+from reflex.config import EnvironmentVariables
 from reflex.event import EventHandler
 from reflex.state import BaseState
 from reflex.utils import (
@@ -598,7 +598,7 @@ def test_style_prop_with_event_handler_value(callable):
 
 def test_is_prod_mode() -> None:
     """Test that the prod mode is correctly determined."""
-    environment.REFLEX_ENV_MODE.set(constants.Env.PROD)
+    EnvironmentVariables.REFLEX_ENV_MODE.set(constants.Env.PROD)
     assert utils_exec.is_prod_mode()
-    environment.REFLEX_ENV_MODE.set(None)
+    EnvironmentVariables.REFLEX_ENV_MODE.set(None)
     assert not utils_exec.is_prod_mode()