| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110 |
- #!/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 '<Box: {0}>'.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 "<BoxList: {0}>".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 '<ConfigBox: {0}>'.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 '<ShorthandBox: {0}>'.format(str(self.to_dict()))
|