Skip to content

Commit

Permalink
default casters now working
Browse files Browse the repository at this point in the history
  • Loading branch information
shaypal5 committed Oct 4, 2020
1 parent 43ca2b9 commit 04ed8fa
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 17 deletions.
9 changes: 9 additions & 0 deletions birch/casters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Custom env var value casters for birch."""


def true_false_caster(val):
"""Casts 'TRUE', 'true', etc. to True, all other strings to False."""
try:
return val.lower() == 'true'
except AttributeError:
raise ValueError(f"Bad value {val} attempted to cast to bool!")
72 changes: 55 additions & 17 deletions birch/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,24 @@ class Birch(collections.abc.Mapping):
obj[key] syntax). This ensures every configuration value retrieved is
up-to-date to all configuration sources (both files and env variables).
defaults : dict of str to object, optional
A dictionary of default value to any number of keys or nested keys.
Nested keys canbe given as either __-separated key sequences or nested
A dictionary of default values for any number of keys or nested keys.
Nested keys can be given as either __-separated key sequences or nested
dict objects. For example, {'ZUBAT__SERVER__PORT': 8888} will set the
int 8888 as the default value for birch_obj['server']['port'].
{'server__port': 8888} will do the same, as will {'server': {'port':
8888}}. Notice that arguments provided to the `default` keyword of
the `get` method will override these constructor-provided defaults. See
the "Resolution order" in the ``README.rst`` file for more details.
default_casters : dict of str to callable, optional
A dictionary of default caster callables for any number of keys or
nested keys. Nested keys can be given as either __-separated key
sequences or nested dict objects. For example,
{'ZUBAT__SERVER__PORT': int} will set the int callable as the default
caster for birch_obj['server']['port']. {'server__port': int} will do
the same, as will {'server': {'port': 8888}}. Notice that arguments
provided to the `caster` keyword of the `get` method will NOT override
these constructor-provided defaults. Instead, per-call caster functions
will be applied AFTER any default caster configured.
"""

class _NoVal(object):
Expand Down Expand Up @@ -84,8 +94,10 @@ class _NoVal(object):
except ImportError: # pragma: no cover
pass

def __init__(self, namespace, directories=None, supported_formats=None,
load_all=False, auto_reload=False, defaults=None):
def __init__(
self, namespace, directories=None, supported_formats=None,
load_all=False, auto_reload=False, defaults=None, default_casters=None,
):
self._xdg_cfg_dpath = _xdg_cfg_dpath(namespace=namespace)
if directories is None:
directories = [
Expand All @@ -112,6 +124,10 @@ def __init__(self, namespace, directories=None, supported_formats=None,
self._auto_reload = auto_reload
self._no_val = Birch._NoVal()
self._defaults = defaults
if default_casters:
self._default_casters = self._build_defaults_dict(default_casters)
else:
self._default_casters = None
self._val_dict = self._build_val_dict()

def xdg_cfg_dpath(self):
Expand Down Expand Up @@ -227,17 +243,14 @@ def _build_val_dict(self):
val_dict = Birch._hierarchical_dict_from_dict(val_dict)
return val_dict

# implementing a collections.abc.Mapping abstract method
def __getitem__(self, key):
def _getitem_helper(self, key, dict_obj):
try:
key = key.upper()
except AttributeError:
raise ValueError((
"Birch does not support non-string keys! "
"{} provided as key!".format(key)
))
if self._auto_reload:
self.reload()
if self._root2 in key:
key = key[self._root_len2:]
elif self._root1 in key:
Expand All @@ -247,24 +260,45 @@ def __getitem__(self, key):
else:
key_tuple = [key]
try:
res = self._val_dict[key]
res = dict_obj[key]
except KeyError:
res = safe_nested_val(key_tuple, self._val_dict, self._no_val)
res = safe_nested_val(key_tuple, dict_obj, self._no_val)
if res == self._no_val:
raise KeyError("{}: No configuration value for {}.".format(
self.namespace, key))
return res

# implementing a collections.abc.Mapping abstract method
def __getitem__(self, key):
if self._auto_reload:
self.reload()
val = self._getitem_helper(key, self._val_dict)
try:
def_caster = self._getitem_helper(key, self._default_casters)
except (KeyError, TypeError):
return val
try:
return def_caster(val)
except ValueError:
raise ValueError(
f"{self.namespace}: Bad configuration value {val} failed "
f"to be casted with default caster {def_caster}."
)
except TypeError:
return val

def mget(self, key, caster=None):
"""Return the value for key if it's in the configuration..
"""Return the value for key if it's in the configuration.
Parameters
----------
key : object
The key of the value to get.
caster : callable, optional
If given, any found value is passed through the caster before
returning.
returning. If a default caster was already configured for this,
then it will be applied first, and this caster callable will be
applied to the casted result of the default caster.
Returns
-------
Expand All @@ -281,15 +315,17 @@ def mget(self, key, caster=None):
>>> zubat_cfg.mget('mport', int)
Traceback (most recent call last):
...
ValueError: zubat: Wrong configuration value Banana casted with <class 'int'>
ValueError: zubat: Bad configuration value Banana failed to be casted with caster <class 'int'>.
""" # noqa: E501
if caster:
try:
return caster(self[key])
val = self[key]
return caster(val)
except ValueError:
raise ValueError(
"{}: Wrong configuration value {} casted with {}".format(
self.namespace, self[key], caster))
f"{self.namespace}: Bad configuration value {val} failed "
f"to be casted with caster {caster}."
)
return self[key]

def get(self, key, default=None, caster=None, throw=False, warn=False):
Expand All @@ -307,7 +343,9 @@ def get(self, key, default=None, caster=None, throw=False, warn=False):
defaults to None, so that this method never raised a KeyError.
caster : callable, optional
If given, any found value is passed through the caster before
returning.
returning. If a default caster was already configured for this,
then it will be applied first, and this caster callable will be
applied to the casted result of the default caster.
throw : bool, default False
If set to True, a KeyError is raised if no matching key is found
AND the default value provided is None (which is the default).
Expand Down
3 changes: 3 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
NSPACE4 = 'ricketyporpoise'
VAL_DICT4 = {
'pik': 'puk',
'biil': 'True',
'bool': 'False',
'baal': 500,
'shik': {
'shuk': '8',
}
Expand Down
40 changes: 40 additions & 0 deletions tests/test_default_casters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Testing default caster functionality for birch."""

import pytest

from birch import Birch
from birch.casters import true_false_caster

from .test_core import (
NSPACE4,
)


def test_default_casters():
cfg = Birch(
namespace=NSPACE4,
default_casters={
'biil': true_false_caster,
'bool': true_false_caster,
'baal': true_false_caster,
'shik': {
'shuk': int,
}
},
)
val = cfg['pik']
assert isinstance(val, str)
val = cfg['biil']
assert isinstance(val, bool)
assert val is True
val = cfg['bool']
assert isinstance(val, bool)
assert val is False
with pytest.raises(ValueError):
val = cfg['baal']
val = cfg['shik__shuk']
assert isinstance(val, int)
assert val == 8
val = cfg['shik']['shuk']
assert isinstance(val, str)
assert val == '8'

0 comments on commit 04ed8fa

Please sign in to comment.