Skip to content

Commit

Permalink
Evaluate environment variables at creation time instead of when accessed
Browse files Browse the repository at this point in the history
  • Loading branch information
isik-kaplan committed Nov 17, 2024
1 parent 3e491b8 commit 4d122f4
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 262 deletions.
19 changes: 16 additions & 3 deletions iubeo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
__version__ = "0.2.0"
__version__ = "0.3.0"

from .casters import boolean, comma_separated_list
from .casters import (
boolean,
caster,
comma_separated_float_list,
comma_separated_int_list,
comma_separated_list,
integer,
string,
)
from .config import ConfigError, config

__all__ = [
"caster",
"config",
"ConfigError",
"comma_separated_list",
"boolean",
"comma_separated_list",
"comma_separated_int_list",
"comma_separated_float_list",
"integer",
"string",
]
37 changes: 34 additions & 3 deletions iubeo/casters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,42 @@
Utility functions to cast the string value from environment to python types.
"""

from .utils import raise_config_error_instead
from functools import wraps


@raise_config_error_instead
def caster(f):
sentinel = object()

def wrapper(missing_default=sentinel, error_default=sentinel):
@wraps(f)
def function_clone(value):
return f(value)

if missing_default is not sentinel:
function_clone.missing_default = missing_default
if error_default is not sentinel:
function_clone.error_default = error_default
return function_clone

return wrapper


@caster
def comma_separated_list(value):
return value.split(",")


@raise_config_error_instead
@caster
def comma_separated_int_list(value):
return [int(i) for i in value.split(",")]


@caster
def comma_separated_float_list(value):
return [float(i) for i in value.split(",")]


@caster
def boolean(value):
_truthy = ["true", "True", "1"]
_false = ["false", "False", "0"]
Expand All @@ -21,3 +48,7 @@ def boolean(value):
return False
else:
raise ValueError('Value "{}" can not be parsed into a boolean.'.format(value))


string = caster(str)
integer = caster(int)
59 changes: 31 additions & 28 deletions iubeo/config.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,56 @@
import copy
import os
from typing import AnyStr, Callable, Dict, Optional, Tuple, Union

from .exceptions import ConfigError
from .utils import raise_config_error_instead

ConfigFormat = Dict[AnyStr, Union["ConfigFormat", Tuple[AnyStr, Callable]]]


class Config(dict):
__setattr__ = dict.__setitem__

def __getattr__(self, item):
value = dict.__getitem__(self, item)
if isinstance(value, list):
var, cast = value
return raise_config_error_instead(cast)(os.environ.get(var))
return value

_START_NODE_NAME = "iubeo_data"

__setattr__ = dict.__setitem__
__getattr__ = dict.__getitem__

@classmethod
def _fix_end_node_name(cls, name, sep):
# Removes sep+_START_NODE_NAME+sep from the end node name.
return name.replace(sep + cls._START_NODE_NAME, "").lstrip(sep)

@staticmethod
def _cast_value(data, key, caster, environment_key):
try:
data[key] = caster(os.environ[environment_key])
except KeyError:
default = getattr(caster, "missing_default", None)
if default:
data[key] = default
else:
raise ConfigError(
f"Environment variable {environment_key} not found."
f" Please set it or provide a `missing_default` to your caster."
)
except Exception as e:
default = getattr(caster, "error_default", None)
if default:
data[key] = default
else:
raise ConfigError(
f"Error while parsing {environment_key}='{os.environ[environment_key]}' with '{caster}'."
" Please check the value and the caster or provide an `error_default` to your caster."
) from e

@classmethod
def _create(cls, data: dict, prefix: str = "", sep: str = "__"):
prefix = prefix or ""
mutated = {}
for key, value in data.items():
mutated = {}
if isinstance(value, dict):
mutated = {prefix + sep + key: cls._create(value, prefix + sep + key, sep)}
elif callable(value):
data[key] = [cls._fix_end_node_name(prefix + sep + key, sep), value]
cls._cast_value(data, key, value, cls._fix_end_node_name(prefix + sep + key, sep))

else:
raise ConfigError("Values either must be callables or other mappings, not {}.".format(value))
raise ConfigError(f"Values either must be callables or other mappings, not {type(value)}. Key={key}.")
return cls(**mutated, **data)

@classmethod
Expand All @@ -60,18 +75,6 @@ def create(cls, data: dict, prefix: str = "", sep: str = "__"):
# Which has, well, a side effect of mutating the object for the end user, hence: deepcopy.
return cls.from_data(cls._create(data, prefix, sep).get(cls._START_NODE_NAME))

@classmethod
def _final_nodes(cls, data: ConfigFormat):
for key, value in data.items():
if isinstance(value, list):
yield value[0]
elif isinstance(value, dict):
yield from cls._final_nodes(value)

@property
def final_nodes(self):
return [*self._final_nodes(self)]


def config(data, *, prefix: Optional[str] = None, sep: str = "__"):
def config(data, *, prefix: str | None = None, sep: str = "__"):
return Config.create(data, prefix, sep)
21 changes: 0 additions & 21 deletions iubeo/utils.py

This file was deleted.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "iubeo"
version = "0.2.2"
version = "0.3.0"
description = "Friendlier way to write your config."
authors = ["isik-kaplan <[email protected]>"]

Expand Down
57 changes: 0 additions & 57 deletions tests/test_iubeo_casters.py

This file was deleted.

Loading

0 comments on commit 4d122f4

Please sign in to comment.