import logging
import os
from contextlib import suppress
from typing import Any, Dict, Final, Optional, Type, TypeVar
from typed_envs import registry
from typed_envs._env_var import EnvironmentVariable
from typed_envs.registry import _register_new_env
from typed_envs.typing import StringConverter, VarName
T = TypeVar("T")
[docs]
class EnvVarFactory:
"""Factory for creating :class:`EnvironmentVariable` instances with optional prefix."""
[docs]
def __init__(self, env_var_prefix: Optional[str] = None) -> None:
"""
Initializes the :class:`EnvVarFactory` with an optional prefix for environment variables.
Args:
env_var_prefix: An optional string prefix to be added to environment variable names.
"""
self.prefix: Final = env_var_prefix
self.__use_prefix: Final = env_var_prefix is not None
self.__default_string_converters: Final[Dict[Type, StringConverter]] = {}
@property
def default_string_converters(self) -> Dict[Type, StringConverter]:
return self.__default_string_converters.copy()
@property
def use_prefix(self) -> bool:
return self.__use_prefix
[docs]
def create_env(
self,
env_var_name: str,
env_var_type: Type[T],
default: Any,
*init_args,
string_converter: Optional[StringConverter] = None,
verbose: bool = True,
**init_kwargs: Any,
) -> "EnvironmentVariable[T]":
"""
Creates a new :class:`EnvironmentVariable` object with the specified parameters.
Args:
env_var_name: The name of the environment variable.
env_var_type: The type of the environment variable.
default: The default value for the environment variable.
*init_args: Additional positional arguments for initialization.
string_converter: An optional callable to convert the string value from the environment.
verbose: If True, logs the environment variable details.
**init_kwargs: Additional keyword arguments for initialization.
Returns:
An instance of :class:`EnvironmentVariable` with the specified type and value.
Example:
Create an environment variable with an integer type using an `EnvVarFactory` instance:
```python
from typed_envs.factory import EnvVarFactory
factory = EnvVarFactory()
some_var = factory.create_env("SET_WITH_THIS_ENV", int, 10)
>>> isinstance(some_var, int)
True
>>> isinstance(some_var, EnvironmentVariable)
True
```
Differences between `some_var` and `int(10)`:
- `some_var` will type check as both `int` and :class:`EnvironmentVariable`.
- `some_var.__repr__()` includes contextual information about the :class:`EnvironmentVariable`.
```python
>>> some_var
<EnvironmentVariable[name=`SET_WITH_THIS_ENV`, type=int, default_value=10, current_value=10, using_default=True]>
>>> str(some_var)
"10"
>>> some_var + 5
15
>>> 20 / some_var
2
```
See Also:
- :func:`typed_envs.create_env` for creating environment variables without a prefix.
"""
# Validate name
if not isinstance(env_var_name, str):
raise TypeError("env_var_name must be string, not {env_var_name}")
if not env_var_name:
raise ValueError("env_var_name must not be empty")
# Get full name
if self.__use_prefix:
full_name = VarName(f"{self.prefix}_{env_var_name}")
else:
full_name = VarName(env_var_name)
# Get value
var_value = os.environ.get(full_name)
using_default = var_value is None
var_value = var_value or default
if env_var_type is bool:
if isinstance(var_value, str) and var_value.lower() == "false":
var_value = False
else:
with suppress(ValueError):
# if var_value is "0" or "1"
var_value = int(var_value)
var_value = bool(var_value)
if any(iter_typ in env_var_type.__bases__ for iter_typ in [list, tuple, set]):
var_value = var_value.split(",")
# Convert value, if applicable
if string_converter is None:
string_converter = self.__default_string_converters.get(env_var_type)
if string_converter is not None and not (
using_default and isinstance(default, env_var_type)
):
var_value = string_converter(var_value)
# Create environment variable
instance = EnvironmentVariable[env_var_type]( # type: ignore [valid-type]
var_value, *init_args, **init_kwargs
)
# Set additional attributes
instance._init_arg0 = var_value
instance._env_name = full_name
instance._default_value = default
instance._using_default = using_default
# Finish up
if verbose:
# This code prints envs on script startup for convenience of your users.
try:
logger.info(instance.__repr__())
except RecursionError:
logger.debug(
"unable to properly display your `%s` %s env due to RecursionError",
full_name,
instance.__class__.__base__,
)
with suppress(RecursionError):
logger.debug(
"Here is your `%s` env in string form: %s",
full_name,
str(instance),
)
_register_new_env(full_name, instance)
return instance
[docs]
def register_string_converter(
self, register_for: Type, converter: StringConverter
) -> None:
if register_for in self.__default_string_converters:
raise ValueError(
f"There is already a string converter registered for {register_for}"
)
elif not callable(converter):
raise ValueError("converter must be callable")
self.__default_string_converters[register_for] = converter
# NOTE: While we create the TYPEDENVS_SHUTUP object in the ENVIRONMENT_VARIABLES file as an example,
# we cannot use it here without creating a circular import.
logger: Final[logging.Logger] = logging.getLogger("typed_envs")
from typed_envs import ENVIRONMENT_VARIABLES
if ENVIRONMENT_VARIABLES.SHUTUP:
logger.disabled = True
else:
if not logger.hasHandlers():
logger.addHandler(logging.StreamHandler())
if not logger.isEnabledFor(logging.INFO):
logger.setLevel(logging.INFO)
default_factory: Final[EnvVarFactory] = EnvVarFactory()