Source code for behave_kit.env.variables

"""`env()` — read environment variables with validation and type conversion.

Falls back to the `KitConfig` attached to `context.config` (see
`behave_kit.env.config.load_env_config`) when the variable is not present
in `os.environ`. Loads a `.env` file once via `python-dotenv` if installed.
"""

from __future__ import annotations

import os
from pathlib import Path
from typing import Any, Literal, TypeVar, overload

from behave_kit._core.compat import HAS_DOTENV
from behave_kit._core.errors import EnvVarError
from behave_kit._core.types import Context

T = TypeVar("T")

_TRUE_VALUES = {"true", "1", "yes", "on"}
_FALSE_VALUES = {"false", "0", "no", "off"}

_dotenv_loaded = False


def _ensure_dotenv_loaded() -> None:
    global _dotenv_loaded
    if _dotenv_loaded:
        return
    _dotenv_loaded = True
    if not HAS_DOTENV:
        return
    from dotenv import load_dotenv

    load_dotenv(Path(".env"))


def _to_bool(raw: str) -> bool:
    normalized = raw.strip().lower()
    if normalized in _TRUE_VALUES:
        return True
    if normalized in _FALSE_VALUES:
        return False
    raise EnvVarError(
        f"Cannot convert '{raw}' to bool",
        suggestion=f"Use one of: {', '.join(sorted(_TRUE_VALUES | _FALSE_VALUES))}",
    )


def _to_int(raw: str) -> int:
    try:
        return int(raw)
    except ValueError as exc:
        raise EnvVarError(
            f"Cannot convert '{raw}' to int",
            cause=exc,
            suggestion="Provide an integer value",
        ) from exc


def _to_float(raw: str) -> float:
    try:
        return float(raw)
    except ValueError as exc:
        raise EnvVarError(
            f"Cannot convert '{raw}' to float",
            cause=exc,
            suggestion="Provide a numeric value",
        ) from exc


def _convert(raw: object, var_type: type) -> Any:
    if var_type is bool:
        if isinstance(raw, bool):
            return raw
        if isinstance(raw, (int, float)):
            return bool(raw)
        return _to_bool(str(raw))
    if var_type is int:
        if isinstance(raw, bool):
            raise EnvVarError(
                f"Cannot convert '{raw}' to int",
                suggestion="Provide an integer value",
            )
        if isinstance(raw, (int, float)):
            return int(raw)
        return _to_int(str(raw).strip())
    if var_type is float:
        if isinstance(raw, bool):
            raise EnvVarError(
                f"Cannot convert '{raw}' to float",
                suggestion="Provide a numeric value",
            )
        if isinstance(raw, (int, float)):
            return float(raw)
        return _to_float(str(raw).strip())
    if var_type is str:
        return str(raw)
    raise EnvVarError(
        f"Unsupported var_type '{var_type.__name__}'",
        suggestion="Use bool, int, float, or str",
    )


def _from_config(context: Context | None, key: str) -> object | None:
    if context is None:
        return None
    config = getattr(context, "config", None)
    if config is None:
        return None
    raw_config = getattr(config, "raw", None) or {}
    return raw_config.get(key)


@overload
def env(
    key: str,
    *,
    required: Literal[True] = True,
    var_type: type[T] = ...,
    default: Any = None,
    context: Context | None = None,
) -> T:
    """Overload: ``required=True`` returns ``T`` (not optional)."""
    ...


@overload
def env(
    key: str,
    *,
    required: Literal[False],
    var_type: type[T] = ...,
    default: Any = None,
    context: Context | None = None,
) -> T | None:
    """Overload: ``required=False`` may return ``None``."""
    ...


[docs] def env( key: str, *, required: bool = True, var_type: type = str, default: Any = None, context: Context | None = None, ) -> Any: """Read environment variable ``key``, converted to ``var_type``. Resolution order: ``os.environ`` -> ``context.config.raw`` (if ``context`` is given) -> ``default`` -> `EnvVarError` (if ``required``). """ if not key: raise EnvVarError( "Environment variable key must not be empty", suggestion="Provide a non-empty key name", ) _ensure_dotenv_loaded() raw: object | None = os.environ.get(key) if raw is None: raw = _from_config(context, key) if raw is None: if default is not None: return _convert(default, var_type) if required: raise EnvVarError( f"Environment variable '{key}' is not set", suggestion=f"Set it with: export {key}=...", ) return None return _convert(raw, var_type)