Browse Source

[compat] Add `compat_dict`

A dict that preserves insertion order and otherwise resembles the
dict builtin (if it isn't it) rather than `collections.OrderedDict`.

Also:
* compat_builtins_dict: the built-in definition in case `compat_dict`
  was imported as `dict`
* compat_dict_items: use instead of `dict.items` to get items from
  a `compat_dict` in insertion order, if you didn't define `dict` as
  `compat_dict`.
dirkf 4 weeks ago
parent
commit
5585d76da6
1 changed files with 141 additions and 0 deletions
  1. 141 0
      youtube_dl/compat.py

+ 141 - 0
youtube_dl/compat.py

@@ -3492,6 +3492,31 @@ except ImportError:
     compat_abc_ABC = _ABCMeta(str('ABC'), (object,), {})
 
 
+# dict mixin used here
+# like UserDict.DictMixin, without methods created by MutableMapping
+class _DictMixin(compat_abc_ABC):
+    def has_key(self, key):
+        return key in self
+
+    # get(), clear(), setdefault() in MM
+
+    def iterkeys(self):
+        return (k for k in self)
+
+    def itervalues(self):
+        return (self[k] for k in self)
+
+    def iteritems(self):
+        return ((k, self[k]) for k in self)
+
+    # pop(), popitem() in MM
+
+    def copy(self):
+        return type(self)(self)
+
+    # update() in MM
+
+
 # compat_collections_chain_map
 # collections.ChainMap: new class
 try:
@@ -3656,6 +3681,119 @@ except ImportError:
         import dummy_thread as compat_thread
 
 
+# compat_dict
+# compat_builtins_dict
+# compat_dict_items
+if sys.version_info >= (3, 6):
+    compat_dict = compat_builtins_dict = dict
+    compat_dict_items = dict.items
+else:
+    _get_ident = compat_thread.get_ident
+
+    class compat_dict(compat_collections_abc.MutableMapping, _DictMixin, dict):
+        """`dict` that preserves insertion order with interface like Py3.7+"""
+
+        _order = []  # default that should never be used
+
+        def __init__(self, *mappings_or_iterables, **kwargs):
+            # order an unordered dict using a list of keys: actual Py 2.7+
+            # OrderedDict uses a doubly linked list for better performance
+            self._order = []
+            for arg in mappings_or_iterables:
+                self.__update(arg)
+            if kwargs:
+                self.__update(kwargs)
+
+        def __getitem__(self, key):
+            return dict.__getitem__(self, key)
+
+        def __setitem__(self, key, value):
+            try:
+                if key not in self._order:
+                    self._order.append(key)
+                dict.__setitem__(self, key, value)
+            except Exception:
+                if key in self._order[-1:] and key not in self:
+                    del self._order[-1]
+                raise
+
+        def __len__(self):
+            return dict.__len__(self)
+
+        def __delitem__(self, key):
+            dict.__delitem__(self, key)
+            try:
+                # expected case, O(len(self)), but who dels anyway?
+                self._order.remove(key)
+            except ValueError:
+                pass
+
+        def __iter__(self):
+            for from_ in self._order:
+                if from_ in self:
+                    yield from_
+
+        def __del__(self):
+            for attr in ('_order',):
+                try:
+                    delattr(self, attr)
+                except Exception:
+                    pass
+
+        def __repr__(self, _repr_running={}):
+            # skip recursive items ...
+            call_key = id(self), _get_ident()
+            if _repr_running.get(call_key):
+                return '...'
+            _repr_running[call_key] = True
+            try:
+                return '%s({%s})' % (
+                    type(self).__name__,
+                    ','.join('%r: %r' % k_v for k_v in self.items()))
+            finally:
+                del _repr_running[call_key]
+
+        # merge/update (PEP 584)
+
+        def __or__(self, other):
+            if not isinstance(other, compat_collections_abc.Mapping):
+                return NotImplemented
+            new = type(self)(self)
+            new.update(other)
+            return new
+
+        def __ror__(self, other):
+            if not isinstance(other, compat_collections_abc.Mapping):
+                return NotImplemented
+            new = type(other)(other)
+            new.update(self)
+            return new
+
+        def __ior__(self, other):
+            self.update(other)
+            return self
+
+        # optimisations
+
+        def __reversed__(self):
+            for from_ in reversed(self._order):
+                if from_ in self:
+                    yield from_
+
+        def __contains__(self, item):
+            return dict.__contains__(self, item)
+
+        # allow overriding update without breaking __init__
+        def __update(self, *args, **kwargs):
+            super(compat_dict, self).update(*args, **kwargs)
+
+    compat_builtins_dict = dict
+    # Using the object's method, not dict's:
+    #   an ordered dict's items can be returned unstably by unordered
+    #   dict.items as if the method was not ((k, self[k]) for k in self)
+    compat_dict_items = lambda d: d.items()
+
+
 legacy = [
     'compat_HTMLParseError',
     'compat_HTMLParser',
@@ -3690,6 +3828,7 @@ __all__ = [
     'compat_base64_b64decode',
     'compat_basestring',
     'compat_brotli',
+    'compat_builtins_dict',
     'compat_casefold',
     'compat_chr',
     'compat_collections_abc',
@@ -3697,6 +3836,8 @@ __all__ = [
     'compat_contextlib_suppress',
     'compat_ctypes_WINFUNCTYPE',
     'compat_datetime_timedelta_total_seconds',
+    'compat_dict',
+    'compat_dict_items',
     'compat_etree_fromstring',
     'compat_etree_iterfind',
     'compat_filter',