1
0
Эх сурвалжийг харах

More env var cleanup (#4248)

* fix and test bug in config env loading

* streamline env var interpretation with @adhami3310

* improve error messages, fix invalid value for TELEMETRY_ENABLED

* just a small hint

* ruffing

* fix typo from review

* refactor - ruff broke the imports..

* cleanup imports

* more

* add internal and enum env var support

* ruff cleanup

* more global imports

* revert telemetry, it lives in rx.Config

* minor fixes/cleanup

* i missed some refs

* fix darglint

* reload config is internal

* fix EnvVar name

* add test for EnvVar + minor typing improvement

* bool tests

* was this broken?

* retain old behavior

* migrate APP_HARNESS_HEADLESS to new env var system

* migrate more APP_HARNESS env vars to new config system

* migrate SCREENSHOT_DIR to new env var system

* refactor EnvVar.get to be a method

* readd deleted functions and deprecate them

* improve EnvVar api, cleanup RELOAD_CONFIG question

* move is_prod_mode back to where it was
benedikt-bartscher 6 сар өмнө
parent
commit
4a6c16e9dc

+ 5 - 6
reflex/app.py

@@ -12,7 +12,6 @@ import inspect
 import io
 import io
 import json
 import json
 import multiprocessing
 import multiprocessing
-import os
 import platform
 import platform
 import sys
 import sys
 import traceback
 import traceback
@@ -96,7 +95,7 @@ from reflex.state import (
     code_uses_state_contexts,
     code_uses_state_contexts,
 )
 )
 from reflex.utils import codespaces, console, exceptions, format, prerequisites, types
 from reflex.utils import codespaces, console, exceptions, format, prerequisites, types
-from reflex.utils.exec import is_prod_mode, is_testing_env, should_skip_compile
+from reflex.utils.exec import is_prod_mode, is_testing_env
 from reflex.utils.imports import ImportVar
 from reflex.utils.imports import ImportVar
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
@@ -507,7 +506,7 @@ class App(MiddlewareMixin, LifespanMixin):
         # Check if the route given is valid
         # Check if the route given is valid
         verify_route_validity(route)
         verify_route_validity(route)
 
 
-        if route in self.unevaluated_pages and os.getenv(constants.RELOAD_CONFIG):
+        if route in self.unevaluated_pages and environment.RELOAD_CONFIG.is_set():
             # when the app is reloaded(typically for app harness tests), we should maintain
             # 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
             # the latest render function of a route.This applies typically to decorated pages
             # since they are only added when app._compile is called.
             # since they are only added when app._compile is called.
@@ -724,7 +723,7 @@ class App(MiddlewareMixin, LifespanMixin):
             Whether the app should be compiled.
             Whether the app should be compiled.
         """
         """
         # Check the environment variable.
         # Check the environment variable.
-        if should_skip_compile():
+        if environment.REFLEX_SKIP_COMPILE.get():
             return False
             return False
 
 
         nocompile = prerequisites.get_web_dir() / constants.NOCOMPILE_FILE
         nocompile = prerequisites.get_web_dir() / constants.NOCOMPILE_FILE
@@ -947,7 +946,7 @@ class App(MiddlewareMixin, LifespanMixin):
         executor = None
         executor = None
         if (
         if (
             platform.system() in ("Linux", "Darwin")
             platform.system() in ("Linux", "Darwin")
-            and (number_of_processes := environment.REFLEX_COMPILE_PROCESSES)
+            and (number_of_processes := environment.REFLEX_COMPILE_PROCESSES.get())
             is not None
             is not None
         ):
         ):
             executor = concurrent.futures.ProcessPoolExecutor(
             executor = concurrent.futures.ProcessPoolExecutor(
@@ -956,7 +955,7 @@ class App(MiddlewareMixin, LifespanMixin):
             )
             )
         else:
         else:
             executor = concurrent.futures.ThreadPoolExecutor(
             executor = concurrent.futures.ThreadPoolExecutor(
-                max_workers=environment.REFLEX_COMPILE_THREADS
+                max_workers=environment.REFLEX_COMPILE_THREADS.get()
             )
             )
 
 
         for route, component in zip(self.pages, page_components):
         for route, component in zip(self.pages, page_components):

+ 2 - 4
reflex/base.py

@@ -16,9 +16,6 @@ except ModuleNotFoundError:
         from pydantic.fields import ModelField  # type: ignore
         from pydantic.fields import ModelField  # type: ignore
 
 
 
 
-from reflex import constants
-
-
 def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None:
 def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None:
     """Ensure that the field's name does not shadow an existing attribute of the model.
     """Ensure that the field's name does not shadow an existing attribute of the model.
 
 
@@ -31,7 +28,8 @@ def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None
     """
     """
     from reflex.utils.exceptions import VarNameError
     from reflex.utils.exceptions import VarNameError
 
 
-    reload = os.getenv(constants.RELOAD_CONFIG) == "True"
+    # can't use reflex.config.environment here cause of circular import
+    reload = os.getenv("__RELOAD_CONFIG", "").lower() == "true"
     for base in bases:
     for base in bases:
         try:
         try:
             if not reload and getattr(base, field_name, None):
             if not reload and getattr(base, field_name, None):

+ 1 - 1
reflex/compiler/compiler.py

@@ -527,7 +527,7 @@ def remove_tailwind_from_postcss() -> tuple[str, str]:
 
 
 def purge_web_pages_dir():
 def purge_web_pages_dir():
     """Empty out .web/pages directory."""
     """Empty out .web/pages directory."""
-    if not is_prod_mode() and environment.REFLEX_PERSIST_WEB_DIR:
+    if not is_prod_mode() and environment.REFLEX_PERSIST_WEB_DIR.get():
         # Skip purging the web directory in dev mode if REFLEX_PERSIST_WEB_DIR is set.
         # Skip purging the web directory in dev mode if REFLEX_PERSIST_WEB_DIR is set.
         return
         return
 
 

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

@@ -132,7 +132,7 @@ def get_upload_dir() -> Path:
     """
     """
     Upload.is_used = True
     Upload.is_used = True
 
 
-    uploaded_files_dir = environment.REFLEX_UPLOADED_FILES_DIR
+    uploaded_files_dir = environment.REFLEX_UPLOADED_FILES_DIR.get()
     uploaded_files_dir.mkdir(parents=True, exist_ok=True)
     uploaded_files_dir.mkdir(parents=True, exist_ok=True)
     return uploaded_files_dir
     return uploaded_files_dir
 
 

+ 195 - 35
reflex/config.py

@@ -10,7 +10,17 @@ import os
 import sys
 import sys
 import urllib.parse
 import urllib.parse
 from pathlib import Path
 from pathlib import Path
-from typing import Any, Dict, List, Optional, Set
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Dict,
+    Generic,
+    List,
+    Optional,
+    Set,
+    TypeVar,
+    get_args,
+)
 
 
 from typing_extensions import Annotated, get_type_hints
 from typing_extensions import Annotated, get_type_hints
 
 
@@ -300,6 +310,141 @@ def interpret_env_var_value(
         )
         )
 
 
 
 
+T = TypeVar("T")
+
+
+class EnvVar(Generic[T]):
+    """Environment variable."""
+
+    name: str
+    default: Any
+    type_: T
+
+    def __init__(self, name: str, default: Any, type_: T) -> None:
+        """Initialize the environment variable.
+
+        Args:
+            name: The environment variable name.
+            default: The default value.
+            type_: The type of the value.
+        """
+        self.name = name
+        self.default = default
+        self.type_ = type_
+
+    def interpret(self, value: str) -> T:
+        """Interpret the environment variable value.
+
+        Args:
+            value: The environment variable value.
+
+        Returns:
+            The interpreted value.
+        """
+        return interpret_env_var_value(value, self.type_, self.name)
+
+    def getenv(self) -> Optional[T]:
+        """Get the interpreted environment variable value.
+
+        Returns:
+            The environment variable value.
+        """
+        env_value = os.getenv(self.name, None)
+        if env_value is not None:
+            return self.interpret(env_value)
+        return None
+
+    def is_set(self) -> bool:
+        """Check if the environment variable is set.
+
+        Returns:
+            True if the environment variable is set.
+        """
+        return self.name in os.environ
+
+    def get(self) -> T:
+        """Get the interpreted environment variable value or the default value if not set.
+
+        Returns:
+            The interpreted value.
+        """
+        env_value = self.getenv()
+        if env_value is not None:
+            return env_value
+        return self.default
+
+    def set(self, value: T | None) -> None:
+        """Set the environment variable. None unsets the variable.
+
+        Args:
+            value: The value to set.
+        """
+        if value is None:
+            _ = os.environ.pop(self.name, None)
+        else:
+            if isinstance(value, enum.Enum):
+                value = value.value
+            os.environ[self.name] = str(value)
+
+
+class env_var:  # type: ignore
+    """Descriptor for environment variables."""
+
+    name: str
+    default: Any
+    internal: bool = False
+
+    def __init__(self, default: Any, internal: bool = False) -> None:
+        """Initialize the descriptor.
+
+        Args:
+            default: The default value.
+            internal: Whether the environment variable is reflex internal.
+        """
+        self.default = default
+        self.internal = internal
+
+    def __set_name__(self, owner, name):
+        """Set the name of the descriptor.
+
+        Args:
+            owner: The owner class.
+            name: The name of the descriptor.
+        """
+        self.name = name
+
+    def __get__(self, instance, owner):
+        """Get the EnvVar instance.
+
+        Args:
+            instance: The instance.
+            owner: The owner class.
+
+        Returns:
+            The EnvVar instance.
+        """
+        type_ = get_args(get_type_hints(owner)[self.name])[0]
+        env_name = self.name
+        if self.internal:
+            env_name = f"__{env_name}"
+        return EnvVar(name=env_name, default=self.default, type_=type_)
+
+
+if TYPE_CHECKING:
+
+    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
+
+
 class PathExistsFlag:
 class PathExistsFlag:
     """Flag to indicate that a path must exist."""
     """Flag to indicate that a path must exist."""
 
 
@@ -307,83 +452,98 @@ class PathExistsFlag:
 ExistingPath = Annotated[Path, PathExistsFlag]
 ExistingPath = Annotated[Path, PathExistsFlag]
 
 
 
 
-@dataclasses.dataclass(init=False)
 class EnvironmentVariables:
 class EnvironmentVariables:
     """Environment variables class to instantiate environment variables."""
     """Environment variables class to instantiate environment variables."""
 
 
     # Whether to use npm over bun to install frontend packages.
     # Whether to use npm over bun to install frontend packages.
-    REFLEX_USE_NPM: bool = False
+    REFLEX_USE_NPM: EnvVar[bool] = env_var(False)
 
 
     # The npm registry to use.
     # The npm registry to use.
-    NPM_CONFIG_REGISTRY: Optional[str] = None
+    NPM_CONFIG_REGISTRY: EnvVar[Optional[str]] = env_var(None)
 
 
     # Whether to use Granian for the backend. Otherwise, use Uvicorn.
     # Whether to use Granian for the backend. Otherwise, use Uvicorn.
-    REFLEX_USE_GRANIAN: bool = False
+    REFLEX_USE_GRANIAN: EnvVar[bool] = env_var(False)
 
 
     # The username to use for authentication on python package repository. Username and password must both be provided.
     # The username to use for authentication on python package repository. Username and password must both be provided.
-    TWINE_USERNAME: Optional[str] = None
+    TWINE_USERNAME: EnvVar[Optional[str]] = env_var(None)
 
 
     # The password to use for authentication on python package repository. Username and password must both be provided.
     # The password to use for authentication on python package repository. Username and password must both be provided.
-    TWINE_PASSWORD: Optional[str] = None
+    TWINE_PASSWORD: EnvVar[Optional[str]] = env_var(None)
 
 
     # Whether to use the system installed bun. If set to false, bun will be bundled with the app.
     # Whether to use the system installed bun. If set to false, bun will be bundled with the app.
-    REFLEX_USE_SYSTEM_BUN: bool = False
+    REFLEX_USE_SYSTEM_BUN: EnvVar[bool] = env_var(False)
 
 
     # Whether to use the system installed node and npm. If set to false, node and npm will be bundled with the app.
     # Whether to use the system installed node and npm. If set to false, node and npm will be bundled with the app.
-    REFLEX_USE_SYSTEM_NODE: bool = False
+    REFLEX_USE_SYSTEM_NODE: EnvVar[bool] = env_var(False)
 
 
     # The working directory for the next.js commands.
     # The working directory for the next.js commands.
-    REFLEX_WEB_WORKDIR: Path = Path(constants.Dirs.WEB)
+    REFLEX_WEB_WORKDIR: EnvVar[Path] = env_var(Path(constants.Dirs.WEB))
 
 
     # Path to the alembic config file
     # Path to the alembic config file
-    ALEMBIC_CONFIG: ExistingPath = Path(constants.ALEMBIC_CONFIG)
+    ALEMBIC_CONFIG: EnvVar[ExistingPath] = env_var(Path(constants.ALEMBIC_CONFIG))
 
 
     # Disable SSL verification for HTTPX requests.
     # Disable SSL verification for HTTPX requests.
-    SSL_NO_VERIFY: bool = False
+    SSL_NO_VERIFY: EnvVar[bool] = env_var(False)
 
 
     # The directory to store uploaded files.
     # The directory to store uploaded files.
-    REFLEX_UPLOADED_FILES_DIR: Path = Path(constants.Dirs.UPLOADED_FILES)
+    REFLEX_UPLOADED_FILES_DIR: EnvVar[Path] = env_var(
+        Path(constants.Dirs.UPLOADED_FILES)
+    )
 
 
-    # Whether to use seperate processes to compile the frontend and how many. If not set, defaults to thread executor.
-    REFLEX_COMPILE_PROCESSES: Optional[int] = None
+    # Whether to use separate processes to compile the frontend and how many. If not set, defaults to thread executor.
+    REFLEX_COMPILE_PROCESSES: EnvVar[Optional[int]] = env_var(None)
 
 
-    # Whether to use seperate threads to compile the frontend and how many. Defaults to `min(32, os.cpu_count() + 4)`.
-    REFLEX_COMPILE_THREADS: Optional[int] = None
+    # Whether to use separate threads to compile the frontend and how many. Defaults to `min(32, os.cpu_count() + 4)`.
+    REFLEX_COMPILE_THREADS: EnvVar[Optional[int]] = env_var(None)
 
 
     # The directory to store reflex dependencies.
     # The directory to store reflex dependencies.
-    REFLEX_DIR: Path = Path(constants.Reflex.DIR)
+    REFLEX_DIR: EnvVar[Path] = env_var(Path(constants.Reflex.DIR))
 
 
     # Whether to print the SQL queries if the log level is INFO or lower.
     # Whether to print the SQL queries if the log level is INFO or lower.
-    SQLALCHEMY_ECHO: bool = False
+    SQLALCHEMY_ECHO: EnvVar[bool] = env_var(False)
 
 
     # Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration.
     # Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration.
-    REFLEX_IGNORE_REDIS_CONFIG_ERROR: bool = False
+    REFLEX_IGNORE_REDIS_CONFIG_ERROR: EnvVar[bool] = env_var(False)
 
 
     # Whether to skip purging the web directory in dev mode.
     # Whether to skip purging the web directory in dev mode.
-    REFLEX_PERSIST_WEB_DIR: bool = False
+    REFLEX_PERSIST_WEB_DIR: EnvVar[bool] = env_var(False)
 
 
     # The reflex.build frontend host.
     # The reflex.build frontend host.
-    REFLEX_BUILD_FRONTEND: str = constants.Templates.REFLEX_BUILD_FRONTEND
+    REFLEX_BUILD_FRONTEND: EnvVar[str] = env_var(
+        constants.Templates.REFLEX_BUILD_FRONTEND
+    )
 
 
     # The reflex.build backend host.
     # The reflex.build backend host.
-    REFLEX_BUILD_BACKEND: str = constants.Templates.REFLEX_BUILD_BACKEND
+    REFLEX_BUILD_BACKEND: EnvVar[str] = env_var(
+        constants.Templates.REFLEX_BUILD_BACKEND
+    )
 
 
-    def __init__(self):
-        """Initialize the environment variables."""
-        type_hints = get_type_hints(type(self))
+    # This env var stores the execution mode of the app
+    REFLEX_ENV_MODE: EnvVar[constants.Env] = env_var(constants.Env.DEV)
 
 
-        for field in dataclasses.fields(self):
-            raw_value = os.getenv(field.name, None)
+    # Whether to run the backend only. Exclusive with REFLEX_FRONTEND_ONLY.
+    REFLEX_BACKEND_ONLY: EnvVar[bool] = env_var(False)
 
 
-            field.type = type_hints.get(field.name) or field.type
+    # Whether to run the frontend only. Exclusive with REFLEX_BACKEND_ONLY.
+    REFLEX_FRONTEND_ONLY: EnvVar[bool] = env_var(False)
 
 
-            value = (
-                interpret_env_var_value(raw_value, field.type, field.name)
-                if raw_value is not None
-                else get_default_value_for_field(field)
-            )
+    # Reflex internal env to reload the config.
+    RELOAD_CONFIG: EnvVar[bool] = env_var(False, internal=True)
+
+    # If this env var is set to "yes", App.compile will be a no-op
+    REFLEX_SKIP_COMPILE: EnvVar[bool] = env_var(False, internal=True)
+
+    # Whether to run app harness tests in headless mode.
+    APP_HARNESS_HEADLESS: EnvVar[bool] = env_var(False)
+
+    # Which app harness driver to use.
+    APP_HARNESS_DRIVER: EnvVar[str] = env_var("Chrome")
+
+    # Arguments to pass to the app harness driver.
+    APP_HARNESS_DRIVER_ARGS: EnvVar[str] = env_var("")
 
 
-            setattr(self, field.name, value)
+    # Where to save screenshots when tests fail.
+    SCREENSHOT_DIR: EnvVar[Optional[Path]] = env_var(None)
 
 
 
 
 environment = EnvironmentVariables()
 environment = EnvironmentVariables()

+ 0 - 7
reflex/constants/__init__.py

@@ -2,18 +2,13 @@
 
 
 from .base import (
 from .base import (
     COOKIES,
     COOKIES,
-    ENV_BACKEND_ONLY_ENV_VAR,
-    ENV_FRONTEND_ONLY_ENV_VAR,
-    ENV_MODE_ENV_VAR,
     IS_WINDOWS,
     IS_WINDOWS,
     LOCAL_STORAGE,
     LOCAL_STORAGE,
     POLLING_MAX_HTTP_BUFFER_SIZE,
     POLLING_MAX_HTTP_BUFFER_SIZE,
     PYTEST_CURRENT_TEST,
     PYTEST_CURRENT_TEST,
     REFLEX_VAR_CLOSING_TAG,
     REFLEX_VAR_CLOSING_TAG,
     REFLEX_VAR_OPENING_TAG,
     REFLEX_VAR_OPENING_TAG,
-    RELOAD_CONFIG,
     SESSION_STORAGE,
     SESSION_STORAGE,
-    SKIP_COMPILE_ENV_VAR,
     ColorMode,
     ColorMode,
     Dirs,
     Dirs,
     Env,
     Env,
@@ -106,7 +101,6 @@ __ALL__ = [
     POLLING_MAX_HTTP_BUFFER_SIZE,
     POLLING_MAX_HTTP_BUFFER_SIZE,
     PYTEST_CURRENT_TEST,
     PYTEST_CURRENT_TEST,
     Reflex,
     Reflex,
-    RELOAD_CONFIG,
     RequirementsTxt,
     RequirementsTxt,
     RouteArgType,
     RouteArgType,
     RouteRegex,
     RouteRegex,
@@ -116,7 +110,6 @@ __ALL__ = [
     ROUTER_DATA_INCLUDE,
     ROUTER_DATA_INCLUDE,
     ROUTE_NOT_FOUND,
     ROUTE_NOT_FOUND,
     SETTER_PREFIX,
     SETTER_PREFIX,
-    SKIP_COMPILE_ENV_VAR,
     SocketEvent,
     SocketEvent,
     StateManagerMode,
     StateManagerMode,
     Tailwind,
     Tailwind,

+ 4 - 13
reflex/constants/base.py

@@ -112,7 +112,7 @@ class Templates(SimpleNamespace):
         from reflex.config import environment
         from reflex.config import environment
 
 
         return (
         return (
-            environment.REFLEX_BUILD_FRONTEND
+            environment.REFLEX_BUILD_FRONTEND.get()
             + "/gen?reflex_init_token={reflex_init_token}"
             + "/gen?reflex_init_token={reflex_init_token}"
         )
         )
 
 
@@ -126,7 +126,7 @@ class Templates(SimpleNamespace):
         """
         """
         from reflex.config import environment
         from reflex.config import environment
 
 
-        return environment.REFLEX_BUILD_BACKEND + "/api/init/{reflex_init_token}"
+        return environment.REFLEX_BUILD_BACKEND.get() + "/api/init/{reflex_init_token}"
 
 
     @classproperty
     @classproperty
     @classmethod
     @classmethod
@@ -139,7 +139,8 @@ class Templates(SimpleNamespace):
         from reflex.config import environment
         from reflex.config import environment
 
 
         return (
         return (
-            environment.REFLEX_BUILD_BACKEND + "/api/gen/{generation_hash}/refactored"
+            environment.REFLEX_BUILD_BACKEND.get()
+            + "/api/gen/{generation_hash}/refactored"
         )
         )
 
 
     class Dirs(SimpleNamespace):
     class Dirs(SimpleNamespace):
@@ -239,19 +240,9 @@ COOKIES = "cookies"
 LOCAL_STORAGE = "local_storage"
 LOCAL_STORAGE = "local_storage"
 SESSION_STORAGE = "session_storage"
 SESSION_STORAGE = "session_storage"
 
 
-# If this env var is set to "yes", App.compile will be a no-op
-SKIP_COMPILE_ENV_VAR = "__REFLEX_SKIP_COMPILE"
-
-# This env var stores the execution mode of the app
-ENV_MODE_ENV_VAR = "REFLEX_ENV_MODE"
-
-ENV_BACKEND_ONLY_ENV_VAR = "REFLEX_BACKEND_ONLY"
-ENV_FRONTEND_ONLY_ENV_VAR = "REFLEX_FRONTEND_ONLY"
-
 # Testing variables.
 # Testing variables.
 # Testing os env set by pytest when running a test case.
 # Testing os env set by pytest when running a test case.
 PYTEST_CURRENT_TEST = "PYTEST_CURRENT_TEST"
 PYTEST_CURRENT_TEST = "PYTEST_CURRENT_TEST"
-RELOAD_CONFIG = "__REFLEX_RELOAD_CONFIG"
 
 
 REFLEX_VAR_OPENING_TAG = "<reflex.Var>"
 REFLEX_VAR_OPENING_TAG = "<reflex.Var>"
 REFLEX_VAR_CLOSING_TAG = "</reflex.Var>"
 REFLEX_VAR_CLOSING_TAG = "</reflex.Var>"

+ 2 - 2
reflex/constants/installer.py

@@ -63,7 +63,7 @@ class Bun(SimpleNamespace):
         """
         """
         from reflex.config import environment
         from reflex.config import environment
 
 
-        return environment.REFLEX_DIR / "bun"
+        return environment.REFLEX_DIR.get() / "bun"
 
 
     @classproperty
     @classproperty
     @classmethod
     @classmethod
@@ -100,7 +100,7 @@ class Fnm(SimpleNamespace):
         """
         """
         from reflex.config import environment
         from reflex.config import environment
 
 
-        return environment.REFLEX_DIR / "fnm"
+        return environment.REFLEX_DIR.get() / "fnm"
 
 
     @classproperty
     @classproperty
     @classmethod
     @classmethod

+ 2 - 2
reflex/custom_components/custom_components.py

@@ -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",
         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(
     username: Optional[str] = typer.Option(
-        environment.TWINE_USERNAME,
+        environment.TWINE_USERNAME.get(),
         "-u",
         "-u",
         "--username",
         "--username",
         show_default="TWINE_USERNAME environment variable value if set",
         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.",
         help="The username to use for authentication on python package repository. Username and password must both be provided.",
     ),
     ),
     password: Optional[str] = typer.Option(
     password: Optional[str] = typer.Option(
-        environment.TWINE_PASSWORD,
+        environment.TWINE_PASSWORD.get(),
         "-p",
         "-p",
         "--password",
         "--password",
         show_default="TWINE_PASSWORD environment variable value if set",
         show_default="TWINE_PASSWORD environment variable value if set",

+ 7 - 7
reflex/model.py

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

+ 7 - 7
reflex/reflex.py

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

+ 1 - 1
reflex/state.py

@@ -3377,7 +3377,7 @@ class StateManagerRedis(StateManager):
             )
             )
         except ResponseError:
         except ResponseError:
             # Some redis servers only allow out-of-band configuration, so ignore errors here.
             # Some redis servers only allow out-of-band configuration, so ignore errors here.
-            if not environment.REFLEX_IGNORE_REDIS_CONFIG_ERROR:
+            if not environment.REFLEX_IGNORE_REDIS_CONFIG_ERROR.get():
                 raise
                 raise
         async with self.redis.pubsub() as pubsub:
         async with self.redis.pubsub() as pubsub:
             await pubsub.psubscribe(lock_key_channel)
             await pubsub.psubscribe(lock_key_channel)

+ 7 - 5
reflex/testing.py

@@ -43,6 +43,7 @@ import reflex.utils.exec
 import reflex.utils.format
 import reflex.utils.format
 import reflex.utils.prerequisites
 import reflex.utils.prerequisites
 import reflex.utils.processes
 import reflex.utils.processes
+from reflex.config import environment
 from reflex.state import (
 from reflex.state import (
     BaseState,
     BaseState,
     StateManager,
     StateManager,
@@ -250,6 +251,7 @@ class AppHarness:
 
 
     def _initialize_app(self):
     def _initialize_app(self):
         # disable telemetry reporting for tests
         # disable telemetry reporting for tests
+
         os.environ["TELEMETRY_ENABLED"] = "false"
         os.environ["TELEMETRY_ENABLED"] = "false"
         self.app_path.mkdir(parents=True, exist_ok=True)
         self.app_path.mkdir(parents=True, exist_ok=True)
         if self.app_source is not None:
         if self.app_source is not None:
@@ -615,10 +617,10 @@ class AppHarness:
         if self.frontend_url is None:
         if self.frontend_url is None:
             raise RuntimeError("Frontend is not running.")
             raise RuntimeError("Frontend is not running.")
         want_headless = False
         want_headless = False
-        if os.environ.get("APP_HARNESS_HEADLESS"):
+        if environment.APP_HARNESS_HEADLESS.get():
             want_headless = True
             want_headless = True
         if driver_clz is None:
         if driver_clz is None:
-            requested_driver = os.environ.get("APP_HARNESS_DRIVER", "Chrome")
+            requested_driver = environment.APP_HARNESS_DRIVER.get()
             driver_clz = getattr(webdriver, requested_driver)
             driver_clz = getattr(webdriver, requested_driver)
             if driver_options is None:
             if driver_options is None:
                 driver_options = getattr(webdriver, f"{requested_driver}Options")()
                 driver_options = getattr(webdriver, f"{requested_driver}Options")()
@@ -640,7 +642,7 @@ class AppHarness:
                 driver_options.add_argument("headless")
                 driver_options.add_argument("headless")
         if driver_options is None:
         if driver_options is None:
             raise RuntimeError(f"Could not determine options for {driver_clz}")
             raise RuntimeError(f"Could not determine options for {driver_clz}")
-        if args := os.environ.get("APP_HARNESS_DRIVER_ARGS"):
+        if args := environment.APP_HARNESS_DRIVER_ARGS.get():
             for arg in args.split(","):
             for arg in args.split(","):
                 driver_options.add_argument(arg)
                 driver_options.add_argument(arg)
         if driver_option_args is not None:
         if driver_option_args is not None:
@@ -944,7 +946,7 @@ class AppHarnessProd(AppHarness):
     def _start_backend(self):
     def _start_backend(self):
         if self.app_instance is None:
         if self.app_instance is None:
             raise RuntimeError("App was not initialized.")
             raise RuntimeError("App was not initialized.")
-        os.environ[reflex.constants.SKIP_COMPILE_ENV_VAR] = "yes"
+        environment.REFLEX_SKIP_COMPILE.set(True)
         self.backend = uvicorn.Server(
         self.backend = uvicorn.Server(
             uvicorn.Config(
             uvicorn.Config(
                 app=self.app_instance,
                 app=self.app_instance,
@@ -961,7 +963,7 @@ class AppHarnessProd(AppHarness):
         try:
         try:
             return super()._poll_for_servers(timeout)
             return super()._poll_for_servers(timeout)
         finally:
         finally:
-            os.environ.pop(reflex.constants.SKIP_COMPILE_ENV_VAR, None)
+            environment.REFLEX_SKIP_COMPILE.set(None)
 
 
     def stop(self):
     def stop(self):
         """Stop the frontend python webserver."""
         """Stop the frontend python webserver."""

+ 28 - 11
reflex/utils/exec.py

@@ -184,7 +184,7 @@ def should_use_granian():
     Returns:
     Returns:
         True if Granian should be used.
         True if Granian should be used.
     """
     """
-    return environment.REFLEX_USE_GRANIAN
+    return environment.REFLEX_USE_GRANIAN.get()
 
 
 
 
 def get_app_module():
 def get_app_module():
@@ -369,7 +369,9 @@ def run_uvicorn_backend_prod(host, port, loglevel):
         command,
         command,
         run=True,
         run=True,
         show_logs=True,
         show_logs=True,
-        env={constants.SKIP_COMPILE_ENV_VAR: "yes"},  # skip compile for prod backend
+        env={
+            environment.REFLEX_SKIP_COMPILE.name: "true"
+        },  # skip compile for prod backend
     )
     )
 
 
 
 
@@ -405,7 +407,7 @@ def run_granian_backend_prod(host, port, loglevel):
             run=True,
             run=True,
             show_logs=True,
             show_logs=True,
             env={
             env={
-                constants.SKIP_COMPILE_ENV_VAR: "yes"
+                environment.REFLEX_SKIP_COMPILE.name: "true"
             },  # skip compile for prod backend
             },  # skip compile for prod backend
         )
         )
     except ImportError:
     except ImportError:
@@ -491,11 +493,8 @@ def is_prod_mode() -> bool:
     Returns:
     Returns:
         True if the app is running in production mode or False if running in dev mode.
         True if the app is running in production mode or False if running in dev mode.
     """
     """
-    current_mode = os.environ.get(
-        constants.ENV_MODE_ENV_VAR,
-        constants.Env.DEV.value,
-    )
-    return current_mode == constants.Env.PROD.value
+    current_mode = environment.REFLEX_ENV_MODE.get()
+    return current_mode == constants.Env.PROD
 
 
 
 
 def is_frontend_only() -> bool:
 def is_frontend_only() -> bool:
@@ -504,7 +503,13 @@ def is_frontend_only() -> bool:
     Returns:
     Returns:
         True if the app is running in frontend-only mode.
         True if the app is running in frontend-only mode.
     """
     """
-    return os.environ.get(constants.ENV_FRONTEND_ONLY_ENV_VAR, "").lower() == "true"
+    console.deprecate(
+        "is_frontend_only() is deprecated and will be removed in a future release.",
+        reason="Use `environment.REFLEX_FRONTEND_ONLY.get()` instead.",
+        deprecation_version="0.6.5",
+        removal_version="0.7.0",
+    )
+    return environment.REFLEX_FRONTEND_ONLY.get()
 
 
 
 
 def is_backend_only() -> bool:
 def is_backend_only() -> bool:
@@ -513,7 +518,13 @@ def is_backend_only() -> bool:
     Returns:
     Returns:
         True if the app is running in backend-only mode.
         True if the app is running in backend-only mode.
     """
     """
-    return os.environ.get(constants.ENV_BACKEND_ONLY_ENV_VAR, "").lower() == "true"
+    console.deprecate(
+        "is_backend_only() is deprecated and will be removed in a future release.",
+        reason="Use `environment.REFLEX_BACKEND_ONLY.get()` instead.",
+        deprecation_version="0.6.5",
+        removal_version="0.7.0",
+    )
+    return environment.REFLEX_BACKEND_ONLY.get()
 
 
 
 
 def should_skip_compile() -> bool:
 def should_skip_compile() -> bool:
@@ -522,4 +533,10 @@ def should_skip_compile() -> bool:
     Returns:
     Returns:
         True if the app should skip compile.
         True if the app should skip compile.
     """
     """
-    return os.environ.get(constants.SKIP_COMPILE_ENV_VAR) == "yes"
+    console.deprecate(
+        "should_skip_compile() is deprecated and will be removed in a future release.",
+        reason="Use `environment.REFLEX_SKIP_COMPILE.get()` instead.",
+        deprecation_version="0.6.5",
+        removal_version="0.7.0",
+    )
+    return environment.REFLEX_SKIP_COMPILE.get()

+ 1 - 1
reflex/utils/net.py

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

+ 2 - 2
reflex/utils/path_ops.py

@@ -136,7 +136,7 @@ def use_system_node() -> bool:
     Returns:
     Returns:
         Whether the system node should be used.
         Whether the system node should be used.
     """
     """
-    return environment.REFLEX_USE_SYSTEM_NODE
+    return environment.REFLEX_USE_SYSTEM_NODE.get()
 
 
 
 
 def use_system_bun() -> bool:
 def use_system_bun() -> bool:
@@ -145,7 +145,7 @@ def use_system_bun() -> bool:
     Returns:
     Returns:
         Whether the system bun should be used.
         Whether the system bun should be used.
     """
     """
-    return environment.REFLEX_USE_SYSTEM_BUN
+    return environment.REFLEX_USE_SYSTEM_BUN.get()
 
 
 
 
 def get_node_bin_path() -> Path | None:
 def get_node_bin_path() -> Path | None:

+ 11 - 8
reflex/utils/prerequisites.py

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

+ 1 - 1
reflex/utils/registry.py

@@ -55,4 +55,4 @@ def _get_npm_registry() -> str:
     Returns:
     Returns:
         str:
         str:
     """
     """
-    return environment.NPM_CONFIG_REGISTRY or get_best_registry()
+    return environment.NPM_CONFIG_REGISTRY.get() or get_best_registry()

+ 3 - 2
reflex/utils/telemetry.py

@@ -8,6 +8,8 @@ import multiprocessing
 import platform
 import platform
 import warnings
 import warnings
 
 
+from reflex.config import environment
+
 try:
 try:
     from datetime import UTC, datetime
     from datetime import UTC, datetime
 except ImportError:
 except ImportError:
@@ -20,7 +22,6 @@ import psutil
 
 
 from reflex import constants
 from reflex import constants
 from reflex.utils import console
 from reflex.utils import console
-from reflex.utils.exec import should_skip_compile
 from reflex.utils.prerequisites import ensure_reflex_installation_id, get_project_hash
 from reflex.utils.prerequisites import ensure_reflex_installation_id, get_project_hash
 
 
 POSTHOG_API_URL: str = "https://app.posthog.com/capture/"
 POSTHOG_API_URL: str = "https://app.posthog.com/capture/"
@@ -94,7 +95,7 @@ def _raise_on_missing_project_hash() -> bool:
         False when compilation should be skipped (i.e. no .web directory is required).
         False when compilation should be skipped (i.e. no .web directory is required).
         Otherwise return True.
         Otherwise return True.
     """
     """
-    return not should_skip_compile()
+    return not environment.REFLEX_SKIP_COMPILE.get()
 
 
 
 
 def _prepare_event(event: str, **kwargs) -> dict:
 def _prepare_event(event: str, **kwargs) -> dict:

+ 3 - 2
tests/integration/conftest.py

@@ -6,6 +6,7 @@ from pathlib import Path
 
 
 import pytest
 import pytest
 
 
+from reflex.config import environment
 from reflex.testing import AppHarness, AppHarnessProd
 from reflex.testing import AppHarness, AppHarnessProd
 
 
 DISPLAY = None
 DISPLAY = None
@@ -21,7 +22,7 @@ def xvfb():
     Yields:
     Yields:
         the pyvirtualdisplay object that the browser will be open on
         the pyvirtualdisplay object that the browser will be open on
     """
     """
-    if os.environ.get("GITHUB_ACTIONS") and not os.environ.get("APP_HARNESS_HEADLESS"):
+    if os.environ.get("GITHUB_ACTIONS") and not environment.APP_HARNESS_HEADLESS.get():
         from pyvirtualdisplay.smartdisplay import (  # pyright: ignore [reportMissingImports]
         from pyvirtualdisplay.smartdisplay import (  # pyright: ignore [reportMissingImports]
             SmartDisplay,
             SmartDisplay,
         )
         )
@@ -42,7 +43,7 @@ def pytest_exception_interact(node, call, report):
         call: The pytest call describing when/where the test was invoked.
         call: The pytest call describing when/where the test was invoked.
         report: The pytest log report object.
         report: The pytest log report object.
     """
     """
-    screenshot_dir = os.environ.get("SCREENSHOT_DIR")
+    screenshot_dir = environment.SCREENSHOT_DIR.get()
     if DISPLAY is None or screenshot_dir is None:
     if DISPLAY is None or screenshot_dir is None:
         return
         return
 
 

+ 39 - 1
tests/units/test_config.py

@@ -8,6 +8,8 @@ import pytest
 import reflex as rx
 import reflex as rx
 import reflex.config
 import reflex.config
 from reflex.config import (
 from reflex.config import (
+    EnvVar,
+    env_var,
     environment,
     environment,
     interpret_boolean_env,
     interpret_boolean_env,
     interpret_enum_env,
     interpret_enum_env,
@@ -214,7 +216,7 @@ def test_replace_defaults(
 
 
 
 
 def reflex_dir_constant() -> Path:
 def reflex_dir_constant() -> Path:
-    return environment.REFLEX_DIR
+    return environment.REFLEX_DIR.get()
 
 
 
 
 def test_reflex_dir_env_var(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
 def test_reflex_dir_env_var(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
@@ -227,6 +229,7 @@ def test_reflex_dir_env_var(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) ->
     monkeypatch.setenv("REFLEX_DIR", str(tmp_path))
     monkeypatch.setenv("REFLEX_DIR", str(tmp_path))
 
 
     mp_ctx = multiprocessing.get_context(method="spawn")
     mp_ctx = multiprocessing.get_context(method="spawn")
+    assert reflex_dir_constant() == tmp_path
     with mp_ctx.Pool(processes=1) as pool:
     with mp_ctx.Pool(processes=1) as pool:
         assert pool.apply(reflex_dir_constant) == tmp_path
         assert pool.apply(reflex_dir_constant) == tmp_path
 
 
@@ -242,3 +245,38 @@ def test_interpret_int_env() -> None:
 @pytest.mark.parametrize("value, expected", [("true", True), ("false", False)])
 @pytest.mark.parametrize("value, expected", [("true", True), ("false", False)])
 def test_interpret_bool_env(value: str, expected: bool) -> None:
 def test_interpret_bool_env(value: str, expected: bool) -> None:
     assert interpret_boolean_env(value, "TELEMETRY_ENABLED") == expected
     assert interpret_boolean_env(value, "TELEMETRY_ENABLED") == expected
+
+
+def test_env_var():
+    class TestEnv:
+        BLUBB: EnvVar[str] = env_var("default")
+        INTERNAL: EnvVar[str] = env_var("default", internal=True)
+        BOOLEAN: EnvVar[bool] = env_var(False)
+
+    assert TestEnv.BLUBB.get() == "default"
+    assert TestEnv.BLUBB.name == "BLUBB"
+    TestEnv.BLUBB.set("new")
+    assert os.environ.get("BLUBB") == "new"
+    assert TestEnv.BLUBB.get() == "new"
+    TestEnv.BLUBB.set(None)
+    assert "BLUBB" not in os.environ
+
+    assert TestEnv.INTERNAL.get() == "default"
+    assert TestEnv.INTERNAL.name == "__INTERNAL"
+    TestEnv.INTERNAL.set("new")
+    assert os.environ.get("__INTERNAL") == "new"
+    assert TestEnv.INTERNAL.get() == "new"
+    assert TestEnv.INTERNAL.getenv() == "new"
+    TestEnv.INTERNAL.set(None)
+    assert "__INTERNAL" not in os.environ
+
+    assert TestEnv.BOOLEAN.get() is False
+    assert TestEnv.BOOLEAN.name == "BOOLEAN"
+    TestEnv.BOOLEAN.set(True)
+    assert os.environ.get("BOOLEAN") == "True"
+    assert TestEnv.BOOLEAN.get() is True
+    TestEnv.BOOLEAN.set(False)
+    assert os.environ.get("BOOLEAN") == "False"
+    assert TestEnv.BOOLEAN.get() is False
+    TestEnv.BOOLEAN.set(None)
+    assert "BOOLEAN" not in os.environ

+ 9 - 0
tests/units/utils/test_utils.py

@@ -10,6 +10,7 @@ from packaging import version
 
 
 from reflex import constants
 from reflex import constants
 from reflex.base import Base
 from reflex.base import Base
+from reflex.config import environment
 from reflex.event import EventHandler
 from reflex.event import EventHandler
 from reflex.state import BaseState
 from reflex.state import BaseState
 from reflex.utils import (
 from reflex.utils import (
@@ -593,3 +594,11 @@ def test_style_prop_with_event_handler_value(callable):
         rx.box(
         rx.box(
             style=style,  # type: ignore
             style=style,  # type: ignore
         )
         )
+
+
+def test_is_prod_mode() -> None:
+    """Test that the prod mode is correctly determined."""
+    environment.REFLEX_ENV_MODE.set(constants.Env.PROD)
+    assert utils_exec.is_prod_mode()
+    environment.REFLEX_ENV_MODE.set(None)
+    assert not utils_exec.is_prod_mode()