From d51e85d2124697519723a885bf2eafd0a3dfb75e Mon Sep 17 00:00:00 2001 From: Marco Sulla Date: Thu, 24 Oct 2019 21:15:04 +0200 Subject: [PATCH] now values of frozendict can be unhashable, as tuple. hash(myfrozendict) will raise an exception in that case; now empty frozendict is a singleton, like tuple and frozenset --- README.md | 14 ++++---- frozendict/VERSION | 2 +- frozendict/core.py | 73 ++++++++++++++++++++++++++--------------- test/test_frozendict.py | 13 ++++++-- 4 files changed, 64 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 7a8f909..f42ba51 100644 --- a/README.md +++ b/README.md @@ -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: @@ -60,8 +57,12 @@ 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 @@ -69,9 +70,6 @@ fd2 == fd fd2 is fd # False -hash(fd) -# 5833699487320513741 - import pickle fd_unpickled = pickle.loads(pickle.dumps(fd)) print(fd_unpickled) diff --git a/frozendict/VERSION b/frozendict/VERSION index d0149fe..80e78df 100644 --- a/frozendict/VERSION +++ b/frozendict/VERSION @@ -1 +1 @@ -1.3.4 +1.3.5 diff --git a/frozendict/core.py b/frozendict/core.py index 1132dd6..baa05d1 100644 --- a/frozendict/core.py +++ b/frozendict/core.py @@ -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 @@ -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): @@ -187,5 +207,4 @@ class frozendict(frozendictbase): __slots__ = ("_initialized", "_hash", "_repr", "_immutable_err", "_klass", "_klass_name") - __all__ = (frozendict.__name__, frozendictbase.__name__) diff --git a/test/test_frozendict.py b/test/test_frozendict.py index 91a2032..c8715ec 100644 --- a/test/test_frozendict.py +++ b/test/test_frozendict.py @@ -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) @@ -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 @@ -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