Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move all environment variables to the same place #4192

Merged
merged 14 commits into from
Oct 21, 2024
9 changes: 5 additions & 4 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
)
from reflex.components.core.upload import Upload, get_upload_dir
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.model import Model, get_db_status
from reflex.page import (
Expand Down Expand Up @@ -957,15 +957,16 @@ def get_compilation_time() -> str:
executor = None
if (
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(
max_workers=int(os.environ.get("REFLEX_COMPILE_PROCESSES", 0)) or None,
max_workers=number_of_processes,
mp_context=multiprocessing.get_context("fork"),
)
else:
executor = concurrent.futures.ThreadPoolExecutor(
max_workers=int(os.environ.get("REFLEX_COMPILE_THREADS", 0)) or None,
max_workers=environment.REFLEX_COMPILE_THREADS
)

with executor:
Expand Down
5 changes: 2 additions & 3 deletions reflex/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

import os
from datetime import datetime
from pathlib import Path
from typing import Dict, Iterable, Optional, Type, Union
Expand All @@ -16,7 +15,7 @@
CustomComponent,
StatefulComponent,
)
from reflex.config import get_config
from reflex.config import environment, get_config
from reflex.state import BaseState
from reflex.style import SYSTEM_COLOR_MODE
from reflex.utils.exec import is_prod_mode
Expand Down Expand Up @@ -527,7 +526,7 @@ def remove_tailwind_from_postcss() -> tuple[str, str]:

def purge_web_pages_dir():
"""Empty out .web/pages directory."""
if not is_prod_mode() and 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.
return

Expand Down
6 changes: 2 additions & 4 deletions reflex/components/core/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

from __future__ import annotations

import os
from pathlib import Path
from typing import Any, Callable, ClassVar, Dict, List, Optional, Tuple

from reflex.components.component import Component, ComponentNamespace, MemoizationLeaf
from reflex.components.el.elements.forms import Input
from reflex.components.radix.themes.layout.box import Box
from reflex.config import environment
from reflex.constants import Dirs
from reflex.event import (
CallableEventSpec,
Expand Down Expand Up @@ -125,9 +125,7 @@ def get_upload_dir() -> Path:
"""
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)
return uploaded_files_dir

Expand Down
198 changes: 197 additions & 1 deletion reflex/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

from __future__ import annotations

import dataclasses
import importlib
import os
import sys
import urllib.parse
from pathlib import Path
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:
import pydantic.v1 as pydantic
Expand Down Expand Up @@ -131,6 +135,198 @@ def get_url(self) -> str:
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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this probably needs handling for Union as well, see #4205

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're not using union for our field types though

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(at least not more than just optional)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're not using union for our field types though

What about bun_path? https://github.com/reflex-dev/reflex/blob/main/reflex/config.py#L194

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bun_path goes through a different code path, and it doesn't use interpret_env_var_value (maybe it should?), the issue here is we accept str but we're going to convert it to Path anyways, so it acts like a nice API but not true "type", i propose changing it to just Path instead of Union (which does kick the bucket of figuring out how to do true unions to a later day)

Copy link
Contributor

@benedikt-bartscher benedikt-bartscher Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, you are right. I haven't realized that your PR did not touch update_from_env at all. Maybe I should just base #4205 on your PR and utilize your interpret_env_var_value to streamline this type parsing logic.

i propose changing it to just Path instead of Union

sounds good!

(which does kick the bucket of figuring out how to do true unions to a later day)

let's raise an exception for unhandled unions to prevent someone introducing a bug by adding a union to env/config
f.e. BUN_PATH is currently broken on main due to this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that sounds alright by me, we can merge this for now

"""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):
"""The config defines runtime settings for the app.

Expand Down
Loading
Loading