Skip to content

Commit

Permalink
now values of frozendict can be unhashable, as tuple. hash(myfrozendi…
Browse files Browse the repository at this point in the history
…ct) will raise an exception in that case; now empty frozendict is a singleton, like tuple and frozenset
  • Loading branch information
Marco-Sulla committed Oct 24, 2019
1 parent 25af0f1 commit d51e85d
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 38 deletions.
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ frozendict({"Sulla": "Marco", 2: 3}) - [2, 4]
# frozendict({'Sulla': 'Marco'})
```

Unlike other implementations, all values of a `frozendict` must be immutable,
i.e. support `hash()`, like `frozenset`.


Some other examples:

Expand Down Expand Up @@ -60,18 +57,19 @@ len(fd)
"God" in fd
# False

frozendict({1: []})
# TypeError: unhashable type: 'list'
hash(fd)
# 5833699487320513741

fd_unhashable = frozendict({1: []})
hash(fd_unhashable)
# TypeError: not all values are hashable

fd2 = fd.copy()
fd2 == fd
# True
fd2 is fd
# False

hash(fd)
# 5833699487320513741

import pickle
fd_unpickled = pickle.loads(pickle.dumps(fd))
print(fd_unpickled)
Expand Down
2 changes: 1 addition & 1 deletion frozendict/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.3.4
1.3.5
73 changes: 46 additions & 27 deletions frozendict/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,29 @@ class frozendictbase(dict):
"""

@classmethod
def fromkeys(cls, seq, value=None, *args, **kwargs):
return cls(dict.fromkeys(seq, value, *args, **kwargs))
def fromkeys(cls, *args, **kwargs):
return cls(dict.fromkeys(*args, **kwargs))

def __new__(klass, *args, **kwargs):
try:
klass.__setattr__ = object.__setattr__
except Exception:
pass

self = super().__new__(klass)
self._initialized = False
empty = hasattr(klass, "_empty")

if empty:
if kwargs:
empty = False
else:
for arg in args:
if arg:
empty = False
break

if empty:
self = klass._empty
else:
if hasattr(klass, "__setattr__"):
klass.__setattr__ = object.__setattr__

self = super().__new__(klass)
self._initialized = False

return self

Expand All @@ -24,31 +36,39 @@ def __init__(self, *args, **kwargs):
Identical to dict.__init__(). It can't be reinvoked
"""

self._klass = type(self)
self._klass_name = self._klass.__name__
_klass = type(self)

self._immutable_err = "'{klass}' object is immutable".format(klass=self._klass_name)
if not (hasattr(_klass, "_empty") and self is _klass._empty):
self._klass = _klass
self._klass_name = self._klass.__name__

if self._initialized:
raise NotImplementedError(self._immutable_err)
self._immutable_err = "'{klass}' object is immutable".format(klass=self._klass_name)

mysuper = super()
if self._initialized:
raise NotImplementedError(self._immutable_err)

super().__init__(*args, **kwargs)

if len(args) == 1 and type(args[0]) == self._klass and not kwargs:
old = args[0]
mysuper.__init__(old)
self._hash = old._hash
self._repr = old._repr
else:
mysuper.__init__(*args, **kwargs)

self._hash = hash(frozenset(mysuper.items()))
self._hash = None
self._repr = None

self._initialized = True
self._klass.__setattr__ = self._klass._notimplemented
self._initialized = True

if not hasattr(self._klass, "_empty") and not self:
self._klass._empty = self

self._klass.__setattr__ = self._klass._notimplemented

def __hash__(self, *args, **kwargs):
if self._hash is None:
try:
object.__setattr__(self, "_hash", hash(frozenset(self.items())))
except Exception:
object.__setattr__(self, "_hash", "unhashable")
raise TypeError("not all values are hashable")
elif self._hash is "unhashable":
raise TypeError("not all values are hashable")

return self._hash

def __repr__(self, *args, **kwargs):
Expand Down Expand Up @@ -187,5 +207,4 @@ class frozendict(frozendictbase):

__slots__ = ("_initialized", "_hash", "_repr", "_immutable_err", "_klass", "_klass_name")


__all__ = (frozendict.__name__, frozendictbase.__name__)
13 changes: 11 additions & 2 deletions test/test_frozendict.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def test_pickle(fd):
assert fd_unpickled == fd

def test_empty(fd_empty):
assert fd_empty == frozendict({})
assert fd_empty is frozendict({}) is frozendict([])

def test_constructor_self(fd):
assert fd == frozendict(fd)
Expand All @@ -110,8 +110,14 @@ def test_constructor_iterator(fd, fd_items):
assert frozendict(fd_items) == fd

def test_unhashable_value():
fd_unhashable = frozendict({1: []})

with pytest.raises(TypeError):
hash(fd_unhashable)

# hash is cached
with pytest.raises(TypeError):
frozendict({1: []})
hash(fd_unhashable)

def test_todict(fd, fd_dict):
assert dict(fd) == fd_dict
Expand All @@ -137,9 +143,12 @@ def test_fromkeys(fd, fd_giulia):

def test_repr(fd, fd_repr):
assert repr(fd) == fd_repr
# repr is cached
assert repr(fd) == fd_repr

def test_str(fd, fd_repr):
assert str(fd) == fd_repr
assert str(fd) == fd_repr

def test_format(fd, fd_repr):
assert format(fd) == fd_repr
Expand Down

0 comments on commit d51e85d

Please sign in to comment.