Skip to content

Commit

Permalink
Version 3.2.0 (#53)
Browse files Browse the repository at this point in the history
* Adding `ordered_box` option to keep key order based on insertion (thanks to pwwang)
* Adding custom `__iter__`, `__revered__`, `pop`, `popitems`
* Fixing ordering of camel_case_killer vs default_box (thanks to Matan Rosenberg)
* Fixing non string keys not being supported correctly (thanks to Matt Wisniewski)
  • Loading branch information
cdgriffith authored May 11, 2018
1 parent 37ad926 commit d6dae6c
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 76 deletions.
3 changes: 3 additions & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Code contributions:
- dhilipsiva (dhilipsiva)
- MAA (FooBarQuaxx)
- Jiang Chen (criver)
- Matan Rosenberg (matan129)
- Matt Wisniewski (polishmatt)

Suggestions and bug reporting:

Expand All @@ -30,3 +32,4 @@ Suggestions and bug reporting:
- askvictor [Hacker News]
- wouter bolsterlee (wbolster)
- Mickaël Thomas (mickael9)
- (pwwang)
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Changelog
---------

Version 3.2.0
~~~~~~~~~~~~~

* Adding `ordered_box` option to keep key order based on insertion (thanks to pwwang)
* Adding custom `__iter__`, `__revered__`, `pop`, `popitems`
* Fixing ordering of camel_case_killer vs default_box (thanks to Matan Rosenberg)
* Fixing non string keys not being supported correctly (thanks to Matt Wisniewski)

Version 3.1.1
~~~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2017 Chris Griffith
Copyright (c) 2017-2018 Chris Griffith

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
24 changes: 21 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ Box's parameters
box_it_up False Recursively create all Boxes from the start (like previous versions)
box_safe_prefix "x" Character or prefix to prepend to otherwise invalid attributes
box_duplicates "ignore" When conversion duplicates are spotted, either ignore, warn or error
ordered_box False Preserve order of keys entered into the box
================ ========= ===========

Box's functions
Expand Down Expand Up @@ -301,8 +302,25 @@ snake_case_attributes.
cameled.bad_habit
# "I just can't stop!"
If this is used along side `conversion_box`, which is enabled by default,
all attributes will only be accessible with lowercase letters.
Ordered Box
~~~~~~~~~~~

Preserve the order that the keys were entered into the box. The preserved order
will be observed while iterating over the box, or calling `.keys()`,
`.values()` or `.items()`

.. code:: python
box_of_order = Box(ordered_box=True)
box_of_order.c = 1
box_of_order.a = 2
box_of_order.d = 3
box_of_order.keys() == ['c', 'a', 'd']
Keep in mind this will not guarantee order of `**kwargs` passed to Box,
as they are inherently not ordered until Python 3.6.



BoxList
Expand Down Expand Up @@ -392,7 +410,7 @@ config values into python types. It supports `list`, `bool`, `int` and `float`.
License
=======

MIT License, Copyright (c) 2017 Chris Griffith. See LICENSE file.
MIT License, Copyright (c) 2017-2018 Chris Griffith. See LICENSE file.


.. |BoxImage| image:: https://raw.githubusercontent.com/cdgriffith/Box/master/box_logo.png
Expand Down
150 changes: 103 additions & 47 deletions box.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@
__all__ = ['Box', 'ConfigBox', 'BoxList', 'SBox',
'BoxError', 'BoxKeyError']
__author__ = 'Chris Griffith'
__version__ = '3.1.1'
__version__ = '3.2.0'

BOX_PARAMETERS = ('default_box', 'default_box_attr', 'conversion_box',
'frozen_box', 'camel_killer_box', 'box_it_up',
'box_safe_prefix', 'box_duplicates')
'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])')
Expand Down Expand Up @@ -243,7 +243,8 @@ def _get_box_config(cls, kwargs):
'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')
'box_duplicates': kwargs.pop('box_duplicates', 'ignore'),
'ordered_box': kwargs.pop('ordered_box', False)
}


Expand All @@ -265,6 +266,7 @@ class Box(dict):
: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',
Expand All @@ -281,6 +283,8 @@ def __new__(cls, *args, **kwargs):

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')
Expand All @@ -292,13 +296,12 @@ def __init__(self, *args, **kwargs):
if v is args[0]:
v = self
self[k] = v
if k == "_box_config":
continue
self.__add_ordered(k)
elif isinstance(args[0], Iterable):
for k, v in args[0]:
self[k] = v
if k == "_box_config":
continue
self.__add_ordered(k)

else:
raise ValueError('First argument must be mapping or iterable')
elif args:
Expand All @@ -310,13 +313,19 @@ def __init__(self, *args, **kwargs):
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,
Expand Down Expand Up @@ -404,32 +413,40 @@ def __setstate__(self, state):
self._box_config = state['_box_config']
self.__dict__.update(state)

def __getitem__(self, item):
def __getitem__(self, item, _ignore_default=False):
try:
value = super(Box, self).__getitem__(item)
except KeyError as err:
if item == '_box_config':
raise BoxError('_box_config key must exist and does not. '
'This is most likely a bug, please report.')
default_value = self._box_config['default_box_attr']
if self._box_config['default_box']:
if default_value is self.__class__:
return self.__class__(__box_heritage=(self, item),
**self.__box_config())
elif isinstance(default_value, collections.Callable):
return default_value()
elif hasattr(default_value, 'copy'):
return default_value.copy()
return default_value
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]
return [self[x] for x in self.keys()]

def items(self):
return [(x, self[x]) for x in 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, collections.Callable):
return default_value()
elif hasattr(default_value, 'copy'):
return default_value.copy()
return default_value

def __box_config(self):
out = {}
Expand Down Expand Up @@ -478,24 +495,23 @@ def __create_lineage(self):
def __getattr__(self, item):
try:
try:
value = self[item]
value = self.__getitem__(item, _ignore_default=True)
except KeyError:
value = object.__getattribute__(self, item)
except AttributeError as err:
try:
return self.__getitem__(item)
except KeyError:
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:
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 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':
Expand All @@ -510,6 +526,7 @@ def __setitem__(self, key, value):
_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):
Expand Down Expand Up @@ -541,12 +558,16 @@ def __setattr__(self, key, value):
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']:
Expand All @@ -561,13 +582,55 @@ def __delattr__(self, item):
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
Expand Down Expand Up @@ -599,7 +662,10 @@ def update(self, item=None, **kwargs):
continue
if isinstance(v, list):
v = BoxList(v)
self.__setattr__(k, v)
try:
self.__setattr__(k, v)
except TypeError:
self.__setitem__(k, v)

def setdefault(self, item, default=None):
if item in self:
Expand Down Expand Up @@ -732,16 +798,6 @@ def insert(self, index, p_object):
BoxList(p_object))
super(BoxList, self).insert(index, p_object)

def __getstate__(self):
return {'box_class': self.box_class,
'box_options': self.box_options,
'box_org_ref': self.box_org_ref}

def __setstate__(self, state):
self.box_class = state['box_class']
self.box_options = state['box_options']
self.box_org_ref = state['box_org_ref']

def __repr__(self):
return "<BoxList: {0}>".format(self.to_list())

Expand Down
Loading

0 comments on commit d6dae6c

Please sign in to comment.