From f3ec16ea8698ae212c2af8999d80a9e65e297764 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 31 Oct 2018 00:47:34 -0500 Subject: [PATCH] Version 3.2.2 (#63) * Adding hash abilities to new frozen BoxList * Fixing hashing returned unpredictable values (thanks to cebaa) * Fixing update to not handle protected words correctly (thanks to deluxghost) * Removing non-collection support for mapping and callable identification --- .travis.yml | 2 +- AUTHORS.rst | 2 ++ CHANGES.rst | 8 ++++++++ box.py | 41 +++++++++++++++---------------------- test/test_functional_box.py | 25 ++++++++++++++++++---- test/test_unittests_box.py | 3 ++- 6 files changed, 51 insertions(+), 30 deletions(-) diff --git a/.travis.yml b/.travis.yml index 07a16fb..7380565 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ python: - "3.6" matrix: include: - - python: 3.7 + - python: "3.7" dist: xenial sudo: true install: diff --git a/AUTHORS.rst b/AUTHORS.rst index 074df6a..dcfca78 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -37,3 +37,5 @@ Suggestions and bug reporting: - (richieadler) - V.Anh Tran (tranvietanh1991) - (ipcoder) +- (cebaa) +- (deluxghost) diff --git a/CHANGES.rst b/CHANGES.rst index 5fcb52b..538c962 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,14 @@ Changelog --------- +Version 3.2.2 +~~~~~~~~~~~~~ + +* Adding hash abilities to new frozen BoxList +* Fixing hashing returned unpredictable values (thanks to cebaa) +* Fixing update to not handle protected words correctly (thanks to deluxghost) +* Removing non-collection support for mapping and callable identification + Version 3.2.1 ~~~~~~~~~~~~~ diff --git a/box.py b/box.py index 1356f85..0a945c5 100644 --- a/box.py +++ b/box.py @@ -1,28 +1,22 @@ #!/usr/bin/env python # -*- coding: UTF-8 -*- # -# Copyright (c) 2017 - Chris Griffith - MIT License +# Copyright (c) 2017-2018 - Chris Griffith - MIT License """ Improved dictionary access through dot notation with additional tools. """ import string import sys import json -from uuid import uuid4 import re -import collections import copy from keyword import kwlist import warnings try: - from collections.abc import Iterable, Mapping + from collections.abc import Iterable, Mapping, Callable except ImportError: - try: - from collections import Iterable, Mapping - except ImportError: - Mapping = dict - Iterable = (tuple, list) + from collections import Iterable, Mapping, Callable yaml_support = True @@ -238,7 +232,6 @@ def _get_box_config(cls, kwargs): # Internal use only '__converted': set(), '__box_heritage': kwargs.pop('__box_heritage', None), - '__hash': None, '__created': False, # Can be changed by user after box creation 'default_box': kwargs.pop('default_box', False), @@ -255,10 +248,7 @@ def _get_box_config(cls, kwargs): class Box(dict): """ - - The lists are turned into BoxLists - so that they can also intercept incoming items and turn - them into Boxes. + 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. @@ -345,12 +335,10 @@ def box_it_up(self): def __hash__(self): if self._box_config['frozen_box']: - if not self._box_config['__hash']: - hashing = hash(uuid4().hex) - for item in self.items(): - hashing ^= hash(item) - self._box_config['__hash'] = hashing - return self._box_config['__hash'] + hashing = 54321 + for item in self.items(): + hashing ^= hash(item) + return hashing raise TypeError("unhashable type: 'Box'") def __dir__(self): @@ -447,7 +435,7 @@ def __get_default(self, item): if default_value is self.__class__: return self.__class__(__box_heritage=(self, item), **self.__box_config()) - elif isinstance(default_value, collections.Callable): + elif isinstance(default_value, Callable): return default_value() elif hasattr(default_value, 'copy'): return default_value.copy() @@ -557,8 +545,6 @@ def __setattr__(self, key, value): if key == _camel_killer(each_key): self[each_key] = value break - else: - self[key] = value else: self[key] = value else: @@ -669,7 +655,7 @@ def update(self, item=None, **kwargs): v = BoxList(v) try: self.__setattr__(k, v) - except TypeError: + except (AttributeError, TypeError): self.__setitem__(k, v) def setdefault(self, item, default=None): @@ -849,6 +835,13 @@ def __deepcopy__(self, memodict=None): 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: diff --git a/test/test_functional_box.py b/test/test_functional_box.py index 5c6608b..fa8d2e1 100644 --- a/test/test_functional_box.py +++ b/test/test_functional_box.py @@ -165,13 +165,13 @@ def test_to_json(self): def test_to_yaml(self): a = Box(test_dict) - assert yaml.load(a.to_yaml()) == test_dict + assert yaml.load(a.to_yaml(), Loader=yaml.SafeLoader) == test_dict def test_to_yaml_file(self): a = Box(test_dict) a.to_yaml(tmp_yaml_file) with open(tmp_yaml_file) as f: - data = yaml.load(f) + data = yaml.load(f, Loader=yaml.SafeLoader) assert data == test_dict def test_boxlist(self): @@ -213,7 +213,9 @@ def test_update(self): 'lister': ['a']}) a.update([('asdf', 'fdsa')]) a.update(testkey=66) + a.update({'items': 'test'}) + assert a['items'] == 'test' assert a.key1.new == 5 assert a['Key 2'].add_key == 6 assert "Key5" in a['Key 2'].Key4 @@ -321,6 +323,20 @@ def test_frozen(self): assert hash(bx3) + def test_hashing(self): + bx1 = Box(t=3, g=4, frozen_box=True) + bx2 = Box(g=4, t=3, frozen_box=True) + assert hash(bx1) == hash(bx2) + + bl1 = BoxList([1, 2, 3, 4], frozen_box=True) + bl2 = BoxList([1, 2, 3, 4], frozen_box=True) + bl3 = BoxList([2, 1, 3, 4], frozen_box=True) + assert hash(bl2) == hash(bl1) + assert hash(bl3) != hash(bl2) + + with pytest.raises(TypeError): + hash(BoxList([1, 2, 3])) + def test_frozen_list(self): bl = BoxList([5, 4, 3], frozen_box=True) with pytest.raises(BoxError): @@ -396,7 +412,8 @@ def test_property_box(self): assert isinstance(pbox.inner, SBox) assert pbox.inner.camel_case == 'Item' assert json.loads(pbox.json)['inner']['CamelCase'] == 'Item' - assert yaml.load(pbox.yaml)['inner']['CamelCase'] == 'Item' + test_item = yaml.load(pbox.yaml, Loader=yaml.SafeLoader) + assert test_item['inner']['CamelCase'] == 'Item' assert repr(pbox['inner']).startswith('