Source code for bootleg.utils.classes.dotted_dict

"""Dotted dict class."""
import keyword
import re
import string

import ujson


[docs]class DottedDict(dict): """ Dotted dictionary. Override for the dict object to allow referencing of keys as attributes, i.e. dict.key. """ def __init__(self, *args, **kwargs): """Dotted dict initializer.""" for arg in args: if isinstance(arg, dict): self._parse_input_(arg) elif isinstance(arg, list): for k, v in arg: self.__setitem__(k, v) elif hasattr(arg, "__iter__"): for k, v in list(arg): self.__setitem__(k, v) if kwargs: self._parse_input_(kwargs) def __delattr__(self, item): """Delete attr.""" self.__delitem__(item) def __delitem__(self, key): """Delete item.""" super(DottedDict, self).__delitem__(key) del self.__dict__[key] def __getattr__(self, attr): """Get attr.""" try: return self.__dict__[attr] # Do this to match python default behavior except KeyError: raise AttributeError(attr) def __getitem__(self, key): """Get item.""" return self.__dict__[key] def __repr__(self): """Wrap the returned dict in DottedDict() on output.""" return "{0}({1})".format( type(self).__name__, super(DottedDict, self).__repr__() ) def __setattr__(self, key, value): """Set attr.""" # No need to run _is_valid_identifier since a syntax error is raised if invalid attr name self.__setitem__(key, value) def __setitem__(self, key, value): """Set item.""" try: self._is_valid_identifier_(key) except ValueError: if not keyword.iskeyword(key): key = self._make_safe_(key) else: raise ValueError('Key "{0}" is a reserved keyword.'.format(key)) super(DottedDict, self).__setitem__(key, value) self.__dict__.update({key: value}) def _is_valid_identifier_(self, identifier): """Test the key name for valid identifier status as considered by the python lexer. Also check that the key name is not a python keyword. https://stackoverflow.com/questions/12700893/how-to-check-if-a-string-is-a-valid-python-identifier-including-keyword-check """ if re.match("[a-zA-Z_][a-zA-Z0-9_]*$", str(identifier)): if not keyword.iskeyword(identifier): return True raise ValueError('Key "{0}" is not a valid identifier.'.format(identifier)) def _make_safe_(self, key): """Replace the space characters on the key with _ to make valid attrs.""" key = str(key) allowed = string.ascii_letters + string.digits + "_" + "/" # Replace spaces with _ if " " in key: key = key.replace(" ", "_") # Find invalid characters for use of key as attr diff = set(key).difference(set(allowed)) # Replace invalid characters with _ if diff: for char in diff: key = key.replace(char, "_") # Add _ if key begins with int try: int(key[0]) except ValueError: pass else: key = "_{0}".format(key) return key def _parse_input_(self, input_item): """Parse the input item if dict into the dotted_dict constructor.""" for key, value in input_item.items(): if isinstance(value, dict): value = DottedDict(**{str(k): v for k, v in value.items()}) if isinstance(value, list): _list = [] for item in value: if isinstance(item, dict): _list.append(DottedDict(item)) else: _list.append(item) value = _list self.__setitem__(key, value)
[docs] def copy(self): """Ensure copy object is DottedDict, not dict.""" return type(self)(self)
[docs] def to_dict(self): """Recursive conversion back to dict.""" out = dict(self) for key, value in out.items(): if value is self: out[key] = out elif hasattr(value, "to_dict"): out[key] = value.to_dict() elif isinstance(value, list): _list = [] for item in value: if hasattr(item, "to_dict"): _list.append(item.to_dict()) else: _list.append(item) out[key] = _list return out
[docs]class PreserveKeysDottedDict(dict): """ Override auto correction of key names to safe attr names. Can result in errors when using attr name resolution. """ def __init__(self, *args, **kwargs): """Preserve keys DottedDict initializer.""" for arg in args: if isinstance(arg, dict): self._parse_input_(arg) elif isinstance(arg, list): for k, v in arg: self.__setitem__(k, v) elif hasattr(arg, "__iter__"): for k, v in list(arg): self.__setitem__(k, v) if kwargs: self._parse_input_(kwargs) def __delattr__(self, item): """Delete attr.""" self.__delitem__(item) def __delitem__(self, key): """Delete item.""" super(PreserveKeysDottedDict, self).__delitem__(key) del self.__dict__[key] def __getattr__(self, attr): """Get attr.""" try: return self.__dict__[attr] # Do this to match python default behavior except KeyError: raise AttributeError(attr) def __getitem__(self, key): """Get item.""" return self.__dict__[key] def __repr__(self): """Wrap the returned dict in DottedDict() on output.""" return "{0}({1})".format( type(self).__name__, super(PreserveKeysDottedDict, self).__repr__() ) def __setattr__(self, key, value): """Set attr.""" self.__setitem__(key, value) def __setitem__(self, key, value): """Set item.""" super(PreserveKeysDottedDict, self).__setitem__(key, value) self.__dict__.update({key: value}) def _parse_input_(self, input_item): """Parse the input item if dict into the dotted_dict constructor.""" for key, value in input_item.items(): if isinstance(value, dict): value = PreserveKeysDottedDict(**{str(k): v for k, v in value.items()}) if isinstance(value, list): _list = [] for item in value: if isinstance(item, dict): _list.append(PreserveKeysDottedDict(item)) else: _list.append(item) value = _list self.__setitem__(key, value)
[docs] def copy(self): """Ensure copy object is DottedDict, not dict.""" return type(self)(self)
[docs] def to_dict(self): """Recursive conversion back to dict.""" out = dict(self) for key, value in out.items(): if value is self: out[key] = out elif hasattr(value, "to_dict"): out[key] = value.to_dict() elif isinstance(value, list): _list = [] for item in value: if hasattr(item, "to_dict"): _list.append(item.to_dict()) else: _list.append(item) out[key] = _list return out
[docs]def create_bool_dotted_dict(d_dict): """Create boolean Dotted Dict.""" if (type(d_dict) is DottedDict) or (type(d_dict) is dict): d_dict = DottedDict(d_dict) if type(d_dict) is str and is_json(d_dict): d_dict = DottedDict(ujson.loads(d_dict)) if type(d_dict) is DottedDict: for k in d_dict: if d_dict[k] == "True": d_dict[k] = True elif d_dict[k] == "False": d_dict[k] = False elif ( (type(d_dict[k]) is DottedDict) or (type(d_dict[k]) is dict) or (type(d_dict[k]) is str and is_json(d_dict[k])) ): d_dict[k] = create_bool_dotted_dict(d_dict[k]) elif type(d_dict[k]) is list: for i in range(len(d_dict[k])): d_dict[k][i] = create_bool_dotted_dict(d_dict[k][i]) return d_dict
[docs]def is_number(s): """Return True is string is a number.""" try: float(s) return True except ValueError: return False
[docs]def is_json(value): """Return true if is json.""" # ujson is weird in that a string of a number is a dictionary; we don't want this if is_number(value): return False try: ujson.loads(value) except ValueError: return False return True