_base.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2018 Joshua Bronson. All Rights Reserved.
  3. #
  4. # This Source Code Form is subject to the terms of the Mozilla Public
  5. # License, v. 2.0. If a copy of the MPL was not distributed with this
  6. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  7. #==============================================================================
  8. # * Welcome to the bidict source code *
  9. #==============================================================================
  10. # Doing a code review? You'll find a "Code review nav" comment like the one
  11. # below at the top and bottom of the most important source files. This provides
  12. # a suggested initial path through the source when reviewing.
  13. #
  14. # Note: If you aren't reading this on https://github.com/jab/bidict, you may be
  15. # viewing an outdated version of the code. Please head to GitHub to review the
  16. # latest version, which contains important improvements over older versions.
  17. #
  18. # Thank you for reading and for any feedback you provide.
  19. # * Code review nav *
  20. #==============================================================================
  21. # ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py →
  22. #==============================================================================
  23. """Provides :class:`BidictBase`."""
  24. from collections import namedtuple
  25. from weakref import ref
  26. from ._abc import BidirectionalMapping
  27. from ._dup import RAISE, OVERWRITE, IGNORE, _OnDup
  28. from ._exc import (
  29. DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError)
  30. from ._miss import _MISS
  31. from ._noop import _NOOP
  32. from ._util import _iteritems_args_kw
  33. from .compat import PY2, iteritems, Mapping
  34. # Since BidirectionalMapping implements __subclasshook__, and BidictBase
  35. # provides all the required attributes that the __subclasshook__ checks for,
  36. # BidictBase would be a (virtual) subclass of BidirectionalMapping even if
  37. # it didn't subclass it explicitly. But subclassing BidirectionalMapping
  38. # explicitly allows BidictBase to inherit any useful methods that
  39. # BidirectionalMapping provides that aren't part of the required interface,
  40. # such as its __inverted__ implementation.
  41. class BidictBase(BidirectionalMapping):
  42. """Base class implementing :class:`BidirectionalMapping`."""
  43. __slots__ = ['_fwdm', '_invm', '_inv', '_invweak', '_hash']
  44. if not PY2:
  45. __slots__.append('__weakref__')
  46. #: The default :class:`DuplicationPolicy`
  47. #: (in effect during e.g. :meth:`~bidict.bidict.__init__` calls)
  48. #: that governs behavior when a provided item
  49. #: duplicates only the key of another item.
  50. #:
  51. #: Defaults to :attr:`~bidict.OVERWRITE`
  52. #: to match :class:`dict`'s behavior.
  53. #:
  54. #: *See also* :ref:`basic-usage:Values Must Be Unique`, :doc:`extending`
  55. on_dup_key = OVERWRITE
  56. #: The default :class:`DuplicationPolicy`
  57. #: (in effect during e.g. :meth:`~bidict.bidict.__init__` calls)
  58. #: that governs behavior when a provided item
  59. #: duplicates only the value of another item.
  60. #:
  61. #: Defaults to :attr:`~bidict.RAISE`
  62. #: to prevent unintended overwrite of another item.
  63. #:
  64. #: *See also* :ref:`basic-usage:Values Must Be Unique`, :doc:`extending`
  65. on_dup_val = RAISE
  66. #: The default :class:`DuplicationPolicy`
  67. #: (in effect during e.g. :meth:`~bidict.bidict.__init__` calls)
  68. #: that governs behavior when a provided item
  69. #: duplicates the key of another item and the value of a third item.
  70. #:
  71. #: Defaults to ``None``, which causes the *on_dup_kv* policy to match
  72. #: whatever *on_dup_val* policy is in effect.
  73. #:
  74. #: *See also* :ref:`basic-usage:Values Must Be Unique`, :doc:`extending`
  75. on_dup_kv = None
  76. _fwdm_cls = dict
  77. _invm_cls = dict
  78. def __init__(self, *args, **kw): # pylint: disable=super-init-not-called
  79. """Make a new bidirectional dictionary.
  80. The signature is the same as that of regular dictionaries.
  81. Items passed in are added in the order they are passed,
  82. respecting the current duplication policies in the process.
  83. *See also* :attr:`on_dup_key`, :attr:`on_dup_val`, :attr:`on_dup_kv`
  84. """
  85. #: The backing :class:`~collections.abc.Mapping`
  86. #: storing the forward mapping data (*key* → *value*).
  87. self._fwdm = self._fwdm_cls()
  88. #: The backing :class:`~collections.abc.Mapping`
  89. #: storing the inverse mapping data (*value* → *key*).
  90. self._invm = self._invm_cls()
  91. self._init_inv() # lgtm [py/init-calls-subclass]
  92. if args or kw:
  93. self._update(True, None, *args, **kw)
  94. def _init_inv(self):
  95. # Compute the type for this bidict's inverse bidict (will be different from this
  96. # bidict's type if _fwdm_cls and _invm_cls are different).
  97. inv_cls = self._inv_cls()
  98. # Create the inverse bidict instance via __new__, bypassing its __init__ so that its
  99. # _fwdm and _invm can be assigned to this bidict's _invm and _fwdm. Store it in self._inv,
  100. # which holds a strong reference to a bidict's inverse, if one is available.
  101. self._inv = inv = inv_cls.__new__(inv_cls)
  102. inv._fwdm = self._invm # pylint: disable=protected-access
  103. inv._invm = self._fwdm # pylint: disable=protected-access
  104. # Only give the inverse a weak reference to this bidict to avoid creating a reference cycle,
  105. # stored in the _invweak attribute. See also the docs in
  106. # :ref:`addendum:\:attr\:\`~bidict.BidictBase.inv\` Avoids Reference Cycles`
  107. inv._inv = None # pylint: disable=protected-access
  108. inv._invweak = ref(self) # pylint: disable=protected-access
  109. # Since this bidict has a strong reference to its inverse already, set its _invweak to None.
  110. self._invweak = None
  111. @classmethod
  112. def _inv_cls(cls):
  113. """The inverse of this bidict type, i.e. one with *_fwdm_cls* and *_invm_cls* swapped."""
  114. if cls._fwdm_cls is cls._invm_cls:
  115. return cls
  116. if not getattr(cls, '_inv_cls_', None):
  117. class _Inv(cls):
  118. _fwdm_cls = cls._invm_cls
  119. _invm_cls = cls._fwdm_cls
  120. _inv_cls_ = cls
  121. _Inv.__name__ = cls.__name__ + 'Inv'
  122. cls._inv_cls_ = _Inv
  123. return cls._inv_cls_
  124. @property
  125. def _isinv(self):
  126. return self._inv is None
  127. @property
  128. def inv(self):
  129. """The inverse of this bidict."""
  130. # Resolve and return a strong reference to the inverse bidict.
  131. # One may be stored in self._inv already.
  132. if self._inv is not None:
  133. return self._inv
  134. # Otherwise a weakref is stored in self._invweak. Try to get a strong ref from it.
  135. inv = self._invweak()
  136. if inv is not None:
  137. return inv
  138. # Refcount of referent must have dropped to zero, as in `bidict().inv.inv`. Init a new one.
  139. self._init_inv() # Now this bidict will retain a strong ref to its inverse.
  140. return self._inv
  141. def __getstate__(self):
  142. """Needed to enable pickling due to use of :attr:`__slots__` and weakrefs.
  143. *See also* :meth:`object.__getstate__`
  144. """
  145. state = {}
  146. for cls in self.__class__.__mro__:
  147. slots = getattr(cls, '__slots__', ())
  148. for slot in slots:
  149. if hasattr(self, slot):
  150. state[slot] = getattr(self, slot)
  151. # These weakrefs can't be pickled, and don't need to be anyway.
  152. state.pop('__weakref__', None)
  153. state.pop('_invweak', None)
  154. return state
  155. def __setstate__(self, state):
  156. """Implemented because use of :attr:`__slots__` would prevent unpickling otherwise.
  157. *See also* :meth:`object.__setstate__`
  158. """
  159. for slot, value in iteritems(state):
  160. setattr(self, slot, value)
  161. def __repr__(self):
  162. """See :func:`repr`."""
  163. clsname = self.__class__.__name__
  164. if not self:
  165. return '%s()' % clsname
  166. return '%s(%r)' % (clsname, self.__repr_delegate__())
  167. def __repr_delegate__(self):
  168. """The object used by :meth:`__repr__` to represent the contained items."""
  169. return self._fwdm
  170. # The inherited Mapping.__eq__ implementation would work, but it's implemented in terms of an
  171. # inefficient ``dict(self.items()) == dict(other.items())`` comparison, so override it with a
  172. # more efficient implementation.
  173. def __eq__(self, other):
  174. u"""*x.__eq__(other) ⟺ x == other*
  175. Equivalent to *dict(x.items()) == dict(other.items())*
  176. but more efficient.
  177. Note that :meth:`bidict's __eq__() <bidict.bidict.__eq__>` implementation
  178. is inherited by subclasses,
  179. in particular by the ordered bidict subclasses,
  180. so even with ordered bidicts,
  181. :ref:`== comparison is order-insensitive <eq-order-insensitive>`.
  182. *See also* :meth:`bidict.FrozenOrderedBidict.equals_order_sensitive`
  183. """
  184. if not isinstance(other, Mapping) or len(self) != len(other):
  185. return False
  186. selfget = self.get
  187. return all(selfget(k, _MISS) == v for (k, v) in iteritems(other))
  188. # The following methods are mutating and so are not public. But they are implemented in this
  189. # non-mutable base class (rather than the mutable `bidict` subclass) because they are used here
  190. # during initialization (starting with the `_update` method). (Why is this? Because `__init__`
  191. # and `update` share a lot of the same behavior (inserting the provided items while respecting
  192. # the active duplication policies), so it makes sense for them to share implementation too.)
  193. def _pop(self, key):
  194. val = self._fwdm.pop(key)
  195. del self._invm[val]
  196. return val
  197. def _put(self, key, val, on_dup):
  198. dedup_result = self._dedup_item(key, val, on_dup)
  199. if dedup_result is not _NOOP:
  200. self._write_item(key, val, dedup_result)
  201. def _dedup_item(self, key, val, on_dup):
  202. """
  203. Check *key* and *val* for any duplication in self.
  204. Handle any duplication as per the duplication policies given in *on_dup*.
  205. (key, val) already present is construed as a no-op, not a duplication.
  206. If duplication is found and the corresponding duplication policy is
  207. :attr:`~bidict.RAISE`, raise the appropriate error.
  208. If duplication is found and the corresponding duplication policy is
  209. :attr:`~bidict.IGNORE`, return *None*.
  210. If duplication is found and the corresponding duplication policy is
  211. :attr:`~bidict.OVERWRITE`,
  212. or if no duplication is found,
  213. return the _DedupResult *(isdupkey, isdupval, oldkey, oldval)*.
  214. """
  215. fwdm = self._fwdm
  216. invm = self._invm
  217. oldval = fwdm.get(key, _MISS)
  218. oldkey = invm.get(val, _MISS)
  219. isdupkey = oldval is not _MISS
  220. isdupval = oldkey is not _MISS
  221. dedup_result = _DedupResult(isdupkey, isdupval, oldkey, oldval)
  222. if isdupkey and isdupval:
  223. if self._isdupitem(key, val, dedup_result):
  224. # (key, val) duplicates an existing item -> no-op.
  225. return _NOOP
  226. # key and val each duplicate a different existing item.
  227. if on_dup.kv is RAISE:
  228. raise KeyAndValueDuplicationError(key, val)
  229. elif on_dup.kv is IGNORE:
  230. return _NOOP
  231. assert on_dup.kv is OVERWRITE, 'invalid on_dup_kv: %r' % on_dup.kv
  232. # Fall through to the return statement on the last line.
  233. elif isdupkey:
  234. if on_dup.key is RAISE:
  235. raise KeyDuplicationError(key)
  236. elif on_dup.key is IGNORE:
  237. return _NOOP
  238. assert on_dup.key is OVERWRITE, 'invalid on_dup.key: %r' % on_dup.key
  239. # Fall through to the return statement on the last line.
  240. elif isdupval:
  241. if on_dup.val is RAISE:
  242. raise ValueDuplicationError(val)
  243. elif on_dup.val is IGNORE:
  244. return _NOOP
  245. assert on_dup.val is OVERWRITE, 'invalid on_dup.val: %r' % on_dup.val
  246. # Fall through to the return statement on the last line.
  247. # else neither isdupkey nor isdupval.
  248. return dedup_result
  249. @staticmethod
  250. def _isdupitem(key, val, dedup_result):
  251. isdupkey, isdupval, oldkey, oldval = dedup_result
  252. isdupitem = oldkey == key
  253. assert isdupitem == (oldval == val), '%r %r %r' % (key, val, dedup_result)
  254. if isdupitem:
  255. assert isdupkey
  256. assert isdupval
  257. return isdupitem
  258. @classmethod
  259. def _get_on_dup(cls, on_dup=None):
  260. if on_dup is None:
  261. on_dup = _OnDup(cls.on_dup_key, cls.on_dup_val, cls.on_dup_kv)
  262. elif not isinstance(on_dup, _OnDup):
  263. on_dup = _OnDup(*on_dup)
  264. if on_dup.kv is None:
  265. on_dup = on_dup._replace(kv=on_dup.val)
  266. return on_dup
  267. def _write_item(self, key, val, dedup_result):
  268. isdupkey, isdupval, oldkey, oldval = dedup_result
  269. fwdm = self._fwdm
  270. invm = self._invm
  271. fwdm[key] = val
  272. invm[val] = key
  273. if isdupkey:
  274. del invm[oldval]
  275. if isdupval:
  276. del fwdm[oldkey]
  277. return _WriteResult(key, val, oldkey, oldval)
  278. def _update(self, init, on_dup, *args, **kw):
  279. if not args and not kw:
  280. return
  281. on_dup = self._get_on_dup(on_dup)
  282. empty = not self
  283. only_copy_from_bimap = empty and not kw and isinstance(args[0], BidirectionalMapping)
  284. if only_copy_from_bimap: # no need to check for duplication
  285. write_item = self._write_item
  286. for (key, val) in iteritems(args[0]):
  287. write_item(key, val, _NODUP)
  288. return
  289. raise_on_dup = RAISE in on_dup
  290. rollback = raise_on_dup and not init
  291. if rollback:
  292. self._update_with_rollback(on_dup, *args, **kw)
  293. return
  294. _put = self._put
  295. for (key, val) in _iteritems_args_kw(*args, **kw):
  296. _put(key, val, on_dup)
  297. def _update_with_rollback(self, on_dup, *args, **kw):
  298. """Update, rolling back on failure."""
  299. writelog = []
  300. appendlog = writelog.append
  301. dedup_item = self._dedup_item
  302. write_item = self._write_item
  303. for (key, val) in _iteritems_args_kw(*args, **kw):
  304. try:
  305. dedup_result = dedup_item(key, val, on_dup)
  306. except DuplicationError:
  307. undo_write = self._undo_write
  308. for dedup_result, write_result in reversed(writelog):
  309. undo_write(dedup_result, write_result)
  310. raise
  311. if dedup_result is not _NOOP:
  312. write_result = write_item(key, val, dedup_result)
  313. appendlog((dedup_result, write_result))
  314. def _undo_write(self, dedup_result, write_result):
  315. isdupkey, isdupval, _, _ = dedup_result
  316. key, val, oldkey, oldval = write_result
  317. if not isdupkey and not isdupval:
  318. self._pop(key)
  319. return
  320. fwdm = self._fwdm
  321. invm = self._invm
  322. if isdupkey:
  323. fwdm[key] = oldval
  324. invm[oldval] = key
  325. if not isdupval:
  326. del invm[val]
  327. if isdupval:
  328. invm[val] = oldkey
  329. fwdm[oldkey] = val
  330. if not isdupkey:
  331. del fwdm[key]
  332. def copy(self):
  333. """A shallow copy."""
  334. # Could just ``return self.__class__(self)`` here instead, but the below is faster. It uses
  335. # __new__ to create a copy instance while bypassing its __init__, which would result
  336. # in copying this bidict's items into the copy instance one at a time. Instead, make whole
  337. # copies of each of the backing mappings, and make them the backing mappings of the copy,
  338. # avoiding copying items one at a time.
  339. copy = self.__class__.__new__(self.__class__)
  340. copy._fwdm = self._fwdm.copy() # pylint: disable=protected-access
  341. copy._invm = self._invm.copy() # pylint: disable=protected-access
  342. copy._init_inv() # pylint: disable=protected-access
  343. return copy
  344. def __copy__(self):
  345. """Used for the copy protocol.
  346. *See also* the :mod:`copy` module
  347. """
  348. return self.copy()
  349. def __len__(self):
  350. """The number of contained items."""
  351. return len(self._fwdm)
  352. def __iter__(self):
  353. """Iterator over the contained items."""
  354. return iter(self._fwdm)
  355. def __getitem__(self, key):
  356. u"""*x.__getitem__(key) ⟺ x[key]*"""
  357. return self._fwdm[key]
  358. if PY2:
  359. # __ne__ added automatically in Python 3 when you implement __eq__, but not in Python 2.
  360. def __ne__(self, other): # noqa: N802
  361. u"""*x.__ne__(other) ⟺ x != other*"""
  362. return not self == other # Implement __ne__ in terms of __eq__.
  363. _DedupResult = namedtuple('_DedupResult', 'isdupkey isdupval invbyval fwdbykey')
  364. _WriteResult = namedtuple('_WriteResult', 'key val oldkey oldval')
  365. _NODUP = _DedupResult(False, False, _MISS, _MISS)
  366. # * Code review nav *
  367. #==============================================================================
  368. # ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py →
  369. #==============================================================================