Răsfoiți Sursa

move all environment variables to the same place (#4192)

* move all environment variables to the same place

* reorder things around

* move more variables to environment

* remove cyclical imports

* forgot default value for field

* for some reason type hints aren't being interpreted

* put the field type *before* not after

* make it get

* move a bit more

* add more fields

* move reflex dir

* add return

* put things somewhere else

* add custom error
Khaleel Al-Adhami 6 luni în urmă
părinte
comite
f39e8c9667

+ 5 - 4
reflex/app.py

@@ -64,7 +64,7 @@ from reflex.components.core.client_side_routing import (
 )
 )
 from reflex.components.core.upload import Upload, get_upload_dir
 from reflex.components.core.upload import Upload, get_upload_dir
 from reflex.components.radix import themes
 from reflex.components.radix import themes
-from reflex.config import get_config
+from reflex.config import environment, get_config
 from reflex.event import Event, EventHandler, EventSpec, window_alert
 from reflex.event import Event, EventHandler, EventSpec, window_alert
 from reflex.model import Model, get_db_status
 from reflex.model import Model, get_db_status
 from reflex.page import (
 from reflex.page import (
@@ -957,15 +957,16 @@ class App(MiddlewareMixin, LifespanMixin, Base):
         executor = None
         executor = None
         if (
         if (
             platform.system() in ("Linux", "Darwin")
             platform.system() in ("Linux", "Darwin")
-            and os.environ.get("REFLEX_COMPILE_PROCESSES") is not None
+            and (number_of_processes := environment.REFLEX_COMPILE_PROCESSES)
+            is not None
         ):
         ):
             executor = concurrent.futures.ProcessPoolExecutor(
             executor = concurrent.futures.ProcessPoolExecutor(
-                max_workers=int(os.environ.get("REFLEX_COMPILE_PROCESSES", 0)) or None,
+                max_workers=number_of_processes,
                 mp_context=multiprocessing.get_context("fork"),
                 mp_context=multiprocessing.get_context("fork"),
             )
             )
         else:
         else:
             executor = concurrent.futures.ThreadPoolExecutor(
             executor = concurrent.futures.ThreadPoolExecutor(
-                max_workers=int(os.environ.get("REFLEX_COMPILE_THREADS", 0)) or None,
+                max_workers=environment.REFLEX_COMPILE_THREADS
             )
             )
 
 
         with executor:
         with executor:

+ 2 - 3
reflex/compiler/compiler.py

@@ -2,7 +2,6 @@
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
-import os
 from datetime import datetime
 from datetime import datetime
 from pathlib import Path
 from pathlib import Path
 from typing import Dict, Iterable, Optional, Type, Union
 from typing import Dict, Iterable, Optional, Type, Union
@@ -16,7 +15,7 @@ from reflex.components.component import (
     CustomComponent,
     CustomComponent,
     StatefulComponent,
     StatefulComponent,
 )
 )
-from reflex.config import get_config
+from reflex.config import environment, get_config
 from reflex.state import BaseState
 from reflex.state import BaseState
 from reflex.style import SYSTEM_COLOR_MODE
 from reflex.style import SYSTEM_COLOR_MODE
 from reflex.utils.exec import is_prod_mode
 from reflex.utils.exec import is_prod_mode
@@ -527,7 +526,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 os.environ.get("REFLEX_PERSIST_WEB_DIR"):
+    if not is_prod_mode() and environment.REFLEX_PERSIST_WEB_DIR:
         # 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
 
 

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

@@ -2,13 +2,13 @@
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
-import os
 from pathlib import Path
 from pathlib import Path
 from typing import Any, Callable, ClassVar, Dict, List, Optional, Tuple
 from typing import Any, Callable, ClassVar, Dict, List, Optional, Tuple
 
 
 from reflex.components.component import Component, ComponentNamespace, MemoizationLeaf
 from reflex.components.component import Component, ComponentNamespace, MemoizationLeaf
 from reflex.components.el.elements.forms import Input
 from reflex.components.el.elements.forms import Input
 from reflex.components.radix.themes.layout.box import Box
 from reflex.components.radix.themes.layout.box import Box
+from reflex.config import environment
 from reflex.constants import Dirs
 from reflex.constants import Dirs
 from reflex.event import (
 from reflex.event import (
     CallableEventSpec,
     CallableEventSpec,
@@ -125,9 +125,7 @@ def get_upload_dir() -> Path:
     """
     """
     Upload.is_used = True
     Upload.is_used = True
 
 
-    uploaded_files_dir = Path(
-        os.environ.get("REFLEX_UPLOADED_FILES_DIR", "./uploaded_files")
-    )
+    uploaded_files_dir = environment.REFLEX_UPLOADED_FILES_DIR
     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
 
 

+ 197 - 1
reflex/config.py

@@ -2,6 +2,7 @@
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
+import dataclasses
 import importlib
 import importlib
 import os
 import os
 import sys
 import sys
@@ -9,7 +10,10 @@ import urllib.parse
 from pathlib import Path
 from pathlib import Path
 from typing import Any, Dict, List, Optional, Set, Union
 from typing import Any, Dict, List, Optional, Set, Union
 
 
-from reflex.utils.exceptions import ConfigError
+from typing_extensions import get_type_hints
+
+from reflex.utils.exceptions import ConfigError, EnvironmentVarValueError
+from reflex.utils.types import value_inside_optional
 
 
 try:
 try:
     import pydantic.v1 as pydantic
     import pydantic.v1 as pydantic
@@ -131,6 +135,198 @@ class DBConfig(Base):
         return f"{self.engine}://{path}/{self.database}"
         return f"{self.engine}://{path}/{self.database}"
 
 
 
 
+def get_default_value_for_field(field: dataclasses.Field) -> Any:
+    """Get the default value for a field.
+
+    Args:
+        field: The field.
+
+    Returns:
+        The default value.
+
+    Raises:
+        ValueError: If no default value is found.
+    """
+    if field.default != dataclasses.MISSING:
+        return field.default
+    elif field.default_factory != dataclasses.MISSING:
+        return field.default_factory()
+    else:
+        raise ValueError(
+            f"Missing value for environment variable {field.name} and no default value found"
+        )
+
+
+def interpret_boolean_env(value: str) -> bool:
+    """Interpret a boolean environment variable value.
+
+    Args:
+        value: The environment variable value.
+
+    Returns:
+        The interpreted value.
+
+    Raises:
+        EnvironmentVarValueError: If the value is invalid.
+    """
+    true_values = ["true", "1", "yes", "y"]
+    false_values = ["false", "0", "no", "n"]
+
+    if value.lower() in true_values:
+        return True
+    elif value.lower() in false_values:
+        return False
+    raise EnvironmentVarValueError(f"Invalid boolean value: {value}")
+
+
+def interpret_int_env(value: str) -> int:
+    """Interpret an integer environment variable value.
+
+    Args:
+        value: The environment variable value.
+
+    Returns:
+        The interpreted value.
+
+    Raises:
+        EnvironmentVarValueError: If the value is invalid.
+    """
+    try:
+        return int(value)
+    except ValueError as ve:
+        raise EnvironmentVarValueError(f"Invalid integer value: {value}") from ve
+
+
+def interpret_path_env(value: str) -> Path:
+    """Interpret a path environment variable value.
+
+    Args:
+        value: The environment variable value.
+
+    Returns:
+        The interpreted value.
+
+    Raises:
+        EnvironmentVarValueError: If the path does not exist.
+    """
+    path = Path(value)
+    if not path.exists():
+        raise EnvironmentVarValueError(f"Path does not exist: {path}")
+    return path
+
+
+def interpret_env_var_value(value: str, field: dataclasses.Field) -> Any:
+    """Interpret an environment variable value based on the field type.
+
+    Args:
+        value: The environment variable value.
+        field: The field.
+
+    Returns:
+        The interpreted value.
+
+    Raises:
+        ValueError: If the value is invalid.
+    """
+    field_type = value_inside_optional(field.type)
+
+    if field_type is bool:
+        return interpret_boolean_env(value)
+    elif field_type is str:
+        return value
+    elif field_type is int:
+        return interpret_int_env(value)
+    elif field_type is Path:
+        return interpret_path_env(value)
+
+    else:
+        raise ValueError(
+            f"Invalid type for environment variable {field.name}: {field_type}. This is probably an issue in Reflex."
+        )
+
+
+@dataclasses.dataclass(init=False)
+class EnvironmentVariables:
+    """Environment variables class to instantiate environment variables."""
+
+    # Whether to use npm over bun to install frontend packages.
+    REFLEX_USE_NPM: bool = False
+
+    # The npm registry to use.
+    NPM_CONFIG_REGISTRY: Optional[str] = None
+
+    # Whether to use Granian for the backend. Otherwise, use Uvicorn.
+    REFLEX_USE_GRANIAN: bool = False
+
+    # The username to use for authentication on python package repository. Username and password must both be provided.
+    TWINE_USERNAME: Optional[str] = None
+
+    # The password to use for authentication on python package repository. Username and password must both be provided.
+    TWINE_PASSWORD: Optional[str] = None
+
+    # Whether to use the system installed bun. If set to false, bun will be bundled with the app.
+    REFLEX_USE_SYSTEM_BUN: bool = False
+
+    # 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
+
+    # The working directory for the next.js commands.
+    REFLEX_WEB_WORKDIR: Path = Path(constants.Dirs.WEB)
+
+    # Path to the alembic config file
+    ALEMBIC_CONFIG: Path = Path(constants.ALEMBIC_CONFIG)
+
+    # Disable SSL verification for HTTPX requests.
+    SSL_NO_VERIFY: bool = False
+
+    # The directory to store uploaded files.
+    REFLEX_UPLOADED_FILES_DIR: Path = 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 seperate threads to compile the frontend and how many. Defaults to `min(32, os.cpu_count() + 4)`.
+    REFLEX_COMPILE_THREADS: Optional[int] = None
+
+    # The directory to store reflex dependencies.
+    REFLEX_DIR: Path = Path(constants.Reflex.DIR)
+
+    # Whether to print the SQL queries if the log level is INFO or lower.
+    SQLALCHEMY_ECHO: bool = False
+
+    # Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration.
+    REFLEX_IGNORE_REDIS_CONFIG_ERROR: bool = False
+
+    # Whether to skip purging the web directory in dev mode.
+    REFLEX_PERSIST_WEB_DIR: bool = False
+
+    # The reflex.build frontend host.
+    REFLEX_BUILD_FRONTEND: str = constants.Templates.REFLEX_BUILD_FRONTEND
+
+    # The reflex.build backend host.
+    REFLEX_BUILD_BACKEND: str = constants.Templates.REFLEX_BUILD_BACKEND
+
+    def __init__(self):
+        """Initialize the environment variables."""
+        type_hints = get_type_hints(type(self))
+
+        for field in dataclasses.fields(self):
+            raw_value = os.getenv(field.name, None)
+
+            field.type = type_hints.get(field.name) or field.type
+
+            value = (
+                interpret_env_var_value(raw_value, field)
+                if raw_value is not None
+                else get_default_value_for_field(field)
+            )
+
+            setattr(self, field.name, value)
+
+
+environment = EnvironmentVariables()
+
+
 class Config(Base):
 class Config(Base):
     """The config defines runtime settings for the app.
     """The config defines runtime settings for the app.
 
 

+ 53 - 34
reflex/constants/base.py

@@ -2,7 +2,6 @@
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
-import os
 import platform
 import platform
 from enum import Enum
 from enum import Enum
 from importlib import metadata
 from importlib import metadata
@@ -11,6 +10,8 @@ from types import SimpleNamespace
 
 
 from platformdirs import PlatformDirs
 from platformdirs import PlatformDirs
 
 
+from .utils import classproperty
+
 IS_WINDOWS = platform.system() == "Windows"
 IS_WINDOWS = platform.system() == "Windows"
 
 
 
 
@@ -20,6 +21,8 @@ class Dirs(SimpleNamespace):
     # The frontend directories in a project.
     # The frontend directories in a project.
     # The web folder where the NextJS app is compiled to.
     # The web folder where the NextJS app is compiled to.
     WEB = ".web"
     WEB = ".web"
+    # The directory where uploaded files are stored.
+    UPLOADED_FILES = "uploaded_files"
     # The name of the assets directory.
     # The name of the assets directory.
     APP_ASSETS = "assets"
     APP_ASSETS = "assets"
     # The name of the assets directory for external ressource (a subfolder of APP_ASSETS).
     # The name of the assets directory for external ressource (a subfolder of APP_ASSETS).
@@ -64,21 +67,13 @@ class Reflex(SimpleNamespace):
 
 
     # Files and directories used to init a new project.
     # Files and directories used to init a new project.
     # The directory to store reflex dependencies.
     # The directory to store reflex dependencies.
-    # Get directory value from enviroment variables if it exists.
-    _dir = os.environ.get("REFLEX_DIR", "")
-
-    DIR = Path(
-        _dir
-        or (
-            # on windows, we use C:/Users/<username>/AppData/Local/reflex.
-            # on macOS, we use ~/Library/Application Support/reflex.
-            # on linux, we use ~/.local/share/reflex.
-            # If user sets REFLEX_DIR envroment variable use that instead.
-            PlatformDirs(MODULE_NAME, False).user_data_dir
-        )
-    )
-    # The root directory of the reflex library.
+    # on windows, we use C:/Users/<username>/AppData/Local/reflex.
+    # on macOS, we use ~/Library/Application Support/reflex.
+    # on linux, we use ~/.local/share/reflex.
+    # If user sets REFLEX_DIR envroment variable use that instead.
+    DIR = PlatformDirs(MODULE_NAME, False).user_data_path
 
 
+    # The root directory of the reflex library.
     ROOT_DIR = Path(__file__).parents[2]
     ROOT_DIR = Path(__file__).parents[2]
 
 
     RELEASES_URL = f"https://api.github.com/repos/reflex-dev/templates/releases"
     RELEASES_URL = f"https://api.github.com/repos/reflex-dev/templates/releases"
@@ -101,27 +96,51 @@ class Templates(SimpleNamespace):
     DEFAULT = "blank"
     DEFAULT = "blank"
 
 
     # The reflex.build frontend host
     # The reflex.build frontend host
-    REFLEX_BUILD_FRONTEND = os.environ.get(
-        "REFLEX_BUILD_FRONTEND", "https://flexgen.reflex.run"
-    )
+    REFLEX_BUILD_FRONTEND = "https://flexgen.reflex.run"
 
 
     # The reflex.build backend host
     # The reflex.build backend host
-    REFLEX_BUILD_BACKEND = os.environ.get(
-        "REFLEX_BUILD_BACKEND", "https://flexgen-prod-flexgen.fly.dev"
-    )
-
-    # The URL to redirect to reflex.build
-    REFLEX_BUILD_URL = (
-        REFLEX_BUILD_FRONTEND + "/gen?reflex_init_token={reflex_init_token}"
-    )
-
-    # The URL to poll waiting for the user to select a generation.
-    REFLEX_BUILD_POLL_URL = REFLEX_BUILD_BACKEND + "/api/init/{reflex_init_token}"
-
-    # The URL to fetch the generation's reflex code
-    REFLEX_BUILD_CODE_URL = (
-        REFLEX_BUILD_BACKEND + "/api/gen/{generation_hash}/refactored"
-    )
+    REFLEX_BUILD_BACKEND = "https://flexgen-prod-flexgen.fly.dev"
+
+    @classproperty
+    @classmethod
+    def REFLEX_BUILD_URL(cls):
+        """The URL to redirect to reflex.build.
+
+        Returns:
+            The URL to redirect to reflex.build.
+        """
+        from reflex.config import environment
+
+        return (
+            environment.REFLEX_BUILD_FRONTEND
+            + "/gen?reflex_init_token={reflex_init_token}"
+        )
+
+    @classproperty
+    @classmethod
+    def REFLEX_BUILD_POLL_URL(cls):
+        """The URL to poll waiting for the user to select a generation.
+
+        Returns:
+            The URL to poll waiting for the user to select a generation.
+        """
+        from reflex.config import environment
+
+        return environment.REFLEX_BUILD_BACKEND + "/api/init/{reflex_init_token}"
+
+    @classproperty
+    @classmethod
+    def REFLEX_BUILD_CODE_URL(cls):
+        """The URL to fetch the generation's reflex code.
+
+        Returns:
+            The URL to fetch the generation's reflex code.
+        """
+        from reflex.config import environment
+
+        return (
+            environment.REFLEX_BUILD_BACKEND + "/api/gen/{generation_hash}/refactored"
+        )
 
 
     class Dirs(SimpleNamespace):
     class Dirs(SimpleNamespace):
         """Folders used by the template system of Reflex."""
         """Folders used by the template system of Reflex."""

+ 1 - 2
reflex/constants/config.py

@@ -1,6 +1,5 @@
 """Config constants."""
 """Config constants."""
 
 
-import os
 from pathlib import Path
 from pathlib import Path
 from types import SimpleNamespace
 from types import SimpleNamespace
 
 
@@ -9,7 +8,7 @@ from reflex.constants.base import Dirs, Reflex
 from .compiler import Ext
 from .compiler import Ext
 
 
 # Alembic migrations
 # Alembic migrations
-ALEMBIC_CONFIG = os.environ.get("ALEMBIC_CONFIG", "alembic.ini")
+ALEMBIC_CONFIG = "alembic.ini"
 
 
 
 
 class Config(SimpleNamespace):
 class Config(SimpleNamespace):

+ 84 - 28
reflex/constants/installer.py

@@ -3,9 +3,11 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
 import platform
 import platform
+from pathlib import Path
 from types import SimpleNamespace
 from types import SimpleNamespace
 
 
-from .base import IS_WINDOWS, Reflex
+from .base import IS_WINDOWS
+from .utils import classproperty
 
 
 
 
 def get_fnm_name() -> str | None:
 def get_fnm_name() -> str | None:
@@ -36,12 +38,9 @@ class Bun(SimpleNamespace):
 
 
     # The Bun version.
     # The Bun version.
     VERSION = "1.1.29"
     VERSION = "1.1.29"
+
     # Min Bun Version
     # Min Bun Version
     MIN_VERSION = "0.7.0"
     MIN_VERSION = "0.7.0"
-    # The directory to store the bun.
-    ROOT_PATH = Reflex.DIR / "bun"
-    # Default bun path.
-    DEFAULT_PATH = ROOT_PATH / "bin" / ("bun" if not IS_WINDOWS else "bun.exe")
 
 
     # URL to bun install script.
     # URL to bun install script.
     INSTALL_URL = "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/bun_install.sh"
     INSTALL_URL = "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/bun_install.sh"
@@ -50,11 +49,31 @@ class Bun(SimpleNamespace):
     WINDOWS_INSTALL_URL = (
     WINDOWS_INSTALL_URL = (
         "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/install.ps1"
         "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/install.ps1"
     )
     )
+
     # Path of the bunfig file
     # Path of the bunfig file
     CONFIG_PATH = "bunfig.toml"
     CONFIG_PATH = "bunfig.toml"
 
 
-    # The environment variable to use the system installed bun.
-    USE_SYSTEM_VAR = "REFLEX_USE_SYSTEM_BUN"
+    @classproperty
+    @classmethod
+    def ROOT_PATH(cls):
+        """The directory to store the bun.
+
+        Returns:
+            The directory to store the bun.
+        """
+        from reflex.config import environment
+
+        return environment.REFLEX_DIR / "bun"
+
+    @classproperty
+    @classmethod
+    def DEFAULT_PATH(cls):
+        """Default bun path.
+
+        Returns:
+            The default bun path.
+        """
+        return cls.ROOT_PATH / "bin" / ("bun" if not IS_WINDOWS else "bun.exe")
 
 
 
 
 # FNM config.
 # FNM config.
@@ -63,17 +82,36 @@ class Fnm(SimpleNamespace):
 
 
     # The FNM version.
     # The FNM version.
     VERSION = "1.35.1"
     VERSION = "1.35.1"
-    # The directory to store fnm.
-    DIR = Reflex.DIR / "fnm"
+
     FILENAME = get_fnm_name()
     FILENAME = get_fnm_name()
-    # The fnm executable binary.
-    EXE = DIR / ("fnm.exe" if IS_WINDOWS else "fnm")
 
 
     # The URL to the fnm release binary
     # The URL to the fnm release binary
     INSTALL_URL = (
     INSTALL_URL = (
         f"https://github.com/Schniz/fnm/releases/download/v{VERSION}/{FILENAME}.zip"
         f"https://github.com/Schniz/fnm/releases/download/v{VERSION}/{FILENAME}.zip"
     )
     )
 
 
+    @classproperty
+    @classmethod
+    def DIR(cls) -> Path:
+        """The directory to store fnm.
+
+        Returns:
+            The directory to store fnm.
+        """
+        from reflex.config import environment
+
+        return environment.REFLEX_DIR / "fnm"
+
+    @classproperty
+    @classmethod
+    def EXE(cls):
+        """The fnm executable binary.
+
+        Returns:
+            The fnm executable binary.
+        """
+        return cls.DIR / ("fnm.exe" if IS_WINDOWS else "fnm")
+
 
 
 # Node / NPM config
 # Node / NPM config
 class Node(SimpleNamespace):
 class Node(SimpleNamespace):
@@ -84,23 +122,41 @@ class Node(SimpleNamespace):
     # The minimum required node version.
     # The minimum required node version.
     MIN_VERSION = "18.17.0"
     MIN_VERSION = "18.17.0"
 
 
-    # The node bin path.
-    BIN_PATH = (
-        Fnm.DIR
-        / "node-versions"
-        / f"v{VERSION}"
-        / "installation"
-        / ("bin" if not IS_WINDOWS else "")
-    )
-
-    # The default path where node is installed.
-    PATH = BIN_PATH / ("node.exe" if IS_WINDOWS else "node")
-
-    # The default path where npm is installed.
-    NPM_PATH = BIN_PATH / "npm"
-
-    # The environment variable to use the system installed node.
-    USE_SYSTEM_VAR = "REFLEX_USE_SYSTEM_NODE"
+    @classproperty
+    @classmethod
+    def BIN_PATH(cls):
+        """The node bin path.
+
+        Returns:
+            The node bin path.
+        """
+        return (
+            Fnm.DIR
+            / "node-versions"
+            / f"v{cls.VERSION}"
+            / "installation"
+            / ("bin" if not IS_WINDOWS else "")
+        )
+
+    @classproperty
+    @classmethod
+    def PATH(cls):
+        """The default path where node is installed.
+
+        Returns:
+            The default path where node is installed.
+        """
+        return cls.BIN_PATH / ("node.exe" if IS_WINDOWS else "node")
+
+    @classproperty
+    @classmethod
+    def NPM_PATH(cls):
+        """The default path where npm is installed.
+
+        Returns:
+            The default path where npm is installed.
+        """
+        return cls.BIN_PATH / "npm"
 
 
 
 
 class PackageJson(SimpleNamespace):
 class PackageJson(SimpleNamespace):

+ 32 - 0
reflex/constants/utils.py

@@ -0,0 +1,32 @@
+"""Utility functions for constants."""
+
+from typing import Any, Callable, Generic, Type
+
+from typing_extensions import TypeVar
+
+T = TypeVar("T")
+V = TypeVar("V")
+
+
+class classproperty(Generic[T, V]):
+    """A class property decorator."""
+
+    def __init__(self, getter: Callable[[Type[T]], V]) -> None:
+        """Initialize the class property.
+
+        Args:
+            getter: The getter function.
+        """
+        self.getter = getattr(getter, "__func__", getter)
+
+    def __get__(self, instance: Any, owner: Type[T]) -> V:
+        """Get the value of the class property.
+
+        Args:
+            instance: The instance of the class.
+            owner: The class itself.
+
+        Returns:
+            The value of the class property.
+        """
+        return self.getter(owner)

+ 3 - 3
reflex/custom_components/custom_components.py

@@ -17,7 +17,7 @@ import typer
 from tomlkit.exceptions import TOMLKitError
 from tomlkit.exceptions import TOMLKitError
 
 
 from reflex import constants
 from reflex import constants
-from reflex.config import get_config
+from reflex.config import environment, get_config
 from reflex.constants import CustomComponents
 from reflex.constants import CustomComponents
 from reflex.utils import console
 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",
         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(
-        os.getenv("TWINE_USERNAME"),
+        environment.TWINE_USERNAME,
         "-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(
-        os.getenv("TWINE_PASSWORD"),
+        environment.TWINE_PASSWORD,
         "-p",
         "-p",
         "--password",
         "--password",
         show_default="TWINE_PASSWORD environment variable value if set",
         show_default="TWINE_PASSWORD environment variable value if set",

+ 8 - 11
reflex/model.py

@@ -2,9 +2,7 @@
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
-import os
 from collections import defaultdict
 from collections import defaultdict
-from pathlib import Path
 from typing import Any, ClassVar, Optional, Type, Union
 from typing import Any, ClassVar, Optional, Type, Union
 
 
 import alembic.autogenerate
 import alembic.autogenerate
@@ -18,9 +16,8 @@ import sqlalchemy
 import sqlalchemy.exc
 import sqlalchemy.exc
 import sqlalchemy.orm
 import sqlalchemy.orm
 
 
-from reflex import constants
 from reflex.base import Base
 from reflex.base import Base
-from reflex.config import get_config
+from reflex.config import environment, get_config
 from reflex.utils import console
 from reflex.utils import console
 from reflex.utils.compat import sqlmodel, sqlmodel_field_has_primary_key
 from reflex.utils.compat import sqlmodel, sqlmodel_field_has_primary_key
 
 
@@ -41,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 Path(constants.ALEMBIC_CONFIG).exists():
+    if environment.ALEMBIC_CONFIG.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 = os.environ.get("SQLALCHEMY_ECHO") == "True"
+    echo_db_query = environment.SQLALCHEMY_ECHO
     # 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)
@@ -234,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(constants.ALEMBIC_CONFIG)
+        config = alembic.config.Config(environment.ALEMBIC_CONFIG)
         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"),
         )
         )
@@ -269,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(constants.ALEMBIC_CONFIG),
-            directory=str(Path(constants.ALEMBIC_CONFIG).parent / "alembic"),
+            config=alembic.config.Config(environment.ALEMBIC_CONFIG),
+            directory=str(environment.ALEMBIC_CONFIG.parent / "alembic"),
         )
         )
 
 
     @classmethod
     @classmethod
@@ -290,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 Path(constants.ALEMBIC_CONFIG).exists():
+        if not environment.ALEMBIC_CONFIG.exists():
             return False
             return False
 
 
         config, script_directory = cls._alembic_config()
         config, script_directory = cls._alembic_config()
@@ -391,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 Path(constants.ALEMBIC_CONFIG).exists():
+        if not environment.ALEMBIC_CONFIG.exists():
             return
             return
 
 
         with cls.get_db_engine().connect() as connection:
         with cls.get_db_engine().connect() as connection:

+ 2 - 2
reflex/reflex.py

@@ -13,7 +13,7 @@ from reflex_cli.deployments import deployments_cli
 from reflex_cli.utils import dependency
 from reflex_cli.utils import dependency
 
 
 from reflex import constants
 from reflex import constants
-from reflex.config import get_config
+from reflex.config import environment, get_config
 from reflex.custom_components.custom_components import custom_components_cli
 from reflex.custom_components.custom_components import custom_components_cli
 from reflex.state import reset_disk_state_manager
 from reflex.state import reset_disk_state_manager
 from reflex.utils import console, redir, telemetry
 from reflex.utils import console, redir, telemetry
@@ -420,7 +420,7 @@ def db_init():
         return
         return
 
 
     # Check the alembic config.
     # Check the alembic config.
-    if Path(constants.ALEMBIC_CONFIG).exists():
+    if environment.ALEMBIC_CONFIG.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 "

+ 2 - 6
reflex/state.py

@@ -8,7 +8,6 @@ import copy
 import dataclasses
 import dataclasses
 import functools
 import functools
 import inspect
 import inspect
-import os
 import pickle
 import pickle
 import sys
 import sys
 import uuid
 import uuid
@@ -64,6 +63,7 @@ from redis.exceptions import ResponseError
 import reflex.istate.dynamic
 import reflex.istate.dynamic
 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 (
 from reflex.event import (
     BACKGROUND_TASK_MARKER,
     BACKGROUND_TASK_MARKER,
     Event,
     Event,
@@ -3274,11 +3274,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.
-            ignore_config_error = os.environ.get(
-                "REFLEX_IGNORE_REDIS_CONFIG_ERROR",
-                None,
-            )
-            if not ignore_config_error:
+            if not environment.REFLEX_IGNORE_REDIS_CONFIG_ERROR:
                 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)

+ 0 - 12
reflex/utils/build.py

@@ -23,18 +23,6 @@ def set_env_json():
     )
     )
 
 
 
 
-def set_os_env(**kwargs):
-    """Set os environment variables.
-
-    Args:
-        kwargs: env key word args.
-    """
-    for key, value in kwargs.items():
-        if not value:
-            continue
-        os.environ[key.upper()] = value
-
-
 def generate_sitemap_config(deploy_url: str, export=False):
 def generate_sitemap_config(deploy_url: str, export=False):
     """Generate the sitemap config file.
     """Generate the sitemap config file.
 
 

+ 4 - 0
reflex/utils/exceptions.py

@@ -135,3 +135,7 @@ class SetUndefinedStateVarError(ReflexError, AttributeError):
 
 
 class StateSchemaMismatchError(ReflexError, TypeError):
 class StateSchemaMismatchError(ReflexError, TypeError):
     """Raised when the serialized schema of a state class does not match the current schema."""
     """Raised when the serialized schema of a state class does not match the current schema."""
+
+
+class EnvironmentVarValueError(ReflexError, ValueError):
+    """Raised when an environment variable is set to an invalid value."""

+ 2 - 2
reflex/utils/exec.py

@@ -15,7 +15,7 @@ from urllib.parse import urljoin
 import psutil
 import psutil
 
 
 from reflex import constants
 from reflex import constants
-from reflex.config import get_config
+from reflex.config import environment, get_config
 from reflex.constants.base import LogLevel
 from reflex.constants.base import LogLevel
 from reflex.utils import console, path_ops
 from reflex.utils import console, path_ops
 from reflex.utils.prerequisites import get_web_dir
 from reflex.utils.prerequisites import get_web_dir
@@ -184,7 +184,7 @@ def should_use_granian():
     Returns:
     Returns:
         True if Granian should be used.
         True if Granian should be used.
     """
     """
-    return os.getenv("REFLEX_USE_GRANIAN", "0") == "1"
+    return environment.REFLEX_USE_GRANIAN
 
 
 
 
 def get_app_module():
 def get_app_module():

+ 2 - 4
reflex/utils/net.py

@@ -1,9 +1,8 @@
 """Helpers for downloading files from the network."""
 """Helpers for downloading files from the network."""
 
 
-import os
-
 import httpx
 import httpx
 
 
+from ..config import environment
 from . import console
 from . import console
 
 
 
 
@@ -13,8 +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
     """
     """
-    ssl_no_verify = os.environ.get("SSL_NO_VERIFY", "").lower() in ["true", "1", "yes"]
-    return not ssl_no_verify
+    return not environment.SSL_NO_VERIFY
 
 
 
 
 def get(url: str, **kwargs) -> httpx.Response:
 def get(url: str, **kwargs) -> httpx.Response:

+ 3 - 19
reflex/utils/path_ops.py

@@ -9,6 +9,7 @@ import shutil
 from pathlib import Path
 from pathlib import Path
 
 
 from reflex import constants
 from reflex import constants
+from reflex.config import environment
 
 
 # Shorthand for join.
 # Shorthand for join.
 join = os.linesep.join
 join = os.linesep.join
@@ -129,30 +130,13 @@ def which(program: str | Path) -> str | Path | None:
     return shutil.which(str(program))
     return shutil.which(str(program))
 
 
 
 
-def use_system_install(var_name: str) -> bool:
-    """Check if the system install should be used.
-
-    Args:
-        var_name: The name of the environment variable.
-
-    Raises:
-        ValueError: If the variable name is invalid.
-
-    Returns:
-        Whether the associated env var should use the system install.
-    """
-    if not var_name.startswith("REFLEX_USE_SYSTEM_"):
-        raise ValueError("Invalid system install variable name.")
-    return os.getenv(var_name, "").lower() in ["true", "1", "yes"]
-
-
 def use_system_node() -> bool:
 def use_system_node() -> bool:
     """Check if the system node should be used.
     """Check if the system node should be used.
 
 
     Returns:
     Returns:
         Whether the system node should be used.
         Whether the system node should be used.
     """
     """
-    return use_system_install(constants.Node.USE_SYSTEM_VAR)
+    return environment.REFLEX_USE_SYSTEM_NODE
 
 
 
 
 def use_system_bun() -> bool:
 def use_system_bun() -> bool:
@@ -161,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 use_system_install(constants.Bun.USE_SYSTEM_VAR)
+    return environment.REFLEX_USE_SYSTEM_BUN
 
 
 
 
 def get_node_bin_path() -> Path | None:
 def get_node_bin_path() -> Path | None:

+ 8 - 9
reflex/utils/prerequisites.py

@@ -33,7 +33,7 @@ from redis.asyncio import Redis
 
 
 from reflex import constants, model
 from reflex import constants, model
 from reflex.compiler import templates
 from reflex.compiler import templates
-from reflex.config import Config, get_config
+from reflex.config import Config, environment, get_config
 from reflex.utils import console, net, path_ops, processes
 from reflex.utils import console, net, path_ops, processes
 from reflex.utils.exceptions import GeneratedCodeHasNoFunctionDefs
 from reflex.utils.exceptions import GeneratedCodeHasNoFunctionDefs
 from reflex.utils.format import format_library_name
 from reflex.utils.format import format_library_name
@@ -69,8 +69,7 @@ def get_web_dir() -> Path:
     Returns:
     Returns:
         The working directory.
         The working directory.
     """
     """
-    workdir = Path(os.getenv("REFLEX_WEB_WORKDIR", constants.Dirs.WEB))
-    return workdir
+    return environment.REFLEX_WEB_WORKDIR
 
 
 
 
 def _python_version_check():
 def _python_version_check():
@@ -250,7 +249,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 os.environ.get("REFLEX_USE_NPM", "").lower() in ["true", "1", "yes"]
+    return environment.REFLEX_USE_NPM
 
 
 
 
 def get_app(reload: bool = False) -> ModuleType:
 def get_app(reload: bool = False) -> ModuleType:
@@ -992,7 +991,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 constants.Reflex.DIR.exists():
+    if not environment.REFLEX_DIR.exists():
         return True
         return True
 
 
     # Make sure the .web directory exists in frontend mode.
     # Make sure the .web directory exists in frontend mode.
@@ -1097,7 +1096,7 @@ def ensure_reflex_installation_id() -> Optional[int]:
     """
     """
     try:
     try:
         initialize_reflex_user_directory()
         initialize_reflex_user_directory()
-        installation_id_file = constants.Reflex.DIR / "installation_id"
+        installation_id_file = environment.REFLEX_DIR / "installation_id"
 
 
         installation_id = None
         installation_id = None
         if installation_id_file.exists():
         if installation_id_file.exists():
@@ -1122,7 +1121,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(constants.Reflex.DIR)
+    path_ops.mkdir(environment.REFLEX_DIR)
 
 
 
 
 def initialize_frontend_dependencies():
 def initialize_frontend_dependencies():
@@ -1145,7 +1144,7 @@ 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 Path(constants.ALEMBIC_CONFIG).exists():
+    if get_config().db_url is not None and not environment.ALEMBIC_CONFIG.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."
         )
         )
@@ -1155,7 +1154,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 Path(constants.ALEMBIC_CONFIG).exists():
+    if get_config().db_url is None or not environment.ALEMBIC_CONFIG.exists():
         return
         return
     with model.Model.get_db_engine().connect() as connection:
     with model.Model.get_db_engine().connect() as connection:
         try:
         try:

+ 2 - 6
reflex/utils/registry.py

@@ -1,9 +1,8 @@
 """Utilities for working with registries."""
 """Utilities for working with registries."""
 
 
-import os
-
 import httpx
 import httpx
 
 
+from reflex.config import environment
 from reflex.utils import console, net
 from reflex.utils import console, net
 
 
 
 
@@ -56,7 +55,4 @@ def _get_npm_registry() -> str:
     Returns:
     Returns:
         str:
         str:
     """
     """
-    if npm_registry := os.environ.get("NPM_CONFIG_REGISTRY", ""):
-        return npm_registry
-    else:
-        return get_best_registry()
+    return environment.NPM_CONFIG_REGISTRY or get_best_registry()

+ 14 - 0
reflex/utils/types.py

@@ -274,6 +274,20 @@ def is_optional(cls: GenericType) -> bool:
     return is_union(cls) and type(None) in get_args(cls)
     return is_union(cls) and type(None) in get_args(cls)
 
 
 
 
+def value_inside_optional(cls: GenericType) -> GenericType:
+    """Get the value inside an Optional type or the original type.
+
+    Args:
+        cls: The class to check.
+
+    Returns:
+        The value inside the Optional type or the original type.
+    """
+    if is_union(cls) and len(args := get_args(cls)) >= 2 and type(None) in args:
+        return unionize(*[arg for arg in args if arg is not type(None)])
+    return cls
+
+
 def get_property_hint(attr: Any | None) -> GenericType | None:
 def get_property_hint(attr: Any | None) -> GenericType | None:
     """Check if an attribute is a property and return its type hint.
     """Check if an attribute is a property and return its type hint.
 
 

+ 2 - 1
tests/units/test_config.py

@@ -5,6 +5,7 @@ import pytest
 
 
 import reflex as rx
 import reflex as rx
 import reflex.config
 import reflex.config
+from reflex.config import environment
 from reflex.constants import Endpoint
 from reflex.constants import Endpoint
 
 
 
 
@@ -178,7 +179,7 @@ def test_replace_defaults(
 
 
 
 
 def reflex_dir_constant():
 def reflex_dir_constant():
-    return rx.constants.Reflex.DIR
+    return environment.REFLEX_DIR
 
 
 
 
 def test_reflex_dir_env_var(monkeypatch, tmp_path):
 def test_reflex_dir_env_var(monkeypatch, tmp_path):