RetroDECK/functions/steam-sync/vdf/vdict.py

220 lines
6.6 KiB
Python

# pylint: disable=no-member,unnecessary-dunder-call
from collections import Counter
_iter_values = 'values'
_range = range
_string_type = str
import collections as _c
class _kView(_c.KeysView):
def __iter__(self):
return self._mapping.iterkeys()
class _vView(_c.ValuesView):
def __iter__(self):
return self._mapping.itervalues()
class _iView(_c.ItemsView):
def __iter__(self):
return self._mapping.iteritems()
class VDFDict(dict):
def __init__(self, data=None):
"""
This is a dictionary that supports duplicate keys and preserves insert order
``data`` can be a ``dict``, or a sequence of key-value tuples. (e.g. ``[('key', 'value'),..]``)
The only supported type for key is str.
Get/set duplicates is done by tuples ``(index, key)``, where index is the duplicate index
for the specified key. (e.g. ``(0, 'key')``, ``(1, 'key')``...)
When the ``key`` is ``str``, instead of tuple, set will create a duplicate and get will look up ``(0, key)``
"""
super().__init__()
self.__omap = []
self.__kcount = Counter()
if data is not None:
if not isinstance(data, (list, dict)):
raise ValueError("Expected data to be list of pairs or dict, got %s" % type(data))
self.update(data)
def __repr__(self):
out = "%s(" % self.__class__.__name__
out += "%s)" % repr(list(self.iteritems()))
return out
def __len__(self):
return len(self.__omap)
def _verify_key_tuple(self, key):
if len(key) != 2:
raise ValueError("Expected key tuple length to be 2, got %d" % len(key))
if not isinstance(key[0], int):
raise TypeError("Key index should be an int")
if not isinstance(key[1], _string_type):
raise TypeError("Key value should be a str")
def _normalize_key(self, key):
if isinstance(key, _string_type):
key = (0, key)
elif isinstance(key, tuple):
self._verify_key_tuple(key)
else:
raise TypeError("Expected key to be a str or tuple, got %s" % type(key))
return key
def __setitem__(self, key, value):
if isinstance(key, _string_type):
key = (self.__kcount[key], key)
self.__omap.append(key)
elif isinstance(key, tuple):
self._verify_key_tuple(key)
if key not in self:
raise KeyError("%s doesn't exist" % repr(key))
else:
raise TypeError("Expected either a str or tuple for key")
super().__setitem__(key, value)
self.__kcount[key[1]] += 1
def __getitem__(self, key):
return super().__getitem__(self._normalize_key(key))
def __delitem__(self, key):
key = self._normalize_key(key)
result = super().__delitem__(key)
start_idx = self.__omap.index(key)
del self.__omap[start_idx]
dup_idx, skey = key
self.__kcount[skey] -= 1
tail_count = self.__kcount[skey] - dup_idx
if tail_count > 0:
for idx in _range(start_idx, len(self.__omap)):
if self.__omap[idx][1] == skey:
oldkey = self.__omap[idx]
newkey = (dup_idx, skey)
super().__setitem__(newkey, self[oldkey])
super().__delitem__(oldkey)
self.__omap[idx] = newkey
dup_idx += 1
tail_count -= 1
if tail_count == 0:
break
if self.__kcount[skey] == 0:
del self.__kcount[skey]
return result
def __iter__(self):
return iter(self.iterkeys())
def __contains__(self, key):
return super().__contains__(self._normalize_key(key))
def __eq__(self, other):
if isinstance(other, VDFDict):
return list(self.items()) == list(other.items())
return False
def __ne__(self, other):
return not self.__eq__(other)
def clear(self):
super().clear()
self.__kcount.clear()
self.__omap = []
def get(self, key, *args):
return super().get(self._normalize_key(key), *args)
def setdefault(self, key, default=None):
if key not in self:
self.__setitem__(key, default)
return self.__getitem__(key)
def pop(self, key):
key = self._normalize_key(key)
value = self.__getitem__(key)
self.__delitem__(key)
return value
def popitem(self):
if not self.__omap:
raise KeyError("VDFDict is empty")
key = self.__omap[-1]
return key[1], self.pop(key)
def update(self, data=None, **kwargs):
if isinstance(data, dict):
data = data.items()
elif not isinstance(data, list):
raise TypeError("Expected data to be a list or dict, got %s" % type(data))
for key, value in data:
self.__setitem__(key, value)
def iterkeys(self):
return (key[1] for key in self.__omap)
def keys(self):
return _kView(self)
def itervalues(self):
return (self[key] for key in self.__omap)
def values(self):
return _vView(self)
def iteritems(self):
return ((key[1], self[key]) for key in self.__omap)
def items(self):
return _iView(self)
def get_all_for(self, key):
""" Returns all values of the given key """
if not isinstance(key, _string_type):
raise TypeError("Key needs to be a string.")
return [self[(idx, key)] for idx in _range(self.__kcount[key])]
def remove_all_for(self, key):
""" Removes all items with the given key """
if not isinstance(key, _string_type):
raise TypeError("Key need to be a string.")
for idx in _range(self.__kcount[key]):
super().__delitem__((idx, key))
self.__omap = list(filter(lambda x: x[1] != key, self.__omap))
del self.__kcount[key]
def has_duplicates(self):
"""
Returns ``True`` if the dict contains keys with duplicates.
Recurses through any all keys with value that is ``VDFDict``.
"""
for n in getattr(self.__kcount, _iter_values)():
if n != 1:
return True
def dict_recurse(obj):
for v in getattr(obj, _iter_values)():
if isinstance(v, VDFDict) and v.has_duplicates():
return True
if isinstance(v, dict):
return dict_recurse(v)
return False
return dict_recurse(self)