Repository URL to install this package:
|
Version:
2.3.0 ▾
|
from copy import deepcopy
def immutable(self, *args, **kwargs):
r"""
Function for not implemented method since the object is immutable
"""
raise AttributeError(f"'{self.__class__.__name__}' object is read-only")
class frozendict(dict):
r"""
A simple immutable dictionary.
The API is the same as `dict`, without methods that can change the
immutability. In addition, it supports __hash__().
"""
__slots__ = (
"_hash",
)
@classmethod
def fromkeys(cls, *args, **kwargs):
r"""
Identical to dict.fromkeys().
"""
return cls(dict.fromkeys(*args, **kwargs))
def __init__(self, *args, **kwargs):
pass
def __hash__(self, *args, **kwargs):
r"""
Calculates the hash if all values are hashable, otherwise raises a
TypeError.
"""
if self._hash != None:
_hash = self._hash
else:
try:
fs = frozenset(self.items())
except TypeError:
_hash = -1
else:
_hash = hash(fs)
object.__setattr__(self, "_hash", _hash)
if _hash == -1:
raise TypeError("Not all values are hashable.")
return _hash
def __repr__(self, *args, **kwargs):
r"""
Identical to dict.__repr__().
"""
body = super().__repr__(*args, **kwargs)
klass = self.__class__
if klass == frozendict or klass == coold:
name = f"frozendict.{klass.__name__}"
else:
name = klass.__name__
return f"{name}({body})"
def copy(self):
r"""
Return the object itself, as it's an immutable.
"""
klass = self.__class__
if (klass == frozendict or klass == coold):
return self
return klass(self)
def __copy__(self, *args, **kwargs):
r"""
See copy().
"""
return self.copy()
def __deepcopy__(self, memo, *args, **kwargs):
r"""
As for tuples, if hashable, see copy(); otherwise, it returns a
deepcopy.
"""
klass = self.__class__
return_copy = (klass == frozendict or klass == coold)
if return_copy:
try:
hash(self)
except TypeError:
return_copy = False
if return_copy:
return self.copy()
tmp = deepcopy(dict(self))
return klass(tmp)
def __reduce__(self, *args, **kwargs):
r"""
Support for `pickle`.
"""
return (self.__class__, (dict(self), ))
def set(self, key, val):
new_self = deepcopy(dict(self))
new_self[key] = val
return self.__class__(new_self)
def setdefault(self, key, default=None):
if key in self:
return self
new_self = deepcopy(dict(self))
new_self[key] = default
return self.__class__(new_self)
def delete(self, key):
new_self = deepcopy(dict(self))
del new_self[key]
if new_self:
return self.__class__(new_self)
return self.__class__()
def __setitem__(self, key, val, *args, **kwargs):
raise TypeError(
f"'{self.__class__.__name__}' object doesn't support item "
"assignment"
)
def __delitem__(self, key, *args, **kwargs):
raise TypeError(
f"'{self.__class__.__name__}' object doesn't support item "
"deletion"
)
def frozendict_or(self, other, *args, **kwargs):
res = {}
res.update(self)
res.update(other)
return self.__class__(res)
try:
frozendict.__or__
except AttributeError:
frozendict.__or__ = frozendict_or
frozendict.__ior__ = frozendict.__or__
try:
frozendict.__reversed__
except AttributeError:
def frozendict_reversed(self, *args, **kwargs):
return reversed(tuple(self))
frozendict.__reversed__ = frozendict_reversed
frozendict.clear = immutable
frozendict.pop = immutable
frozendict.popitem = immutable
frozendict.update = immutable
frozendict.__delattr__ = immutable
frozendict.__setattr__ = immutable
_sentinel = object()
out_of_range_err_tpl = "{name} index {index} out of max range {sign}{maxpos}"
by_values = ("key", "value")
def sortByKey(x):
return x[0]
def sortByValue(x):
return x[1]
def checkPosition(obj, index):
length = len(obj)
if abs(index) >= length:
name = obj.__class__.__name__
maxpos = length - 1
sign = "-" if index < 0 else ""
err = out_of_range_err_tpl.format(
name=name,
index=index,
sign=sign,
maxpos=maxpos
)
return IndexError(err)
return None
class coold(frozendict):
def __getitem__(self, key, *args, **kwargs):
try:
start = key.start
stop = key.stop
step = key.step
except AttributeError:
return dict.__getitem__(self, key)
else:
items = tuple(self.items())
new_items = items[start:stop:step]
return self.__class__(new_items)
def delete_by_index(self, index=None):
length = len(self)
if index == None:
index = length - 1
err = checkPosition(self, index)
if err != None:
raise err
if index < 0:
index = length + index
new_self = self[0:index]
dict.update(new_self, self[index+1:None])
if new_self:
return new_self
return self.__class__()
def move(self, pos, end_pos=None):
length = len(self)
if end_pos == None:
end_pos = length - 1
bad1 = abs(pos) >= length
if bad1 or abs(end_pos) >= length:
err1 = checkPosition(self, pos)
if err1 != None:
raise err1
err2 = checkPosition(self, end_pos)
if err2 != None:
raise err2
if pos < 0:
pos = length + pos
if end_pos < 0:
end_pos = length + end_pos
item = self[pos:pos+1]
if end_pos > pos:
new_self = self[0:pos]
dict.update(new_self, self[pos+1:end_pos+1])
dict.update(new_self, item)
dict.update(new_self, self[end_pos+1:None])
else:
new_self = self[0:end_pos]
dict.update(new_self, item)
dict.update(new_self, self[end_pos:pos])
dict.update(new_self, self[pos+1:None])
return new_self
def insert(self, index, key, val):
err = checkPosition(self, index)
if err != None:
raise err
if key in self:
name = self.__class__.__name__
raise KeyError(f"Key `{key}` is already in the {name}")
res = self[0:index]
dict.update(res, {key: val})
dict.update(res, self[index:None])
return res
def index(self, val, by="key"):
if by == "key":
obj = self
Exc = KeyError
elif by == "value":
obj = self.values()
Exc = ValueError
else:
by_values = ", ".join(by_values)
raise ValueError(
f"`by` parameter accept one of this values: {by_values}"
)
for i, v in enumerate(obj):
if v == val:
return i
if by == "value":
name = self.__class__.__name__
raise Exc(f"{val} is not in {name} values")
raise Exc(val)
def _get_by_index(self, collection, index):
try:
return collection[index]
except IndexError:
maxindex = len(collection) - 1
name = self.__class__.__name__
raise IndexError(f"{name} index {index} out of range {maxindex}") from None
def value(self, index=0):
collection = tuple(self.values())
return self._get_by_index(collection, index)
def key(self, index=0):
collection = tuple(self.keys())
return self._get_by_index(collection, index)
def item(self, index=0):
collection = tuple(self.items())
return self._get_by_index(collection, index)
def sort(self, by=None, **kwargs):
key = kwargs.get("key")
if by != None and key != None:
raise ValueError("You can't specify both `by` and `key` parameters")
elif key == None:
if by == None or by == "key":
key = sortByKey
elif by == "value":
key = sortByValue
else:
by_values = ", ".join(by_values)
raise ValueError(
f"`by` parameter accept one of this values: {by_values}"
)
kwargs["key"] = key
new_self = list(self.items())
new_self_sorted = sorted(new_self, **kwargs)
return self.__class__(new_self_sorted)
def get_deep(self, *args, default=_sentinel):
r"""
Get a nested element of the `coold`.
The method accepts multiple arguments or a single one. If a single
arguments is passed, it must be an iterable. These represents the
keys or indexes of the nested element.
The method first tries to get the value v1 of `coold` using the
first key. If it found v1 and there's no other key, v1 is
returned. Otherwise, the method tries to retrieve the value from v1
associated to the second key/index, and so on.
If in any point, for any reason, the value can't be retrieved, if
`default` parameter is specified, its value is returned. Otherwise, a
KeyError or a IndexError is raised.
"""
if len(args) == 1:
single = True
it_tpm = args[0]
try:
len(it_tpm)
it = it_tpm
except Exception:
# maybe it's an iterator
try:
it = tuple(it_tpm)
except Exception:
err = (
f"`{self.get_deep.__name__}` called with a single " +
"argument supports only iterables"
)
raise TypeError(err) from None
else:
it = args
single = False
if not it:
if single:
raise ValueError(
f"`{self.get_deep.__name__}` argument is empty"
)
else:
raise TypeError(
f"`{self.get_deep.__name__}` expects at least one argument"
)
obj = self
for k in it:
try:
obj = obj[k]
except (KeyError, IndexError) as e:
if default is _sentinel:
raise e from None
return default
return obj
def __sub__(self, other, *args, **kwargs):
r"""
The method will create a new `coold`, result of the subtraction
by `other`.
If `other` is a `dict`-like, the result will have the items of the
`coold` that are *not* in common with `other`.
If `other` is another type of iterable, the result will have the
items of `coold` without the keys that are in `other`.
"""
try:
iter(other)
except Exception:
err = (
f"Unsupported operand type(s) for -: " +
"`{self.__class__.__name__}` and `{other.__class__.__name__}`"
)
raise TypeError(err) from None
try:
res = {k: v for k, v in self.items() if (k, v) not in other.items()}
except Exception:
if not hasattr(other, "gi_running"):
true_other = other
else:
true_other = tuple(other)
res = {k: v for k, v in self.items() if k not in true_other}
return self.__class__(res)
def __and__(self, other, *args, **kwargs):
r"""
Returns a new `coold`, that is the intersection between `self`
and `other`.
If `other` is a `dict`-like object, the intersection will contain
only the *items* in common.
If `other` is another iterable, the intersection will contain
the items of `self` which keys are in `other`.
Iterables of pairs are *not* managed differently. This is for
consistency.
Beware! The final order is dictated by the order of `other`. This
allows the coder to change the order of the original `coold`.
The last two behaviors breaks voluntarly the `dict.items()` API, for
consistency and practical reasons.
"""
try:
try:
res = {k: v for k, v in other.items() if (k, v) in self.items()}
except Exception:
res = {k: self[k] for k in other if k in self}
except Exception:
err = (
f"Unsupported operand type(s) for &: " +
"`{self.__class__.__name__}` and `{other.__class__.__name__}`"
)
raise TypeError(err) from None
return self.__class__(res)
def isdisjoint(self, other):
r"""
Returns True if `other` dict-like object has no items in common,
otherwise False. Equivalent to `not (coold & dict_like)`
"""
try:
other.items
except AttributeError:
err = (
f"Unsupported operand type(s) for &: " +
f"`{self.__class__.__name__}` and `{other.__class__.__name__}`"
)
raise TypeError(err) from None
else:
res = self & other
return not res
def frozen_new(e4b37cdf_d78a_4632_bade_6f0579d8efac, *args, **kwargs):
cls = e4b37cdf_d78a_4632_bade_6f0579d8efac
has_kwargs = bool(kwargs)
continue_creation = True
# check if there's only an argument and it's of the same class
if len(args) == 1 and not has_kwargs:
it = args[0]
# no isinstance, to avoid subclassing problems
if (
(it.__class__ == frozendict and cls == frozendict) or
(it.__class__ == coold and cls == coold)
):
self = it
continue_creation = False
if continue_creation:
self = dict.__new__(cls, *args, **kwargs)
dict.__init__(self, *args, **kwargs)
# empty singleton - start
if (
(self.__class__ == frozendict or self.__class__ == coold)
and not len(self)
):
try:
self = cls.empty
continue_creation = False
except AttributeError:
cls.empty = self
# empty singleton - end
if continue_creation:
object.__setattr__(self, "_hash", None)
return self
frozendict.__new__ = frozen_new
coold.__new__ = frozen_new
__all__ = (frozendict.__name__, )