diff options
author | morpheus65535 <[email protected]> | 2021-05-08 10:25:29 -0400 |
---|---|---|
committer | GitHub <[email protected]> | 2021-05-08 10:25:29 -0400 |
commit | 72b6ab3c6a11e1c12d86563989d88d73e4e64377 (patch) | |
tree | 3739d75ba8814a226f241828afb888826977ef78 /libs/bidict | |
parent | 09a31cf9a42f4b08ef9953fc9dc3af47bbc39217 (diff) | |
download | bazarr-72b6ab3c6a11e1c12d86563989d88d73e4e64377.tar.gz bazarr-72b6ab3c6a11e1c12d86563989d88d73e4e64377.zip |
Added live update of UI using websocket. Make sure your reverse proxy upgrade the connection!
Diffstat (limited to 'libs/bidict')
-rw-r--r-- | libs/bidict/__init__.py | 111 | ||||
-rw-r--r-- | libs/bidict/_abc.py | 81 | ||||
-rw-r--r-- | libs/bidict/_base.py | 462 | ||||
-rw-r--r-- | libs/bidict/_bidict.py | 46 | ||||
-rw-r--r-- | libs/bidict/_delegating_mixins.py | 92 | ||||
-rw-r--r-- | libs/bidict/_dup.py | 36 | ||||
-rw-r--r-- | libs/bidict/_exc.py | 35 | ||||
-rw-r--r-- | libs/bidict/_frozenbidict.py | 51 | ||||
-rw-r--r-- | libs/bidict/_frozenordered.py | 65 | ||||
-rw-r--r-- | libs/bidict/_marker.py | 19 | ||||
-rw-r--r-- | libs/bidict/_miss.py | 14 | ||||
-rw-r--r-- | libs/bidict/_mut.py | 177 | ||||
-rw-r--r-- | libs/bidict/_named.py | 103 | ||||
-rw-r--r-- | libs/bidict/_noop.py | 14 | ||||
-rw-r--r-- | libs/bidict/_orderedbase.py | 302 | ||||
-rw-r--r-- | libs/bidict/_orderedbidict.py | 87 | ||||
-rw-r--r-- | libs/bidict/_util.py | 61 | ||||
-rw-r--r-- | libs/bidict/compat.py | 78 | ||||
-rw-r--r-- | libs/bidict/metadata.py | 49 |
19 files changed, 1883 insertions, 0 deletions
diff --git a/libs/bidict/__init__.py b/libs/bidict/__init__.py new file mode 100644 index 000000000..725e18750 --- /dev/null +++ b/libs/bidict/__init__.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +#============================================================================== +# * Welcome to the bidict source code * +#============================================================================== + +# Doing a code review? You'll find a "Code review nav" comment like the one +# below at the top and bottom of the most important source files. This provides +# a suggested initial path through the source when reviewing. +# +# Note: If you aren't reading this on https://github.com/jab/bidict, you may be +# viewing an outdated version of the code. Please head to GitHub to review the +# latest version, which contains important improvements over older versions. +# +# Thank you for reading and for any feedback you provide. + +# * Code review nav * +#============================================================================== +# Current: __init__.py Next: _abc.py → +#============================================================================== + + +""" +Efficient, Pythonic bidirectional map implementation and related functionality. + +.. code-block:: python + + >>> from bidict import bidict + >>> element_by_symbol = bidict({'H': 'hydrogen'}) + >>> element_by_symbol['H'] + 'hydrogen' + >>> element_by_symbol.inverse['hydrogen'] + 'H' + + +Please see https://github.com/jab/bidict for the most up-to-date code and +https://bidict.readthedocs.io for the most up-to-date documentation +if you are reading this elsewhere. + + +.. :copyright: (c) 2019 Joshua Bronson. +.. :license: MPLv2. See LICENSE for details. +""" + +# This __init__.py only collects functionality implemented in the rest of the +# source and exports it under the `bidict` module namespace (via `__all__`). + +from ._abc import BidirectionalMapping +from ._base import BidictBase +from ._mut import MutableBidict +from ._bidict import bidict +from ._dup import DuplicationPolicy, IGNORE, OVERWRITE, RAISE +from ._exc import ( + BidictException, DuplicationError, + KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError) +from ._util import inverted +from ._frozenbidict import frozenbidict +from ._frozenordered import FrozenOrderedBidict +from ._named import namedbidict +from ._orderedbase import OrderedBidictBase +from ._orderedbidict import OrderedBidict +from .metadata import ( + __author__, __maintainer__, __copyright__, __email__, __credits__, __url__, + __license__, __status__, __description__, __keywords__, __version__, __version_info__) + + +__all__ = ( + '__author__', + '__maintainer__', + '__copyright__', + '__email__', + '__credits__', + '__license__', + '__status__', + '__description__', + '__keywords__', + '__url__', + '__version__', + '__version_info__', + 'BidirectionalMapping', + 'BidictException', + 'DuplicationPolicy', + 'IGNORE', + 'OVERWRITE', + 'RAISE', + 'DuplicationError', + 'KeyDuplicationError', + 'ValueDuplicationError', + 'KeyAndValueDuplicationError', + 'BidictBase', + 'MutableBidict', + 'frozenbidict', + 'bidict', + 'namedbidict', + 'FrozenOrderedBidict', + 'OrderedBidictBase', + 'OrderedBidict', + 'inverted', +) + + +# * Code review nav * +#============================================================================== +# Current: __init__.py Next: _abc.py → +#============================================================================== diff --git a/libs/bidict/_abc.py b/libs/bidict/_abc.py new file mode 100644 index 000000000..268b00480 --- /dev/null +++ b/libs/bidict/_abc.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +#============================================================================== +# * Welcome to the bidict source code * +#============================================================================== + +# Doing a code review? You'll find a "Code review nav" comment like the one +# below at the top and bottom of the most important source files. This provides +# a suggested initial path through the source when reviewing. +# +# Note: If you aren't reading this on https://github.com/jab/bidict, you may be +# viewing an outdated version of the code. Please head to GitHub to review the +# latest version, which contains important improvements over older versions. +# +# Thank you for reading and for any feedback you provide. + +# * Code review nav * +#============================================================================== +# ← Prev: __init__.py Current: _abc.py Next: _base.py → +#============================================================================== + + +"""Provides the :class:`BidirectionalMapping` abstract base class.""" + +from .compat import Mapping, abstractproperty, iteritems + + +class BidirectionalMapping(Mapping): # pylint: disable=abstract-method,no-init + """Abstract base class (ABC) for bidirectional mapping types. + + Extends :class:`collections.abc.Mapping` primarily by adding the + (abstract) :attr:`inverse` property, + which implementors of :class:`BidirectionalMapping` + should override to return a reference to the inverse + :class:`BidirectionalMapping` instance. + """ + + __slots__ = () + + @abstractproperty + def inverse(self): + """The inverse of this bidirectional mapping instance. + + *See also* :attr:`bidict.BidictBase.inverse`, :attr:`bidict.BidictBase.inv` + + :raises NotImplementedError: Meant to be overridden in subclasses. + """ + # The @abstractproperty decorator prevents BidirectionalMapping subclasses from being + # instantiated unless they override this method. So users shouldn't be able to get to the + # point where they can unintentionally call this implementation of .inverse on something + # anyway. Could leave the method body empty, but raise NotImplementedError so it's extra + # clear there's no reason to call this implementation (e.g. via super() after overriding). + raise NotImplementedError + + def __inverted__(self): + """Get an iterator over the items in :attr:`inverse`. + + This is functionally equivalent to iterating over the items in the + forward mapping and inverting each one on the fly, but this provides a + more efficient implementation: Assuming the already-inverted items + are stored in :attr:`inverse`, just return an iterator over them directly. + + Providing this default implementation enables external functions, + particularly :func:`~bidict.inverted`, to use this optimized + implementation when available, instead of having to invert on the fly. + + *See also* :func:`bidict.inverted` + """ + return iteritems(self.inverse) + + +# * Code review nav * +#============================================================================== +# ← Prev: __init__.py Current: _abc.py Next: _base.py → +#============================================================================== diff --git a/libs/bidict/_base.py b/libs/bidict/_base.py new file mode 100644 index 000000000..b2a852df2 --- /dev/null +++ b/libs/bidict/_base.py @@ -0,0 +1,462 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +#============================================================================== +# * Welcome to the bidict source code * +#============================================================================== + +# Doing a code review? You'll find a "Code review nav" comment like the one +# below at the top and bottom of the most important source files. This provides +# a suggested initial path through the source when reviewing. +# +# Note: If you aren't reading this on https://github.com/jab/bidict, you may be +# viewing an outdated version of the code. Please head to GitHub to review the +# latest version, which contains important improvements over older versions. +# +# Thank you for reading and for any feedback you provide. + +# * Code review nav * +#============================================================================== +# ← Prev: _abc.py Current: _base.py Next: _delegating_mixins.py → +#============================================================================== + + +"""Provides :class:`BidictBase`.""" + +from collections import namedtuple +from weakref import ref + +from ._abc import BidirectionalMapping +from ._dup import RAISE, OVERWRITE, IGNORE, _OnDup +from ._exc import ( + DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError) +from ._miss import _MISS +from ._noop import _NOOP +from ._util import _iteritems_args_kw +from .compat import PY2, KeysView, ItemsView, Mapping, iteritems + + +_DedupResult = namedtuple('_DedupResult', 'isdupkey isdupval invbyval fwdbykey') +_WriteResult = namedtuple('_WriteResult', 'key val oldkey oldval') +_NODUP = _DedupResult(False, False, _MISS, _MISS) + + +class BidictBase(BidirectionalMapping): + """Base class implementing :class:`BidirectionalMapping`.""" + + __slots__ = ('_fwdm', '_invm', '_inv', '_invweak', '_hash') + (() if PY2 else ('__weakref__',)) + + #: The default :class:`DuplicationPolicy` + #: (in effect during e.g. :meth:`~bidict.bidict.__init__` calls) + #: that governs behavior when a provided item + #: duplicates only the key of another item. + #: + #: Defaults to :attr:`~bidict.OVERWRITE` + #: to match :class:`dict`'s behavior. + #: + #: *See also* :ref:`basic-usage:Values Must Be Unique`, :doc:`extending` + on_dup_key = OVERWRITE + + #: The default :class:`DuplicationPolicy` + #: (in effect during e.g. :meth:`~bidict.bidict.__init__` calls) + #: that governs behavior when a provided item + #: duplicates only the value of another item. + #: + #: Defaults to :attr:`~bidict.RAISE` + #: to prevent unintended overwrite of another item. + #: + #: *See also* :ref:`basic-usage:Values Must Be Unique`, :doc:`extending` + on_dup_val = RAISE + + #: The default :class:`DuplicationPolicy` + #: (in effect during e.g. :meth:`~bidict.bidict.__init__` calls) + #: that governs behavior when a provided item + #: duplicates the key of another item and the value of a third item. + #: + #: Defaults to ``None``, which causes the *on_dup_kv* policy to match + #: whatever *on_dup_val* policy is in effect. + #: + #: *See also* :ref:`basic-usage:Values Must Be Unique`, :doc:`extending` + on_dup_kv = None + + _fwdm_cls = dict + _invm_cls = dict + + #: The object used by :meth:`__repr__` for printing the contained items. + _repr_delegate = dict + + def __init__(self, *args, **kw): # pylint: disable=super-init-not-called + """Make a new bidirectional dictionary. + The signature is the same as that of regular dictionaries. + Items passed in are added in the order they are passed, + respecting the current duplication policies in the process. + + *See also* :attr:`on_dup_key`, :attr:`on_dup_val`, :attr:`on_dup_kv` + """ + #: The backing :class:`~collections.abc.Mapping` + #: storing the forward mapping data (*key* → *value*). + self._fwdm = self._fwdm_cls() + #: The backing :class:`~collections.abc.Mapping` + #: storing the inverse mapping data (*value* → *key*). + self._invm = self._invm_cls() + self._init_inv() # lgtm [py/init-calls-subclass] + if args or kw: + self._update(True, None, *args, **kw) + + def _init_inv(self): + # Compute the type for this bidict's inverse bidict (will be different from this + # bidict's type if _fwdm_cls and _invm_cls are different). + inv_cls = self._inv_cls() + # Create the inverse bidict instance via __new__, bypassing its __init__ so that its + # _fwdm and _invm can be assigned to this bidict's _invm and _fwdm. Store it in self._inv, + # which holds a strong reference to a bidict's inverse, if one is available. + self._inv = inv = inv_cls.__new__(inv_cls) + inv._fwdm = self._invm # pylint: disable=protected-access + inv._invm = self._fwdm # pylint: disable=protected-access + # Only give the inverse a weak reference to this bidict to avoid creating a reference cycle, + # stored in the _invweak attribute. See also the docs in + # :ref:`addendum:Bidict Avoids Reference Cycles` + inv._inv = None # pylint: disable=protected-access + inv._invweak = ref(self) # pylint: disable=protected-access + # Since this bidict has a strong reference to its inverse already, set its _invweak to None. + self._invweak = None + + @classmethod + def _inv_cls(cls): + """The inverse of this bidict type, i.e. one with *_fwdm_cls* and *_invm_cls* swapped.""" + if cls._fwdm_cls is cls._invm_cls: + return cls + if not getattr(cls, '_inv_cls_', None): + class _Inv(cls): + _fwdm_cls = cls._invm_cls + _invm_cls = cls._fwdm_cls + _inv_cls_ = cls + _Inv.__name__ = cls.__name__ + 'Inv' + cls._inv_cls_ = _Inv + return cls._inv_cls_ + + @property + def _isinv(self): + return self._inv is None + + @property + def inverse(self): + """The inverse of this bidict. + + *See also* :attr:`inv` + """ + # Resolve and return a strong reference to the inverse bidict. + # One may be stored in self._inv already. + if self._inv is not None: + return self._inv + # Otherwise a weakref is stored in self._invweak. Try to get a strong ref from it. + inv = self._invweak() + if inv is not None: + return inv + # Refcount of referent must have dropped to zero, as in `bidict().inv.inv`. Init a new one. + self._init_inv() # Now this bidict will retain a strong ref to its inverse. + return self._inv + + @property + def inv(self): + """Alias for :attr:`inverse`.""" + return self.inverse + + def __getstate__(self): + """Needed to enable pickling due to use of :attr:`__slots__` and weakrefs. + + *See also* :meth:`object.__getstate__` + """ + state = {} + for cls in self.__class__.__mro__: + slots = getattr(cls, '__slots__', ()) + for slot in slots: + if hasattr(self, slot): + state[slot] = getattr(self, slot) + # weakrefs can't be pickled. + state.pop('_invweak', None) # Added back in __setstate__ via _init_inv call. + state.pop('__weakref__', None) # Not added back in __setstate__. Python manages this one. + return state + + def __setstate__(self, state): + """Implemented because use of :attr:`__slots__` would prevent unpickling otherwise. + + *See also* :meth:`object.__setstate__` + """ + for slot, value in iteritems(state): + setattr(self, slot, value) + self._init_inv() + + def __repr__(self): + """See :func:`repr`.""" + clsname = self.__class__.__name__ + if not self: + return '%s()' % clsname + return '%s(%r)' % (clsname, self._repr_delegate(iteritems(self))) + + # The inherited Mapping.__eq__ implementation would work, but it's implemented in terms of an + # inefficient ``dict(self.items()) == dict(other.items())`` comparison, so override it with a + # more efficient implementation. + def __eq__(self, other): + u"""*x.__eq__(other) ⟺ x == other* + + Equivalent to *dict(x.items()) == dict(other.items())* + but more efficient. + + Note that :meth:`bidict's __eq__() <bidict.bidict.__eq__>` implementation + is inherited by subclasses, + in particular by the ordered bidict subclasses, + so even with ordered bidicts, + :ref:`== comparison is order-insensitive <eq-order-insensitive>`. + + *See also* :meth:`bidict.FrozenOrderedBidict.equals_order_sensitive` + """ + if not isinstance(other, Mapping) or len(self) != len(other): + return False + selfget = self.get + return all(selfget(k, _MISS) == v for (k, v) in iteritems(other)) + + # The following methods are mutating and so are not public. But they are implemented in this + # non-mutable base class (rather than the mutable `bidict` subclass) because they are used here + # during initialization (starting with the `_update` method). (Why is this? Because `__init__` + # and `update` share a lot of the same behavior (inserting the provided items while respecting + # the active duplication policies), so it makes sense for them to share implementation too.) + def _pop(self, key): + val = self._fwdm.pop(key) + del self._invm[val] + return val + + def _put(self, key, val, on_dup): + dedup_result = self._dedup_item(key, val, on_dup) + if dedup_result is not _NOOP: + self._write_item(key, val, dedup_result) + + def _dedup_item(self, key, val, on_dup): + """ + Check *key* and *val* for any duplication in self. + + Handle any duplication as per the duplication policies given in *on_dup*. + + (key, val) already present is construed as a no-op, not a duplication. + + If duplication is found and the corresponding duplication policy is + :attr:`~bidict.RAISE`, raise the appropriate error. + + If duplication is found and the corresponding duplication policy is + :attr:`~bidict.IGNORE`, return *None*. + + If duplication is found and the corresponding duplication policy is + :attr:`~bidict.OVERWRITE`, + or if no duplication is found, + return the _DedupResult *(isdupkey, isdupval, oldkey, oldval)*. + """ + fwdm = self._fwdm + invm = self._invm + oldval = fwdm.get(key, _MISS) + oldkey = invm.get(val, _MISS) + isdupkey = oldval is not _MISS + isdupval = oldkey is not _MISS + dedup_result = _DedupResult(isdupkey, isdupval, oldkey, oldval) + if isdupkey and isdupval: + if self._isdupitem(key, val, dedup_result): + # (key, val) duplicates an existing item -> no-op. + return _NOOP + # key and val each duplicate a different existing item. + if on_dup.kv is RAISE: + raise KeyAndValueDuplicationError(key, val) + elif on_dup.kv is IGNORE: + return _NOOP + assert on_dup.kv is OVERWRITE, 'invalid on_dup_kv: %r' % on_dup.kv + # Fall through to the return statement on the last line. + elif isdupkey: + if on_dup.key is RAISE: + raise KeyDuplicationError(key) + elif on_dup.key is IGNORE: + return _NOOP + assert on_dup.key is OVERWRITE, 'invalid on_dup.key: %r' % on_dup.key + # Fall through to the return statement on the last line. + elif isdupval: + if on_dup.val is RAISE: + raise ValueDuplicationError(val) + elif on_dup.val is IGNORE: + return _NOOP + assert on_dup.val is OVERWRITE, 'invalid on_dup.val: %r' % on_dup.val + # Fall through to the return statement on the last line. + # else neither isdupkey nor isdupval. + return dedup_result + + @staticmethod + def _isdupitem(key, val, dedup_result): + isdupkey, isdupval, oldkey, oldval = dedup_result + isdupitem = oldkey == key + assert isdupitem == (oldval == val), '%r %r %r' % (key, val, dedup_result) + if isdupitem: + assert isdupkey + assert isdupval + return isdupitem + + @classmethod + def _get_on_dup(cls, on_dup=None): + if on_dup is None: + on_dup = _OnDup(cls.on_dup_key, cls.on_dup_val, cls.on_dup_kv) + elif not isinstance(on_dup, _OnDup): + on_dup = _OnDup(*on_dup) + if on_dup.kv is None: + on_dup = on_dup._replace(kv=on_dup.val) + return on_dup + + def _write_item(self, key, val, dedup_result): + isdupkey, isdupval, oldkey, oldval = dedup_result + fwdm = self._fwdm + invm = self._invm + fwdm[key] = val + invm[val] = key + if isdupkey: + del invm[oldval] + if isdupval: + del fwdm[oldkey] + return _WriteResult(key, val, oldkey, oldval) + + def _update(self, init, on_dup, *args, **kw): + # args[0] may be a generator that yields many items, so process input in a single pass. + if not args and not kw: + return + can_skip_dup_check = not self and not kw and isinstance(args[0], BidirectionalMapping) + if can_skip_dup_check: + self._update_no_dup_check(args[0]) + return + on_dup = self._get_on_dup(on_dup) + can_skip_rollback = init or RAISE not in on_dup + if can_skip_rollback: + self._update_no_rollback(on_dup, *args, **kw) + else: + self._update_with_rollback(on_dup, *args, **kw) + + def _update_no_dup_check(self, other, _nodup=_NODUP): + write_item = self._write_item + for (key, val) in iteritems(other): + write_item(key, val, _nodup) + + def _update_no_rollback(self, on_dup, *args, **kw): + put = self._put + for (key, val) in _iteritems_args_kw(*args, **kw): + put(key, val, on_dup) + + def _update_with_rollback(self, on_dup, *args, **kw): + """Update, rolling back on failure.""" + writelog = [] + appendlog = writelog.append + dedup_item = self._dedup_item + write_item = self._write_item + for (key, val) in _iteritems_args_kw(*args, **kw): + try: + dedup_result = dedup_item(key, val, on_dup) + except DuplicationError: + undo_write = self._undo_write + for dedup_result, write_result in reversed(writelog): + undo_write(dedup_result, write_result) + raise + if dedup_result is not _NOOP: + write_result = write_item(key, val, dedup_result) + appendlog((dedup_result, write_result)) + + def _undo_write(self, dedup_result, write_result): + isdupkey, isdupval, _, _ = dedup_result + key, val, oldkey, oldval = write_result + if not isdupkey and not isdupval: + self._pop(key) + return + fwdm = self._fwdm + invm = self._invm + if isdupkey: + fwdm[key] = oldval + invm[oldval] = key + if not isdupval: + del invm[val] + if isdupval: + invm[val] = oldkey + fwdm[oldkey] = val + if not isdupkey: + del fwdm[key] + + def copy(self): + """A shallow copy.""" + # Could just ``return self.__class__(self)`` here instead, but the below is faster. It uses + # __new__ to create a copy instance while bypassing its __init__, which would result + # in copying this bidict's items into the copy instance one at a time. Instead, make whole + # copies of each of the backing mappings, and make them the backing mappings of the copy, + # avoiding copying items one at a time. + copy = self.__class__.__new__(self.__class__) + copy._fwdm = self._fwdm.copy() # pylint: disable=protected-access + copy._invm = self._invm.copy() # pylint: disable=protected-access + copy._init_inv() # pylint: disable=protected-access + return copy + + def __copy__(self): + """Used for the copy protocol. + + *See also* the :mod:`copy` module + """ + return self.copy() + + def __len__(self): + """The number of contained items.""" + return len(self._fwdm) + + def __iter__(self): # lgtm [py/inheritance/incorrect-overridden-signature] + """Iterator over the contained items.""" + # No default implementation for __iter__ inherited from Mapping -> + # always delegate to _fwdm. + return iter(self._fwdm) + + def __getitem__(self, key): + u"""*x.__getitem__(key) ⟺ x[key]*""" + return self._fwdm[key] + + def values(self): + """A set-like object providing a view on the contained values. + + Note that because the values of a :class:`~bidict.BidirectionalMapping` + are the keys of its inverse, + this returns a :class:`~collections.abc.KeysView` + rather than a :class:`~collections.abc.ValuesView`, + which has the advantages of constant-time containment checks + and supporting set operations. + """ + return self.inverse.keys() + + if PY2: + # For iterkeys and iteritems, inheriting from Mapping already provides + # the best default implementations so no need to define here. + + def itervalues(self): + """An iterator over the contained values.""" + return self.inverse.iterkeys() + + def viewkeys(self): # noqa: D102; pylint: disable=missing-docstring + return KeysView(self) + + def viewvalues(self): # noqa: D102; pylint: disable=missing-docstring + return self.inverse.viewkeys() + + viewvalues.__doc__ = values.__doc__ + values.__doc__ = 'A list of the contained values.' + + def viewitems(self): # noqa: D102; pylint: disable=missing-docstring + return ItemsView(self) + + # __ne__ added automatically in Python 3 when you implement __eq__, but not in Python 2. + def __ne__(self, other): # noqa: N802 + u"""*x.__ne__(other) ⟺ x != other*""" + return not self == other # Implement __ne__ in terms of __eq__. + + +# * Code review nav * +#============================================================================== +# ← Prev: _abc.py Current: _base.py Next: _delegating_mixins.py → +#============================================================================== diff --git a/libs/bidict/_bidict.py b/libs/bidict/_bidict.py new file mode 100644 index 000000000..9082775b6 --- /dev/null +++ b/libs/bidict/_bidict.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +#============================================================================== +# * Welcome to the bidict source code * +#============================================================================== + +# Doing a code review? You'll find a "Code review nav" comment like the one +# below at the top and bottom of the most important source files. This provides +# a suggested initial path through the source when reviewing. +# +# Note: If you aren't reading this on https://github.com/jab/bidict, you may be +# viewing an outdated version of the code. Please head to GitHub to review the +# latest version, which contains important improvements over older versions. +# +# Thank you for reading and for any feedback you provide. + +# * Code review nav * +#============================================================================== +# ← Prev: _mut.py Current: _bidict.py Next: _orderedbase.py → +#============================================================================== + + +"""Provides :class:`bidict`.""" + +from ._mut import MutableBidict +from ._delegating_mixins import _DelegateKeysAndItemsToFwdm + + +class bidict(_DelegateKeysAndItemsToFwdm, MutableBidict): # noqa: N801,E501; pylint: disable=invalid-name + """Base class for mutable bidirectional mappings.""" + + __slots__ = () + + __hash__ = None # since this class is mutable; explicit > implicit. + + +# * Code review nav * +#============================================================================== +# ← Prev: _mut.py Current: _bidict.py Next: _orderedbase.py → +#============================================================================== diff --git a/libs/bidict/_delegating_mixins.py b/libs/bidict/_delegating_mixins.py new file mode 100644 index 000000000..8772490c7 --- /dev/null +++ b/libs/bidict/_delegating_mixins.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +#============================================================================== +# * Welcome to the bidict source code * +#============================================================================== + +# Doing a code review? You'll find a "Code review nav" comment like the one +# below at the top and bottom of the most important source files. This provides +# a suggested initial path through the source when reviewing. +# +# Note: If you aren't reading this on https://github.com/jab/bidict, you may be +# viewing an outdated version of the code. Please head to GitHub to review the +# latest version, which contains important improvements over older versions. +# +# Thank you for reading and for any feedback you provide. + +# * Code review nav * +#============================================================================== +# ← Prev: _base.py Current: _delegating_mixins.py Next: _frozenbidict.py → +#============================================================================== + + +r"""Provides mixin classes that delegate to ``self._fwdm`` for various operations. + +This allows methods such as :meth:`bidict.bidict.items` +to be implemented in terms of a ``self._fwdm.items()`` call, +which is potentially much more efficient (e.g. in CPython 2) +compared to the implementation inherited from :class:`~collections.abc.Mapping` +(which returns ``[(key, self[key]) for key in self]`` in Python 2). + +Because this depends on implementation details that aren't necessarily true +(such as the bidict's values being the same as its ``self._fwdm.values()``, +which is not true for e.g. ordered bidicts where ``_fwdm``\'s values are nodes), +these should always be mixed in at a layer below a more general layer, +as they are in e.g. :class:`~bidict.frozenbidict` +which extends :class:`~bidict.BidictBase`. + +See the :ref:`extending:Sorted Bidict Recipes` +for another example of where this comes into play. +``SortedBidict`` extends :class:`bidict.MutableBidict` +rather than :class:`bidict.bidict` +to avoid inheriting these mixins, +which are incompatible with the backing +:class:`sortedcontainers.SortedDict`s. +""" + +from .compat import PY2 + + +_KEYS_METHODS = ('keys',) + (('viewkeys', 'iterkeys') if PY2 else ()) +_ITEMS_METHODS = ('items',) + (('viewitems', 'iteritems') if PY2 else ()) +_DOCSTRING_BY_METHOD = { + 'keys': 'A set-like object providing a view on the contained keys.', + 'items': 'A set-like object providing a view on the contained items.', +} +if PY2: + _DOCSTRING_BY_METHOD['viewkeys'] = _DOCSTRING_BY_METHOD['keys'] + _DOCSTRING_BY_METHOD['viewitems'] = _DOCSTRING_BY_METHOD['items'] + _DOCSTRING_BY_METHOD['keys'] = 'A list of the contained keys.' + _DOCSTRING_BY_METHOD['items'] = 'A list of the contained items.' + + +def _make_method(methodname): + def method(self): + return getattr(self._fwdm, methodname)() # pylint: disable=protected-access + method.__name__ = methodname + method.__doc__ = _DOCSTRING_BY_METHOD.get(methodname, '') + return method + + +def _make_fwdm_delegating_mixin(clsname, methodnames): + clsdict = dict({name: _make_method(name) for name in methodnames}, __slots__=()) + return type(clsname, (object,), clsdict) + + +_DelegateKeysToFwdm = _make_fwdm_delegating_mixin('_DelegateKeysToFwdm', _KEYS_METHODS) +_DelegateItemsToFwdm = _make_fwdm_delegating_mixin('_DelegateItemsToFwdm', _ITEMS_METHODS) +_DelegateKeysAndItemsToFwdm = type( + '_DelegateKeysAndItemsToFwdm', + (_DelegateKeysToFwdm, _DelegateItemsToFwdm), + {'__slots__': ()}) + +# * Code review nav * +#============================================================================== +# ← Prev: _base.py Current: _delegating_mixins.py Next: _frozenbidict.py → +#============================================================================== diff --git a/libs/bidict/_dup.py b/libs/bidict/_dup.py new file mode 100644 index 000000000..4670dcc57 --- /dev/null +++ b/libs/bidict/_dup.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Provides bidict duplication policies and the :class:`_OnDup` class.""" + + +from collections import namedtuple + +from ._marker import _Marker + + +_OnDup = namedtuple('_OnDup', 'key val kv') + + +class DuplicationPolicy(_Marker): + """Base class for bidict's duplication policies. + + *See also* :ref:`basic-usage:Values Must Be Unique` + """ + + __slots__ = () + + +#: Raise an exception when a duplication is encountered. +RAISE = DuplicationPolicy('DUP_POLICY.RAISE') + +#: Overwrite an existing item when a duplication is encountered. +OVERWRITE = DuplicationPolicy('DUP_POLICY.OVERWRITE') + +#: Keep the existing item and ignore the new item when a duplication is encountered. +IGNORE = DuplicationPolicy('DUP_POLICY.IGNORE') diff --git a/libs/bidict/_exc.py b/libs/bidict/_exc.py new file mode 100644 index 000000000..5370361e6 --- /dev/null +++ b/libs/bidict/_exc.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Provides all bidict exceptions.""" + + +class BidictException(Exception): + """Base class for bidict exceptions.""" + + +class DuplicationError(BidictException): + """Base class for exceptions raised when uniqueness is violated + as per the RAISE duplication policy. + """ + + +class KeyDuplicationError(DuplicationError): + """Raised when a given key is not unique.""" + + +class ValueDuplicationError(DuplicationError): + """Raised when a given value is not unique.""" + + +class KeyAndValueDuplicationError(KeyDuplicationError, ValueDuplicationError): + """Raised when a given item's key and value are not unique. + + That is, its key duplicates that of another item, + and its value duplicates that of a different other item. + """ diff --git a/libs/bidict/_frozenbidict.py b/libs/bidict/_frozenbidict.py new file mode 100644 index 000000000..07831fd91 --- /dev/null +++ b/libs/bidict/_frozenbidict.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +#============================================================================== +# * Welcome to the bidict source code * +#============================================================================== + +# Doing a code review? You'll find a "Code review nav" comment like the one +# below at the top and bottom of the most important source files. This provides +# a suggested initial path through the source when reviewing. +# +# Note: If you aren't reading this on https://github.com/jab/bidict, you may be +# viewing an outdated version of the code. Please head to GitHub to review the +# latest version, which contains important improvements over older versions. +# +# Thank you for reading and for any feedback you provide. + +# * Code review nav * +#============================================================================== +# ← Prev: _delegating_mixins.py Current: _frozenbidict.py Next: _mut.py → +#============================================================================== + +"""Provides :class:`frozenbidict`, an immutable, hashable bidirectional mapping type.""" + +from ._base import BidictBase +from ._delegating_mixins import _DelegateKeysAndItemsToFwdm +from .compat import ItemsView + + +class frozenbidict(_DelegateKeysAndItemsToFwdm, BidictBase): # noqa: N801,E501; pylint: disable=invalid-name + """Immutable, hashable bidict type.""" + + __slots__ = () + + def __hash__(self): # lgtm [py/equals-hash-mismatch] + """The hash of this bidict as determined by its items.""" + if getattr(self, '_hash', None) is None: + # pylint: disable=protected-access,attribute-defined-outside-init + self._hash = ItemsView(self)._hash() + return self._hash + + +# * Code review nav * +#============================================================================== +# ← Prev: _delegating_mixins.py Current: _frozenbidict.py Next: _mut.py → +#============================================================================== diff --git a/libs/bidict/_frozenordered.py b/libs/bidict/_frozenordered.py new file mode 100644 index 000000000..25cbace3b --- /dev/null +++ b/libs/bidict/_frozenordered.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +#============================================================================== +# * Welcome to the bidict source code * +#============================================================================== + +# Doing a code review? You'll find a "Code review nav" comment like the one +# below at the top and bottom of the most important source files. This provides +# a suggested initial path through the source when reviewing. +# +# Note: If you aren't reading this on https://github.com/jab/bidict, you may be +# viewing an outdated version of the code. Please head to GitHub to review the +# latest version, which contains important improvements over older versions. +# +# Thank you for reading and for any feedback you provide. + +# * Code review nav * +#============================================================================== +#← Prev: _orderedbase.py Current: _frozenordered.py Next: _orderedbidict.py → +#============================================================================== + +"""Provides :class:`FrozenOrderedBidict`, an immutable, hashable, ordered bidict.""" + +from ._delegating_mixins import _DelegateKeysToFwdm +from ._frozenbidict import frozenbidict +from ._orderedbase import OrderedBidictBase +from .compat import DICTS_ORDERED, PY2, izip + + +# If the Python implementation's dict type is ordered (e.g. PyPy or CPython >= 3.6), then +# `FrozenOrderedBidict` can delegate to `_fwdm` for keys: Both `_fwdm` and `_invm` will always +# be initialized with the provided items in the correct order, and since `FrozenOrderedBidict` +# is immutable, their respective orders can't get out of sync after a mutation. (Can't delegate +# to `_fwdm` for items though because values in `_fwdm` are nodes.) +_BASES = ((_DelegateKeysToFwdm,) if DICTS_ORDERED else ()) + (OrderedBidictBase,) +_CLSDICT = dict( + __slots__=(), + # Must set __hash__ explicitly, Python prevents inheriting it. + # frozenbidict.__hash__ can be reused for FrozenOrderedBidict: + # FrozenOrderedBidict inherits BidictBase.__eq__ which is order-insensitive, + # and frozenbidict.__hash__ is consistent with BidictBase.__eq__. + __hash__=frozenbidict.__hash__.__func__ if PY2 else frozenbidict.__hash__, + __doc__='Hashable, immutable, ordered bidict type.', + __module__=__name__, # Otherwise unpickling fails in Python 2. +) + +# When PY2 (so we provide iteritems) and DICTS_ORDERED, e.g. on PyPy, the following implementation +# of iteritems may be more efficient than that inherited from `Mapping`. This exploits the property +# that the keys in `_fwdm` and `_invm` are already in the right order: +if PY2 and DICTS_ORDERED: + _CLSDICT['iteritems'] = lambda self: izip(self._fwdm, self._invm) # noqa: E501; pylint: disable=protected-access + +FrozenOrderedBidict = type('FrozenOrderedBidict', _BASES, _CLSDICT) # pylint: disable=invalid-name + + +# * Code review nav * +#============================================================================== +#← Prev: _orderedbase.py Current: _frozenordered.py Next: _orderedbidict.py → +#============================================================================== diff --git a/libs/bidict/_marker.py b/libs/bidict/_marker.py new file mode 100644 index 000000000..f2f9c57cb --- /dev/null +++ b/libs/bidict/_marker.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Provides :class:`_Marker`, an internal type for representing singletons.""" + +from collections import namedtuple + + +class _Marker(namedtuple('_Marker', 'name')): + + __slots__ = () + + def __repr__(self): + return '<%s>' % self.name # pragma: no cover diff --git a/libs/bidict/_miss.py b/libs/bidict/_miss.py new file mode 100644 index 000000000..32d02c584 --- /dev/null +++ b/libs/bidict/_miss.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Provides the :obj:`_MISS` sentinel, for internally signaling "missing/not found".""" + +from ._marker import _Marker + + +_MISS = _Marker('MISSING') diff --git a/libs/bidict/_mut.py b/libs/bidict/_mut.py new file mode 100644 index 000000000..1a117c2ab --- /dev/null +++ b/libs/bidict/_mut.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +#============================================================================== +# * Welcome to the bidict source code * +#============================================================================== + +# Doing a code review? You'll find a "Code review nav" comment like the one +# below at the top and bottom of the most important source files. This provides +# a suggested initial path through the source when reviewing. +# +# Note: If you aren't reading this on https://github.com/jab/bidict, you may be +# viewing an outdated version of the code. Please head to GitHub to review the +# latest version, which contains important improvements over older versions. +# +# Thank you for reading and for any feedback you provide. + +# * Code review nav * +#============================================================================== +# ← Prev: _frozenbidict.py Current: _mut.py Next: _bidict.py → +#============================================================================== + + +"""Provides :class:`bidict`.""" + +from ._base import BidictBase +from ._dup import OVERWRITE, RAISE, _OnDup +from ._miss import _MISS +from .compat import MutableMapping + + +# Extend MutableMapping explicitly because it doesn't implement __subclasshook__, as well as to +# inherit method implementations it provides that we can reuse (namely `setdefault`). +class MutableBidict(BidictBase, MutableMapping): + """Base class for mutable bidirectional mappings.""" + + __slots__ = () + + __hash__ = None # since this class is mutable; explicit > implicit. + + _ON_DUP_OVERWRITE = _OnDup(key=OVERWRITE, val=OVERWRITE, kv=OVERWRITE) + + def __delitem__(self, key): + u"""*x.__delitem__(y) ⟺ del x[y]*""" + self._pop(key) + + def __setitem__(self, key, val): + """ + Set the value for *key* to *val*. + + If *key* is already associated with *val*, this is a no-op. + + If *key* is already associated with a different value, + the old value will be replaced with *val*, + as with dict's :meth:`__setitem__`. + + If *val* is already associated with a different key, + an exception is raised + to protect against accidental removal of the key + that's currently associated with *val*. + + Use :meth:`put` instead if you want to specify different policy in + the case that the provided key or value duplicates an existing one. + Or use :meth:`forceput` to unconditionally associate *key* with *val*, + replacing any existing items as necessary to preserve uniqueness. + + :raises bidict.ValueDuplicationError: if *val* duplicates that of an + existing item. + + :raises bidict.KeyAndValueDuplicationError: if *key* duplicates the key of an + existing item and *val* duplicates the value of a different + existing item. + """ + on_dup = self._get_on_dup() + self._put(key, val, on_dup) + + def put(self, key, val, on_dup_key=RAISE, on_dup_val=RAISE, on_dup_kv=None): + """ + Associate *key* with *val* with the specified duplication policies. + + If *on_dup_kv* is ``None``, the *on_dup_val* policy will be used for it. + + For example, if all given duplication policies are :attr:`~bidict.RAISE`, + then *key* will be associated with *val* if and only if + *key* is not already associated with an existing value and + *val* is not already associated with an existing key, + otherwise an exception will be raised. + + If *key* is already associated with *val*, this is a no-op. + + :raises bidict.KeyDuplicationError: if attempting to insert an item + whose key only duplicates an existing item's, and *on_dup_key* is + :attr:`~bidict.RAISE`. + + :raises bidict.ValueDuplicationError: if attempting to insert an item + whose value only duplicates an existing item's, and *on_dup_val* is + :attr:`~bidict.RAISE`. + + :raises bidict.KeyAndValueDuplicationError: if attempting to insert an + item whose key duplicates one existing item's, and whose value + duplicates another existing item's, and *on_dup_kv* is + :attr:`~bidict.RAISE`. + """ + on_dup = self._get_on_dup((on_dup_key, on_dup_val, on_dup_kv)) + self._put(key, val, on_dup) + + def forceput(self, key, val): + """ + Associate *key* with *val* unconditionally. + + Replace any existing mappings containing key *key* or value *val* + as necessary to preserve uniqueness. + """ + self._put(key, val, self._ON_DUP_OVERWRITE) + + def clear(self): + """Remove all items.""" + self._fwdm.clear() + self._invm.clear() + + def pop(self, key, default=_MISS): + u"""*x.pop(k[, d]) → v* + + Remove specified key and return the corresponding value. + + :raises KeyError: if *key* is not found and no *default* is provided. + """ + try: + return self._pop(key) + except KeyError: + if default is _MISS: + raise + return default + + def popitem(self): + u"""*x.popitem() → (k, v)* + + Remove and return some item as a (key, value) pair. + + :raises KeyError: if *x* is empty. + """ + if not self: + raise KeyError('mapping is empty') + key, val = self._fwdm.popitem() + del self._invm[val] + return key, val + + def update(self, *args, **kw): + """Like :meth:`putall` with default duplication policies.""" + if args or kw: + self._update(False, None, *args, **kw) + + def forceupdate(self, *args, **kw): + """Like a bulk :meth:`forceput`.""" + self._update(False, self._ON_DUP_OVERWRITE, *args, **kw) + + def putall(self, items, on_dup_key=RAISE, on_dup_val=RAISE, on_dup_kv=None): + """ + Like a bulk :meth:`put`. + + If one of the given items causes an exception to be raised, + none of the items is inserted. + """ + if items: + on_dup = self._get_on_dup((on_dup_key, on_dup_val, on_dup_kv)) + self._update(False, on_dup, items) + + +# * Code review nav * +#============================================================================== +# ← Prev: _frozenbidict.py Current: _mut.py Next: _bidict.py → +#============================================================================== diff --git a/libs/bidict/_named.py b/libs/bidict/_named.py new file mode 100644 index 000000000..8748b98cc --- /dev/null +++ b/libs/bidict/_named.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +"""Provides :func:`bidict.namedbidict`.""" + +import re + +from ._abc import BidirectionalMapping +from ._bidict import bidict +from .compat import PY2 + + +_isidentifier = ( # pylint: disable=invalid-name + re.compile('[A-Za-z_][A-Za-z0-9_]*$').match if PY2 else str.isidentifier +) + + +def namedbidict(typename, keyname, valname, base_type=bidict): + r"""Create a new subclass of *base_type* with custom accessors. + + Analagous to :func:`collections.namedtuple`. + + The new class's ``__name__`` and ``__qualname__`` + will be set based on *typename*. + + Instances of it will provide access to their + :attr:`inverse <BidirectionalMapping.inverse>`\s + via the custom *keyname*\_for property, + and access to themselves + via the custom *valname*\_for property. + + *See also* the :ref:`namedbidict usage documentation + <other-bidict-types:\:func\:\`~bidict.namedbidict\`>` + + :raises ValueError: if any of the *typename*, *keyname*, or *valname* + strings is not a valid Python identifier, or if *keyname == valname*. + + :raises TypeError: if *base_type* is not a subclass of + :class:`BidirectionalMapping`. + (This function requires slightly more of *base_type*, + e.g. the availability of an ``_isinv`` attribute, + but all the :ref:`concrete bidict types + <other-bidict-types:Bidict Types Diagram>` + that the :mod:`bidict` module provides can be passed in. + Check out the code if you actually need to pass in something else.) + """ + # Re the `base_type` docs above: + # The additional requirements (providing _isinv and __getstate__) do not belong in the + # BidirectionalMapping interface, and it's overkill to create additional interface(s) for this. + # On the other hand, it's overkill to require that base_type be a subclass of BidictBase, since + # that's too specific. The BidirectionalMapping check along with the docs above should suffice. + if not issubclass(base_type, BidirectionalMapping): + raise TypeError(base_type) + names = (typename, keyname, valname) + if not all(map(_isidentifier, names)) or keyname == valname: + raise ValueError(names) + + class _Named(base_type): # pylint: disable=too-many-ancestors + + __slots__ = () + + def _getfwd(self): + return self.inverse if self._isinv else self + + def _getinv(self): + return self if self._isinv else self.inverse + + @property + def _keyname(self): + return valname if self._isinv else keyname + + @property + def _valname(self): + return keyname if self._isinv else valname + + def __reduce__(self): + return (_make_empty, (typename, keyname, valname, base_type), self.__getstate__()) + + bname = base_type.__name__ + fname = valname + '_for' + iname = keyname + '_for' + names = dict(typename=typename, bname=bname, keyname=keyname, valname=valname) + fdoc = u'{typename} forward {bname}: {keyname} → {valname}'.format(**names) + idoc = u'{typename} inverse {bname}: {valname} → {keyname}'.format(**names) + setattr(_Named, fname, property(_Named._getfwd, doc=fdoc)) # pylint: disable=protected-access + setattr(_Named, iname, property(_Named._getinv, doc=idoc)) # pylint: disable=protected-access + + if not PY2: + _Named.__qualname__ = _Named.__qualname__[:-len(_Named.__name__)] + typename + _Named.__name__ = typename + return _Named + + +def _make_empty(typename, keyname, valname, base_type): + """Create a named bidict with the indicated arguments and return an empty instance. + Used to make :func:`bidict.namedbidict` instances picklable. + """ + cls = namedbidict(typename, keyname, valname, base_type=base_type) + return cls() diff --git a/libs/bidict/_noop.py b/libs/bidict/_noop.py new file mode 100644 index 000000000..b045e8d72 --- /dev/null +++ b/libs/bidict/_noop.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Provides the :obj:`_NOOP` sentinel, for internally signaling "no-op".""" + +from ._marker import _Marker + + +_NOOP = _Marker('NO-OP') diff --git a/libs/bidict/_orderedbase.py b/libs/bidict/_orderedbase.py new file mode 100644 index 000000000..aa085a2d5 --- /dev/null +++ b/libs/bidict/_orderedbase.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +#============================================================================== +# * Welcome to the bidict source code * +#============================================================================== + +# Doing a code review? You'll find a "Code review nav" comment like the one +# below at the top and bottom of the most important source files. This provides +# a suggested initial path through the source when reviewing. +# +# Note: If you aren't reading this on https://github.com/jab/bidict, you may be +# viewing an outdated version of the code. Please head to GitHub to review the +# latest version, which contains important improvements over older versions. +# +# Thank you for reading and for any feedback you provide. + +# * Code review nav * +#============================================================================== +# ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py → +#============================================================================== + + +"""Provides :class:`OrderedBidictBase`.""" + +from weakref import ref + +from ._base import _WriteResult, BidictBase +from ._bidict import bidict +from ._miss import _MISS +from .compat import Mapping, PY2, iteritems, izip + + +class _Node(object): # pylint: disable=too-few-public-methods + """A node in a circular doubly-linked list + used to encode the order of items in an ordered bidict. + + Only weak references to the next and previous nodes + are held to avoid creating strong reference cycles. + + Because an ordered bidict retains two strong references + to each node instance (one from its backing `_fwdm` mapping + and one from its `_invm` mapping), a node's refcount will not + drop to zero (and so will not be garbage collected) as long as + the ordered bidict that contains it is still alive. + Because nodes don't have strong reference cycles, + once their containing bidict is freed, + they too are immediately freed. + """ + + __slots__ = ('_prv', '_nxt', '__weakref__') + + def __init__(self, prv=None, nxt=None): + self._setprv(prv) + self._setnxt(nxt) + + def __repr__(self): # pragma: no cover + clsname = self.__class__.__name__ + prv = id(self.prv) + nxt = id(self.nxt) + return '%s(prv=%s, self=%s, nxt=%s)' % (clsname, prv, id(self), nxt) + + def _getprv(self): + return self._prv() if isinstance(self._prv, ref) else self._prv + + def _setprv(self, prv): + self._prv = prv and ref(prv) + + prv = property(_getprv, _setprv) + + def _getnxt(self): + return self._nxt() if isinstance(self._nxt, ref) else self._nxt + + def _setnxt(self, nxt): + self._nxt = nxt and ref(nxt) + + nxt = property(_getnxt, _setnxt) + + def __getstate__(self): + """Return the instance state dictionary + but with weakrefs converted to strong refs + so that it can be pickled. + + *See also* :meth:`object.__getstate__` + """ + return dict(_prv=self.prv, _nxt=self.nxt) + + def __setstate__(self, state): + """Set the instance state from *state*.""" + self._setprv(state['_prv']) + self._setnxt(state['_nxt']) + + +class _Sentinel(_Node): # pylint: disable=too-few-public-methods + """Special node in a circular doubly-linked list + that links the first node with the last node. + When its next and previous references point back to itself + it represents an empty list. + """ + + __slots__ = () + + def __init__(self, prv=None, nxt=None): + super(_Sentinel, self).__init__(prv or self, nxt or self) + + def __repr__(self): # pragma: no cover + return '<SENTINEL>' + + def __bool__(self): + return False + + if PY2: + __nonzero__ = __bool__ + + def __iter__(self, reverse=False): + """Iterator yielding nodes in the requested order, + i.e. traverse the linked list via :attr:`nxt` + (or :attr:`prv` if *reverse* is truthy) + until reaching a falsy (i.e. sentinel) node. + """ + attr = 'prv' if reverse else 'nxt' + node = getattr(self, attr) + while node: + yield node + node = getattr(node, attr) + + +class OrderedBidictBase(BidictBase): + """Base class implementing an ordered :class:`BidirectionalMapping`.""" + + __slots__ = ('_sntl',) + + _fwdm_cls = bidict + _invm_cls = bidict + + #: The object used by :meth:`__repr__` for printing the contained items. + _repr_delegate = list + + def __init__(self, *args, **kw): + """Make a new ordered bidirectional mapping. + The signature is the same as that of regular dictionaries. + Items passed in are added in the order they are passed, + respecting this bidict type's duplication policies along the way. + The order in which items are inserted is remembered, + similar to :class:`collections.OrderedDict`. + """ + self._sntl = _Sentinel() + + # Like unordered bidicts, ordered bidicts also store two backing one-directional mappings + # `_fwdm` and `_invm`. But rather than mapping `key` to `val` and `val` to `key` + # (respectively), they map `key` to `nodefwd` and `val` to `nodeinv` (respectively), where + # `nodefwd` is `nodeinv` when `key` and `val` are associated with one another. + + # To effect this difference, `_write_item` and `_undo_write` are overridden. But much of the + # rest of BidictBase's implementation, including BidictBase.__init__ and BidictBase._update, + # are inherited and are able to be reused without modification. + super(OrderedBidictBase, self).__init__(*args, **kw) + + def _init_inv(self): + super(OrderedBidictBase, self)._init_inv() + self.inverse._sntl = self._sntl # pylint: disable=protected-access + + # Can't reuse BidictBase.copy since ordered bidicts have different internal structure. + def copy(self): + """A shallow copy of this ordered bidict.""" + # Fast copy implementation bypassing __init__. See comments in :meth:`BidictBase.copy`. + copy = self.__class__.__new__(self.__class__) + sntl = _Sentinel() + fwdm = self._fwdm.copy() + invm = self._invm.copy() + cur = sntl + nxt = sntl.nxt + for (key, val) in iteritems(self): + nxt = _Node(cur, sntl) + cur.nxt = fwdm[key] = invm[val] = nxt + cur = nxt + sntl.prv = nxt + copy._sntl = sntl # pylint: disable=protected-access + copy._fwdm = fwdm # pylint: disable=protected-access + copy._invm = invm # pylint: disable=protected-access + copy._init_inv() # pylint: disable=protected-access + return copy + + def __getitem__(self, key): + nodefwd = self._fwdm[key] + val = self._invm.inverse[nodefwd] + return val + + def _pop(self, key): + nodefwd = self._fwdm.pop(key) + val = self._invm.inverse.pop(nodefwd) + nodefwd.prv.nxt = nodefwd.nxt + nodefwd.nxt.prv = nodefwd.prv + return val + + def _isdupitem(self, key, val, dedup_result): + """Return whether (key, val) duplicates an existing item.""" + isdupkey, isdupval, nodeinv, nodefwd = dedup_result + isdupitem = nodeinv is nodefwd + if isdupitem: + assert isdupkey + assert isdupval + return isdupitem + + def _write_item(self, key, val, dedup_result): # pylint: disable=too-many-locals + fwdm = self._fwdm # bidict mapping keys to nodes + invm = self._invm # bidict mapping vals to nodes + isdupkey, isdupval, nodeinv, nodefwd = dedup_result + if not isdupkey and not isdupval: + # No key or value duplication -> create and append a new node. + sntl = self._sntl + last = sntl.prv + node = _Node(last, sntl) + last.nxt = sntl.prv = fwdm[key] = invm[val] = node + oldkey = oldval = _MISS + elif isdupkey and isdupval: + # Key and value duplication across two different nodes. + assert nodefwd is not nodeinv + oldval = invm.inverse[nodefwd] + oldkey = fwdm.inverse[nodeinv] + assert oldkey != key + assert oldval != val + # We have to collapse nodefwd and nodeinv into a single node, i.e. drop one of them. + # Drop nodeinv, so that the item with the same key is the one overwritten in place. + nodeinv.prv.nxt = nodeinv.nxt + nodeinv.nxt.prv = nodeinv.prv + # Don't remove nodeinv's references to its neighbors since + # if the update fails, we'll need them to undo this write. + # Update fwdm and invm. + tmp = fwdm.pop(oldkey) + assert tmp is nodeinv + tmp = invm.pop(oldval) + assert tmp is nodefwd + fwdm[key] = invm[val] = nodefwd + elif isdupkey: + oldval = invm.inverse[nodefwd] + oldkey = _MISS + oldnodeinv = invm.pop(oldval) + assert oldnodeinv is nodefwd + invm[val] = nodefwd + else: # isdupval + oldkey = fwdm.inverse[nodeinv] + oldval = _MISS + oldnodefwd = fwdm.pop(oldkey) + assert oldnodefwd is nodeinv + fwdm[key] = nodeinv + return _WriteResult(key, val, oldkey, oldval) + + def _undo_write(self, dedup_result, write_result): # pylint: disable=too-many-locals + fwdm = self._fwdm + invm = self._invm + isdupkey, isdupval, nodeinv, nodefwd = dedup_result + key, val, oldkey, oldval = write_result + if not isdupkey and not isdupval: + self._pop(key) + elif isdupkey and isdupval: + # Restore original items. + nodeinv.prv.nxt = nodeinv.nxt.prv = nodeinv + fwdm[oldkey] = invm[val] = nodeinv + invm[oldval] = fwdm[key] = nodefwd + elif isdupkey: + tmp = invm.pop(val) + assert tmp is nodefwd + invm[oldval] = nodefwd + assert fwdm[key] is nodefwd + else: # isdupval + tmp = fwdm.pop(key) + assert tmp is nodeinv + fwdm[oldkey] = nodeinv + assert invm[val] is nodeinv + + def __iter__(self, reverse=False): + """An iterator over this bidict's items in order.""" + fwdm_inv = self._fwdm.inverse + for node in self._sntl.__iter__(reverse=reverse): + yield fwdm_inv[node] + + def __reversed__(self): + """An iterator over this bidict's items in reverse order.""" + for key in self.__iter__(reverse=True): + yield key + + def equals_order_sensitive(self, other): + """Order-sensitive equality check. + + *See also* :ref:`eq-order-insensitive` + """ + # Same short-circuit as BidictBase.__eq__. Factoring out not worth function call overhead. + if not isinstance(other, Mapping) or len(self) != len(other): + return False + return all(i == j for (i, j) in izip(iteritems(self), iteritems(other))) + + +# * Code review nav * +#============================================================================== +# ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py → +#============================================================================== diff --git a/libs/bidict/_orderedbidict.py b/libs/bidict/_orderedbidict.py new file mode 100644 index 000000000..874954838 --- /dev/null +++ b/libs/bidict/_orderedbidict.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +#============================================================================== +# * Welcome to the bidict source code * +#============================================================================== + +# Doing a code review? You'll find a "Code review nav" comment like the one +# below at the top and bottom of the most important source files. This provides +# a suggested initial path through the source when reviewing. +# +# Note: If you aren't reading this on https://github.com/jab/bidict, you may be +# viewing an outdated version of the code. Please head to GitHub to review the +# latest version, which contains important improvements over older versions. +# +# Thank you for reading and for any feedback you provide. + +# * Code review nav * +#============================================================================== +# ← Prev: _frozenordered.py Current: _orderedbidict.py <FIN> +#============================================================================== + + +"""Provides :class:`OrderedBidict`.""" + +from ._mut import MutableBidict +from ._orderedbase import OrderedBidictBase + + +class OrderedBidict(OrderedBidictBase, MutableBidict): + """Mutable bidict type that maintains items in insertion order.""" + + __slots__ = () + __hash__ = None # since this class is mutable; explicit > implicit. + + def clear(self): + """Remove all items.""" + self._fwdm.clear() + self._invm.clear() + self._sntl.nxt = self._sntl.prv = self._sntl + + def popitem(self, last=True): # pylint: disable=arguments-differ + u"""*x.popitem() → (k, v)* + + Remove and return the most recently added item as a (key, value) pair + if *last* is True, else the least recently added item. + + :raises KeyError: if *x* is empty. + """ + if not self: + raise KeyError('mapping is empty') + key = next((reversed if last else iter)(self)) + val = self._pop(key) + return key, val + + def move_to_end(self, key, last=True): + """Move an existing key to the beginning or end of this ordered bidict. + + The item is moved to the end if *last* is True, else to the beginning. + + :raises KeyError: if the key does not exist + """ + node = self._fwdm[key] + node.prv.nxt = node.nxt + node.nxt.prv = node.prv + sntl = self._sntl + if last: + last = sntl.prv + node.prv = last + node.nxt = sntl + sntl.prv = last.nxt = node + else: + first = sntl.nxt + node.prv = sntl + node.nxt = first + sntl.nxt = first.prv = node + + +# * Code review nav * +#============================================================================== +# ← Prev: _frozenordered.py Current: _orderedbidict.py <FIN> +#============================================================================== diff --git a/libs/bidict/_util.py b/libs/bidict/_util.py new file mode 100644 index 000000000..89636e66c --- /dev/null +++ b/libs/bidict/_util.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Useful functions for working with bidirectional mappings and related data.""" + +from itertools import chain, repeat + +from .compat import iteritems, Mapping + + +_NULL_IT = repeat(None, 0) # repeat 0 times -> raise StopIteration from the start + + +def _iteritems_mapping_or_iterable(arg): + """Yield the items in *arg*. + + If *arg* is a :class:`~collections.abc.Mapping`, return an iterator over its items. + Otherwise return an iterator over *arg* itself. + """ + return iteritems(arg) if isinstance(arg, Mapping) else iter(arg) + + +def _iteritems_args_kw(*args, **kw): + """Yield the items from the positional argument (if given) and then any from *kw*. + + :raises TypeError: if more than one positional argument is given. + """ + args_len = len(args) + if args_len > 1: + raise TypeError('Expected at most 1 positional argument, got %d' % args_len) + itemchain = None + if args: + arg = args[0] + if arg: + itemchain = _iteritems_mapping_or_iterable(arg) + if kw: + iterkw = iteritems(kw) + itemchain = chain(itemchain, iterkw) if itemchain else iterkw + return itemchain or _NULL_IT + + +def inverted(arg): + """Yield the inverse items of the provided object. + + If *arg* has a :func:`callable` ``__inverted__`` attribute, + return the result of calling it. + + Otherwise, return an iterator over the items in `arg`, + inverting each item on the fly. + + *See also* :attr:`bidict.BidirectionalMapping.__inverted__` + """ + inv = getattr(arg, '__inverted__', None) + if callable(inv): + return inv() + return ((val, key) for (key, val) in _iteritems_mapping_or_iterable(arg)) diff --git a/libs/bidict/compat.py b/libs/bidict/compat.py new file mode 100644 index 000000000..dc095c920 --- /dev/null +++ b/libs/bidict/compat.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Compatibility helpers.""" + +from operator import methodcaller +from platform import python_implementation +from sys import version_info +from warnings import warn + + +# Use #: (before or) at the end of each line with a member we want to show up in the docs, +# otherwise Sphinx won't include (even though we configure automodule with undoc-members). + +PYMAJOR, PYMINOR = version_info[:2] #: +PY2 = PYMAJOR == 2 #: +PYIMPL = python_implementation() #: +CPY = PYIMPL == 'CPython' #: +PYPY = PYIMPL == 'PyPy' #: +DICTS_ORDERED = PYPY or (CPY and (PYMAJOR, PYMINOR) >= (3, 6)) #: + +# Without the following, pylint gives lots of false positives. +# pylint: disable=invalid-name,unused-import,ungrouped-imports,no-name-in-module + +if PY2: + if PYMINOR < 7: # pragma: no cover + raise ImportError('Python 2.7 or 3.5+ is required.') + warn('Python 2 support will be dropped in a future release.') + + # abstractproperty deprecated in Python 3.3 in favor of using @property with @abstractmethod. + # Before 3.3, this silently fails to detect when an abstract property has not been overridden. + from abc import abstractproperty #: + + from itertools import izip #: + + # In Python 3, the collections ABCs were moved into collections.abc, which does not exist in + # Python 2. Support for importing them directly from collections is dropped in Python 3.8. + import collections as collections_abc # noqa: F401 (imported but unused) + from collections import ( # noqa: F401 (imported but unused) + Mapping, MutableMapping, KeysView, ValuesView, ItemsView) + + viewkeys = lambda m: m.viewkeys() if hasattr(m, 'viewkeys') else KeysView(m) #: + viewvalues = lambda m: m.viewvalues() if hasattr(m, 'viewvalues') else ValuesView(m) #: + viewitems = lambda m: m.viewitems() if hasattr(m, 'viewitems') else ItemsView(m) #: + + iterkeys = lambda m: m.iterkeys() if hasattr(m, 'iterkeys') else iter(m.keys()) #: + itervalues = lambda m: m.itervalues() if hasattr(m, 'itervalues') else iter(m.values()) #: + iteritems = lambda m: m.iteritems() if hasattr(m, 'iteritems') else iter(m.items()) #: + +else: + # Assume Python 3 when not PY2, but explicitly check before showing this warning. + if PYMAJOR == 3 and PYMINOR < 5: # pragma: no cover + warn('Python 3.4 and below are not supported.') + + import collections.abc as collections_abc # noqa: F401 (imported but unused) + from collections.abc import ( # noqa: F401 (imported but unused) + Mapping, MutableMapping, KeysView, ValuesView, ItemsView) + + viewkeys = methodcaller('keys') #: + viewvalues = methodcaller('values') #: + viewitems = methodcaller('items') #: + + def _compose(f, g): + return lambda x: f(g(x)) + + iterkeys = _compose(iter, viewkeys) #: + itervalues = _compose(iter, viewvalues) #: + iteritems = _compose(iter, viewitems) #: + + from abc import abstractmethod + abstractproperty = _compose(property, abstractmethod) #: + + izip = zip #: diff --git a/libs/bidict/metadata.py b/libs/bidict/metadata.py new file mode 100644 index 000000000..95ec8af78 --- /dev/null +++ b/libs/bidict/metadata.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2019 Joshua Bronson. All Rights Reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +"""Define bidict package metadata.""" + + +__version__ = '0.0.0.VERSION_NOT_FOUND' + +# _version.py is generated by setuptools_scm (via its `write_to` param, see setup.py) +try: + from ._version import version as __version__ # pylint: disable=unused-import +except (ImportError, ValueError, SystemError): # pragma: no cover + try: + import pkg_resources + except ImportError: + pass + else: + try: + __version__ = pkg_resources.get_distribution('bidict').version + except pkg_resources.DistributionNotFound: + pass + +try: + __version_info__ = tuple(int(p) if i < 3 else p for (i, p) in enumerate(__version__.split('.'))) +except Exception: # noqa: E722; pragma: no cover; pylint: disable=broad-except + __vesion_info__ = (0, 0, 0, 'PARSE FAILURE: __version__=%s' % __version__) + +__author__ = u'Joshua Bronson' +__maintainer__ = u'Joshua Bronson' +__copyright__ = u'Copyright 2019 Joshua Bronson' +__email__ = u'[email protected]' + +# See: ../docs/thanks.rst +__credits__ = [i.strip() for i in u""" +Joshua Bronson, Michael Arntzenius, Francis Carr, Gregory Ewing, Raymond Hettinger, Jozef Knaperek, +Daniel Pope, Terry Reedy, David Turner, Tom Viner, Richard Sanger, Zeyi Wang +""".split(u',')] + +__description__ = u'Efficient, Pythonic bidirectional map implementation and related functionality' +__keywords__ = 'dict dictionary mapping datastructure bimap bijection bijective ' \ + 'injective inverse reverse bidirectional two-way 2-way' + +__license__ = u'MPL 2.0' +__status__ = u'Beta' +__url__ = u'https://bidict.readthedocs.io' |