|
@@ -1,1110 +0,0 @@
|
|
|
-#!/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()))
|