#!/usr/bin/env python # -*- coding: UTF-8 -*- # # Copyright (c) 2017-2019 - Chris Griffith - MIT License """ Improved dictionary access through dot notation with additional tools. """ import string import sys import json import re import copy from keyword import kwlist import warnings try: from collections.abc import Iterable, Mapping, Callable except ImportError: from collections import Iterable, Mapping, Callable yaml_support = True try: import yaml except ImportError: try: import ruamel.yaml as yaml except ImportError: yaml = None yaml_support = False if sys.version_info >= (3, 0): basestring = str else: from io import open __all__ = ['Box', 'ConfigBox', 'BoxList', 'SBox', 'BoxError', 'BoxKeyError'] __author__ = 'Chris Griffith' __version__ = '3.2.4' BOX_PARAMETERS = ('default_box', 'default_box_attr', 'conversion_box', 'frozen_box', 'camel_killer_box', 'box_it_up', 'box_safe_prefix', 'box_duplicates', 'ordered_box') _first_cap_re = re.compile('(.)([A-Z][a-z]+)') _all_cap_re = re.compile('([a-z0-9])([A-Z])') class BoxError(Exception): """Non standard dictionary exceptions""" class BoxKeyError(BoxError, KeyError, AttributeError): """Key does not exist""" # Abstract converter functions for use in any Box class def _to_json(obj, filename=None, encoding="utf-8", errors="strict", **json_kwargs): json_dump = json.dumps(obj, ensure_ascii=False, **json_kwargs) if filename: with open(filename, 'w', encoding=encoding, errors=errors) as f: f.write(json_dump if sys.version_info >= (3, 0) else json_dump.decode("utf-8")) else: return json_dump def _from_json(json_string=None, filename=None, encoding="utf-8", errors="strict", multiline=False, **kwargs): if filename: with open(filename, 'r', encoding=encoding, errors=errors) as f: if multiline: data = [json.loads(line.strip(), **kwargs) for line in f if line.strip() and not line.strip().startswith("#")] else: data = json.load(f, **kwargs) elif json_string: data = json.loads(json_string, **kwargs) else: raise BoxError('from_json requires a string or filename') return data def _to_yaml(obj, filename=None, default_flow_style=False, encoding="utf-8", errors="strict", **yaml_kwargs): if filename: with open(filename, 'w', encoding=encoding, errors=errors) as f: yaml.dump(obj, stream=f, default_flow_style=default_flow_style, **yaml_kwargs) else: return yaml.dump(obj, default_flow_style=default_flow_style, **yaml_kwargs) def _from_yaml(yaml_string=None, filename=None, encoding="utf-8", errors="strict", **kwargs): if filename: with open(filename, 'r', encoding=encoding, errors=errors) as f: data = yaml.load(f, **kwargs) elif yaml_string: data = yaml.load(yaml_string, **kwargs) else: raise BoxError('from_yaml requires a string or filename') return data # Helper functions def _safe_key(key): try: return str(key) except UnicodeEncodeError: return key.encode("utf-8", "ignore") def _safe_attr(attr, camel_killer=False, replacement_char='x'): """Convert a key into something that is accessible as an attribute""" allowed = string.ascii_letters + string.digits + '_' attr = _safe_key(attr) if camel_killer: attr = _camel_killer(attr) attr = attr.replace(' ', '_') out = '' for character in attr: out += character if character in allowed else "_" out = out.strip("_") try: int(out[0]) except (ValueError, IndexError): pass else: out = '{0}{1}'.format(replacement_char, out) if out in kwlist: out = '{0}{1}'.format(replacement_char, out) return re.sub('_+', '_', out) def _camel_killer(attr): """ CamelKiller, qu'est-ce que c'est? Taken from http://stackoverflow.com/a/1176023/3244542 """ try: attr = str(attr) except UnicodeEncodeError: attr = attr.encode("utf-8", "ignore") s1 = _first_cap_re.sub(r'\1_\2', attr) s2 = _all_cap_re.sub(r'\1_\2', s1) return re.sub('_+', '_', s2.casefold() if hasattr(s2, 'casefold') else s2.lower()) def _recursive_tuples(iterable, box_class, recreate_tuples=False, **kwargs): out_list = [] for i in iterable: if isinstance(i, dict): out_list.append(box_class(i, **kwargs)) elif isinstance(i, list) or (recreate_tuples and isinstance(i, tuple)): out_list.append(_recursive_tuples(i, box_class, recreate_tuples, **kwargs)) else: out_list.append(i) return tuple(out_list) def _conversion_checks(item, keys, box_config, check_only=False, pre_check=False): """ Internal use for checking if a duplicate safe attribute already exists :param item: Item to see if a dup exists :param keys: Keys to check against :param box_config: Easier to pass in than ask for specfic items :param check_only: Don't bother doing the conversion work :param pre_check: Need to add the item to the list of keys to check :return: the original unmodified key, if exists and not check_only """ if box_config['box_duplicates'] != 'ignore': if pre_check: keys = list(keys) + [item] key_list = [(k, _safe_attr(k, camel_killer=box_config['camel_killer_box'], replacement_char=box_config['box_safe_prefix'] )) for k in keys] if len(key_list) > len(set(x[1] for x in key_list)): seen = set() dups = set() for x in key_list: if x[1] in seen: dups.add("{0}({1})".format(x[0], x[1])) seen.add(x[1]) if box_config['box_duplicates'].startswith("warn"): warnings.warn('Duplicate conversion attributes exist: ' '{0}'.format(dups)) else: raise BoxError('Duplicate conversion attributes exist: ' '{0}'.format(dups)) if check_only: return # This way will be slower for warnings, as it will have double work # But faster for the default 'ignore' for k in keys: if item == _safe_attr(k, camel_killer=box_config['camel_killer_box'], replacement_char=box_config['box_safe_prefix']): return k def _get_box_config(cls, kwargs): return { # Internal use only '__converted': set(), '__box_heritage': kwargs.pop('__box_heritage', None), '__created': False, '__ordered_box_values': [], # Can be changed by user after box creation 'default_box': kwargs.pop('default_box', False), 'default_box_attr': kwargs.pop('default_box_attr', cls), 'conversion_box': kwargs.pop('conversion_box', True), 'box_safe_prefix': kwargs.pop('box_safe_prefix', 'x'), 'frozen_box': kwargs.pop('frozen_box', False), 'camel_killer_box': kwargs.pop('camel_killer_box', False), 'modify_tuples_box': kwargs.pop('modify_tuples_box', False), 'box_duplicates': kwargs.pop('box_duplicates', 'ignore'), 'ordered_box': kwargs.pop('ordered_box', False) } class Box(dict): """ Improved dictionary access through dot notation with additional tools. :param default_box: Similar to defaultdict, return a default value :param default_box_attr: Specify the default replacement. WARNING: If this is not the default 'Box', it will not be recursive :param frozen_box: After creation, the box cannot be modified :param camel_killer_box: Convert CamelCase to snake_case :param conversion_box: Check for near matching keys as attributes :param modify_tuples_box: Recreate incoming tuples with dicts into Boxes :param box_it_up: Recursively create all Boxes from the start :param box_safe_prefix: Conversion box prefix for unsafe attributes :param box_duplicates: "ignore", "error" or "warn" when duplicates exists in a conversion_box :param ordered_box: Preserve the order of keys entered into the box """ _protected_keys = dir({}) + ['to_dict', 'tree_view', 'to_json', 'to_yaml', 'from_yaml', 'from_json'] def __new__(cls, *args, **kwargs): """ Due to the way pickling works in python 3, we need to make sure the box config is created as early as possible. """ obj = super(Box, cls).__new__(cls, *args, **kwargs) obj._box_config = _get_box_config(cls, kwargs) return obj def __init__(self, *args, **kwargs): self._box_config = _get_box_config(self.__class__, kwargs) if self._box_config['ordered_box']: self._box_config['__ordered_box_values'] = [] if (not self._box_config['conversion_box'] and self._box_config['box_duplicates'] != "ignore"): raise BoxError('box_duplicates are only for conversion_boxes') if len(args) == 1: if isinstance(args[0], basestring): raise ValueError('Cannot extrapolate Box from string') if isinstance(args[0], Mapping): for k, v in args[0].items(): if v is args[0]: v = self self[k] = v self.__add_ordered(k) elif isinstance(args[0], Iterable): for k, v in args[0]: self[k] = v self.__add_ordered(k) else: raise ValueError('First argument must be mapping or iterable') elif args: raise TypeError('Box expected at most 1 argument, ' 'got {0}'.format(len(args))) box_it = kwargs.pop('box_it_up', False) for k, v in kwargs.items(): if args and isinstance(args[0], Mapping) and v is args[0]: v = self self[k] = v self.__add_ordered(k) if (self._box_config['frozen_box'] or box_it or self._box_config['box_duplicates'] != 'ignore'): self.box_it_up() self._box_config['__created'] = True def __add_ordered(self, key): if (self._box_config['ordered_box'] and key not in self._box_config['__ordered_box_values']): self._box_config['__ordered_box_values'].append(key) def box_it_up(self): """ Perform value lookup for all items in current dictionary, generating all sub Box objects, while also running `box_it_up` on any of those sub box objects. """ for k in self: _conversion_checks(k, self.keys(), self._box_config, check_only=True) if self[k] is not self and hasattr(self[k], 'box_it_up'): self[k].box_it_up() def __hash__(self): if self._box_config['frozen_box']: hashing = 54321 for item in self.items(): hashing ^= hash(item) return hashing raise TypeError("unhashable type: 'Box'") def __dir__(self): allowed = string.ascii_letters + string.digits + '_' kill_camel = self._box_config['camel_killer_box'] items = set(dir(dict) + ['to_dict', 'to_json', 'from_json', 'box_it_up']) # Only show items accessible by dot notation for key in self.keys(): key = _safe_key(key) if (' ' not in key and key[0] not in string.digits and key not in kwlist): for letter in key: if letter not in allowed: break else: items.add(key) for key in self.keys(): key = _safe_key(key) if key not in items: if self._box_config['conversion_box']: key = _safe_attr(key, camel_killer=kill_camel, replacement_char=self._box_config[ 'box_safe_prefix']) if key: items.add(key) if kill_camel: snake_key = _camel_killer(key) if snake_key: items.remove(key) items.add(snake_key) if yaml_support: items.add('to_yaml') items.add('from_yaml') return list(items) def get(self, key, default=None): try: return self[key] except KeyError: if isinstance(default, dict) and not isinstance(default, Box): return Box(default) if isinstance(default, list) and not isinstance(default, BoxList): return BoxList(default) return default def copy(self): return self.__class__(super(self.__class__, self).copy()) def __copy__(self): return self.__class__(super(self.__class__, self).copy()) def __deepcopy__(self, memodict=None): out = self.__class__() memodict = memodict or {} memodict[id(self)] = out for k, v in self.items(): out[copy.deepcopy(k, memodict)] = copy.deepcopy(v, memodict) return out def __setstate__(self, state): self._box_config = state['_box_config'] self.__dict__.update(state) def __getitem__(self, item, _ignore_default=False): try: value = super(Box, self).__getitem__(item) except KeyError as err: if item == '_box_config': raise BoxKeyError('_box_config should only exist as an ' 'attribute and is never defaulted') if self._box_config['default_box'] and not _ignore_default: return self.__get_default(item) raise BoxKeyError(str(err)) else: return self.__convert_and_store(item, value) def keys(self): if self._box_config['ordered_box']: return self._box_config['__ordered_box_values'] return super(Box, self).keys() def values(self): return [self[x] for x in self.keys()] def items(self): return [(x, self[x]) for x in self.keys()] def __get_default(self, item): default_value = self._box_config['default_box_attr'] if default_value is self.__class__: return self.__class__(__box_heritage=(self, item), **self.__box_config()) elif isinstance(default_value, Callable): return default_value() elif hasattr(default_value, 'copy'): return default_value.copy() return default_value def __box_config(self): out = {} for k, v in self._box_config.copy().items(): if not k.startswith("__"): out[k] = v return out def __convert_and_store(self, item, value): if item in self._box_config['__converted']: return value if isinstance(value, dict) and not isinstance(value, Box): value = self.__class__(value, __box_heritage=(self, item), **self.__box_config()) self[item] = value elif isinstance(value, list) and not isinstance(value, BoxList): if self._box_config['frozen_box']: value = _recursive_tuples(value, self.__class__, recreate_tuples=self._box_config[ 'modify_tuples_box'], __box_heritage=(self, item), **self.__box_config()) else: value = BoxList(value, __box_heritage=(self, item), box_class=self.__class__, **self.__box_config()) self[item] = value elif (self._box_config['modify_tuples_box'] and isinstance(value, tuple)): value = _recursive_tuples(value, self.__class__, recreate_tuples=True, __box_heritage=(self, item), **self.__box_config()) self[item] = value self._box_config['__converted'].add(item) return value def __create_lineage(self): if (self._box_config['__box_heritage'] and self._box_config['__created']): past, item = self._box_config['__box_heritage'] if not past[item]: past[item] = self self._box_config['__box_heritage'] = None def __getattr__(self, item): try: try: value = self.__getitem__(item, _ignore_default=True) except KeyError: value = object.__getattribute__(self, item) except AttributeError as err: if item == "__getstate__": raise AttributeError(item) if item == '_box_config': raise BoxError('_box_config key must exist') kill_camel = self._box_config['camel_killer_box'] if self._box_config['conversion_box'] and item: k = _conversion_checks(item, self.keys(), self._box_config) if k: return self.__getitem__(k) if kill_camel: for k in self.keys(): if item == _camel_killer(k): return self.__getitem__(k) if self._box_config['default_box']: return self.__get_default(item) raise BoxKeyError(str(err)) else: if item == '_box_config': return value return self.__convert_and_store(item, value) def __setitem__(self, key, value): if (key != '_box_config' and self._box_config['__created'] and self._box_config['frozen_box']): raise BoxError('Box is frozen') if self._box_config['conversion_box']: _conversion_checks(key, self.keys(), self._box_config, check_only=True, pre_check=True) super(Box, self).__setitem__(key, value) self.__add_ordered(key) self.__create_lineage() def __setattr__(self, key, value): if (key != '_box_config' and self._box_config['frozen_box'] and self._box_config['__created']): raise BoxError('Box is frozen') if key in self._protected_keys: raise AttributeError("Key name '{0}' is protected".format(key)) if key == '_box_config': return object.__setattr__(self, key, value) try: object.__getattribute__(self, key) except (AttributeError, UnicodeEncodeError): if (key not in self.keys() and (self._box_config['conversion_box'] or self._box_config['camel_killer_box'])): if self._box_config['conversion_box']: k = _conversion_checks(key, self.keys(), self._box_config) self[key if not k else k] = value elif self._box_config['camel_killer_box']: for each_key in self: if key == _camel_killer(each_key): self[each_key] = value break else: self[key] = value else: object.__setattr__(self, key, value) self.__add_ordered(key) self.__create_lineage() def __delitem__(self, key): if self._box_config['frozen_box']: raise BoxError('Box is frozen') super(Box, self).__delitem__(key) if (self._box_config['ordered_box'] and key in self._box_config['__ordered_box_values']): self._box_config['__ordered_box_values'].remove(key) def __delattr__(self, item): if self._box_config['frozen_box']: raise BoxError('Box is frozen') if item == '_box_config': raise BoxError('"_box_config" is protected') if item in self._protected_keys: raise AttributeError("Key name '{0}' is protected".format(item)) try: object.__getattribute__(self, item) except AttributeError: del self[item] else: object.__delattr__(self, item) if (self._box_config['ordered_box'] and item in self._box_config['__ordered_box_values']): self._box_config['__ordered_box_values'].remove(item) def pop(self, key, *args): if args: if len(args) != 1: raise BoxError('pop() takes only one optional' ' argument "default"') try: item = self[key] except KeyError: return args[0] else: del self[key] return item try: item = self[key] except KeyError: raise BoxKeyError('{0}'.format(key)) else: del self[key] return item def clear(self): self._box_config['__ordered_box_values'] = [] super(Box, self).clear() def popitem(self): try: key = next(self.__iter__()) except StopIteration: raise BoxKeyError('Empty box') return key, self.pop(key) def __repr__(self): return ''.format(str(self.to_dict())) def __str__(self): return str(self.to_dict()) def __iter__(self): for key in self.keys(): yield key def __reversed__(self): for key in reversed(list(self.keys())): yield key def to_dict(self): """ Turn the Box and sub Boxes back into a native python dictionary. :return: python dictionary of this Box """ out_dict = dict(self) for k, v in out_dict.items(): if v is self: out_dict[k] = out_dict elif hasattr(v, 'to_dict'): out_dict[k] = v.to_dict() elif hasattr(v, 'to_list'): out_dict[k] = v.to_list() return out_dict def update(self, item=None, **kwargs): if not item: item = kwargs iter_over = item.items() if hasattr(item, 'items') else item for k, v in iter_over: if isinstance(v, dict): # Box objects must be created in case they are already # in the `converted` box_config set v = self.__class__(v) if k in self and isinstance(self[k], dict): self[k].update(v) continue if isinstance(v, list): v = BoxList(v) try: self.__setattr__(k, v) except (AttributeError, TypeError): self.__setitem__(k, v) def setdefault(self, item, default=None): if item in self: return self[item] if isinstance(default, dict): default = self.__class__(default) if isinstance(default, list): default = BoxList(default) self[item] = default return default def to_json(self, filename=None, encoding="utf-8", errors="strict", **json_kwargs): """ Transform the Box object into a JSON string. :param filename: If provided will save to file :param encoding: File encoding :param errors: How to handle encoding errors :param json_kwargs: additional arguments to pass to json.dump(s) :return: string of JSON or return of `json.dump` """ return _to_json(self.to_dict(), filename=filename, encoding=encoding, errors=errors, **json_kwargs) @classmethod def from_json(cls, json_string=None, filename=None, encoding="utf-8", errors="strict", **kwargs): """ Transform a json object string into a Box object. If the incoming json is a list, you must use BoxList.from_json. :param json_string: string to pass to `json.loads` :param filename: filename to open and pass to `json.load` :param encoding: File encoding :param errors: How to handle encoding errors :param kwargs: parameters to pass to `Box()` or `json.loads` :return: Box object from json data """ bx_args = {} for arg in kwargs.copy(): if arg in BOX_PARAMETERS: bx_args[arg] = kwargs.pop(arg) data = _from_json(json_string, filename=filename, encoding=encoding, errors=errors, **kwargs) if not isinstance(data, dict): raise BoxError('json data not returned as a dictionary, ' 'but rather a {0}'.format(type(data).__name__)) return cls(data, **bx_args) if yaml_support: def to_yaml(self, filename=None, default_flow_style=False, encoding="utf-8", errors="strict", **yaml_kwargs): """ Transform the Box object into a YAML string. :param filename: If provided will save to file :param default_flow_style: False will recursively dump dicts :param encoding: File encoding :param errors: How to handle encoding errors :param yaml_kwargs: additional arguments to pass to yaml.dump :return: string of YAML or return of `yaml.dump` """ return _to_yaml(self.to_dict(), filename=filename, default_flow_style=default_flow_style, encoding=encoding, errors=errors, **yaml_kwargs) @classmethod def from_yaml(cls, yaml_string=None, filename=None, encoding="utf-8", errors="strict", loader=yaml.SafeLoader, **kwargs): """ Transform a yaml object string into a Box object. :param yaml_string: string to pass to `yaml.load` :param filename: filename to open and pass to `yaml.load` :param encoding: File encoding :param errors: How to handle encoding errors :param loader: YAML Loader, defaults to SafeLoader :param kwargs: parameters to pass to `Box()` or `yaml.load` :return: Box object from yaml data """ bx_args = {} for arg in kwargs.copy(): if arg in BOX_PARAMETERS: bx_args[arg] = kwargs.pop(arg) data = _from_yaml(yaml_string=yaml_string, filename=filename, encoding=encoding, errors=errors, Loader=loader, **kwargs) if not isinstance(data, dict): raise BoxError('yaml data not returned as a dictionary' 'but rather a {0}'.format(type(data).__name__)) return cls(data, **bx_args) class BoxList(list): """ Drop in replacement of list, that converts added objects to Box or BoxList objects as necessary. """ def __init__(self, iterable=None, box_class=Box, **box_options): self.box_class = box_class self.box_options = box_options self.box_org_ref = self.box_org_ref = id(iterable) if iterable else 0 if iterable: for x in iterable: self.append(x) if box_options.get('frozen_box'): def frozen(*args, **kwargs): raise BoxError('BoxList is frozen') for method in ['append', 'extend', 'insert', 'pop', 'remove', 'reverse', 'sort']: self.__setattr__(method, frozen) def __delitem__(self, key): if self.box_options.get('frozen_box'): raise BoxError('BoxList is frozen') super(BoxList, self).__delitem__(key) def __setitem__(self, key, value): if self.box_options.get('frozen_box'): raise BoxError('BoxList is frozen') super(BoxList, self).__setitem__(key, value) def append(self, p_object): if isinstance(p_object, dict): try: p_object = self.box_class(p_object, **self.box_options) except AttributeError as err: if 'box_class' in self.__dict__: raise err elif isinstance(p_object, list): try: p_object = (self if id(p_object) == self.box_org_ref else BoxList(p_object)) except AttributeError as err: if 'box_org_ref' in self.__dict__: raise err super(BoxList, self).append(p_object) def extend(self, iterable): for item in iterable: self.append(item) def insert(self, index, p_object): if isinstance(p_object, dict): p_object = self.box_class(p_object, **self.box_options) elif isinstance(p_object, list): p_object = (self if id(p_object) == self.box_org_ref else BoxList(p_object)) super(BoxList, self).insert(index, p_object) def __repr__(self): return "".format(self.to_list()) def __str__(self): return str(self.to_list()) def __copy__(self): return BoxList((x for x in self), self.box_class, **self.box_options) def __deepcopy__(self, memodict=None): out = self.__class__() memodict = memodict or {} memodict[id(self)] = out for k in self: out.append(copy.deepcopy(k)) return out def __hash__(self): if self.box_options.get('frozen_box'): hashing = 98765 hashing ^= hash(tuple(self)) return hashing raise TypeError("unhashable type: 'BoxList'") def to_list(self): new_list = [] for x in self: if x is self: new_list.append(new_list) elif isinstance(x, Box): new_list.append(x.to_dict()) elif isinstance(x, BoxList): new_list.append(x.to_list()) else: new_list.append(x) return new_list def to_json(self, filename=None, encoding="utf-8", errors="strict", multiline=False, **json_kwargs): """ Transform the BoxList object into a JSON string. :param filename: If provided will save to file :param encoding: File encoding :param errors: How to handle encoding errors :param multiline: Put each item in list onto it's own line :param json_kwargs: additional arguments to pass to json.dump(s) :return: string of JSON or return of `json.dump` """ if filename and multiline: lines = [_to_json(item, filename=False, encoding=encoding, errors=errors, **json_kwargs) for item in self] with open(filename, 'w', encoding=encoding, errors=errors) as f: f.write("\n".join(lines).decode('utf-8') if sys.version_info < (3, 0) else "\n".join(lines)) else: return _to_json(self.to_list(), filename=filename, encoding=encoding, errors=errors, **json_kwargs) @classmethod def from_json(cls, json_string=None, filename=None, encoding="utf-8", errors="strict", multiline=False, **kwargs): """ Transform a json object string into a BoxList object. If the incoming json is a dict, you must use Box.from_json. :param json_string: string to pass to `json.loads` :param filename: filename to open and pass to `json.load` :param encoding: File encoding :param errors: How to handle encoding errors :param multiline: One object per line :param kwargs: parameters to pass to `Box()` or `json.loads` :return: BoxList object from json data """ bx_args = {} for arg in kwargs.copy(): if arg in BOX_PARAMETERS: bx_args[arg] = kwargs.pop(arg) data = _from_json(json_string, filename=filename, encoding=encoding, errors=errors, multiline=multiline, **kwargs) if not isinstance(data, list): raise BoxError('json data not returned as a list, ' 'but rather a {0}'.format(type(data).__name__)) return cls(data, **bx_args) if yaml_support: def to_yaml(self, filename=None, default_flow_style=False, encoding="utf-8", errors="strict", **yaml_kwargs): """ Transform the BoxList object into a YAML string. :param filename: If provided will save to file :param default_flow_style: False will recursively dump dicts :param encoding: File encoding :param errors: How to handle encoding errors :param yaml_kwargs: additional arguments to pass to yaml.dump :return: string of YAML or return of `yaml.dump` """ return _to_yaml(self.to_list(), filename=filename, default_flow_style=default_flow_style, encoding=encoding, errors=errors, **yaml_kwargs) @classmethod def from_yaml(cls, yaml_string=None, filename=None, encoding="utf-8", errors="strict", loader=yaml.SafeLoader, **kwargs): """ Transform a yaml object string into a BoxList object. :param yaml_string: string to pass to `yaml.load` :param filename: filename to open and pass to `yaml.load` :param encoding: File encoding :param errors: How to handle encoding errors :param loader: YAML Loader, defaults to SafeLoader :param kwargs: parameters to pass to `BoxList()` or `yaml.load` :return: BoxList object from yaml data """ bx_args = {} for arg in kwargs.copy(): if arg in BOX_PARAMETERS: bx_args[arg] = kwargs.pop(arg) data = _from_yaml(yaml_string=yaml_string, filename=filename, encoding=encoding, errors=errors, Loader=loader, **kwargs) if not isinstance(data, list): raise BoxError('yaml data not returned as a list' 'but rather a {0}'.format(type(data).__name__)) return cls(data, **bx_args) def box_it_up(self): for v in self: if hasattr(v, 'box_it_up') and v is not self: v.box_it_up() class ConfigBox(Box): """ Modified box object to add object transforms. Allows for build in transforms like: cns = ConfigBox(my_bool='yes', my_int='5', my_list='5,4,3,3,2') cns.bool('my_bool') # True cns.int('my_int') # 5 cns.list('my_list', mod=lambda x: int(x)) # [5, 4, 3, 3, 2] """ _protected_keys = dir({}) + ['to_dict', 'bool', 'int', 'float', 'list', 'getboolean', 'to_json', 'to_yaml', 'getfloat', 'getint', 'from_json', 'from_yaml'] def __getattr__(self, item): """Config file keys are stored in lower case, be a little more loosey goosey""" try: return super(ConfigBox, self).__getattr__(item) except AttributeError: return super(ConfigBox, self).__getattr__(item.lower()) def __dir__(self): return super(ConfigBox, self).__dir__() + ['bool', 'int', 'float', 'list', 'getboolean', 'getfloat', 'getint'] def bool(self, item, default=None): """ Return value of key as a boolean :param item: key of value to transform :param default: value to return if item does not exist :return: approximated bool of value """ try: item = self.__getattr__(item) except AttributeError as err: if default is not None: return default raise err if isinstance(item, (bool, int)): return bool(item) if (isinstance(item, str) and item.lower() in ('n', 'no', 'false', 'f', '0')): return False return True if item else False def int(self, item, default=None): """ Return value of key as an int :param item: key of value to transform :param default: value to return if item does not exist :return: int of value """ try: item = self.__getattr__(item) except AttributeError as err: if default is not None: return default raise err return int(item) def float(self, item, default=None): """ Return value of key as a float :param item: key of value to transform :param default: value to return if item does not exist :return: float of value """ try: item = self.__getattr__(item) except AttributeError as err: if default is not None: return default raise err return float(item) def list(self, item, default=None, spliter=",", strip=True, mod=None): """ Return value of key as a list :param item: key of value to transform :param mod: function to map against list :param default: value to return if item does not exist :param spliter: character to split str on :param strip: clean the list with the `strip` :return: list of items """ try: item = self.__getattr__(item) except AttributeError as err: if default is not None: return default raise err if strip: item = item.lstrip('[').rstrip(']') out = [x.strip() if strip else x for x in item.split(spliter)] if mod: return list(map(mod, out)) return out # loose configparser compatibility def getboolean(self, item, default=None): return self.bool(item, default) def getint(self, item, default=None): return self.int(item, default) def getfloat(self, item, default=None): return self.float(item, default) def __repr__(self): return ''.format(str(self.to_dict())) class SBox(Box): """ ShorthandBox (SBox) allows for property access of `dict` `json` and `yaml` """ _protected_keys = dir({}) + ['to_dict', 'tree_view', 'to_json', 'to_yaml', 'json', 'yaml', 'from_yaml', 'from_json', 'dict'] @property def dict(self): return self.to_dict() @property def json(self): return self.to_json() if yaml_support: @property def yaml(self): return self.to_yaml() def __repr__(self): return ''.format(str(self.to_dict()))