GithubHelp home page GithubHelp logo

marco-sulla / python-frozendict Goto Github PK

View Code? Open in Web Editor NEW
128.0 5.0 15.0 1.06 MB

A simple immutable dictionary for Python

Home Page: https://github.com/Marco-Sulla/python-frozendict

License: GNU Lesser General Public License v3.0

Python 5.72% C 94.27% Shell 0.01% Batchfile 0.01%
immutable hashable-dictionaries pickle frozendict dict dictionary map developer developer-tools development

python-frozendict's Introduction

frozendict

Table of Contents

Introduction

Welcome, fellow programmer, to the house of frozendict and deepfreeze!

frozendict is a simple immutable dictionary. It's fast as dict, and sometimes faster!

Unlike other similar implementations, immutability is guaranteed: you can't change the internal variables of the class, and they are all immutable objects. Reinvoking __init__ does not alter the object.

The API is the same as dict, without methods that can change the immutability. So it supports also fromkeys, unlike other implementations. Furthermore it can be pickled, unpickled and have an hash, if all values are hashable.

You can also add any dict to a frozendict using the | operator. The result is a new frozendict.

Install

You can install frozendict by simply typing in a command line:

pip install frozendict

The C Extension is optional by default from version 2.3.5. You can make it mandatory using:

CIBUILDWHEEL=1 pip install frozendict

On the contrary, if you want the pure py implementation:

FROZENDICT_PURE_PY=1 pip install frozendict

API

frozendict API

The API is the same of dict of Python 3.10, without the methods and operands which alter the map. Additionally, frozendict supports these methods:

__hash__()

If all the values of the frozendict are hashable, returns an hash, otherwise raises a TypeError.

set(key, value)

It returns a new frozendict. If key is already in the original frozendict, the new one will have it with the new value associated. Otherwise, the new frozendict will contain the new (key, value) item.

delete(key)

It returns a new frozendict without the item corresponding to the key. If the key is not present, a KeyError is raised.

setdefault(key[, default])

If key is already in frozendict, the object itself is returned unchanged. Otherwise, the new frozendict will contain the new (key, default) item. The parameter default defaults to None.

key([index])

It returns the key at the specified index (determined by the insertion order). If index is not passed, it defaults to 0. If the index is negative, the position will be the size of the frozendict + index

value([index])

Same as key(index), but it returns the value at the given index.

item([index])

Same as key(index), but it returns a tuple with (key, value) at the given index.

deepfreeze API

The frozendict module has also these static methods:

frozendict.deepfreeze(o, custom_converters = None, custom_inverse_converters = None)

Converts the object and all the objects nested in it, into their immutable counterparts.

The conversion map is in getFreezeConversionMap().

You can register a new conversion using register() You can also pass a map of custom converters with custom_converters and a map of custom inverse converters with custom_inverse_converters, without using register().

By default, if the type is not registered and has a __dict__ attribute, it's converted to the frozendict of that __dict__.

This function assumes that hashable == immutable (that is not always true).

This function uses recursion, with all the limits of recursions in Python.

Where is a good old tail call when you need it?

frozendict.register(to_convert, converter, *, inverse = False)

Adds a converter for a type to_convert. converter must be callable. The new converter will be used by deepfreeze() and has precedence over any previous converter.

If to_covert has already a converter, a FreezeWarning is raised.

If inverse is True, the conversion is considered from an immutable type to a mutable one. This make it possible to convert mutable objects nested in the registered immutable one.

frozendict.unregister(type, inverse = False)

Unregister a type from custom conversion. If inverse is True, the unregistered conversion is an inverse conversion (see register()).

Examples

frozendict examples

from frozendict import frozendict

fd = frozendict(Guzzanti = "Corrado", Hicks = "Bill")

print(fd)
# frozendict({'Guzzanti': 'Corrado', 'Hicks': 'Bill'})

frozendict({"Guzzanti": "Corrado", "Hicks": "Bill"})
# frozendict({'Guzzanti': 'Corrado', 'Hicks': 'Bill'})

hash(fd)
# 5833699487320513741

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

frozendict({frozendict(nested = 4, key = 2): 42})
# frozendict({frozendict({'nested': 4, 'key': 2}): 42})

fd | {1: 2}
# frozendict({'Guzzanti': 'Corrado', 'Hicks': 'Bill', 1: 2})

fd.set(1, 2)
# frozendict.frozendict({'Guzzanti': 'Corrado', 'Hicks': 'Bill', 1: 2})

fd.set("Guzzanti", "Sabina")
# frozendict.frozendict({'Guzzanti': 'Sabina', 'Hicks': 'Bill'})

fd.delete("Guzzanti")
# frozendict.frozendict({'Hicks': 'Bill'})

fd.setdefault("Guzzanti", "Sabina")
# frozendict.frozendict({'Guzzanti': 'Corrado', 'Hicks': 'Bill'})

fd.setdefault(1, 2)
# frozendict.frozendict({'Guzzanti': 'Corrado', 'Hicks': 'Bill', 1: 2})

fd.key()
# 'Guzzanti'

fd.value(1)
# 'Bill'

fd.item(-1)
# (1, 2)

print(fd["Guzzanti"])
# Corrado

fd["Brignano"]
# KeyError: 'Brignano'

len(fd)
# 2

"Guzzanti" in fd
# True

"Guzzanti" not in fd
# False

"Brignano" in fd
# False

fd5 = frozendict(fd)
id_fd5 = id(fd5)
fd5 |= {1: 2}
fd5
# frozendict.frozendict({'Guzzanti': 'Corrado', 'Hicks': 'Bill', 1: 2})
id(fd5) != id_fd5
# True

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

fd3 = frozendict(fd)
fd3 == fd
# True

fd4 = frozendict({"Hicks": "Bill", "Guzzanti": "Corrado"})

print(fd4)
# frozendict({'Hicks': 'Bill', 'Guzzanti': 'Corrado'})

fd4 == fd
# True

import pickle
fd_unpickled = pickle.loads(pickle.dumps(fd))
print(fd_unpickled)
# frozendict({'Guzzanti': 'Corrado', 'Hicks': 'Bill'})
fd_unpickled == fd
# True

frozendict(Guzzanti="Corrado", Hicks="Bill")
# frozendict({'Guzzanti': 'Corrado', 'Hicks': 'Bill'}

fd.get("Guzzanti")
# 'Corrado'

print(fd.get("Brignano"))
# None

tuple(fd.keys())
# ('Guzzanti', 'Hicks')

tuple(fd.values())
# ('Corrado', 'Bill')

tuple(fd.items())
# (('Guzzanti', 'Corrado'), ('Hicks', 'Bill'))

frozendict.fromkeys(["Corrado", "Sabina"], "Guzzanti")
# frozendict({'Corrado': 'Guzzanti', 'Sabina': 'Guzzanti'})

iter(fd)
# <dict_keyiterator object at 0x7feb75c49188>

fd["Guzzanti"] = "Caterina"
# TypeError: 'frozendict' object doesn't support item assignment

deepfreeze examples

import frozendict as cool

from frozendict import frozendict
from array import array
from collections import OrderedDict
from types import MappingProxyType

class A:
    def __init__(self, x):
        self.x = x

a = A(3)
        
o = {"x": [
    5, 
    frozendict(y = {5, "b", memoryview(b"b")}), 
    array("B", (0, 1, 2)), 
    OrderedDict(a=bytearray(b"a")),
    MappingProxyType({2: []}),
    a
]}

cool.deepfreeze(o)
# frozendict(x = (
#     5, 
#     frozendict(y = frozenset({5, "b", memoryview(b"b")})), 
#     (0, 1, 2), 
#     frozendict(a = b'a'),
#     MappingProxyType({2: ()}),
#     frozendict(x = 3),
# ))

Building

You can build frozendict directly from the code, using

python3 setup.py bdist_wheel

The C Extension is optional by default from version 2.3.5. You can make it mandatory by passing the environment variable CIBUILDWHEEL with value 1

On the contrary, if you want the pure py implementation, you can pass the env var FROZENDICT_PURE_PY with value 1

Benchmarks

Some benchmarks between dict and frozendict[1]:

################################################################################
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(d)`;         Size:    5; Keys: str; Type:       dict; Time: 8.02e-08; Sigma: 4e-09
Name: `constructor(d)`;         Size:    5; Keys: str; Type: frozendict; Time: 8.81e-08; Sigma: 3e-09
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(d)`;         Size:    5; Keys: int; Type:       dict; Time: 7.96e-08; Sigma: 5e-09
Name: `constructor(d)`;         Size:    5; Keys: int; Type: frozendict; Time: 8.97e-08; Sigma: 2e-09
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(d)`;         Size: 1000; Keys: str; Type:       dict; Time: 6.38e-06; Sigma: 9e-08
Name: `constructor(d)`;         Size: 1000; Keys: str; Type: frozendict; Time: 6.21e-06; Sigma: 2e-07
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(d)`;         Size: 1000; Keys: int; Type:       dict; Time: 3.49e-06; Sigma: 3e-07
Name: `constructor(d)`;         Size: 1000; Keys: int; Type: frozendict; Time: 3.48e-06; Sigma: 2e-07
################################################################################
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(kwargs)`;    Size:    5; Keys: str; Type:       dict; Time: 2.40e-07; Sigma: 1e-09
Name: `constructor(kwargs)`;    Size:    5; Keys: str; Type: frozendict; Time: 2.48e-07; Sigma: 2e-09
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(kwargs)`;    Size: 1000; Keys: str; Type:       dict; Time: 4.80e-05; Sigma: 1e-06
Name: `constructor(kwargs)`;    Size: 1000; Keys: str; Type: frozendict; Time: 2.90e-05; Sigma: 7e-07
################################################################################
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(seq2)`;      Size:    5; Keys: str; Type:       dict; Time: 2.01e-07; Sigma: 9e-10
Name: `constructor(seq2)`;      Size:    5; Keys: str; Type: frozendict; Time: 2.50e-07; Sigma: 1e-09
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(seq2)`;      Size:    5; Keys: int; Type:       dict; Time: 2.18e-07; Sigma: 2e-09
Name: `constructor(seq2)`;      Size:    5; Keys: int; Type: frozendict; Time: 2.73e-07; Sigma: 1e-09
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(seq2)`;      Size: 1000; Keys: str; Type:       dict; Time: 4.29e-05; Sigma: 6e-07
Name: `constructor(seq2)`;      Size: 1000; Keys: str; Type: frozendict; Time: 4.33e-05; Sigma: 6e-07
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(seq2)`;      Size: 1000; Keys: int; Type:       dict; Time: 3.04e-05; Sigma: 4e-07
Name: `constructor(seq2)`;      Size: 1000; Keys: int; Type: frozendict; Time: 3.45e-05; Sigma: 4e-07
################################################################################
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(o)`;         Size:    5; Keys: str; Type:       dict; Time: 7.93e-08; Sigma: 3e-09
Name: `constructor(o)`;         Size:    5; Keys: str; Type: frozendict; Time: 2.41e-08; Sigma: 6e-10
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(o)`;         Size:    5; Keys: int; Type:       dict; Time: 7.94e-08; Sigma: 5e-09
Name: `constructor(o)`;         Size:    5; Keys: int; Type: frozendict; Time: 2.41e-08; Sigma: 6e-10
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(o)`;         Size: 1000; Keys: str; Type:       dict; Time: 6.18e-06; Sigma: 3e-07
Name: `constructor(o)`;         Size: 1000; Keys: str; Type: frozendict; Time: 2.41e-08; Sigma: 6e-10
////////////////////////////////////////////////////////////////////////////////
Name: `constructor(o)`;         Size: 1000; Keys: int; Type:       dict; Time: 3.47e-06; Sigma: 2e-07
Name: `constructor(o)`;         Size: 1000; Keys: int; Type: frozendict; Time: 2.41e-08; Sigma: 6e-10
################################################################################
////////////////////////////////////////////////////////////////////////////////
Name: `o.copy()`;               Size:    5; Keys: str; Type:       dict; Time: 7.28e-08; Sigma: 2e-09
Name: `o.copy()`;               Size:    5; Keys: str; Type: frozendict; Time: 3.18e-08; Sigma: 2e-09
////////////////////////////////////////////////////////////////////////////////
Name: `o.copy()`;               Size:    5; Keys: int; Type:       dict; Time: 7.21e-08; Sigma: 4e-09
Name: `o.copy()`;               Size:    5; Keys: int; Type: frozendict; Time: 3.32e-08; Sigma: 2e-09
////////////////////////////////////////////////////////////////////////////////
Name: `o.copy()`;               Size: 1000; Keys: str; Type:       dict; Time: 6.16e-06; Sigma: 3e-07
Name: `o.copy()`;               Size: 1000; Keys: str; Type: frozendict; Time: 3.18e-08; Sigma: 2e-09
////////////////////////////////////////////////////////////////////////////////
Name: `o.copy()`;               Size: 1000; Keys: int; Type:       dict; Time: 3.46e-06; Sigma: 1e-07
Name: `o.copy()`;               Size: 1000; Keys: int; Type: frozendict; Time: 3.18e-08; Sigma: 2e-09
################################################################################
////////////////////////////////////////////////////////////////////////////////
Name: `o == o`;                 Size:    5; Keys: str; Type:       dict; Time: 7.23e-08; Sigma: 8e-10
Name: `o == o`;                 Size:    5; Keys: str; Type: frozendict; Time: 2.44e-08; Sigma: 2e-09
////////////////////////////////////////////////////////////////////////////////
Name: `o == o`;                 Size:    5; Keys: int; Type:       dict; Time: 7.30e-08; Sigma: 1e-09
Name: `o == o`;                 Size:    5; Keys: int; Type: frozendict; Time: 2.44e-08; Sigma: 2e-09
////////////////////////////////////////////////////////////////////////////////
Name: `o == o`;                 Size: 1000; Keys: str; Type:       dict; Time: 1.38e-05; Sigma: 1e-07
Name: `o == o`;                 Size: 1000; Keys: str; Type: frozendict; Time: 2.44e-08; Sigma: 2e-09
////////////////////////////////////////////////////////////////////////////////
Name: `o == o`;                 Size: 1000; Keys: int; Type:       dict; Time: 1.05e-05; Sigma: 7e-08
Name: `o == o`;                 Size: 1000; Keys: int; Type: frozendict; Time: 2.44e-08; Sigma: 2e-09
################################################################################
////////////////////////////////////////////////////////////////////////////////
Name: `for x in o`;             Size:    5; Keys: str; Type:       dict; Time: 7.33e-08; Sigma: 2e-09
Name: `for x in o`;             Size:    5; Keys: str; Type: frozendict; Time: 6.70e-08; Sigma: 1e-09
////////////////////////////////////////////////////////////////////////////////
Name: `for x in o`;             Size:    5; Keys: int; Type:       dict; Time: 7.33e-08; Sigma: 2e-09
Name: `for x in o`;             Size:    5; Keys: int; Type: frozendict; Time: 6.70e-08; Sigma: 1e-09
////////////////////////////////////////////////////////////////////////////////
Name: `for x in o`;             Size: 1000; Keys: str; Type:       dict; Time: 8.84e-06; Sigma: 5e-08
Name: `for x in o`;             Size: 1000; Keys: str; Type: frozendict; Time: 7.06e-06; Sigma: 6e-08
////////////////////////////////////////////////////////////////////////////////
Name: `for x in o`;             Size: 1000; Keys: int; Type:       dict; Time: 8.67e-06; Sigma: 7e-08
Name: `for x in o`;             Size: 1000; Keys: int; Type: frozendict; Time: 6.94e-06; Sigma: 3e-08
################################################################################
////////////////////////////////////////////////////////////////////////////////
Name: `for x in o.values()`;    Size:    5; Keys: str; Type:       dict; Time: 7.28e-08; Sigma: 9e-10
Name: `for x in o.values()`;    Size:    5; Keys: str; Type: frozendict; Time: 6.48e-08; Sigma: 8e-10
////////////////////////////////////////////////////////////////////////////////
Name: `for x in o.values()`;    Size:    5; Keys: int; Type:       dict; Time: 7.25e-08; Sigma: 1e-09
Name: `for x in o.values()`;    Size:    5; Keys: int; Type: frozendict; Time: 6.45e-08; Sigma: 1e-09
////////////////////////////////////////////////////////////////////////////////
Name: `for x in o.values()`;    Size: 1000; Keys: str; Type:       dict; Time: 9.06e-06; Sigma: 5e-07
Name: `for x in o.values()`;    Size: 1000; Keys: str; Type: frozendict; Time: 7.04e-06; Sigma: 4e-08
////////////////////////////////////////////////////////////////////////////////
Name: `for x in o.values()`;    Size: 1000; Keys: int; Type:       dict; Time: 9.53e-06; Sigma: 3e-08
Name: `for x in o.values()`;    Size: 1000; Keys: int; Type: frozendict; Time: 6.97e-06; Sigma: 3e-08
################################################################################
////////////////////////////////////////////////////////////////////////////////
Name: `for x in o.items()`;     Size:    5; Keys: str; Type:       dict; Time: 1.13e-07; Sigma: 3e-09
Name: `for x in o.items()`;     Size:    5; Keys: str; Type: frozendict; Time: 1.16e-07; Sigma: 2e-09
////////////////////////////////////////////////////////////////////////////////
Name: `for x in o.items()`;     Size:    5; Keys: int; Type:       dict; Time: 1.14e-07; Sigma: 3e-09
Name: `for x in o.items()`;     Size:    5; Keys: int; Type: frozendict; Time: 1.17e-07; Sigma: 2e-09
////////////////////////////////////////////////////////////////////////////////
Name: `for x in o.items()`;     Size: 1000; Keys: str; Type:       dict; Time: 1.53e-05; Sigma: 3e-07
Name: `for x in o.items()`;     Size: 1000; Keys: str; Type: frozendict; Time: 1.53e-05; Sigma: 4e-07
////////////////////////////////////////////////////////////////////////////////
Name: `for x in o.items()`;     Size: 1000; Keys: int; Type:       dict; Time: 1.53e-05; Sigma: 3e-07
Name: `for x in o.items()`;     Size: 1000; Keys: int; Type: frozendict; Time: 1.55e-05; Sigma: 4e-07
################################################################################
////////////////////////////////////////////////////////////////////////////////
Name: `pickle.dumps(o)`;        Size:    5; Keys: str; Type:       dict; Time: 6.82e-07; Sigma: 2e-08
Name: `pickle.dumps(o)`;        Size:    5; Keys: str; Type: frozendict; Time: 2.86e-06; Sigma: 1e-07
////////////////////////////////////////////////////////////////////////////////
Name: `pickle.dumps(o)`;        Size:    5; Keys: int; Type:       dict; Time: 4.77e-07; Sigma: 2e-08
Name: `pickle.dumps(o)`;        Size:    5; Keys: int; Type: frozendict; Time: 2.72e-06; Sigma: 8e-08
////////////////////////////////////////////////////////////////////////////////
Name: `pickle.dumps(o)`;        Size: 1000; Keys: str; Type:       dict; Time: 1.24e-04; Sigma: 4e-06
Name: `pickle.dumps(o)`;        Size: 1000; Keys: str; Type: frozendict; Time: 1.92e-04; Sigma: 5e-06
////////////////////////////////////////////////////////////////////////////////
Name: `pickle.dumps(o)`;        Size: 1000; Keys: int; Type:       dict; Time: 2.81e-05; Sigma: 6e-07
Name: `pickle.dumps(o)`;        Size: 1000; Keys: int; Type: frozendict; Time: 7.37e-05; Sigma: 1e-06
################################################################################
////////////////////////////////////////////////////////////////////////////////
Name: `pickle.loads(dump)`;     Size:    5; Keys: str; Type:       dict; Time: 9.08e-07; Sigma: 6e-09
Name: `pickle.loads(dump)`;     Size:    5; Keys: str; Type: frozendict; Time: 1.79e-06; Sigma: 9e-08
////////////////////////////////////////////////////////////////////////////////
Name: `pickle.loads(dump)`;     Size:    5; Keys: int; Type:       dict; Time: 4.46e-07; Sigma: 6e-09
Name: `pickle.loads(dump)`;     Size:    5; Keys: int; Type: frozendict; Time: 1.32e-06; Sigma: 7e-08
////////////////////////////////////////////////////////////////////////////////
Name: `pickle.loads(dump)`;     Size: 1000; Keys: str; Type:       dict; Time: 1.57e-04; Sigma: 8e-06
Name: `pickle.loads(dump)`;     Size: 1000; Keys: str; Type: frozendict; Time: 1.69e-04; Sigma: 7e-06
////////////////////////////////////////////////////////////////////////////////
Name: `pickle.loads(dump)`;     Size: 1000; Keys: int; Type:       dict; Time: 5.97e-05; Sigma: 5e-06
Name: `pickle.loads(dump)`;     Size: 1000; Keys: int; Type: frozendict; Time: 6.68e-05; Sigma: 2e-06
################################################################################
////////////////////////////////////////////////////////////////////////////////
Name: `class.fromkeys()`;       Size:    5; Keys: str; Type:       dict; Time: 1.88e-07; Sigma: 1e-09
Name: `class.fromkeys()`;       Size:    5; Keys: str; Type: frozendict; Time: 2.22e-07; Sigma: 7e-09
////////////////////////////////////////////////////////////////////////////////
Name: `class.fromkeys()`;       Size:    5; Keys: int; Type:       dict; Time: 2.08e-07; Sigma: 6e-09
Name: `class.fromkeys()`;       Size:    5; Keys: int; Type: frozendict; Time: 2.44e-07; Sigma: 2e-09
////////////////////////////////////////////////////////////////////////////////
Name: `class.fromkeys()`;       Size: 1000; Keys: str; Type:       dict; Time: 4.05e-05; Sigma: 4e-06
Name: `class.fromkeys()`;       Size: 1000; Keys: str; Type: frozendict; Time: 3.84e-05; Sigma: 5e-07
////////////////////////////////////////////////////////////////////////////////
Name: `class.fromkeys()`;       Size: 1000; Keys: int; Type:       dict; Time: 2.93e-05; Sigma: 7e-07
Name: `class.fromkeys()`;       Size: 1000; Keys: int; Type: frozendict; Time: 3.08e-05; Sigma: 2e-06
################################################################################

[1] Benchmarks done under Linux 64 bit, Python 3.10.2, using the C Extension.

python-frozendict's People

Contributors

apmorton avatar arkamar avatar bengt avatar eischaefer avatar eugene-eeo avatar gauravmm avatar jaraco avatar jbarberu avatar kenodegard avatar lurch avatar marco-sulla avatar pasztorpisti avatar ppolewicz avatar sunpoet avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

python-frozendict's Issues

Recursively convert dicts in __init__()?

I came across this awesome package looking for a way to convert a dict to a hashable thing. However, sometimes I have a dict contains other dicts, so even if I convert the dict to a frozendict, the frozendict is not hashable, because it contains dicts. It would be awesome to have some sort of constructor that could iterate recursively through iterable values and convert any dicts to frozendicts. I guess this would not be good default behaviour because it might be slow, and switching the behaviour on with a keyword argument to __new__() might clash with keys in **kwargs, I guess a classmethod might be the best approach?

In my actual use case, I don't expect to have deeply nested inputs, so I can convert dict->frozendict without worrying about recursion, but it'd be great to have an automagic, general solution!

[FEATURE] keys_of() method

keys_of(value, deep=False) method should return a generator, that checks in frozendict values if they are equal to value. If yes, the keys are yielded.

If deep=True, value does not match the current value and it's a iterable, that is not an iterator-like or a set-like, the function will search recursively for the value in the iterable and any sub-iterable. If the iterable is a sequence, the indexes of the value is yielded. If it's a map-like, the keys are yielded.

Example:

fd = frozendict({1: 2, 7:2, 2: {5: 2, 7: 2, 0: [2, 1]}, 3: [0, 4, 2, 1, 2]})
tuple(fd.keys_of(2))
# (1, 7)

for x in fd.keys_of(2, deep=True)):
    print(tuple(x))
# (1, )
# (7, )
# (2, 5)
# (2, 7)
# (2, 0, 0)
# (3, 2)
# (3, 4)

See also #14

Polymorphic type of `initialized`

I noticed recently that the initialized attribute is set to either True, False or 2. Python is a weakly-typed language, so this doesn't really matter.
But IMHO it feels kinda wrong having an attribute which is sometimes a boolean and sometimes an integer ๐Ÿ˜•

Also, is this an internal-use-only attribute, and should it therefore be renamed to _initialized ?

Segmentation fault on Python3.6 / Linux / frozendict 2.2 on compare.

Hi,

On debian:bullseye-slim docker image, with Python 3.6, frozendict 2.2 I get a segfault, I see this when running on CI on github actions, and managed to reproduce this locally (not easy), the code that crashes is:

> /vaex/packages/vaex-core/vaex/dataset.py(494)__eq__()
-> return self._ids == rhs._ids
(Pdb) self._ids
frozendict.frozendict({'x': '13c1af5d9cc28fb7a08eceeaa5ef703087cc5006154be10f64188937804de3c8', 'y': '7f869c9fd6388e26a085815327a1f92cbe50cdd79e4be7c816eeaafa21b66e23'})
(Pdb) rhs._ids
frozendict.frozendict({'z': '13c1af5d9cc28fb7a08eceeaa5ef703087cc5006154be10f64188937804de3c8', 'y': '7f869c9fd6388e26a085815327a1f92cbe50cdd79e4be7c816eeaafa21b66e23'})
False
(Pdb) rhs._ids == self._ids
Fatal Python error: Segmentation fault

Current thread 0x00007f9e72bbd740 (most recent call first):
  File "<stdin>", line 1 in <module>
  File "/opt/conda/envs/vaex-dev/lib/python3.6/pdb.py", line 376 in default
  File "/opt/conda/envs/vaex-dev/lib/python3.6/cmd.py", line 216 in onecmd
  File "/opt/conda/envs/vaex-dev/lib/python3.6/pdb.py", line 418 in onecmd
  File "/opt/conda/envs/vaex-dev/lib/python3.6/cmd.py", line 138 in cmdloop
  File "/opt/conda/envs/vaex-dev/lib/python3.6/pdb.py", line 321 in _cmdloop
  File "/opt/conda/envs/vaex-dev/lib/python3.6/pdb.py", line 352 in interaction
  File "/opt/conda/envs/vaex-dev/lib/python3.6/pdb.py", line 261 in user_line
  File "/opt/conda/envs/vaex-dev/lib/python3.6/bdb.py", line 69 in dispatch_line
  File "/opt/conda/envs/vaex-dev/lib/python3.6/bdb.py", line 51 in trace_dispatch
  File "/vaex/packages/vaex-core/vaex/dataset.py", line 494 in __eq__
  File "/home/maartenbreddels/github/vaexio/vaex/tests/dataset_test.py", line 96 in test_array_rename
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/python.py", line 183 in pytest_pyfunc_call
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_callers.py", line 39 in _multicall
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_manager.py", line 80 in _hookexec
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_hooks.py", line 265 in __call__
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/python.py", line 1641 in runtest
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/runner.py", line 162 in pytest_runtest_call
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_callers.py", line 39 in _multicall
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_manager.py", line 80 in _hookexec
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_hooks.py", line 265 in __call__
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/runner.py", line 255 in <lambda>
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/runner.py", line 311 in from_call
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/runner.py", line 255 in call_runtest_hook
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/runner.py", line 215 in call_and_report
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/runner.py", line 126 in runtestprotocol
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/runner.py", line 109 in pytest_runtest_protocol
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_callers.py", line 39 in _multicall
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_manager.py", line 80 in _hookexec
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_hooks.py", line 265 in __call__
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/main.py", line 348 in pytest_runtestloop
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_callers.py", line 39 in _multicall
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_manager.py", line 80 in _hookexec
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_hooks.py", line 265 in __call__
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/main.py", line 323 in _main
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/main.py", line 269 in wrap_session
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/main.py", line 316 in pytest_cmdline_main
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_callers.py", line 39 in _multicall
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_manager.py", line 80 in _hookexec
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/pluggy/_hooks.py", line 265 in __call__
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/config/__init__.py", line 163 in main
  File "/opt/conda/envs/vaex-dev/lib/python3.6/site-packages/_pytest/config/__init__.py", line 185 in console_main
  File "/opt/conda/envs/vaex-dev/bin/py.test", line 11 in <module>
Segmentation fault (core dumped)

So it's simply a dict with strings. Downgrading to 2.1.x solves the issue for us.

I hope this is useful.

Regards,

Maarten

Type hinting on C extension does not work

I hope this question isn't too stupid; I've spent a ton of time reading through typing and mypy docs but I'm not getting anywhere.

I'm trying to figure out how to annotate a class field as a frozendict for a data class, e.g.

@dataclass(frozen=True)
class Foo:
    a: frozendict[str, str]

Of course, I get TypeError: 'type' object is not subscriptable if I do that.

I'm using python 3.8 if that matters.

Thanks!

[BUG] Memory leak in __hash__()

Welcome! You should write in your Bug Report:

I found a case where frozendict does not free memory. The old v1 implementation does free memory in this situation.

OS version (https://www.google.com/search?channel=fs&q=check+os+version&ie=utf-8&oe=utf-8):
your OS version

$ uname -r
3.10.0-1160.59.1.el7.x86_64

Python3 version (python3 -V -V):
your Python version

3.7.6

Steps to reproduce:

import resource
import frozendict

print("frozendict version:", frozendict.__version__)
print()

c = 0
while True:
    mapping = {}
    fz = frozendict.frozendict({i: i for i in range(1000)})
    none = mapping.setdefault(fz, None)

    if (c % 10000) == 0:
        max_rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
        print(f"iteration: {c}, max rss: {max_rss} kb")

    c += 1

Actual result (with the python stack trace if present):
the result you see

frozendict version: 2.3.0

iteration: 0, max rss: 32196 kb
iteration: 10000, max rss: 1793552 kb
iteration: 20000, max rss: 3554452 kb
iteration: 30000, max rss: 5315972 kb
iteration: 40000, max rss: 7076784 kb
iteration: 50000, max rss: 8838472 kb

Note that this will use up memory rather fast on your machine.

[BUG] bug in isdisjoint

Using the latest master branch ( ef555e3 at the time of writing)

>>> from frozendict import frozendict
>>> fd1 = frozendict({'a': 1})
>>> fd2 = frozendict({'c': 5, 'b':4})
>>> fd1.isdisjoint(fd2)
False
>>> fd2.isdisjoint(fd1)
True

[FEATURE] New features for frozendict

TL;DR

I would add new features for frozendict:

  • set(key, value)
  • delete(key)
  • setdefault(key, default=None)

All these methods will not alter the original frozendict, but will return a modified copy.

The other features?

All the other features listed in #34 will be implemented in coold only, since:

  1. they implement methods from Sequence or Set API, or
  2. they could be implemented in a future in dict with different names, or
  3. They can be implemented in C only

Please public sdist on PyPI

Could you please publish sdist on PyPI?
It is preferred to build FreeBSD port (devel/py-frozendict) from sdist from PyPI.
Thanks!

[FEATURE] Show `==` being used in the README examples

In the list of examples in the README, I wonder if it might be worth demonstrating something like:

fd4 = frozendict({"Sulla": "Marco", "Hicks": "Bill"})
fd4 is fd
# False

fd4 == fd
# True

fd5 = frozendict({"Hicks": "Bill", "Sulla": "Marco"})
fd5 == fd4
# True

Or maybe that's too trivial to be worth demonstrating? ๐Ÿคทโ€โ™‚๏ธ

Return to dict API

From version 1.4.0, I added several methods to frozendict. Now I think they are not good for a simple reason: if in a future a similar feature will be added using a method with another name, it will be a mess.

For example, PEP 584 added union using the pipe operator, while I used the plus operator until now.

Issues to revert and close:
#2
#3
#13

Issues closed to be reverted:
#12
#14

Methods and attributes to remove without an issue:
__add__() and _sub__()
hash_no_errors() and initialized (make them private)

Typo in README?

It currently says:

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

frozendict(fd)
fd3 is fd
# True

Is the last bit supposed to say:

fd3 = frozendict(fd)
fd3 is fd

?

[BUG] setup.py build fails with python 3.10

OS version:
Exherbo Linux (rolling)

Python3 version (python3 -V -V):
Python 3.10.0 (default, Oct 16 2021, 00:16:14) [GCC 11.2.0]

Steps to reproduce:

  1. python3.10 -B setup.py build

Actual result:

 /var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6 /var/tmp/paludis/build/dev-python-frozendict-2.0.6/work
pushd /var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6
python3.10 -B setup.py build
running build
running build_py
creating build
creating build/lib.linux-x86_64-3.10
creating build/lib.linux-x86_64-3.10/frozendict
copying frozendict/core.py -> build/lib.linux-x86_64-3.10/frozendict
copying frozendict/__init__.py -> build/lib.linux-x86_64-3.10/frozendict
copying frozendict/VERSION -> build/lib.linux-x86_64-3.10/frozendict
running build_ext
building 'frozendict._frozendict' extension
creating build/temp.linux-x86_64-3.10
creating build/temp.linux-x86_64-3.10/var
creating build/temp.linux-x86_64-3.10/var/tmp
creating build/temp.linux-x86_64-3.10/var/tmp/paludis
creating build/temp.linux-x86_64-3.10/var/tmp/paludis/build
creating build/temp.linux-x86_64-3.10/var/tmp/paludis/build/dev-python-frozendict-2.0.6
creating build/temp.linux-x86_64-3.10/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work
creating build/temp.linux-x86_64-3.10/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS
creating build/temp.linux-x86_64-3.10/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10
creating build/temp.linux-x86_64-3.10/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6
creating build/temp.linux-x86_64-3.10/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict
creating build/temp.linux-x86_64-3.10/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src
creating build/temp.linux-x86_64-3.10/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10
creating build/temp.linux-x86_64-3.10/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10/cpython_src
creating build/temp.linux-x86_64-3.10/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10/cpython_src/Objects
x86_64-pc-linux-gnu-cc -Wno-unused-result -Wsign-compare -DNDEBUG -fwrapv -march=native -O2 -pipe -march=native -O2 -pipe -march=native -O2 -pipe -march=native -O2 -pipe -fPIC -I/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10/Include -I/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10/cpython_src/Include -I/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10/cpython_src/Include/internal -I/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10/cpython_src/Objects -I/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10/cpython_src/Objects/stringlib -I/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10/cpython_src/Objects/clinic -I/usr/include/python3.10 -c /var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10/cpython_src/Objects/dictobject.c -o build/temp.linux-x86_64-3.10/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10/cpython_src/Objects/dictobject.o -DPY_SSIZE_T_CLEAN -DPy_BUILD_CORE
warning: build_py: byte-compiling is disabled, skipping.

In file included from /var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10/cpython_src/Objects/dictobject.c:113:
/var/tmp/paludis/build/dev-python-frozendict-2.0.6/work/PYTHON_ABIS/3.10/frozendict-2.0.6/frozendict/src/3_10/cpython_src/Include/Python.h:72:10: fatal error: pytime.h: No such file or directory
   72 | #include "pytime.h"
      |          ^~~~~~~~~~
compilation terminated.
error: command '/usr/bin/x86_64-pc-linux-gnu-cc' failed with exit code 1

I suppose this might be due to the move of pytime.h from ../include/python to ../include/python/cpython (python/cpython#23988)

[BUG] `cls` kwarg when instantiating breaks frozendict pure py

First of all thanks for implementing this! The absence of a frozen dict has always bugged me.

OS: Arch Linux 5.4.0-72-generic
Python: 3.8.5

Steps to reproduce + actual result:

>>>frozendict({"a":2, "b":3, "cls":4})
frozendict({'a': 2, 'b': 3, 'cls': 4})
>>>dict(a=2, b=3, cls=4)
{'a': 2, 'b': 3, 'cls': 4}
>>>frozendict(a=2, b=3, cls=4)
Traceback (most recent call last):
  File "/root/.pycharm_helpers/pydev/_pydevd_bundle/pydevd_exec2.py", line 3, in Exec
    exec(exp, global_vars, local_vars)
  File "<input>", line 1, in <module>
TypeError: __new__() got multiple values for argument 'cls'

Its not hard to work around, but finding the problem was quite tricky. And it can "lurk" if you create frozen dicts with dynamic keys: E.g. in a setting like this:

json1 = get_json_from_somewhere()
json2 = get_json_from_somehwere_else()
frozendict(**json1, **json2)

[BUG] Python 3.10+ compat

Mapping was moved to collections.abc

(cpy39_1) (base) oberstet@intel-nuci7:~/myproject$ python -c "from collections import Mapping"
<string>:1: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.10 it will stop working
(cpy39_1) (base) oberstet@intel-nuci7:~/myproject$ python -V
Python 3.9.4

and consequently the library import fails

(cpy310_2) (base) oberstet@intel-nuci7:~/myproject$ python -c "from frozendict import frozendict"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/oberstet/cpy310_2/lib/python3.10/site-packages/frozendict/__init__.py", line 16, in <module>
    class frozendict(collections.Mapping):
AttributeError: module 'collections' has no attribute 'Mapping'
(cpy310_2) (base) oberstet@intel-nuci7:~/myproject$ python -V
Python 3.10.0
(cpy310_2) (base) oberstet@intel-nuci7:~/myproject$ pip show frozendict
Name: frozendict
Version: 1.2
Summary: An immutable dictionary
Home-page: https://github.com/slezica/python-frozendict
Author: Santiago Lezica
Author-email: [email protected]
License: MIT License
Location: /home/oberstet/cpy310_2/lib/python3.10/site-packages
Requires: 
Required-by: cairo-lang

[FEATURE] Add support for operand types like a tuple

When working with default arguments for positional (list) and keyworded (dict) arguments for dynamic functions and their calls I stumbled upon the warning of PyCharm that those arguments are mutable, which is, obviously not good.
So I thought, that I can use a tuple for the positional arguments, which supports adding elements using the + operator. This returns a new tuple instance.
It might be worth it to support these functionality with the frozendict type as well.

[FEATURE] add a `sorted()` method

Since dict remembers now the order of inserting, I think it's useful to have also a sorted() method.

It will return a new frozendict, with keys or values sorted.

The signature should be the same of sorted(), with the addition of an by parameter, that can be "keys" (the default) or "values".

[FEATURE] Add frozendict intersections test

...where the values in one frozendict don't match the values in the other frozendict. E.g.

fd1 = frozendict({"spam": 1, "eggs": 2})
fd2 = frozendict({"ham": 3, "eggs": 4})
fd3 = fd1 & fd2
fd4 = fd2 & fd1

and then check the contents of fd3 and fd4.

[FEATURE] Add other `set` operations

Legenda:
fd: a frozendict
d: a dict-like object
it: an iterable

frozendict could implement also:

  1. fd <= d: check if the two fd and d are equal, or if fd has the same items of d (subset).
  2. fd < d: check if the two fd and d are not equal, and if fd has the same items of d (proper subset).
  3. fd >= d, fd1 > d: the same concept above, inverted.
  4. fd & it: returns a new frozendict, that is fd with only the keys present in it (intersection, or join)
  5. fd ^ it: returns a new frozendict without the keys in common (xor)
  6. fd.isdisjoint(d): syntactic sugar for bool(fd & d)

TODO list

New features for frozendict:

  • support for Python 3.11 in the C Extension (WIP) #68
  • deepfreeze(): #30 (comment) (WIP)
  • improve JSON and pickle speed #76
  • #90
  • delete_by_index()
  • Slicing
  • index()
  • move()
  • insert()
  • sort()
  • getdeep(): #14
  • keysof(): #13
  • the entire Set API (?)
  • Possibility to get a value from frozendict values object by index
  • Possibility to get a key from frozendict keys object by index
  • Possibility to get a item from frozendict items object by index
  • restore freelists for 3.10+. Explore thread safe solutions.
  • optimize reverse

Bugs:

Internal cleanup:

  • try Py_TPFLAGS_IMMUTABLETYPE for CPython >= 3.10

โœ… DONE โœ…

  • __class_getitem__: https://github.com/python/cpython/pull/28450/files
  • dict_or also for python 3.6 - 3.8
  • Set up a CI/CD, as suggested by Lurch and explained here: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs-or-python?langId=py
  • remove asserts on old_value in frozendict_insert
  • remove ASSERT_CONSISTENT from _new
  • add ASSERT_CONSISTENT to fromkeys
  • replace copy method as in 3.9 for other versions
  • fix typo in PyFrozenDict_Check
  • make coold a subclass of frozendict
  • add new gc track control in frozendictiter_iternextitem introduced in dict 3.10
  • investigate why creation from dict is so slower than the dict constructor itself
  • restore old __reduce__ impl
  • implement reverse for all versions
  • do more tests
  • check that the code across the different py version is consistent, object by object, function by function
  • make the C extension working on Windows. Conda complains about it.
  • be sure all branches are tested
  • make a function for empty_frozendict generation
  • create a define for module name
  • rename _hash and _hash_calculated to ma_hash etc
  • deepcopy should return the frozendict untouched if it's hashable. But it seems that the trick to remove the function if it's not hashable and let the default deepcopy to fire does not work. Check how tuple works.
  • make executable created with cibuildwheel working on Debian (#41)
  • check original dict code to detect possible errors in frozendict
  • set()
  • delete()
  • setdefault()
  • remove coold
  • do not cache hash if hashing is not possible.
  • remove personal and controversial references from the tests and examples
  • key()
  • value()
  • item()
  • Add py only wheel on Conda
  • implement a memory leak tracer, like this one (thanks @lelit): https://github.com/python-rapidjson/python-rapidjson/blob/master/tests/test_memory_leaks.py
  • Ponder to remove the py wheel and make the C Extension optional
  • explore if there's a less hacky way to do the same test with different implementations of frozendict (pure py, C) with pytest, or explore other unit test frameworks. Possible solution: class tests.
  • restructure test dir

`deepcopy(frozendict[<key type>, <value type>])` returns empty dict instead of type

OS version: macOS Big Sur 11.4
Python3 version (python3 -V -V):

Python 3.9.0 (default, Nov 13 2020, 17:57:54)
[Clang 10.0.1 (clang-1001.0.46.4)]

Steps to reproduce:

>>> from copy import copy, deepcopy
>>>
>>> from frozendict import frozendict
>>>
>>> deepcopy(dict)
<class 'dict'>
>>> deepcopy(dict[str, int])
dict[str, int]
>>>
>>> deepcopy(frozendict)
<class 'frozendict.core.frozendict'>
>>> deepcopy(frozendict[str, int])
{}

I don't exactly know why this is happening, but a bit of debugging info:

  • __new__ and __init__ are never called (but did confirm it is called for something like frozendict())
  • __deepcopy__ is called, but only for the deepcopy(frozendict[str, int]) case... and then type(self) is dict, not frozendict which seems weird.

[BUG] deepcopy doesn't work as expected

OS version:
Ubuntu 14.04.6 x64

Python3 version (python3 -V -V):
Python 3.6.8 (default, Dec 25 2018, 00:00:00)
[GCC 4.8.4]

Steps to reproduce:

>>> from frozendict import frozendict
>>> import copy
>>> fd1 = frozendict({'tuple': (1, 2), 'list': ['a', 'b', 'c']})
>>> fd2 = copy.deepcopy(fd1)
>>> fd2['list'].append('d')
>>> print(fd1)
frozendict({'tuple': (1, 2), 'list': ['a', 'b', 'c', 'd']})

After the deepcopy I'd expect modifying fd2 to not also modify fd1?
I guess the current __deepcopy__ implementation would only work if the entire frozendict was hashable (and there's nothing currently in frozendict to ensure that all the values are hashable).
On a side-note, would there be any value (no pun intended) in a frozendict that did ensure that all its values were hashable (i.e. entirely immutable) ?

[BUG] v2.1.2 segfaults on Python 3.7 on Debian

Hi,

OS version: Debian 10, in Docker; installed using this Dockerfile: https://forge.softwareheritage.org/source/swh-jenkins-dockerfiles/browse/master/sphinx/Dockerfile

Python3 version (python3 -V -V):

Python 3.7.3 (default, Jan 22 2021, 20:04:44)
--
[GCC 8.3.0]

Steps to reproduce:

  1. run python3 -c "from frozendict import frozendict; frozendict({'_uuid': '9c20cbe8-6275-11ec-a64f-0242ac110001', 'processingMode': 'json-ld-1.1', 'mappings': {}})"

Actual result (with the python stack trace if present):

Segmentation fault (core dumped)

In details:

(.venv) jenkins@30f908325208:~/swh-environment/swh-search$ python3 -c "from frozendict import frozendict; frozendict({'_uuid': '9c20cbe8-6275-11ec-a64f-0242ac110001', 'processingMode': 'json-ld-1.1', 'mappings': {}})"
--
Segmentation fault (core dumped)
(.venv) jenkins@30f908325208:~/swh-environment/swh-search$ gdb python core
[...]
ย 
warning: core file may not match specified executable file.
[New LWP 5590]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `python3 -c from frozendict import frozendict; frozendict({'_uuid': '9c20cbe8-62'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00007f9db196bd46 in frozendict_new_barebone (type=0x7f9db197b0e0 <PyFrozenDict_Type>) at /project/frozendict/src/3_7/frozendictobject.c:2214
2214	/project/frozendict/src/3_7/frozendictobject.c: No such file or directory.
(gdb) where
#0  0x00007f9db196bd46 in frozendict_new_barebone (type=0x7f9db197b0e0 <PyFrozenDict_Type>) at /project/frozendict/src/3_7/frozendictobject.c:2214
#1  _frozendict_new (use_empty_frozendict=1, kwds=0x0, args=<optimized out>, type=0x7f9db197b0e0 <PyFrozenDict_Type>) at /project/frozendict/src/3_7/frozendictobject.c:2255
#2  frozendict_new (type=0x7f9db197b0e0 <PyFrozenDict_Type>, args=<optimized out>, kwds=0x0) at /project/frozendict/src/3_7/frozendictobject.c:2290
#3  0x00000000005d9bd7 in _PyObject_FastCallKeywords ()
#4  0x000000000055274b in _PyEval_EvalFrameDefault ()
#5  0x000000000054bcc2 in _PyEval_EvalCodeWithName ()
#6  0x00000000005317ef in PyRun_StringFlags ()
#7  0x000000000063177d in PyRun_SimpleStringFlags ()
#8  0x0000000000654558 in ?? ()
#9  0x000000000065468e in _Py_UnixMain ()
#10 0x00007f9db1d7409b in __libc_start_main (main=0x4bc560 <main>, argc=3, argv=0x7ffd87efc938, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffd87efc928) at ../csu/libc-start.c:308
#11 0x00000000005e0e8a in _start ()
(gdb) quit
(.venv) jenkins@30f908325208:~/swh-environment/swh-search$ python3 -V -V
Python 3.7.3 (default, Jan 22 2021, 20:04:44)
[GCC 8.3.0]
(.venv) jenkins@30f908325208:~/swh-environment/swh-search$ pip3 show frozendict
WARNING: The directory '/home/jenkins/.cache/pip' or its parent directory is not owned or is not writable by the current user. The cache has been disabled. Check the permissions and owner of that directory. If executing pip with sudo, you should use sudo's -H flag.
Name: frozendict
Version: 2.1.2
Summary: A simple immutable dictionary
Home-page: https://github.com/Marco-Sulla/python-frozendict
Author: Marco Sulla
Author-email: [email protected]
License: LGPL v3
Location: /home/jenkins/swh-environment/.venv/lib/python3.7/site-packages
Requires:
Required-by: PyLD, swh.indexer

Here is the core dump: core.gz

This issue does not seem to occur outside Docker or on frozendict 2.1.1.

Thanks

Lost JSON serialising ability with frozendict==2.2.0

in 2.1.3: orjson.dumps(frozendict.frozendict({"A": "B"})) == "{\"A\":\"B\"}"
in 2.2.0: frozendict.frozendict is not serializable

This is not a real bug, but we lost something that was coming for free with MutableMapping.
What's the recommended workaround?

[BUG] Mutation is possible on nested fields

OS version: OS X 11.5.2, ARM M1

Python3 version (python3 -V -V):

Python 3.7.9 (default, Dec 18 2020, 05:32:43) 
[GCC 8.3.0]

Steps to reproduce:

  1. open up a python shell
>>> from frozendict import frozendict
>>> frozen = frozendict({"outer": "outer", "inner": {"test": "can mutate"}})
>>> frozen
frozendict({'outer': 'outer', 'inner': {'test': 'can mutate'}})

>>> frozen['outer'] = 'uhoh'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.7/site-packages/frozendict/core.py", line 146, in __setitem__
    f"'{self.__class__.__name__}' object doesn't support item "
TypeError: 'frozendict' object doesn't support item assignment
# nice, we can't mutate top level fields

>>> frozen['inner']['test'] = 'i totally did just mutate that'
>>> frozen
frozendict({'outer': 'outer', 'inner': {'test': 'i totally did just mutate that'}})
# damn, we can mutate any nested fields

For what it's worth, MappingProxyType has the same problem.
Should I be recursively serializing each field as a frozendict?

[FEATURE] New Features for frozendict

I would slowly add a bunch of new features for frozendict. See TODO List, fist part: #34

I originally thought to add these features to an experimental type, coold. Currently, coold is undocumented and unused, so I don't think it's much of use to add these features to an obscure type. I think will remove coold.

[BUG] Memory leak when constructing empty frozendicts

Similar to #55, frozendict() and frozendict({}) leak memory.

OS version: Ubuntu 20.04.4 LTS
Python3 version (python3 -V -V): Python 3.8.10

Steps to reproduce

Borrowing code from #55:

import resource
import frozendict

print("frozendict version:", frozendict.__version__)
print()

c = 0
while True:
    fz = frozendict.frozendict({})
    
    if (c % 10000) == 0:
        max_rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
        print(f"iteration: {c}, max rss: {max_rss} kb")
    
    c += 1

Actual result

frozendict version: 2.3.2

iteration: 0, max rss: 11292 kb
iteration: 10000, max rss: 12188 kb
iteration: 20000, max rss: 12980 kb
iteration: 30000, max rss: 13772 kb
...
iteration: 3150000, max rss: 263252 kb
iteration: 3160000, max rss: 264044 kb
iteration: 3170000, max rss: 265100 kb

[DEFECT] Use `pyperf` for benchmarks

The benchmarks now uses simply timeit. pyperf is a tool used by py devs themselves to test the differences of speed between two py releases. I used it and it seems very much more reliable than timeit, as the pyperf devs themselves state.

The problem is the only way I found to work with it is by command line. I would use it in a py script, so it can be run on every OS.

Please consider fixing type annotations

It appears that there are some missing files:

from frozendict import frozendict  # error: Library stubs not installed for "frozendict" (or incompatible with Python 3.10)
reveal_type(dict.fromkeys("abc", 0))  # Revealed type is "builtins.dict[builtins.str*, builtins.int*]"
reveal_type(frozendict.fromkeys("abc", 0))  # Revealed type is "Any"

If you want type annotations to work, you could consider

  • exposing a py.typed file so that static typing is enabled,
  • annotating the methods in core.py, which should be easy now that Python 3.6 is (almost) dead.

[BUG] "functional change" to `__hash__`

OS version:
Ubuntu 14.04.6 x64

Python3 version (python3 -V -V):
Python 3.6.8 (default, Dec 25 2018, 00:00:00)
[GCC 4.8.4]

Steps to reproduce:

>>> from frozendict import frozendict
>>> fd1 = frozendict({'a': 'b', 1:2})
>>> fd2 = frozendict({'a': 'b', 1:2})
>>> fd3 = frozendict({1:2, 'a': 'b'})
>>> fd1 == fd2
True
>>> fd2 == fd3
True
>>> fd1 == fd3
True
>>> hash(fd1)
7248413867072730019
>>> hash(fd2)
7248413867072730019
>>> hash(fd3)
6961149576487961955

Note that fd2 == fd3 but hash(fd2) != hash(fd3). When I tried running this code yesterday (prior to 0125b86 ) the hash values for fd2 and fd3 equalled (hash(fd2) == hash(fd3)).

[BUG] Build failure with Python 3.11

Welcome! You should write in your Bug Report:

OS version (https://www.google.com/search?channel=fs&q=check+os+version&ie=utf-8&oe=utf-8):
Fedora Rawhide

Python3 version (python3 -V -V):
Python 3.11.0b3 (main, Jun 24 2022, 00:00:00) [GCC 12.1.1 20220507 (Red Hat 12.1.1-1)]

Actual result (with the python stack trace if present):

+ /usr/bin/python3 setup.py build '--executable=/usr/bin/python3 -s'
running build
running build_py
creating build
creating build/lib.linux-x86_64-cpython-311
creating build/lib.linux-x86_64-cpython-311/frozendict
copying frozendict/__init__.py -> build/lib.linux-x86_64-cpython-311/frozendict
copying frozendict/core.py -> build/lib.linux-x86_64-cpython-311/frozendict
copying frozendict/VERSION -> build/lib.linux-x86_64-cpython-311/frozendict
copying frozendict/py.typed -> build/lib.linux-x86_64-cpython-311/frozendict
running build_ext
building 'frozendict._frozendict' extension
creating build/temp.linux-x86_64-cpython-311
creating build/temp.linux-x86_64-cpython-311/frozendict
creating build/temp.linux-x86_64-cpython-311/frozendict/src
creating build/temp.linux-x86_64-cpython-311/frozendict/src/3_11
gcc -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -fPIC -I/home/orion/fedora/python-frozendict/frozendict-2.3.2/frozendict/src/3_11/Include -I/home/orion/fedora/python-frozendict/frozendict-2.3.2/frozendict/src/3_11/cpython_src/Objects -I/home/orion/fedora/python-frozendict/frozendict-2.3.2/frozendict/src/3_11/cpython_src/Objects/stringlib -I/home/orion/fedora/python-frozendict/frozendict-2.3.2/frozendict/src/3_11/cpython_src/Objects/clinic -I/home/orion/fedora/python-frozendict/frozendict-2.3.2/frozendict/src/3_11/cpython_src -I/usr/include/python3.11 -c frozendict/src/3_11/frozendictobject.c -o build/temp.linux-x86_64-cpython-311/frozendict/src/3_11/frozendictobject.o -DPY_SSIZE_T_CLEAN
cc1: fatal error: frozendict/src/3_11/frozendictobject.c: No such file or directory
compilation terminated.

[FEATURE] Add `r` operators

frozendict now implements __add__ and __sub__. It could implement also __radd__ and __rsub__. So, for example:

d + fd == fd + d

where fd is a frozendict and d is a dict-like object.

This could be applied also to #2.

[BUG] Memory leak in `__repr__`

Similar to #55, __repr__ leaks memory.

OS version: Ubuntu 20.04.4 LTS
Python3 version (python3 -V -V): Python 3.8.10

Steps to reproduce

Borrowing code from #55:

import resource
import frozendict

print("frozendict version:", frozendict.__version__)
print()

c = 0
while True:
    fz = frozendict.frozendict({i: i for i in range(1000)})
    _repr = repr(fz)
    
    if (c % 10000) == 0:
        max_rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
        print(f"iteration: {c}, max rss: {max_rss} kb")
    
    c += 1

Actual result

frozendict version: 2.3.2

iteration: 0, max rss: 11712 kb
iteration: 10000, max rss: 118104 kb
iteration: 20000, max rss: 224496 kb
iteration: 30000, max rss: 330888 kb
iteration: 40000, max rss: 437280 kb
iteration: 50000, max rss: 543672 kb

[BUG] Copy `frozenset.__hash__()` method from CPython

The hash method implemented currently is the hash method proposed in the first PEP that proposed a frozendict. It's simple, elegant and reliable. The only change is that now uses tuple instead of frozendict, that's more fast and uses less memory.

Anyway, the creation of a temporary tuple frozenset is useless. Even if a map usually does not contains more than 20 items, there could be huge dictionaries which hash can't be calculated with the current implementation.

IMHO we should lurk the CPython code of tuple.__hash__ frozenset.__hash__, and convert it in Py code.

[BUG] Incorrect exception type thrown when doing `attr` assignment using `dict` and `object`.

OS version: Any
Python3 version: Any

Steps to reproduce:

foo = frozendict({"foo": "bar"})
dict.__setattr__(foo, 'foo', "let's foo this!")
object.__setattr__(foo, 'foo', "let's foo this!")

Actual result:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'frozendict.frozendict' object has no attribute 'foo'

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'frozendict.frozendict' object has no attribute 'foo'

It should instead raize the following:

TypeError: 'frozendict.frozendict' object does not support item assignment

Besides, I am pretty amazed that this package is capable of creating actual immutable objects(i thought it is impossible to do so in python). Kudos. I tried to do similar stuff but wasn't aware of these edge cases. I did also a little comparison between the two packages. Not relevant, but it was a fun project to work on. If you don't mind, I would like to improve the codebase especially this part , where you can take advantage of decorators like what I did over here, and add type annotation.

Edit 1: Oh! wait... It is actually a feature, not a bug, __setattr__ would behave the same as foo.foo = ... which is not present presumably because the usage of __slots__. You are absolutely a fu**ing nit-picker. Now, I am wondering how did you prevent these cases: object.__setattr__ and dict.__setattr__.

Edit 2: Figured out the reason why it throws these exceptions because it inherits from dict:

>>> foo = {"foo": "bar"}
>>> foo.foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'dict' object has no attribute 'foo'
>>> object.__setattr__(foo, 'foo', 'bar')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'dict' object has no attribute 'foo'

Good to know.

Question about `notimplemented` method

Not sure if this counts as an enhancement request, but worth a question anyway...

When I did dir() on a frozendict object yesterday, I was surprised to see a notimplemented method listed!
Looking at the implementation should it
a) have a leading underscore, to mark it as not-a-public-method?
b) be named something more appropriate like _raise_not_implemented ?

[FEATURE] build and publish precompiled binary wheels to PyPI

Hello,
thanks for this project!

It would be great if you set up Github Actions CI to build wheels for all supported python versions and platforms (Linux, Mac, Windows) and publish them (either automatically when pushing git tags, or manually if you prefer) to the Python Package Index (PyPI) so that pip install frozendict works without needing a compiler toolchain installed.
There's https://cibuildwheel.readthedocs.io/ that makes this relatively easy, it takes care of basically everything, as long a package can be built with pip wheel, which it does in your case.

Thanks for considering!

Cosimo

Was FrozenOrderedDict class dropped intentionally?

OS version:
macOS 11.1

Python3 version (python3 -V -V):
Python 3.9.2 (default, Mar 26 2021, 23:22:38)
[Clang 12.0.0 (clang-1200.0.32.29)]

It looks like the FrozenOrderedDict class was dropped when moving code from frozendict/__init__.py to frozendict/core.py in d9b96d1. Was this intentional? I can't find any reference to this change anywhere than in the diff itself.

Thanks!

Please consider using composition over inheritance

In my opinion, frozendict shouldn't inherit from dict and therefore MutableMapping. Ideally, you would inherit from Mapping, and use composition over inheritance. I realize that may be a significant change, but it would make the code more correct.

from collections.abc import Mapping, MutableMapping
print(issubclass(frozendict, Mapping))  # True :)
print(issubclass(frozendict, MutableMapping))  # True, unfortunately.

This means doing something like:

class frozendict(Mapping):
    def __init__(...) -> ...:
        self._dict = dict(...)
    # Delegate __getitem__, __iter__, and __len__ to self._dict, along with any methods that would benefit like __contains__.

[Discussion] Why not MappingProxyType?

Thank you for writing this wonderful python package.
I don't think this is the right place to ask this question. However curious about this module. What are the differences from this package from types.MappingProxyType?
We can make the dictionary as read-only by using MappingProxyType?
Best Regards,
Levan Begashvili

[BUG] >=2.0 no longer compiles against pypy3

OS version:
Gentoo Linux (rolling)

p version (python3 -V -V):
Python 3.8.12 (9ef55f6fc3697bf8d85334ce2d70fdbba888e3ab, Jan 10 2022, 23:10:59)
[PyPy 7.3.7 with GCC 10.3.0]

Steps to reproduce:

  1. pypy3 setup.py build
  2. compile fails because certain cpython includes are used, though commenting them out doesn't seem to impact a normal python3_8 build, this does not happen with any of the 1.x release.

Actual result:

python-frozendict/frozendict/src/3_8/cpython_src/Include/internal/pycore_pystate.h:11:10: fatal error: cpython/initconfig.h: No such file or directory
   11 | #include "cpython/initconfig.h"
      |          ^~~~~~~~~~~~~~~~~~~~~~

The 3_8 c code seems to be quite divergent from the code for 3_9 and others, so I'm not entirely sure what is going on here.

[BUG] test/test_frozendict_c_subclass.py segfaults in a chroot

OS version (https://www.google.com/search?channel=fs&q=check+os+version&ie=utf-8&oe=utf-8):
Void Linux (x86_64-glibc)

Python3 version (python3 -V -V):
Python 3.10.0 (default, Oct 9 2021, 12:07:44) [GCC 10.2.1 20201203]

Steps to reproduce:

  1. Create a linux chroot (in this case, I created a Void chroot using base-voidstrap)
  2. Build frozendict
  3. Run tests using PYTHONPATH="$(cd build/lib* && pwd)" pytest3
  4. Get a segmentation fault in the middle of test/test_frozendict_c_subclass.py.

Actual result (with the python stack trace if present):
strace attached.

bash-5.1# PYTHONPATH="$(cd build/lib* && pwd)" pytest3
================================================= test session starts =================================================
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-0.13.1
rootdir: /python-frozendict
collected 308 items                                                                                                   

test/test_coold.py ........................................................                                     [ 18%]
test/test_coold_subclass.py .................................................                                   [ 34%]
test/test_frozendict.py ....................................................                                    [ 50%]
test/test_frozendict_c.py .....................................................                                 [ 68%]
test/test_frozendict_c_subclass.py .......Fatal Python error: Segmentation fault

Current thread 0x00007f4d2c093740 (most recent call first):
  Garbage-collecting
  File "/usr/lib/python3.10/site-packages/pluggy/callers.py", line 171 in _multicall
  File "/usr/lib/python3.10/site-packages/pluggy/manager.py", line 84 in <lambda>
  File "/usr/lib/python3.10/site-packages/pluggy/manager.py", line 93 in _hookexec
  File "/usr/lib/python3.10/site-packages/pluggy/hooks.py", line 286 in __call__
  File "/usr/lib/python3.10/site-packages/_pytest/fixtures.py", line 1072 in execute
  File "/usr/lib/python3.10/site-packages/_pytest/fixtures.py", line 687 in _compute_fixture_value
  File "/usr/lib/python3.10/site-packages/_pytest/fixtures.py", line 601 in _get_active_fixturedef
  File "/usr/lib/python3.10/site-packages/_pytest/fixtures.py", line 581 in getfixturevalue
  File "/usr/lib/python3.10/site-packages/_pytest/fixtures.py", line 568 in _fillfixtures
  File "/usr/lib/python3.10/site-packages/_pytest/python.py", line 1647 in setup
  File "/usr/lib/python3.10/site-packages/_pytest/runner.py", line 449 in prepare
  File "/usr/lib/python3.10/site-packages/_pytest/runner.py", line 150 in pytest_runtest_setup
  File "/usr/lib/python3.10/site-packages/pluggy/callers.py", line 187 in _multicall
  File "/usr/lib/python3.10/site-packages/pluggy/manager.py", line 84 in <lambda>
  File "/usr/lib/python3.10/site-packages/pluggy/manager.py", line 93 in _hookexec
  File "/usr/lib/python3.10/site-packages/pluggy/hooks.py", line 286 in __call__
  File "/usr/lib/python3.10/site-packages/_pytest/runner.py", line 255 in <lambda>
  File "/usr/lib/python3.10/site-packages/_pytest/runner.py", line 311 in from_call
  File "/usr/lib/python3.10/site-packages/_pytest/runner.py", line 254 in call_runtest_hook
  File "/usr/lib/python3.10/site-packages/_pytest/runner.py", line 215 in call_and_report
  File "/usr/lib/python3.10/site-packages/_pytest/runner.py", line 120 in runtestprotocol
  File "/usr/lib/python3.10/site-packages/_pytest/runner.py", line 109 in pytest_runtest_protocol
  File "/usr/lib/python3.10/site-packages/pluggy/callers.py", line 187 in _multicall
  File "/usr/lib/python3.10/site-packages/pluggy/manager.py", line 84 in <lambda>
  File "/usr/lib/python3.10/site-packages/pluggy/manager.py", line 93 in _hookexec
  File "/usr/lib/python3.10/site-packages/pluggy/hooks.py", line 286 in __call__
  File "/usr/lib/python3.10/site-packages/_pytest/main.py", line 348 in pytest_runtestloop
  File "/usr/lib/python3.10/site-packages/pluggy/callers.py", line 187 in _multicall
  File "/usr/lib/python3.10/site-packages/pluggy/manager.py", line 84 in <lambda>
  File "/usr/lib/python3.10/site-packages/pluggy/manager.py", line 93 in _hookexec
  File "/usr/lib/python3.10/site-packages/pluggy/hooks.py", line 286 in __call__
  File "/usr/lib/python3.10/site-packages/_pytest/main.py", line 323 in _main
  File "/usr/lib/python3.10/site-packages/_pytest/main.py", line 269 in wrap_session
  File "/usr/lib/python3.10/site-packages/_pytest/main.py", line 316 in pytest_cmdline_main
  File "/usr/lib/python3.10/site-packages/pluggy/callers.py", line 187 in _multicall
  File "/usr/lib/python3.10/site-packages/pluggy/manager.py", line 84 in <lambda>
  File "/usr/lib/python3.10/site-packages/pluggy/manager.py", line 93 in _hookexec
  File "/usr/lib/python3.10/site-packages/pluggy/hooks.py", line 286 in __call__
  File "/usr/lib/python3.10/site-packages/_pytest/config/__init__.py", line 162 in main
  File "/usr/lib/python3.10/site-packages/_pytest/config/__init__.py", line 185 in console_main
  File "/usr/bin/pytest3", line 33 in <module>
Segmentation fault

x86_64-glibc.fail.strace.txt

[BUG] Inconsistent hash values across different processes

Hashing a frozendict object multiple times provides the same value within the same process, but hashing the same frozendict (i.e., with the same values) in a different process generates a different value.

Steps to reproduce:

  1. In a python repl:
>> from frozendict import frozendict
>> hash(frozendict({"Sulla": "Marco", "Hicks": "Bill"}))
-4323838861655620389
>> hash(frozendict({"Sulla": "Marco", "Hicks": "Bill"}))
-4323838861655620389
  1. In another python repl:
>> from frozendict import frozendict
>> hash(frozendict({"Sulla": "Marco", "Hicks": "Bill"}))
-6311690871902122079
>> hash(frozendict({"Sulla": "Marco", "Hicks": "Bill"}))
-6311690871902122079

Tested on macos and Python 3.8.9

[BUG] Version on PyPi is newer than GitHub

I noticed PyPi has version 2.0.6, but GitHub still has the newest version as 2.0.4. Could you update the releases on GitHub, so that users can check the changes between 2.0.4 and the newest version?

dict.__init__ call in __new__ breaks subclasses with overridden __init__

frozendict.__new__ calls dict.__init__(self, *args, **kwargs).

As a result, if a frozendict subclass defines its own __init__ which is incompatible with dict.__init__, then its __new__ call with fail with a confusing error.

For a concrete example, consider the following:

from frozendict import frozendict

class FrozenDefaultDict(frozendict):

    def __init__(self, default_factory, *args, **kwargs):
        super().__init__(*args, **kwargs)
        object.__setattr__(self, '_default_factory', default_factory)

    def __getitem__(self, item):
        try:
            return super().__getitem__(item)
        except KeyError:
            return self._default_factory()

d = FrozenDefaultDict(int, dict(foo=1, bar=2))
print(d['foo'])
print (d['baz'])

The above raises an error on construction of d:

Traceback (most recent call last):
  File "scratch_28.py", line 17, in <module>
    d = FrozenDefaultDict(lambda k: k, dict(foo=1, bar=2))
  File ".venv/lib/python3.6/site-packages/frozendict/core.py", line 52, in __new__
    dict.__init__(self, *args, **kwargs)
TypeError: dict expected at most 1 arguments, got 2

If I change the superclass of FrozenDefaultDict to be a plain dict, the snippet behaves as expected, printing 1 then 0.

It is both surprising and restrictive that extending frozendict in the above manner does not work.

This can be worked around by extending __new__ instead of __init__ and making the appropriate super().__new__ call, but that feels rather leaky.

Is this behaviour an intentional design decision or a bug?

[FEATURE] get_deep() method

get_deep(*args, default=_sentinel) can accept a single argument, that must be an iterable, or multiple arguments.

The first element must be a key of the frozendict. If there's no a second element, the value is returned. If it's present, it tries to use it as argument for the eventual __getitem__() of the value object, and so on.

In this process, if a KeyError, an IndexError or a TypeError is raised, if default is set its value is returned, otherwise the exception will be re-raised.

Example:

fd = frozendict({1: [42]})
fd.get_deep(1, 0)
# 42
fd.get_deep(range(3), default=1981)
# 1981
fd.get_deep((1, 1))
# IndexError: list index out of range

See also #13

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.