Skip to content
7 changes: 3 additions & 4 deletions litestar/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import inspect
import itertools
import logging
import os
import pdb # noqa: T100
import warnings
from collections import defaultdict
Expand Down Expand Up @@ -53,7 +52,7 @@
from litestar.stores.registry import StoreRegistry
from litestar.types import Empty, TypeDecodersSequence
from litestar.types.internal_types import PathParameterDefinition, RouteHandlerMapItem, TemplateConfigType
from litestar.utils import ensure_async_callable, join_paths, unique
from litestar.utils import ensure_async_callable, envflag, join_paths, unique
from litestar.utils.dataclass import extract_dataclass_items
from litestar.utils.predicates import is_async_callable, is_class_and_subclass
from litestar.utils.warnings import warn_pdb_on_exception
Expand Down Expand Up @@ -334,10 +333,10 @@ def __init__(
logging_config = LoggingConfig()

if debug is None:
debug = os.getenv("LITESTAR_DEBUG", "0") == "1"
debug = envflag("LITESTAR_DEBUG")

if pdb_on_exception is None:
pdb_on_exception = os.getenv("LITESTAR_PDB", "0") == "1"
pdb_on_exception = envflag("LITESTAR_PDB")

config = AppConfig(
after_exception=list(after_exception or []),
Expand Down
6 changes: 3 additions & 3 deletions litestar/cli/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

from litestar import Litestar, __version__
from litestar.middleware import DefineMiddleware
from litestar.utils import get_name
from litestar.utils import envflag, get_name

if sys.version_info >= (3, 10):
from importlib.metadata import entry_points
Expand Down Expand Up @@ -113,7 +113,7 @@ def from_env(cls, app_path: str | None, app_dir: Path | None = None) -> Litestar
dotenv.load_dotenv()
app_path = app_path or getenv("LITESTAR_APP")
app_name = getenv("LITESTAR_APP_NAME") or "Litestar"
quiet_console = getenv("LITESTAR_QUIET_CONSOLE") or False
quiet_console = envflag("LITESTAR_QUIET_CONSOLE")
if app_path and getenv("LITESTAR_APP") is None:
os.environ["LITESTAR_APP"] = app_path
if app_path:
Expand Down Expand Up @@ -345,7 +345,7 @@ def _autodiscovery_paths(base_dir: Path, arbitrary: bool = True) -> Generator[Pa

def _autodiscover_app(cwd: Path) -> LoadedApp:
app_name = getenv("LITESTAR_APP_NAME") or "Litestar"
quiet_console = getenv("LITESTAR_QUIET_CONSOLE") or False
quiet_console = envflag("LITESTAR_QUIET_CONSOLE")
for file_path in _autodiscovery_paths(cwd):
import_path = _path_to_dotted_path(file_path.relative_to(cwd))
module = importlib.import_module(import_path)
Expand Down
11 changes: 10 additions & 1 deletion litestar/cli/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ def info_command(app: Litestar) -> None:
is_flag=True,
envvar="LITESTAR_CREATE_SELF_SIGNED_CERT",
)
@click.option(
"-q",
"--quiet-console",
envvar="LITESTAR_QUIET_CONSOLE",
help="Suppress formatted console output (useful for non-TTY environments, logs, and CI/CD",
is_flag=True,
default=False,
)
def run_command(
reload: bool,
port: int,
Expand All @@ -220,6 +228,7 @@ def run_command(
ssl_certfile: str | None,
ssl_keyfile: str | None,
create_self_signed_cert: bool,
quiet_console: bool,
ctx: click.Context,
) -> None:
"""Run a Litestar app. (requires 'uvicorn' to be installed).
Expand All @@ -234,7 +243,7 @@ def run_command(

if pdb:
os.environ["LITESTAR_PDB"] = "1"
quiet_console = os.getenv("LITESTAR_QUIET_CONSOLE") or False

if not UVICORN_INSTALLED:
console.print(
r"uvicorn is not installed. Please install the standard group, litestar\[standard], to use this command."
Expand Down
3 changes: 2 additions & 1 deletion litestar/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from litestar.utils.deprecation import deprecated, warn_deprecation

from .helpers import get_enum_string_value, get_name, unique_name_for_scope, url_quote
from .helpers import envflag, get_enum_string_value, get_name, unique_name_for_scope, url_quote
from .path import join_paths, normalize_path
from .predicates import (
is_annotated_type,
Expand Down Expand Up @@ -29,6 +29,7 @@
"AsyncIteratorWrapper",
"deprecated",
"ensure_async_callable",
"envflag",
"find_index",
"get_enum_string_value",
"get_name",
Expand Down
23 changes: 23 additions & 0 deletions litestar/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from enum import Enum
from functools import partial
from os import getenv
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
from os import getenv
import os

from typing import TYPE_CHECKING, TypeVar, cast
from urllib.parse import quote

Expand Down Expand Up @@ -102,3 +103,25 @@ def get_exception_group() -> type[BaseException]:
from exceptiongroup import ExceptionGroup as _ExceptionGroup # pyright: ignore

return cast("type[BaseException]", _ExceptionGroup)


def envflag(varname: str, default: bool = False) -> bool:
Copy link
Member

Choose a reason for hiding this comment

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

This does not behave like click. It will consider all values it doesn't consider True to be False, so e.g. a typo like Tru or 2 would also be considered False.

It should raise in that case instead. Please consult the click docs in the original issue I've linked on the actual behaviour

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback! I’ve addressed your comments.

According to the click documentation you linked: "Any other value is interpreted as deactivating the flag". This means invalid values like Tru or 2 should return False, not raise an error. Could you clarify what behavior you're expecting?

"""Get the boolean value of an environment variable.

Determines whether an environment variable is set to a truthy value.
Returns True if the variable exists and its value matches one of:
"1", "true", "t", "yes", "on", "y" (case-insensitive).
Otherwise, including when the variable is not set, returns False.

Args:
varname (str): The name of the environment variable to check.
default (bool): Value to return if the variable is not set OR empty.

Returns:
bool: True if the environment variable is set to a truthy value,
otherwise False.
"""
envvar = getenv(varname)
if not envvar:
return default
Copy link
Member

Choose a reason for hiding this comment

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

This will return default if the value is set to "", which is not what we want.

return envvar.lower() in ("1", "true", "t", "yes", "on", "y")
10 changes: 5 additions & 5 deletions litestar/utils/warnings.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from __future__ import annotations

import os
import warnings
from typing import TYPE_CHECKING

from litestar.exceptions import LitestarWarning
from litestar.utils import envflag

if TYPE_CHECKING:
import re
Expand All @@ -13,7 +13,7 @@


def warn_implicit_sync_to_thread(source: AnyCallable, stacklevel: int = 2) -> None:
if os.getenv("LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD") == "0":
if not envflag("LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD", default=True):
return

warnings.warn(
Expand All @@ -29,7 +29,7 @@ def warn_implicit_sync_to_thread(source: AnyCallable, stacklevel: int = 2) -> No


def warn_sync_to_thread_with_async_callable(source: AnyCallable, stacklevel: int = 2) -> None:
if os.getenv("LITESTAR_WARN_SYNC_TO_THREAD_WITH_ASYNC") == "0":
if not envflag("LITESTAR_WARN_SYNC_TO_THREAD_WITH_ASYNC", default=True):
return

warnings.warn(
Expand All @@ -42,7 +42,7 @@ def warn_sync_to_thread_with_async_callable(source: AnyCallable, stacklevel: int


def warn_sync_to_thread_with_generator(source: AnyGenerator, stacklevel: int = 2) -> None:
if os.getenv("LITESTAR_WARN_SYNC_TO_THREAD_WITH_GENERATOR") == "0":
if not envflag("LITESTAR_WARN_SYNC_TO_THREAD_WITH_GENERATOR", default=True):
return

warnings.warn(
Expand Down Expand Up @@ -73,7 +73,7 @@ def warn_middleware_excluded_on_all_routes(


def warn_signature_namespace_override(signature_key: str, stacklevel: int = 2) -> None:
if os.getenv("LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE") == "0":
if not envflag("LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE", default=True):
return

warnings.warn(
Expand Down
40 changes: 39 additions & 1 deletion tests/unit/test_utils/test_helpers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import os
from functools import partial
from typing import Any, Generic, TypeVar

import pytest

from litestar.utils.helpers import get_name, unique_name_for_scope, unwrap_partial
from litestar.utils.helpers import envflag, get_name, unique_name_for_scope, unwrap_partial

T = TypeVar("T")

Expand Down Expand Up @@ -45,3 +46,40 @@ def test_unique_name_for_scope() -> None:
assert unique_name_for_scope("a", ["a", "a_0", "b"]) == "a_1"

assert unique_name_for_scope("b", ["a", "a_0", "b"]) == "b_0"


def test_envflag_truthy_values() -> None:
for value in ("1", "true", "TRUE", "t", "T", "yes", "YES", "on", "ON", "y", "Y"):
os.environ["TEST_FLAG"] = value
assert envflag("TEST_FLAG") is True
del os.environ["TEST_FLAG"]
Copy link
Member

Choose a reason for hiding this comment

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

You should use the monkeypatch fixture for this instead.

https://docs.pytest.org/en/stable/how-to/monkeypatch.html



def test_envflag_falsy_values() -> None:
for value in ("0", "false", "no", "off", ""):
os.environ["TEST_FLAG"] = value
assert envflag("TEST_FLAG") is False
del os.environ["TEST_FLAG"]


def test_envflag_missing() -> None:
assert envflag("NONEXISTENT_VAR") is False
assert envflag("NONEXISTENT_VAR_123", default=True) is True
assert envflag("NONEXISTENT_VAR_456", default=False) is False


def test_envflag_overrides_default() -> None:
os.environ["TEST_FLAG"] = "true"
assert envflag("TEST_FLAG", default=False) is True
del os.environ["TEST_FLAG"]

os.environ["TEST_FLAG"] = "0"
assert envflag("TEST_FLAG", default=True) is False
del os.environ["TEST_FLAG"]


def test_envflag_empty_string_uses_default() -> None:
os.environ["TEST_FLAG"] = ""
assert envflag("TEST_FLAG", default=True) is True
assert envflag("TEST_FLAG", default=False) is False
del os.environ["TEST_FLAG"]
3 changes: 2 additions & 1 deletion tools/sphinx_ext/run_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from sphinx.addnodes import highlightlang

from litestar import Litestar
from litestar.utils import envflag

if TYPE_CHECKING:
from collections.abc import Generator
Expand All @@ -34,7 +35,7 @@

logger = logging.getLogger("sphinx")

ignore_missing_output = os.getenv("LITESTAR_DOCS_IGNORE_MISSING_EXAMPLE_OUTPUT", "") == "1"
ignore_missing_output = envflag("LITESTAR_DOCS_IGNORE_MISSING_EXAMPLE_OUTPUT")


class StartupError(RuntimeError):
Expand Down
Loading