summaryrefslogtreecommitdiffhomepage
path: root/libs/bidict
diff options
context:
space:
mode:
authormorpheus65535 <[email protected]>2022-11-07 13:06:49 -0500
committermorpheus65535 <[email protected]>2022-11-07 13:08:27 -0500
commitbbe2483e21c2c1549ceeed16f021f9581b899f70 (patch)
treebcc2bef2f55789ec6e6c64809c07fb4f4d3d9c86 /libs/bidict
parent708fbfcd8ec0620647975be39a1f6acbbf08f767 (diff)
downloadbazarr-bbe2483e21c2c1549ceeed16f021f9581b899f70.tar.gz
bazarr-bbe2483e21c2c1549ceeed16f021f9581b899f70.zip
Updated vendored dependencies.
Diffstat (limited to 'libs/bidict')
-rw-r--r--libs/bidict/__init__.py78
-rw-r--r--libs/bidict/_abc.py57
-rw-r--r--libs/bidict/_base.py679
-rw-r--r--libs/bidict/_bidict.py198
-rw-r--r--libs/bidict/_delegating.py39
-rw-r--r--libs/bidict/_dup.py24
-rw-r--r--libs/bidict/_exc.py3
-rw-r--r--libs/bidict/_frozenbidict.py39
-rw-r--r--libs/bidict/_frozenordered.py53
-rw-r--r--libs/bidict/_iter.py54
-rw-r--r--libs/bidict/_mut.py188
-rw-r--r--libs/bidict/_named.py116
-rw-r--r--libs/bidict/_orderedbase.py389
-rw-r--r--libs/bidict/_orderedbidict.py148
-rw-r--r--libs/bidict/_typing.py33
-rw-r--r--libs/bidict/metadata.py24
-rw-r--r--libs/bidict/py.typed1
17 files changed, 1049 insertions, 1074 deletions
diff --git a/libs/bidict/__init__.py b/libs/bidict/__init__.py
index e83811052..f41b81222 100644
--- a/libs/bidict/__init__.py
+++ b/libs/bidict/__init__.py
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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
@@ -10,24 +9,26 @@
# * 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.
+# Reading through the code? You'll find a "Code review nav" comment like the one
+# below at the top and bottom of the key source files. Follow these cues to take
+# a path through the code that's optimized for familiarizing yourself with it.
#
-# 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.
+# If you're not reading this on https://github.com/jab/bidict already, go there
+# to ensure you have the latest version of the code. While there, you can also
+# star the project, watch it for updates, fork the code, and submit an issue or
+# pull request with any proposed changes. More information can be found linked
+# from README.rst, which is also shown on https://github.com/jab/bidict.
# * Code review nav *
#==============================================================================
-# Current: __init__.py Next: _abc.py →
+# Current: __init__.py Next: _abc.py →
#==============================================================================
"""The bidirectional mapping library for Python.
+----
+
bidict by example:
.. code-block:: python
@@ -44,8 +45,9 @@ 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) 2009-2021 Joshua Bronson.
+.. :copyright: (c) 2009-2022 Joshua Bronson.
.. :license: MPLv2. See LICENSE for details.
"""
@@ -53,39 +55,45 @@ if you are reading this elsewhere.
from sys import version_info as _version_info
-if _version_info < (3, 6): # pragma: no cover
- raise ImportError('Python 3.6+ is required.')
-
-from ._abc import BidirectionalMapping, MutableBidirectionalMapping
-from ._base import BidictBase
-from ._mut import MutableBidict
-from ._bidict import bidict
-from ._frozenbidict import frozenbidict
-from ._frozenordered import FrozenOrderedBidict
-from ._named import namedbidict
-from ._orderedbase import OrderedBidictBase
-from ._orderedbidict import OrderedBidict
-from ._dup import ON_DUP_DEFAULT, ON_DUP_RAISE, ON_DUP_DROP_OLD, RAISE, DROP_OLD, DROP_NEW, OnDup, OnDupAction
-from ._exc import BidictException, DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError
-from ._iter import inverted
+if _version_info < (3, 7): # pragma: no cover
+ raise ImportError('Python 3.7+ is required.')
+
+from ._abc import BidirectionalMapping as BidirectionalMapping, MutableBidirectionalMapping as MutableBidirectionalMapping
+from ._base import BidictBase as BidictBase, GeneratedBidictInverse as GeneratedBidictInverse, BidictKeysView as BidictKeysView
+from ._bidict import MutableBidict as MutableBidict, bidict as bidict
+from ._frozenbidict import frozenbidict as frozenbidict
+from ._frozenordered import FrozenOrderedBidict as FrozenOrderedBidict
+from ._named import NamedBidictBase as NamedBidictBase, namedbidict as namedbidict
+from ._orderedbase import OrderedBidictBase as OrderedBidictBase
+from ._orderedbidict import OrderedBidict as OrderedBidict
+from ._dup import ON_DUP_DEFAULT as ON_DUP_DEFAULT, ON_DUP_RAISE as ON_DUP_RAISE, ON_DUP_DROP_OLD as ON_DUP_DROP_OLD
+from ._dup import RAISE as RAISE, DROP_OLD as DROP_OLD, DROP_NEW as DROP_NEW, OnDup as OnDup, OD as OD
+from ._exc import BidictException as BidictException, DuplicationError as DuplicationError
+from ._exc import KeyDuplicationError as KeyDuplicationError, ValueDuplicationError as ValueDuplicationError, KeyAndValueDuplicationError as KeyAndValueDuplicationError
+from ._iter import inverted as inverted
from .metadata import (
- __author__, __maintainer__, __copyright__, __email__, __credits__, __url__,
- __license__, __status__, __description__, __keywords__, __version__,
+ __author__ as __author__, __maintainer__ as __maintainer__, __copyright__ as __copyright__, __email__ as __email__,
+ __url__ as __url__, __license__ as __license__, __status__ as __status__, __description__ as __description__,
+ __keywords__ as __keywords__, __version__ as __version__, __project_urls__ as __project_urls__,
)
-# Set __module__ of re-exported classes to the 'bidict' top-level module name
-# so that private/internal submodules are not exposed to users e.g. in repr strings.
-_locals = tuple(locals().items())
-for _name, _obj in _locals: # pragma: no cover
+
+#: Alias
+OnDupAction = OD
+
+
+# Set __module__ of re-exported classes to the 'bidict' top-level module, so that e.g.
+# 'bidict.bidict' shows up as 'bidict.bidict` rather than 'bidict._bidict.bidict'.
+for _obj in tuple(locals().values()): # pragma: no cover
if not getattr(_obj, '__module__', '').startswith('bidict.'):
continue
try:
_obj.__module__ = 'bidict'
- except AttributeError: # raised when __module__ is read-only (as in OnDup)
+ except AttributeError: # __module__ is read-only (as in namedtuples like `OnDup`)
pass
# * Code review nav *
#==============================================================================
-# Current: __init__.py Next: _abc.py →
+# Current: __init__.py Next: _abc.py →
#==============================================================================
diff --git a/libs/bidict/_abc.py b/libs/bidict/_abc.py
index 2fd130a29..c1bdf6e32 100644
--- a/libs/bidict/_abc.py
+++ b/libs/bidict/_abc.py
@@ -1,41 +1,27 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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 *
+# (see comments in __init__.py)
#==============================================================================
-# ← Prev: __init__.py Current: _abc.py Next: _base.py →
+# ← Prev: __init__.py Current: _abc.py Next: _base.py →
#==============================================================================
"""Provide the :class:`BidirectionalMapping` abstract base class."""
-import typing as _t
+import typing as t
from abc import abstractmethod
from ._typing import KT, VT
-class BidirectionalMapping(_t.Mapping[KT, VT]):
- """Abstract base class (ABC) for bidirectional mapping types.
+class BidirectionalMapping(t.Mapping[KT, VT]):
+ """Abstract base class for bidirectional mapping types.
Extends :class:`collections.abc.Mapping` primarily by adding the
(abstract) :attr:`inverse` property,
@@ -55,14 +41,13 @@ class BidirectionalMapping(_t.Mapping[KT, VT]):
: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).
+ # The @abstractmethod decorator prevents BidirectionalMapping subclasses from being
+ # instantiated unless they override ``.inverse``. So this implementation of ``.inverse``
+ # should never be unintentionally resolved from subclass instances. But raise here
+ # anyway, so it's extra clear that this implementation should never be called.
raise NotImplementedError
- def __inverted__(self) -> _t.Iterator[_t.Tuple[VT, KT]]:
+ def __inverted__(self) -> t.Iterator[t.Tuple[VT, KT]]:
"""Get an iterator over the items in :attr:`inverse`.
This is functionally equivalent to iterating over the items in the
@@ -78,28 +63,14 @@ class BidirectionalMapping(_t.Mapping[KT, VT]):
"""
return iter(self.inverse.items())
- def values(self) -> _t.KeysView[VT]: # type: ignore [override] # https://github.com/python/typeshed/issues/4435
- """A set-like object providing a view on the contained values.
-
- Override the implementation inherited from
- :class:`~collections.abc.Mapping`.
- 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() # type: ignore [return-value]
-
-class MutableBidirectionalMapping(BidirectionalMapping[KT, VT], _t.MutableMapping[KT, VT]):
- """Abstract base class (ABC) for mutable bidirectional mapping types."""
+class MutableBidirectionalMapping(BidirectionalMapping[KT, VT], t.MutableMapping[KT, VT]):
+ """Abstract base class for mutable bidirectional mapping types."""
__slots__ = ()
# * Code review nav *
#==============================================================================
-# ← Prev: __init__.py Current: _abc.py Next: _base.py →
+# ← Prev: __init__.py Current: _abc.py Next: _base.py →
#==============================================================================
diff --git a/libs/bidict/_base.py b/libs/bidict/_base.py
index f1b40a416..90826018c 100644
--- a/libs/bidict/_base.py
+++ b/libs/bidict/_base.py
@@ -1,183 +1,278 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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 *
+# (see comments in __init__.py)
#==============================================================================
-# ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py →
+# ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py →
#==============================================================================
"""Provide :class:`BidictBase`."""
-import typing as _t
-from collections import namedtuple
-from copy import copy
-from weakref import ref
+import typing as t
+import weakref
+from functools import partial
+from itertools import starmap
+from operator import eq
+from types import MappingProxyType
from ._abc import BidirectionalMapping
from ._dup import ON_DUP_DEFAULT, RAISE, DROP_OLD, DROP_NEW, OnDup
from ._exc import DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError
-from ._iter import _iteritems_args_kw
-from ._typing import _NONE, KT, VT, OKT, OVT, IterItems, MapOrIterItems
+from ._iter import iteritems, inverted
+from ._typing import KT, VT, MISSING, OKT, OVT, IterItems, MapOrIterItems
+
+
+# Disable pyright strict diagnostics that are causing many false positives or are just not helpful in this file:
+# pyright: reportPrivateUsage=false, reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnnecessaryIsInstance=false
+
+
+OldKV = t.Tuple[OKT[KT], OVT[VT]]
+DedupResult = t.Optional[OldKV[KT, VT]]
+Write = t.List[t.Callable[[], None]]
+Unwrite = Write
+PreparedWrite = t.Tuple[Write, Unwrite]
+BT = t.TypeVar('BT', bound='BidictBase[t.Any, t.Any]')
-_WriteResult = namedtuple('_WriteResult', 'key val oldkey oldval')
-_DedupResult = namedtuple('_DedupResult', 'isdupkey isdupval invbyval fwdbykey')
-_NODUP = _DedupResult(False, False, _NONE, _NONE)
+class BidictKeysView(t.KeysView[KT], t.ValuesView[KT]):
+ """Since the keys of a bidict are the values of its inverse (and vice versa),
+ the :class:`~collections.abc.ValuesView` result of calling *bi.values()*
+ is also a :class:`~collections.abc.KeysView` of *bi.inverse*.
+ """
-BT = _t.TypeVar('BT', bound='BidictBase') # typevar for BidictBase.copy
+
+dict_keys: t.Type[t.KeysView[t.Any]] = type({}.keys())
+BidictKeysView.register(dict_keys)
+
+
+def get_arg(*args: MapOrIterItems[KT, VT]) -> MapOrIterItems[KT, VT]:
+ """Ensure there's only a single arg in *args*, then return it."""
+ if len(args) > 1:
+ raise TypeError(f'Expected at most 1 positional argument, got {len(args)}')
+ return args[0] if args else ()
class BidictBase(BidirectionalMapping[KT, VT]):
"""Base class implementing :class:`BidirectionalMapping`."""
- __slots__ = ['_fwdm', '_invm', '_inv', '_invweak', '__weakref__']
-
#: The default :class:`~bidict.OnDup`
#: that governs behavior when a provided item
#: duplicates the key or value of other item(s).
#:
- #: *See also* :ref:`basic-usage:Values Must Be Unique`, :doc:`extending`
+ #: *See also*
+ #: :ref:`basic-usage:Values Must Be Unique` (https://bidict.rtfd.io/basic-usage.html#values-must-be-unique),
+ #: :doc:`extending` (https://bidict.rtfd.io/extending.html)
on_dup = ON_DUP_DEFAULT
- _fwdm_cls: _t.Type[_t.MutableMapping[KT, VT]] = dict #: class of the backing forward mapping
- _invm_cls: _t.Type[_t.MutableMapping[VT, KT]] = dict #: class of the backing inverse mapping
+ _fwdm: t.MutableMapping[KT, VT] #: the backing forward mapping (*key* → *val*)
+ _invm: t.MutableMapping[VT, KT] #: the backing inverse mapping (*val* → *key*)
- #: The object used by :meth:`__repr__` for printing the contained items.
- _repr_delegate: _t.Callable = dict
+ # Use Any rather than KT/VT in the following to avoid "ClassVar cannot contain type variables" errors:
+ _fwdm_cls: t.ClassVar[t.Type[t.MutableMapping[t.Any, t.Any]]] = dict #: class of the backing forward mapping
+ _invm_cls: t.ClassVar[t.Type[t.MutableMapping[t.Any, t.Any]]] = dict #: class of the backing inverse mapping
- _inv: 'BidictBase[VT, KT]'
- _inv_cls: '_t.Type[BidictBase[VT, KT]]'
+ #: The class of the inverse bidict instance.
+ _inv_cls: 't.ClassVar[t.Type[BidictBase[t.Any, t.Any]]]'
- def __init_subclass__(cls, **kw):
- super().__init_subclass__(**kw)
- # Compute and set _inv_cls, the inverse of this bidict class.
- if '_inv_cls' in cls.__dict__:
- return
- if cls._fwdm_cls is cls._invm_cls:
- cls._inv_cls = cls
- return
- inv_cls = type(cls.__name__ + 'Inv', cls.__bases__, {
- **cls.__dict__,
- '_inv_cls': cls,
+ #: Used by :meth:`__repr__` for the contained items.
+ _repr_delegate: t.ClassVar[t.Any] = dict
+
+ def __init_subclass__(cls) -> None:
+ super().__init_subclass__()
+ cls._init_class()
+
+ @classmethod
+ def _init_class(cls) -> None:
+ cls._ensure_inv_cls()
+ cls._set_reversed()
+
+ __reversed__: t.Any
+
+ @classmethod
+ def _set_reversed(cls) -> None:
+ """Set __reversed__ for subclasses that do not set it explicitly
+ according to whether backing mappings are reversible.
+ """
+ if cls is not BidictBase:
+ resolved = cls.__reversed__
+ overridden = resolved is not BidictBase.__reversed__
+ if overridden: # E.g. OrderedBidictBase, OrderedBidict, FrozenOrderedBidict
+ return
+ # The following will be False for MutableBidict, bidict, and frozenbidict on Python < 3.8,
+ # and True for them on 3.8+ (where dicts are reversible). Will also be True for custom
+ # subclasses like SortedBidict (see https://bidict.rtfd.io/extending.html#sortedbidict-recipes).
+ backing_reversible = all(issubclass(i, t.Reversible) for i in (cls._fwdm_cls, cls._invm_cls))
+ cls.__reversed__ = _fwdm_reversed if backing_reversible else None
+
+ @classmethod
+ def _ensure_inv_cls(cls) -> None:
+ """Ensure :attr:`_inv_cls` is set, computing it dynamically if necessary.
+
+ See: :ref:`extending:Dynamic Inverse Class Generation`
+ (https://bidict.rtfd.io/extending.html#dynamic-inverse-class-generation)
+
+ Most subclasses will be their own inverse classes, but some
+ (e.g. those created via namedbidict) will have distinct inverse classes.
+ """
+ if cls.__dict__.get('_inv_cls'):
+ return # Already set, nothing to do.
+ cls._inv_cls = cls._make_inv_cls()
+
+ @classmethod
+ def _make_inv_cls(cls: t.Type[BT], _miss: t.Any = object()) -> 't.Type[BT]':
+ diff = cls._inv_cls_dict_diff()
+ cls_is_own_inv = all(getattr(cls, k, _miss) == v for (k, v) in diff.items())
+ if cls_is_own_inv:
+ return cls
+ # Suppress auto-calculation of _inv_cls's _inv_cls since we know it already.
+ # Works with the guard in BidictBase._ensure_inv_cls() to prevent infinite recursion.
+ diff['_inv_cls'] = cls
+ inv_cls = type(f'{cls.__name__}Inv', (cls, GeneratedBidictInverse), diff)
+ inv_cls.__module__ = cls.__module__
+ return t.cast(t.Type[BT], inv_cls)
+
+ @classmethod
+ def _inv_cls_dict_diff(cls) -> t.Dict[str, t.Any]:
+ return {
'_fwdm_cls': cls._invm_cls,
'_invm_cls': cls._fwdm_cls,
- })
- cls._inv_cls = inv_cls
-
- @_t.overload
- def __init__(self, __arg: _t.Mapping[KT, VT], **kw: VT) -> None: ...
- @_t.overload
- def __init__(self, __arg: IterItems[KT, VT], **kw: VT) -> None: ...
- @_t.overload
+ }
+
+ @t.overload
def __init__(self, **kw: VT) -> None: ...
+ @t.overload
+ def __init__(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ...
+ @t.overload
+ def __init__(self, __i: IterItems[KT, VT], **kw: VT) -> None: ...
+
def __init__(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
- """Make a new bidirectional dictionary.
+ """Make a new bidirectional mapping.
The signature behaves like that of :class:`dict`.
Items passed in are added in the order they are passed,
respecting the :attr:`on_dup` class attribute in the process.
"""
- #: The backing :class:`~collections.abc.Mapping`
- #: storing the forward mapping data (*key* → *value*).
- self._fwdm: _t.MutableMapping[KT, VT] = self._fwdm_cls()
- #: The backing :class:`~collections.abc.Mapping`
- #: storing the inverse mapping data (*value* → *key*).
- self._invm: _t.MutableMapping[VT, KT] = self._invm_cls()
- self._init_inv()
+ self._fwdm = self._fwdm_cls()
+ self._invm = self._invm_cls()
if args or kw:
- self._update(True, self.on_dup, *args, **kw)
+ self._update(get_arg(*args), kw, rbof=False)
- def _init_inv(self) -> None:
- # 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 = self._inv_cls.__new__(self._inv_cls)
+ # If Python ever adds support for higher-kinded types, `inverse` could use them, e.g.
+ # def inverse(self: BT[KT, VT]) -> BT[VT, KT]:
+ # Ref: https://github.com/python/typing/issues/548#issuecomment-621571821
+ @property
+ def inverse(self) -> 'BidictBase[VT, KT]':
+ """The inverse of this bidirectional mapping instance."""
+ # When `bi.inverse` is called for the first time, this method
+ # computes the inverse instance, stores it for subsequent use, and then
+ # returns it. It also stores a reference on `bi.inverse` back to `bi`,
+ # but uses a weakref to avoid creating a reference cycle. Strong references
+ # to inverse instances are stored in ._inv, and weak references are stored
+ # in ._invweak.
+
+ # First check if a strong reference is already stored.
+ inv: 't.Optional[BidictBase[VT, KT]]' = getattr(self, '_inv', None)
+ if inv is not None:
+ return inv
+ # Next check if a weak reference is already stored.
+ invweak = getattr(self, '_invweak', None)
+ if invweak is not None:
+ inv = invweak() # Try to resolve a strong reference and return it.
+ if inv is not None:
+ return inv
+ # No luck. Compute the inverse reference and store it for subsequent use.
+ inv = self._make_inverse()
+ self._inv: 't.Optional[BidictBase[VT, KT]]' = inv
+ self._invweak: 't.Optional[weakref.ReferenceType[BidictBase[VT, KT]]]' = None
+ # Also store a weak reference back to `instance` on its inverse instance, so that
+ # the second `.inverse` access in `bi.inverse.inverse` hits the cached weakref.
+ inv._inv = None
+ inv._invweak = weakref.ref(self)
+ # In e.g. `bidict().inverse.inverse`, this design ensures that a strong reference
+ # back to the original instance is retained before its refcount drops to zero,
+ # avoiding an unintended potential deallocation.
+ return inv
+
+ def _make_inverse(self) -> 'BidictBase[VT, KT]':
+ inv: 'BidictBase[VT, KT]' = self._inv_cls()
inv._fwdm = self._invm
inv._invm = self._fwdm
- # 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
- inv._invweak = ref(self)
- # Since this bidict has a strong reference to its inverse already, set its _invweak to None.
- self._invweak = None
+ return inv
@property
- def _isinv(self) -> bool:
- return self._inv is None
+ def inv(self) -> 'BidictBase[VT, KT]':
+ """Alias for :attr:`inverse`."""
+ return self.inverse
- @property
- def inverse(self) -> 'BidictBase[VT, KT]':
- """The inverse of this bidict."""
- # 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.
- assert self._invweak is not None
- 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
+ def __repr__(self) -> str:
+ """See :func:`repr`."""
+ clsname = self.__class__.__name__
+ items = self._repr_delegate(self.items()) if self else ''
+ return f'{clsname}({items})'
- #: Alias for :attr:`inverse`.
- inv = inverse
+ def values(self) -> BidictKeysView[VT]:
+ """A set-like object providing a view on the contained values.
- def __getstate__(self) -> dict:
- """Needed to enable pickling due to use of :attr:`__slots__` and weakrefs.
+ Since the values of a bidict are equivalent to the keys of its inverse,
+ this method returns a set-like object for this bidict's values
+ rather than just a collections.abc.ValuesView.
+ This object supports set operations like union and difference,
+ and constant- rather than linear-time containment checks,
+ and is no more expensive to provide than the less capable
+ collections.abc.ValuesView would be.
- *See also* :meth:`object.__getstate__`
+ See :meth:`keys` for more information.
"""
- 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: dict) -> None:
- """Implemented because use of :attr:`__slots__` would prevent unpickling otherwise.
-
- *See also* :meth:`object.__setstate__`
+ return t.cast(BidictKeysView[VT], self.inverse.keys())
+
+ def keys(self) -> t.KeysView[KT]:
+ """A set-like object providing a view on the contained keys.
+
+ When *b._fwdm* is a :class:`dict`, *b.keys()* returns a
+ *dict_keys* object that behaves exactly the same as
+ *collections.abc.KeysView(b)*, except for
+
+ - offering better performance
+
+ - being reversible on Python 3.8+
+
+ - having a .mapping attribute in Python 3.10+
+ that exposes a mappingproxy to *b._fwdm*.
"""
- for slot, value in state.items():
- setattr(self, slot, value)
- self._init_inv()
+ fwdm = self._fwdm
+ kv = fwdm.keys() if isinstance(fwdm, dict) else BidictKeysView(self)
+ return kv
- def __repr__(self) -> str:
- """See :func:`repr`."""
- clsname = self.__class__.__name__
- if not self:
- return f'{clsname}()'
- return f'{clsname}({self._repr_delegate(self.items())})'
+ def items(self) -> t.ItemsView[KT, VT]:
+ """A set-like object providing a view on the contained items.
+
+ When *b._fwdm* is a :class:`dict`, *b.items()* returns a
+ *dict_items* object that behaves exactly the same as
+ *collections.abc.ItemsView(b)*, except for:
+
+ - offering better performance
- # 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
+ - being reversible on Python 3.8+
+
+ - having a .mapping attribute in Python 3.10+
+ that exposes a mappingproxy to *b._fwdm*.
+ """
+ return self._fwdm.items() if isinstance(self._fwdm, dict) else super().items()
+
+ # The inherited collections.abc.Mapping.__contains__() method is implemented by doing a `try`
+ # `except KeyError` around `self[key]`. The following implementation is much faster,
+ # especially in the missing case.
+ def __contains__(self, key: t.Any) -> bool:
+ """True if the mapping contains the specified key, else False."""
+ return key in self._fwdm
+
+ # The inherited collections.abc.Mapping.__eq__() method is 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: object) -> bool:
"""*x.__eq__(other) ⟺ x == other*
@@ -189,67 +284,51 @@ class BidictBase(BidirectionalMapping[KT, VT]):
is inherited by subclasses,
in particular by the ordered bidict subclasses,
so even with ordered bidicts,
- :ref:`== comparison is order-insensitive <eq-order-insensitive>`.
+ :ref:`== comparison is order-insensitive <eq-order-insensitive>`
+ (https://bidict.rtfd.io/other-bidict-types.html#eq-is-order-insensitive).
- *See also* :meth:`bidict.FrozenOrderedBidict.equals_order_sensitive`
+ *See also* :meth:`equals_order_sensitive`
"""
- if not isinstance(other, _t.Mapping) or len(self) != len(other):
- return False
- selfget = self.get
- return all(selfget(k, _NONE) == v for (k, v) in other.items()) # type: ignore [arg-type]
+ if isinstance(other, t.Mapping):
+ return self._fwdm.items() == other.items()
+ # Ref: https://docs.python.org/3/library/constants.html#NotImplemented
+ return NotImplemented
def equals_order_sensitive(self, other: object) -> bool:
"""Order-sensitive equality check.
*See also* :ref:`eq-order-insensitive`
+ (https://bidict.rtfd.io/other-bidict-types.html#eq-is-order-insensitive)
"""
- # Same short-circuit as in __eq__ above. Factoring out not worth function call overhead.
- if not isinstance(other, _t.Mapping) or len(self) != len(other):
+ if not isinstance(other, t.Mapping) or len(self) != len(other):
return False
- return all(i == j for (i, j) in zip(self.items(), other.items()))
-
- # 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
- # `on_dup`), so it makes sense for them to share implementation too.)
- def _pop(self, key: KT) -> VT:
- val = self._fwdm.pop(key)
- del self._invm[val]
- return val
-
- def _put(self, key: KT, val: VT, on_dup: OnDup) -> None:
- dedup_result = self._dedup_item(key, val, on_dup)
- if dedup_result is not None:
- self._write_item(key, val, dedup_result)
-
- def _dedup_item(self, key: KT, val: VT, on_dup: OnDup) -> _t.Optional[_DedupResult]:
+ return all(starmap(eq, zip(self.items(), other.items())))
+
+ def _dedup(self, key: KT, val: VT, on_dup: OnDup) -> DedupResult[KT, VT]:
"""Check *key* and *val* for any duplication in self.
Handle any duplication as per the passed in *on_dup*.
- (key, val) already present is construed as a no-op, not a duplication.
+ If (key, val) is already present, return None
+ since writing (key, val) would be a no-op.
If duplication is found and the corresponding :class:`~bidict.OnDupAction` is
:attr:`~bidict.DROP_NEW`, return None.
If duplication is found and the corresponding :class:`~bidict.OnDupAction` is
- :attr:`~bidict.RAISE`, raise the appropriate error.
+ :attr:`~bidict.RAISE`, raise the appropriate exception.
If duplication is found and the corresponding :class:`~bidict.OnDupAction` is
- :attr:`~bidict.DROP_OLD`,
- or if no duplication is found,
- return the :class:`_DedupResult` *(isdupkey, isdupval, oldkey, oldval)*.
+ :attr:`~bidict.DROP_OLD`, or if no duplication is found,
+ return *(oldkey, oldval)*.
"""
- fwdm = self._fwdm
- invm = self._invm
- oldval: OVT = fwdm.get(key, _NONE)
- oldkey: OKT = invm.get(val, _NONE)
- isdupkey = oldval is not _NONE
- isdupval = oldkey is not _NONE
- dedup_result = _DedupResult(isdupkey, isdupval, oldkey, oldval)
+ fwdm, invm = self._fwdm, self._invm
+ oldval: OVT[VT] = fwdm.get(key, MISSING)
+ oldkey: OKT[KT] = invm.get(val, MISSING)
+ isdupkey, isdupval = oldval is not MISSING, oldkey is not MISSING
if isdupkey and isdupval:
- if self._already_have(key, val, oldkey, oldval):
+ if key == oldkey:
+ assert val == oldval
# (key, val) duplicates an existing item -> no-op.
return None
# key and val each duplicate a different existing item.
@@ -274,131 +353,207 @@ class BidictBase(BidirectionalMapping[KT, VT]):
assert on_dup.val is DROP_OLD
# Fall through to the return statement on the last line.
# else neither isdupkey nor isdupval.
- return dedup_result
+ return oldkey, oldval
- @staticmethod
- def _already_have(key: KT, val: VT, oldkey: OKT, oldval: OVT) -> bool:
- # Overridden by _orderedbase.OrderedBidictBase.
- isdup = oldkey == key
- assert isdup == (oldval == val), f'{key} {val} {oldkey} {oldval}'
- return isdup
-
- def _write_item(self, key: KT, val: VT, dedup_result: _DedupResult) -> _WriteResult:
- # Overridden by _orderedbase.OrderedBidictBase.
- 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: bool, on_dup: OnDup, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
- # args[0] may be a generator that yields many items, so process input in a single pass.
- if not args and not kw:
+ def _prep_write(self, newkey: KT, newval: VT, oldkey: OKT[KT], oldval: OVT[VT], save_unwrite: bool) -> PreparedWrite:
+ """Given (newkey, newval) to insert, return the list of operations necessary to perform the write.
+
+ *oldkey* and *oldval* are as returned by :meth:`_dedup`.
+
+ If *save_unwrite* is true, also return the list of inverse operations necessary to undo the write.
+ This design allows :meth:`_update` to roll back a partially applied update that fails part-way through
+ when necessary. This design also allows subclasses that require additional operations to complete
+ a write to easily extend this implementation. For example, :class:`bidict.OrderedBidictBase` calls this
+ inherited implementation, and then extends the list of ops returned with additional operations
+ needed to keep its internal linked list nodes consistent with its items' order as changes are made.
+ """
+ fwdm, invm = self._fwdm, self._invm
+ write: t.List[t.Callable[[], None]] = [
+ partial(fwdm.__setitem__, newkey, newval),
+ partial(invm.__setitem__, newval, newkey),
+ ]
+ unwrite: t.List[t.Callable[[], None]]
+ if oldval is MISSING and oldkey is MISSING: # no key or value duplication
+ # {0: 1, 2: 3} + (4, 5) => {0: 1, 2: 3, 4: 5}
+ unwrite = [
+ partial(fwdm.__delitem__, newkey),
+ partial(invm.__delitem__, newval),
+ ] if save_unwrite else []
+ elif oldval is not MISSING and oldkey is not MISSING: # key and value duplication across two different items
+ # {0: 1, 2: 3} + (0, 3) => {0: 3}
+ write.extend((
+ partial(fwdm.__delitem__, oldkey),
+ partial(invm.__delitem__, oldval),
+ ))
+ unwrite = [
+ partial(fwdm.__setitem__, newkey, oldval),
+ partial(invm.__setitem__, oldval, newkey),
+ partial(fwdm.__setitem__, oldkey, newval),
+ partial(invm.__setitem__, newval, oldkey),
+ ] if save_unwrite else []
+ elif oldval is not MISSING: # just key duplication
+ # {0: 1, 2: 3} + (2, 4) => {0: 1, 2: 4}
+ write.append(partial(invm.__delitem__, oldval))
+ unwrite = [
+ partial(fwdm.__setitem__, newkey, oldval),
+ partial(invm.__setitem__, oldval, newkey),
+ partial(invm.__delitem__, newval),
+ ] if save_unwrite else []
+ else:
+ assert oldkey is not MISSING # just value duplication
+ # {0: 1, 2: 3} + (4, 3) => {0: 1, 4: 3}
+ write.append(partial(fwdm.__delitem__, oldkey))
+ unwrite = [
+ partial(fwdm.__setitem__, oldkey, newval),
+ partial(invm.__setitem__, newval, oldkey),
+ partial(fwdm.__delitem__, newkey),
+ ] if save_unwrite else []
+ return write, unwrite
+
+ def _update(
+ self,
+ arg: MapOrIterItems[KT, VT],
+ kw: t.Mapping[str, VT] = MappingProxyType({}),
+ *,
+ rbof: t.Optional[bool] = None,
+ on_dup: t.Optional[OnDup] = None,
+ ) -> None:
+ """Update, possibly rolling back on failure as per *rbof*."""
+ # Must process input in a single pass, since arg may be a generator.
+ if not arg 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]) # type: ignore [arg-type]
+ if on_dup is None:
+ on_dup = self.on_dup
+ if rbof is None:
+ rbof = RAISE in on_dup
+ if not self and not kw:
+ if isinstance(arg, BidictBase): # can skip dup check
+ self._init_from(arg)
+ return
+ # If arg is not a BidictBase, fall through to the general treatment below,
+ # which includes duplication checking. (If arg is some BidirectionalMapping
+ # that does not inherit from BidictBase, it's a foreign implementation, so we
+ # perform duplication checking to err on the safe side.)
+
+ # If we roll back on failure and we know that there are more updates to process than
+ # already-contained items, our rollback strategy is to update a copy of self (without
+ # rolling back on failure), and then to become the copy if all updates succeed.
+ if rbof and isinstance(arg, t.Sized) and len(arg) + len(kw) > len(self):
+ target = self.copy()
+ target._update(arg, kw, rbof=False, on_dup=on_dup)
+ self._init_from(target)
return
- 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: BidirectionalMapping[KT, VT]) -> None:
- write_item = self._write_item
- for (key, val) in other.items():
- write_item(key, val, _NODUP)
-
- def _update_no_rollback(self, on_dup: OnDup, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
- put = self._put
- for (key, val) in _iteritems_args_kw(*args, **kw):
- put(key, val, on_dup)
-
- def _update_with_rollback(self, on_dup: OnDup, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
- """Update, rolling back on failure."""
- writes: _t.List[_t.Tuple[_DedupResult, _WriteResult]] = []
- append_write = writes.append
- dedup_item = self._dedup_item
- write_item = self._write_item
- for (key, val) in _iteritems_args_kw(*args, **kw):
+
+ # There are more already-contained items than updates to process, or we don't know
+ # how many updates there are to process. If we need to roll back on failure,
+ # save a log of Unwrites as we update so we can undo changes if the update fails.
+ unwrites: t.List[Unwrite] = []
+ append_unwrite = unwrites.append
+ prep_write = self._prep_write
+ for (key, val) in iteritems(arg, **kw):
try:
- dedup_result = dedup_item(key, val, on_dup)
+ dedup_result = self._dedup(key, val, on_dup)
except DuplicationError:
- undo_write = self._undo_write
- for dedup_result, write_result in reversed(writes):
- undo_write(dedup_result, write_result)
+ if rbof:
+ while unwrites: # apply saved unwrites
+ unwrite = unwrites.pop()
+ for unwriteop in unwrite:
+ unwriteop()
raise
- if dedup_result is not None:
- write_result = write_item(key, val, dedup_result)
- append_write((dedup_result, write_result))
-
- def _undo_write(self, dedup_result: _DedupResult, write_result: _WriteResult) -> None:
- 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]
+ if dedup_result is None: # no-op
+ continue
+ write, unwrite = prep_write(key, val, *dedup_result, save_unwrite=rbof)
+ for writeop in write: # apply the write
+ writeop()
+ if rbof and unwrite: # save the unwrite for later application if needed
+ append_unwrite(unwrite)
def copy(self: BT) -> BT:
- """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.
- cp: BT = self.__class__.__new__(self.__class__)
- cp._fwdm = copy(self._fwdm)
- cp._invm = copy(self._invm)
- cp._init_inv()
- return cp
+ """Make a (shallow) copy of this bidict."""
+ # Could just `return self.__class__(self)` here, but the below is faster. The former
+ # would copy this bidict's items into a new instance one at a time (checking for duplication
+ # for each item), whereas the below copies from the backing mappings all at once, and foregoes
+ # item-by-item duplication checking since the backing mappings have been checked already.
+ return self._from_other(self.__class__, self)
+
+ @staticmethod
+ def _from_other(bt: t.Type[BT], other: MapOrIterItems[KT, VT], inv: bool = False) -> BT:
+ """Fast, private constructor based on :meth:`_init_from`.
+
+ If *inv* is true, return the inverse of the instance instead of the instance itself.
+ (Useful for pickling with dynamically-generated inverse classes -- see :meth:`__reduce__`.)
+ """
+ inst = bt()
+ inst._init_from(other)
+ return t.cast(BT, inst.inverse) if inv else inst
+
+ def _init_from(self, other: MapOrIterItems[KT, VT]) -> None:
+ """Fast init from *other*, bypassing item-by-item duplication checking."""
+ self._fwdm.clear()
+ self._invm.clear()
+ self._fwdm.update(other)
+ # If other is a bidict, use its existing backing inverse mapping, otherwise
+ # other could be a generator that's now exhausted, so invert self._fwdm on the fly.
+ inv = other.inverse if isinstance(other, BidictBase) else inverted(self._fwdm)
+ self._invm.update(inv) # pyright: ignore # https://github.com/jab/bidict/pull/242#discussion_r824223403
#: Used for the copy protocol.
#: *See also* the :mod:`copy` module
__copy__ = copy
+ def __or__(self: BT, other: t.Mapping[KT, VT]) -> BT:
+ """Return self|other."""
+ if not isinstance(other, t.Mapping):
+ return NotImplemented
+ new = self.copy()
+ new._update(other, rbof=False)
+ return new
+
+ def __ror__(self: BT, other: t.Mapping[KT, VT]) -> BT:
+ """Return other|self."""
+ if not isinstance(other, t.Mapping):
+ return NotImplemented
+ new = self.__class__(other)
+ new._update(self, rbof=False)
+ return new
+
def __len__(self) -> int:
"""The number of contained items."""
return len(self._fwdm)
- def __iter__(self) -> _t.Iterator[KT]:
+ def __iter__(self) -> t.Iterator[KT]:
"""Iterator over the contained keys."""
return iter(self._fwdm)
def __getitem__(self, key: KT) -> VT:
- """*x.__getitem__(key) ⟺ x[key]*"""
+ """*x.__getitem__(key) ⟺ x[key]*"""
return self._fwdm[key]
- # On Python 3.8+, dicts are reversible, so even non-Ordered bidicts can provide an efficient
- # __reversed__ implementation. (On Python < 3.8, they cannot.) Once support is dropped for
- # Python < 3.8, can remove the following if statement to provide __reversed__ unconditionally.
- if hasattr(_fwdm_cls, '__reversed__'):
- def __reversed__(self) -> _t.Iterator[KT]:
- """Iterator over the contained keys in reverse order."""
- return reversed(self._fwdm) # type: ignore [no-any-return,call-overload]
+ def __reduce__(self) -> t.Tuple[t.Any, ...]:
+ """Return state information for pickling."""
+ # If this bidict's class is dynamically generated, pickle the inverse instead, whose
+ # (presumably not dynamically generated) class the caller is more likely to have a reference to
+ # somewhere in sys.modules that pickle can discover.
+ should_invert = isinstance(self, GeneratedBidictInverse)
+ cls, init_from = (self._inv_cls, self.inverse) if should_invert else (self.__class__, self)
+ return self._from_other, (cls, dict(init_from), should_invert) # type: ignore [call-overload]
+
+
+# See BidictBase._set_reversed() above.
+def _fwdm_reversed(self: BidictBase[KT, t.Any]) -> t.Iterator[KT]:
+ """Iterator over the contained keys in reverse order."""
+ assert isinstance(self._fwdm, t.Reversible)
+ return reversed(self._fwdm)
+
+
+BidictBase._init_class()
+
+class GeneratedBidictInverse:
+ """Base class for dynamically-generated inverse bidict classes."""
-# Work around weakref slot with Generics bug on Python 3.6 (https://bugs.python.org/issue41451):
-BidictBase.__slots__.remove('__weakref__')
# * Code review nav *
#==============================================================================
-# ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py →
+# ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py →
#==============================================================================
diff --git a/libs/bidict/_bidict.py b/libs/bidict/_bidict.py
index 3a98a61df..722e63a01 100644
--- a/libs/bidict/_bidict.py
+++ b/libs/bidict/_bidict.py
@@ -1,51 +1,197 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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 *
+# (see comments in __init__.py)
#==============================================================================
-# ← Prev: _mut.py Current: _bidict.py Next: _orderedbase.py →
+# ← Prev: _frozenbidict.py Current: _bidict.py Next: _orderedbase.py →
#==============================================================================
-"""Provide :class:`bidict`."""
+"""Provide :class:`MutableBidict`."""
-import typing as _t
+import typing as t
-from ._delegating import _DelegatingBidict
-from ._mut import MutableBidict
-from ._typing import KT, VT
+from ._abc import MutableBidirectionalMapping
+from ._base import BidictBase, get_arg
+from ._dup import OnDup, ON_DUP_RAISE, ON_DUP_DROP_OLD
+from ._typing import KT, VT, DT, ODT, MISSING, IterItems, MapOrIterItems
-class bidict(_DelegatingBidict[KT, VT], MutableBidict[KT, VT]):
+class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]):
"""Base class for mutable bidirectional mappings."""
- __slots__ = ()
+ if t.TYPE_CHECKING:
+ @property
+ def inverse(self) -> 'MutableBidict[VT, KT]': ...
+
+ def _pop(self, key: KT) -> VT:
+ val = self._fwdm.pop(key)
+ del self._invm[val]
+ return val
+
+ def __delitem__(self, key: KT) -> None:
+ """*x.__delitem__(y) ⟺ del x[y]*"""
+ self._pop(key)
+
+ def __setitem__(self, key: KT, val: VT) -> None:
+ """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 behavior 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.
+ """
+ self.put(key, val, on_dup=self.on_dup)
+
+ def put(self, key: KT, val: VT, on_dup: OnDup = ON_DUP_RAISE) -> None:
+ """Associate *key* with *val*, honoring the :class:`OnDup` given in *on_dup*.
+
+ For example, if *on_dup* is :attr:`~bidict.ON_DUP_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`.
+ """
+ self._update([(key, val)], on_dup=on_dup)
+
+ def forceput(self, key: KT, val: VT) -> None:
+ """Associate *key* with *val* unconditionally.
+
+ Replace any existing mappings containing key *key* or value *val*
+ as necessary to preserve uniqueness.
+ """
+ self.put(key, val, on_dup=ON_DUP_DROP_OLD)
+
+ def clear(self) -> None:
+ """Remove all items."""
+ self._fwdm.clear()
+ self._invm.clear()
+
+ @t.overload
+ def pop(self, __key: KT) -> VT: ...
+ @t.overload
+ def pop(self, __key: KT, __default: DT = ...) -> t.Union[VT, DT]: ...
+
+ def pop(self, key: KT, default: ODT[DT] = MISSING) -> t.Union[VT, DT]:
+ """*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 MISSING:
+ raise
+ return default
+
+ def popitem(self) -> t.Tuple[KT, VT]:
+ """*x.popitem() → (k, v)*
+
+ Remove and return some item as a (key, value) pair.
+
+ :raises KeyError: if *x* is empty.
+ """
+ key, val = self._fwdm.popitem()
+ del self._invm[val]
+ return key, val
+
+ @t.overload # type: ignore [override] # https://github.com/jab/bidict/pull/242#discussion_r825464731
+ def update(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ...
+ @t.overload
+ def update(self, __i: IterItems[KT, VT], **kw: VT) -> None: ...
+ @t.overload
+ def update(self, **kw: VT) -> None: ...
+
+ def update(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
+ """Like calling :meth:`putall` with *self.on_dup* passed for *on_dup*."""
+ if args or kw:
+ self._update(get_arg(*args), kw)
+
+ @t.overload
+ def forceupdate(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ...
+ @t.overload
+ def forceupdate(self, __i: IterItems[KT, VT], **kw: VT) -> None: ...
+ @t.overload
+ def forceupdate(self, **kw: VT) -> None: ...
+
+ def forceupdate(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
+ """Like a bulk :meth:`forceput`."""
+ if args or kw:
+ self._update(get_arg(*args), kw, on_dup=ON_DUP_DROP_OLD)
+
+ def __ior__(self, other: t.Mapping[KT, VT]) -> 'MutableBidict[KT, VT]':
+ """Return self|=other."""
+ self.update(other)
+ return self
+
+ @t.overload
+ def putall(self, items: t.Mapping[KT, VT], on_dup: OnDup) -> None: ...
+ @t.overload
+ def putall(self, items: IterItems[KT, VT], on_dup: OnDup = ...) -> None: ...
+
+ def putall(self, items: MapOrIterItems[KT, VT], on_dup: OnDup = ON_DUP_RAISE) -> 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:
+ self._update(items, on_dup=on_dup)
+
+
+class bidict(MutableBidict[KT, VT]):
+ """The main bidirectional mapping type.
+
+ See :ref:`intro:Introduction` and :ref:`basic-usage:Basic Usage`
+ to get started (also available at https://bidict.rtfd.io).
+ """
- if _t.TYPE_CHECKING:
+ if t.TYPE_CHECKING:
@property
def inverse(self) -> 'bidict[VT, KT]': ...
# * Code review nav *
#==============================================================================
-# ← Prev: _mut.py Current: _bidict.py Next: _orderedbase.py →
+# ← Prev: _frozenbidict.py Current: _bidict.py Next: _orderedbase.py →
#==============================================================================
diff --git a/libs/bidict/_delegating.py b/libs/bidict/_delegating.py
deleted file mode 100644
index fa935dd2e..000000000
--- a/libs/bidict/_delegating.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 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/.
-
-
-"""Provide :class:`_DelegatingBidict`."""
-
-import typing as _t
-
-from ._base import BidictBase
-from ._typing import KT, VT
-
-
-class _DelegatingBidict(BidictBase[KT, VT]):
- """Provide optimized implementations of several methods by delegating to backing dicts.
-
- Used to override less efficient implementations inherited by :class:`~collections.abc.Mapping`.
- """
-
- __slots__ = ()
-
- def __iter__(self) -> _t.Iterator[KT]:
- """Iterator over the contained keys."""
- return iter(self._fwdm)
-
- def keys(self) -> _t.KeysView[KT]:
- """A set-like object providing a view on the contained keys."""
- return self._fwdm.keys() # type: ignore [return-value]
-
- def values(self) -> _t.KeysView[VT]: # type: ignore [override] # https://github.com/python/typeshed/issues/4435
- """A set-like object providing a view on the contained values."""
- return self._invm.keys() # type: ignore [return-value]
-
- def items(self) -> _t.ItemsView[KT, VT]:
- """A set-like object providing a view on the contained items."""
- return self._fwdm.items() # type: ignore [return-value]
diff --git a/libs/bidict/_dup.py b/libs/bidict/_dup.py
index 3693424a7..2ce937ad2 100644
--- a/libs/bidict/_dup.py
+++ b/libs/bidict/_dup.py
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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
@@ -9,11 +8,11 @@
"""Provide :class:`OnDup` and related functionality."""
-from collections import namedtuple
from enum import Enum
+import typing as t
-class OnDupAction(Enum):
+class OD(Enum):
"""An action to take to prevent duplication from occurring."""
#: Raise a :class:`~bidict.DuplicationError`.
@@ -24,25 +23,26 @@ class OnDupAction(Enum):
DROP_NEW = 'DROP_NEW'
def __repr__(self) -> str:
- return f'<{self.name}>'
+ return f'{self.__class__.__name__}.{self.name}'
-RAISE = OnDupAction.RAISE
-DROP_OLD = OnDupAction.DROP_OLD
-DROP_NEW = OnDupAction.DROP_NEW
+RAISE = OD.RAISE
+DROP_OLD = OD.DROP_OLD
+DROP_NEW = OD.DROP_NEW
-class OnDup(namedtuple('_OnDup', 'key val kv')):
- r"""A 3-tuple of :class:`OnDupAction`\s specifying how to handle the 3 kinds of duplication.
+class OnDup(t.NamedTuple('_OnDup', [('key', OD), ('val', OD), ('kv', OD)])):
+ r"""A 3-tuple of :class:`OD`\s specifying how to handle the 3 kinds of duplication.
*See also* :ref:`basic-usage:Values Must Be Unique`
+ (https://bidict.rtfd.io/basic-usage.html#values-must-be-unique)
If *kv* is not specified, *val* will be used for *kv*.
"""
__slots__ = ()
- def __new__(cls, key: OnDupAction = DROP_OLD, val: OnDupAction = RAISE, kv: OnDupAction = RAISE) -> 'OnDup':
+ def __new__(cls, key: OD = DROP_OLD, val: OD = RAISE, kv: t.Optional[OD] = None) -> 'OnDup':
"""Override to provide user-friendly default values."""
return super().__new__(cls, key, val, kv or val)
@@ -51,7 +51,7 @@ class OnDup(namedtuple('_OnDup', 'key val kv')):
#: :meth:`~bidict.bidict.__init__`,
#: :meth:`~bidict.bidict.__setitem__`, and
#: :meth:`~bidict.bidict.update` methods.
-ON_DUP_DEFAULT = OnDup()
+ON_DUP_DEFAULT = OnDup(key=DROP_OLD, val=RAISE, kv=RAISE)
#: An :class:`OnDup` whose members are all :obj:`RAISE`.
ON_DUP_RAISE = OnDup(key=RAISE, val=RAISE, kv=RAISE)
#: An :class:`OnDup` whose members are all :obj:`DROP_OLD`.
diff --git a/libs/bidict/_exc.py b/libs/bidict/_exc.py
index d6f2af5ea..25fae7c37 100644
--- a/libs/bidict/_exc.py
+++ b/libs/bidict/_exc.py
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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
diff --git a/libs/bidict/_frozenbidict.py b/libs/bidict/_frozenbidict.py
index 2b652029d..863eff7c7 100644
--- a/libs/bidict/_frozenbidict.py
+++ b/libs/bidict/_frozenbidict.py
@@ -1,60 +1,45 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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 *
+# (see comments in __init__.py)
#==============================================================================
-# ← Prev: _base.py Current: _frozenbidict.py Next: _mut.py →
+# ← Prev: _base.py Current: _frozenbidict.py Next: _bidict.py →
#==============================================================================
"""Provide :class:`frozenbidict`, an immutable, hashable bidirectional mapping type."""
-import typing as _t
+import typing as t
-from ._delegating import _DelegatingBidict
+from ._base import BidictBase
from ._typing import KT, VT
-class frozenbidict(_DelegatingBidict[KT, VT]):
+class frozenbidict(BidictBase[KT, VT]):
"""Immutable, hashable bidict type."""
- __slots__ = ('_hash',)
-
_hash: int
- # Work around lack of support for higher-kinded types in mypy.
+ # Work around lack of support for higher-kinded types in Python.
# Ref: https://github.com/python/typing/issues/548#issuecomment-621571821
- # Remove this and similar type stubs from other classes if support is ever added.
- if _t.TYPE_CHECKING:
+ if t.TYPE_CHECKING:
@property
def inverse(self) -> 'frozenbidict[VT, KT]': ...
def __hash__(self) -> int:
"""The hash of this bidict as determined by its items."""
if getattr(self, '_hash', None) is None:
- self._hash = _t.ItemsView(self)._hash() # type: ignore [attr-defined]
+ # The following is like hash(frozenset(self.items()))
+ # but more memory efficient. See also: https://bugs.python.org/issue46684
+ self._hash = t.ItemsView(self)._hash() # type: ignore [attr-defined] # https://github.com/python/typeshed/pull/7153
return self._hash
# * Code review nav *
#==============================================================================
-# ← Prev: _base.py Current: _frozenbidict.py Next: _mut.py →
+# ← Prev: _base.py Current: _frozenbidict.py Next: _bidict.py →
#==============================================================================
diff --git a/libs/bidict/_frozenordered.py b/libs/bidict/_frozenordered.py
index c9f47a766..de1b74e5f 100644
--- a/libs/bidict/_frozenordered.py
+++ b/libs/bidict/_frozenordered.py
@@ -1,33 +1,19 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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 *
+# (see comments in __init__.py)
#==============================================================================
#← Prev: _orderedbase.py Current: _frozenordered.py Next: _orderedbidict.py →
#==============================================================================
"""Provide :class:`FrozenOrderedBidict`, an immutable, hashable, ordered bidict."""
-import typing as _t
+import typing as t
from ._frozenbidict import frozenbidict
from ._orderedbase import OrderedBidictBase
@@ -46,41 +32,16 @@ class FrozenOrderedBidict(OrderedBidictBase[KT, VT]):
If you are using Python 3.8+, frozenbidict gives you everything that
FrozenOrderedBidict gives you, but with less space overhead.
+ On the other hand, using FrozenOrderedBidict when you are depending on
+ the ordering of the items can make the ordering dependence more explicit.
"""
- __slots__ = ('_hash',)
- __hash__ = frozenbidict.__hash__
+ __hash__: t.Callable[[t.Any], int] = frozenbidict.__hash__ # pyright: ignore
- if _t.TYPE_CHECKING:
+ if t.TYPE_CHECKING:
@property
def inverse(self) -> 'FrozenOrderedBidict[VT, KT]': ...
- # Delegate to backing dicts for more efficient implementations of keys() and values().
- # Possible with FrozenOrderedBidict but not OrderedBidict since FrozenOrderedBidict
- # is immutable, i.e. these can't get out of sync after initialization due to mutation.
- def keys(self) -> _t.KeysView[KT]:
- """A set-like object providing a view on the contained keys."""
- return self._fwdm._fwdm.keys() # type: ignore [return-value]
-
- def values(self) -> _t.KeysView[VT]: # type: ignore [override]
- """A set-like object providing a view on the contained values."""
- return self._invm._fwdm.keys() # type: ignore [return-value]
-
- # Can't delegate for items() because values in _fwdm and _invm are nodes.
-
- # On Python 3.8+, delegate to backing dicts for a more efficient implementation
- # of __iter__ and __reversed__ (both of which call this _iter() method):
- if hasattr(dict, '__reversed__'):
- def _iter(self, *, reverse: bool = False) -> _t.Iterator[KT]:
- itfn = reversed if reverse else iter
- return itfn(self._fwdm._fwdm) # type: ignore [operator,no-any-return]
- else:
- # On Python < 3.8, just optimize __iter__:
- def _iter(self, *, reverse: bool = False) -> _t.Iterator[KT]:
- if not reverse:
- return iter(self._fwdm._fwdm)
- return super()._iter(reverse=True)
-
# * Code review nav *
#==============================================================================
diff --git a/libs/bidict/_iter.py b/libs/bidict/_iter.py
index 6a5996d9b..ee10063d0 100644
--- a/libs/bidict/_iter.py
+++ b/libs/bidict/_iter.py
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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
@@ -8,48 +7,26 @@
"""Functions for iterating over items in a mapping."""
-import typing as _t
-from collections.abc import Mapping
-from itertools import chain
+from operator import itemgetter
+import typing as t
from ._typing import KT, VT, IterItems, MapOrIterItems
-_NULL_IT: IterItems = iter(())
+def iteritems_mapping_or_iterable(arg: MapOrIterItems[KT, VT]) -> IterItems[KT, VT]:
+ """Yield the items in *arg* based on whether it's a mapping."""
+ yield from arg.items() if isinstance(arg, t.Mapping) else arg # pyright: ignore
-def _iteritems_mapping_or_iterable(arg: MapOrIterItems[KT, VT]) -> IterItems[KT, VT]:
- """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 iter(arg.items() if isinstance(arg, Mapping) else arg)
+def iteritems(__arg: MapOrIterItems[KT, VT], **kw: VT) -> IterItems[KT, VT]:
+ """Yield the items from *arg* and then any from *kw* in the order given."""
+ yield from iteritems_mapping_or_iterable(__arg)
+ yield from kw.items() # type: ignore [misc]
-def _iteritems_args_kw(*args: MapOrIterItems[KT, VT], **kw: VT) -> IterItems[KT, VT]:
- """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(f'Expected at most 1 positional argument, got {args_len}')
- it: IterItems = ()
- if args:
- arg = args[0]
- if arg:
- it = _iteritems_mapping_or_iterable(arg)
- if kw:
- iterkw = iter(kw.items())
- it = chain(it, iterkw) if it else iterkw
- return it or _NULL_IT
+swap = itemgetter(1, 0)
-@_t.overload
-def inverted(arg: _t.Mapping[KT, VT]) -> IterItems[VT, KT]: ...
-@_t.overload
-def inverted(arg: IterItems[KT, VT]) -> IterItems[VT, KT]: ...
def inverted(arg: MapOrIterItems[KT, VT]) -> IterItems[VT, KT]:
"""Yield the inverse items of the provided object.
@@ -61,7 +38,8 @@ def inverted(arg: MapOrIterItems[KT, VT]) -> IterItems[VT, KT]:
*See also* :attr:`bidict.BidirectionalMapping.__inverted__`
"""
- inv = getattr(arg, '__inverted__', None)
- if callable(inv):
- return inv() # type: ignore [no-any-return]
- return ((val, key) for (key, val) in _iteritems_mapping_or_iterable(arg))
+ invattr = getattr(arg, '__inverted__', None)
+ if callable(invattr):
+ inv: IterItems[VT, KT] = invattr()
+ return inv
+ return map(swap, iteritems_mapping_or_iterable(arg))
diff --git a/libs/bidict/_mut.py b/libs/bidict/_mut.py
deleted file mode 100644
index 1787e6eaa..000000000
--- a/libs/bidict/_mut.py
+++ /dev/null
@@ -1,188 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 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 →
-#==============================================================================
-
-
-"""Provide :class:`MutableBidict`."""
-
-import typing as _t
-
-from ._abc import MutableBidirectionalMapping
-from ._base import BidictBase
-from ._dup import OnDup, ON_DUP_RAISE, ON_DUP_DROP_OLD
-from ._typing import _NONE, KT, VT, VDT, IterItems, MapOrIterItems
-
-
-class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]):
- """Base class for mutable bidirectional mappings."""
-
- __slots__ = ()
-
- if _t.TYPE_CHECKING:
- @property
- def inverse(self) -> 'MutableBidict[VT, KT]': ...
-
- def __delitem__(self, key: KT) -> None:
- """*x.__delitem__(y) ⟺ del x[y]*"""
- self._pop(key)
-
- def __setitem__(self, key: KT, val: VT) -> None:
- """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 behavior 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.
- """
- self._put(key, val, self.on_dup)
-
- def put(self, key: KT, val: VT, on_dup: OnDup = ON_DUP_RAISE) -> None:
- """Associate *key* with *val*, honoring the :class:`OnDup` given in *on_dup*.
-
- For example, if *on_dup* is :attr:`~bidict.ON_DUP_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`.
- """
- self._put(key, val, on_dup)
-
- def forceput(self, key: KT, val: VT) -> None:
- """Associate *key* with *val* unconditionally.
-
- Replace any existing mappings containing key *key* or value *val*
- as necessary to preserve uniqueness.
- """
- self._put(key, val, ON_DUP_DROP_OLD)
-
- def clear(self) -> None:
- """Remove all items."""
- self._fwdm.clear()
- self._invm.clear()
-
- @_t.overload
- def pop(self, key: KT) -> VT: ...
- @_t.overload
- def pop(self, key: KT, default: VDT = ...) -> VDT: ...
- def pop(self, key: KT, default: VDT = _NONE) -> VDT:
- """*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 _NONE:
- raise
- return default
-
- def popitem(self) -> _t.Tuple[KT, VT]:
- """*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
-
- @_t.overload
- def update(self, __arg: _t.Mapping[KT, VT], **kw: VT) -> None: ...
- @_t.overload
- def update(self, __arg: IterItems[KT, VT], **kw: VT) -> None: ...
- @_t.overload
- def update(self, **kw: VT) -> None: ...
- def update(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
- """Like calling :meth:`putall` with *self.on_dup* passed for *on_dup*."""
- if args or kw:
- self._update(False, self.on_dup, *args, **kw)
-
- @_t.overload
- def forceupdate(self, __arg: _t.Mapping[KT, VT], **kw: VT) -> None: ...
- @_t.overload
- def forceupdate(self, __arg: IterItems[KT, VT], **kw: VT) -> None: ...
- @_t.overload
- def forceupdate(self, **kw: VT) -> None: ...
- def forceupdate(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
- """Like a bulk :meth:`forceput`."""
- self._update(False, ON_DUP_DROP_OLD, *args, **kw)
-
- @_t.overload
- def putall(self, items: _t.Mapping[KT, VT], on_dup: OnDup) -> None: ...
- @_t.overload
- def putall(self, items: IterItems[KT, VT], on_dup: OnDup = ON_DUP_RAISE) -> None: ...
- def putall(self, items: MapOrIterItems[KT, VT], on_dup: OnDup = ON_DUP_RAISE) -> 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:
- 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
index b761060b7..538addf68 100644
--- a/libs/bidict/_named.py
+++ b/libs/bidict/_named.py
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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
@@ -7,11 +6,19 @@
"""Provide :func:`bidict.namedbidict`."""
-import typing as _t
+import typing as t
from sys import _getframe
-from ._abc import BidirectionalMapping, KT, VT
+from ._base import BidictBase
from ._bidict import bidict
+from ._typing import KT, VT
+
+
+# pyright: reportPrivateUsage=false, reportUnnecessaryIsInstance=false
+
+
+class NamedBidictBase:
+ """Base class that namedbidicts derive from."""
def namedbidict(
@@ -19,8 +26,8 @@ def namedbidict(
keyname: str,
valname: str,
*,
- base_type: _t.Type[BidirectionalMapping[KT, VT]] = bidict,
-) -> _t.Type[BidirectionalMapping[KT, VT]]:
+ base_type: t.Type[BidictBase[KT, VT]] = bidict,
+) -> t.Type[BidictBase[KT, VT]]:
r"""Create a new subclass of *base_type* with custom accessors.
Like :func:`collections.namedtuple` for bidicts.
@@ -36,64 +43,57 @@ def namedbidict(
*See also* the :ref:`namedbidict usage documentation
<other-bidict-types:\:func\:\`~bidict.namedbidict\`>`
+ (https://bidict.rtfd.io/other-bidict-types.html#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 :class:`BidirectionalMapping` subclass
- that provides ``_isinv`` and :meth:`~object.__getstate__` attributes.
- (Any :class:`~bidict.BidictBase` subclass can be passed in, including all the
- concrete bidict types pictured in the :ref:`other-bidict-types:Bidict Types Diagram`.
+ :raises TypeError: if *base_type* is not a :class:`bidict.BidictBase` subclass.
+ Any of the concrete bidict types pictured in the
+ :ref:`other-bidict-types:Bidict Types Diagram` may be provided
+ (https://bidict.rtfd.io/other-bidict-types.html#bidict-types-diagram).
"""
- if not issubclass(base_type, BidirectionalMapping) or not all(hasattr(base_type, i) for i in ('_isinv', '__getstate__')):
- raise TypeError(base_type)
+ if not issubclass(base_type, BidictBase):
+ raise TypeError(f'{base_type} is not a BidictBase subclass')
names = (typename, keyname, valname)
if not all(map(str.isidentifier, names)) or keyname == valname:
raise ValueError(names)
- class _Named(base_type): # type: ignore [valid-type,misc]
-
- __slots__ = ()
-
- def _getfwd(self) -> '_Named':
- return self.inverse if self._isinv else self # type: ignore [no-any-return]
-
- def _getinv(self) -> '_Named':
- return self if self._isinv else self.inverse # type: ignore [no-any-return]
-
- @property
- def _keyname(self) -> str:
- return valname if self._isinv else keyname
-
- @property
- def _valname(self) -> str:
- return keyname if self._isinv else valname
-
- def __reduce__(self) -> '_t.Tuple[_t.Callable[[str, str, str, _t.Type[BidirectionalMapping]], BidirectionalMapping], _t.Tuple[str, str, str, _t.Type[BidirectionalMapping]], dict]':
- return (_make_empty, (typename, keyname, valname, base_type), self.__getstate__())
-
- bname = base_type.__name__
- fname = valname + '_for'
- iname = keyname + '_for'
- fdoc = f'{typename} forward {bname}: {keyname} → {valname}'
- idoc = f'{typename} inverse {bname}: {valname} → {keyname}'
- setattr(_Named, fname, property(_Named._getfwd, doc=fdoc))
- setattr(_Named, iname, property(_Named._getinv, doc=idoc))
-
- _Named.__name__ = typename
- _Named.__qualname__ = typename
- _Named.__module__ = _getframe(1).f_globals.get('__name__') # type: ignore [assignment]
- return _Named
-
-
-def _make_empty(
- typename: str,
- keyname: str,
- valname: str,
- base_type: _t.Type[BidirectionalMapping] = bidict,
-) -> BidirectionalMapping:
- """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()
+ basename = base_type.__name__
+ get_keyname = property(lambda self: keyname, doc='The keyname of this namedbidict.')
+ get_valname = property(lambda self: valname, doc='The valname of this namedbidict.')
+ val_by_key_name = f'{valname}_for'
+ key_by_val_name = f'{keyname}_for'
+ val_by_key_doc = f'{typename} forward {basename}: {keyname} -> {valname}'
+ key_by_val_doc = f'{typename} inverse {basename}: {valname} -> {keyname}'
+ get_val_by_key = property(lambda self: self, doc=val_by_key_doc)
+ get_key_by_val = property(lambda self: self.inverse, doc=key_by_val_doc)
+
+ class NamedBidict(base_type, NamedBidictBase): # type: ignore [valid-type,misc] # https://github.com/python/mypy/issues/5865
+ """NamedBidict."""
+
+ keyname = get_keyname
+ valname = get_valname
+
+ @classmethod
+ def _inv_cls_dict_diff(cls) -> t.Dict[str, t.Any]:
+ base_diff = super()._inv_cls_dict_diff()
+ return {
+ **base_diff,
+ 'keyname': get_valname,
+ 'valname': get_keyname,
+ val_by_key_name: get_key_by_val,
+ key_by_val_name: get_val_by_key,
+ }
+
+ NamedInv = NamedBidict._inv_cls
+ assert NamedInv is not NamedBidict, 'namedbidict classes are not their own inverses'
+ setattr(NamedBidict, val_by_key_name, get_val_by_key)
+ setattr(NamedBidict, key_by_val_name, get_key_by_val)
+ NamedBidict.__name__ = NamedBidict.__qualname__ = typename
+ NamedInv.__name__ = NamedInv.__qualname__ = f'{typename}Inv'
+ NamedBidict.__doc__ = f'NamedBidict({basename}) {typename!r}: {keyname} -> {valname}'
+ NamedInv.__doc__ = f'NamedBidictInv({basename}) {typename!r}: {valname} -> {keyname}'
+ caller_module = _getframe(1).f_globals.get('__name__', '__main__')
+ NamedBidict.__module__ = NamedInv.__module__ = caller_module
+ return NamedBidict # pyright: ignore [reportUnknownVariableType]
diff --git a/libs/bidict/_orderedbase.py b/libs/bidict/_orderedbase.py
index 8681d3be5..1c273a5df 100644
--- a/libs/bidict/_orderedbase.py
+++ b/libs/bidict/_orderedbase.py
@@ -1,26 +1,12 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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 *
+# (see comments in __init__.py)
#==============================================================================
# ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py →
#==============================================================================
@@ -28,126 +14,109 @@
"""Provide :class:`OrderedBidictBase`."""
-import typing as _t
-from copy import copy
-from weakref import ref
+import typing as t
+from functools import partial
+from weakref import ref as weakref
-from ._abc import MutableBidirectionalMapping
-from ._base import _NONE, _DedupResult, _WriteResult, BidictBase, BT
+from ._base import BidictBase, PreparedWrite
from ._bidict import bidict
-from ._typing import KT, VT, OKT, OVT, IterItems, MapOrIterItems
+from ._iter import iteritems
+from ._typing import KT, VT, OKT, OVT, MISSING, IterItems, MapOrIterItems
-class _Node:
- """A node in a circular doubly-linked list
- used to encode the order of items in an ordered bidict.
+IT = t.TypeVar('IT') # instance type
+AT = t.TypeVar('AT') # attr type
- 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__')
+class WeakAttr(t.Generic[IT, AT]):
+ """Descriptor to automatically manage (de)referencing the given slot as a weakref.
- def __init__(self, prv: '_Node' = None, nxt: '_Node' = None) -> None:
- self._setprv(prv)
- self._setnxt(nxt)
+ See https://docs.python.org/3/howto/descriptor.html#managed-attributes
+ for an intro to using descriptors like this for managed attributes.
+ """
- def __repr__(self) -> str:
- clsname = self.__class__.__name__
- prv = id(self.prv)
- nxt = id(self.nxt)
- return f'{clsname}(prv={prv}, self={id(self)}, nxt={nxt})'
+ def __init__(self, *, slot: str) -> None:
+ self.slot = slot
- def _getprv(self) -> '_t.Optional[_Node]':
- return self._prv() if isinstance(self._prv, ref) else self._prv
+ def __set__(self, instance: IT, value: AT) -> None:
+ setattr(instance, self.slot, weakref(value))
- def _setprv(self, prv: '_t.Optional[_Node]') -> None:
- self._prv = prv and ref(prv)
+ def __get__(self, instance: IT, owner: t.Any) -> AT:
+ return getattr(instance, self.slot)() # type: ignore [no-any-return]
- prv = property(_getprv, _setprv)
- def _getnxt(self) -> '_t.Optional[_Node]':
- return self._nxt() if isinstance(self._nxt, ref) else self._nxt
+class Node:
+ """A node in a circular doubly-linked list
+ used to encode the order of items in an ordered bidict.
- def _setnxt(self, nxt: '_t.Optional[_Node]') -> None:
- self._nxt = nxt and ref(nxt)
+ A weak reference to the previous node is stored
+ to avoid creating strong reference cycles.
+ Referencing/dereferencing the weakref is handled automatically by :class:`WeakAttr`.
+ """
- nxt = property(_getnxt, _setnxt)
+ prv: 'WeakAttr[Node, Node]' = WeakAttr(slot='_prv_weak')
+ __slots__ = ('_prv_weak', 'nxt', '__weakref__')
- def __getstate__(self) -> dict:
- """Return the instance state dictionary
- but with weakrefs converted to strong refs
- so that it can be pickled.
+ def __init__(self, prv: 'Node', nxt: 'Node') -> None:
+ self.prv = prv
+ self.nxt = nxt
- *See also* :meth:`object.__getstate__`
+ def unlink(self) -> None:
+ """Remove self from in between prv and nxt.
+ Self's references to prv and nxt are retained so it can be relinked (see below).
"""
- return dict(_prv=self.prv, _nxt=self.nxt)
+ self.prv.nxt = self.nxt
+ self.nxt.prv = self.prv
- def __setstate__(self, state: dict) -> None:
- """Set the instance state from *state*."""
- self._setprv(state['_prv'])
- self._setnxt(state['_nxt'])
+ def relink(self) -> None:
+ """Restore self between prv and nxt after unlinking (see above)."""
+ self.prv.nxt = self.nxt.prv = self
-class _SentinelNode(_Node):
+class SentinelNode(Node):
"""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: _Node = None, nxt: _Node = None) -> None:
- super().__init__(prv or self, nxt or self)
+ nxt: WeakAttr['SentinelNode', Node] = WeakAttr(slot='_nxt_weak') # type: ignore [assignment]
+ __slots__ = ('_nxt_weak',)
- def __repr__(self) -> str:
- return '<SNTL>'
+ def __init__(self) -> None:
+ super().__init__(self, self)
- def __bool__(self) -> bool:
- return False
-
- def _iter(self, *, reverse: bool = False) -> _t.Iterator[_Node]:
- """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.
- """
+ def iternodes(self, *, reverse: bool = False) -> t.Iterator[Node]:
+ """Iterator yielding nodes in the requested order."""
attr = 'prv' if reverse else 'nxt'
node = getattr(self, attr)
- while node:
+ while node is not self:
yield node
node = getattr(node, attr)
+ def new_last_node(self) -> Node:
+ """Create and return a new terminal node."""
+ old_last = self.prv
+ new_last = Node(old_last, self)
+ old_last.nxt = self.prv = new_last
+ return new_last
+
class OrderedBidictBase(BidictBase[KT, VT]):
"""Base class implementing an ordered :class:`BidirectionalMapping`."""
- __slots__ = ('_sntl',)
+ _repr_delegate: t.ClassVar[t.Any] = list
- _fwdm_cls: _t.Type[MutableBidirectionalMapping[KT, _Node]] = bidict # type: ignore [assignment]
- _invm_cls: _t.Type[MutableBidirectionalMapping[VT, _Node]] = bidict # type: ignore [assignment]
- _fwdm: bidict[KT, _Node] # type: ignore [assignment]
- _invm: bidict[VT, _Node] # type: ignore [assignment]
+ _node_by_korv: bidict[t.Any, Node]
+ _bykey: bool
- #: The object used by :meth:`__repr__` for printing the contained items.
- _repr_delegate = list
-
- @_t.overload
- def __init__(self, __arg: _t.Mapping[KT, VT], **kw: VT) -> None: ...
- @_t.overload
- def __init__(self, __arg: IterItems[KT, VT], **kw: VT) -> None: ...
- @_t.overload
+ @t.overload
+ def __init__(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ...
+ @t.overload
+ def __init__(self, __i: IterItems[KT, VT], **kw: VT) -> None: ...
+ @t.overload
def __init__(self, **kw: VT) -> None: ...
+
def __init__(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
"""Make a new ordered bidirectional mapping.
The signature behaves like that of :class:`dict`.
@@ -157,147 +126,109 @@ class OrderedBidictBase(BidictBase[KT, VT]):
The order in which items are inserted is remembered,
similar to :class:`collections.OrderedDict`.
"""
- self._sntl = _SentinelNode()
-
- # 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.
+ self._sntl = SentinelNode()
+ self._node_by_korv = bidict()
+ self._bykey = True
super().__init__(*args, **kw)
- if _t.TYPE_CHECKING:
+ if t.TYPE_CHECKING:
@property
def inverse(self) -> 'OrderedBidictBase[VT, KT]': ...
- def _init_inv(self) -> None:
- super()._init_inv()
- self.inverse._sntl = self._sntl
-
- # Can't reuse BidictBase.copy since ordered bidicts have different internal structure.
- def copy(self: BT) -> BT:
- """A shallow copy of this ordered bidict."""
- # Fast copy implementation bypassing __init__. See comments in :meth:`BidictBase.copy`.
- cp: BT = self.__class__.__new__(self.__class__)
- sntl = _SentinelNode()
- fwdm = copy(self._fwdm)
- invm = copy(self._invm)
- cur = sntl
- nxt = sntl.nxt
- for (key, val) in self.items():
- nxt = _Node(cur, sntl)
- cur.nxt = fwdm[key] = invm[val] = nxt
- cur = nxt
- sntl.prv = nxt
- cp._sntl = sntl # type: ignore [attr-defined]
- cp._fwdm = fwdm
- cp._invm = invm
- cp._init_inv()
- return cp
-
- __copy__ = copy
-
- def __getitem__(self, key: KT) -> VT:
- nodefwd = self._fwdm[key]
- val = self._invm.inverse[nodefwd]
- return val
-
- def _pop(self, key: KT) -> VT:
- nodefwd = self._fwdm.pop(key)
- val = self._invm.inverse.pop(nodefwd)
- nodefwd.prv.nxt = nodefwd.nxt
- nodefwd.nxt.prv = nodefwd.prv
- return val
-
- @staticmethod
- def _already_have(key: KT, val: VT, nodeinv: _Node, nodefwd: _Node) -> bool: # type: ignore [override]
- # Overrides _base.BidictBase.
- return nodeinv is nodefwd
-
- def _write_item(self, key: KT, val: VT, dedup_result: _DedupResult) -> _WriteResult:
- # Overrides _base.BidictBase.
- 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: OKT = _NONE
- oldval: OVT = _NONE
- 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 = _NONE
- oldnodeinv = invm.pop(oldval)
- assert oldnodeinv is nodefwd
- invm[val] = nodefwd
- else: # isdupval
- oldkey = fwdm.inverse[nodeinv]
- oldval = _NONE
- oldnodefwd = fwdm.pop(oldkey)
- assert oldnodefwd is nodeinv
- fwdm[key] = nodeinv
- return _WriteResult(key, val, oldkey, oldval)
-
- def _undo_write(self, dedup_result: _DedupResult, write_result: _WriteResult) -> None:
- 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) -> _t.Iterator[KT]:
+ def _make_inverse(self) -> 'OrderedBidictBase[VT, KT]':
+ inv = t.cast(OrderedBidictBase[VT, KT], super()._make_inverse())
+ inv._sntl = self._sntl
+ inv._node_by_korv = self._node_by_korv
+ inv._bykey = not self._bykey
+ return inv
+
+ def _assoc_node(self, node: Node, key: KT, val: VT) -> None:
+ korv = key if self._bykey else val
+ self._node_by_korv.forceput(korv, node)
+
+ def _dissoc_node(self, node: Node) -> None:
+ del self._node_by_korv.inverse[node]
+ node.unlink()
+
+ def _init_from(self, other: MapOrIterItems[KT, VT]) -> None:
+ """See :meth:`BidictBase._init_from`."""
+ super()._init_from(other)
+ bykey = self._bykey
+ korv_by_node = self._node_by_korv.inverse
+ korv_by_node.clear()
+ korv_by_node_set = korv_by_node.__setitem__
+ self._sntl.nxt = self._sntl.prv = self._sntl
+ new_node = self._sntl.new_last_node
+ for (k, v) in iteritems(other):
+ korv_by_node_set(new_node(), k if bykey else v)
+
+ def _prep_write(self, newkey: KT, newval: VT, oldkey: OKT[KT], oldval: OVT[VT], save_unwrite: bool) -> PreparedWrite:
+ """See :meth:`bidict.BidictBase._prep_write`."""
+ write, unwrite = super()._prep_write(newkey, newval, oldkey, oldval, save_unwrite)
+ assoc, dissoc = self._assoc_node, self._dissoc_node
+ node_by_korv, bykey = self._node_by_korv, self._bykey
+ if oldval is MISSING and oldkey is MISSING: # no key or value duplication
+ # {0: 1, 2: 3} + (4, 5) => {0: 1, 2: 3, 4: 5}
+ newnode = self._sntl.new_last_node()
+ write.append(partial(assoc, newnode, newkey, newval))
+ if save_unwrite:
+ unwrite.append(partial(dissoc, newnode))
+ elif oldval is not MISSING and oldkey is not MISSING: # key and value duplication across two different items
+ # {0: 1, 2: 3} + (0, 3) => {0: 3}
+ # n1, n2 => n1 (collapse n1 and n2 into n1)
+ # oldkey: 2, oldval: 1, oldnode: n2, newkey: 0, newval: 3, newnode: n1
+ if bykey:
+ oldnode = node_by_korv[oldkey]
+ newnode = node_by_korv[newkey]
+ else:
+ oldnode = node_by_korv[newval]
+ newnode = node_by_korv[oldval]
+ write.extend((
+ partial(dissoc, oldnode),
+ partial(assoc, newnode, newkey, newval),
+ ))
+ if save_unwrite:
+ unwrite.extend((
+ partial(assoc, newnode, newkey, oldval),
+ partial(assoc, oldnode, oldkey, newval),
+ partial(oldnode.relink,),
+ ))
+ elif oldval is not MISSING: # just key duplication
+ # {0: 1, 2: 3} + (2, 4) => {0: 1, 2: 4}
+ # oldkey: MISSING, oldval: 3, newkey: 2, newval: 4
+ node = node_by_korv[newkey if bykey else oldval]
+ write.append(partial(assoc, node, newkey, newval))
+ if save_unwrite:
+ unwrite.append(partial(assoc, node, newkey, oldval))
+ else:
+ assert oldkey is not MISSING # just value duplication
+ # {0: 1, 2: 3} + (4, 3) => {0: 1, 4: 3}
+ # oldkey: 2, oldval: MISSING, newkey: 4, newval: 3
+ node = node_by_korv[oldkey if bykey else newval]
+ write.append(partial(assoc, node, newkey, newval))
+ if save_unwrite:
+ unwrite.append(partial(assoc, node, oldkey, newval))
+ return write, unwrite
+
+ def __iter__(self) -> t.Iterator[KT]:
"""Iterator over the contained keys in insertion order."""
- return self._iter()
-
- def _iter(self, *, reverse: bool = False) -> _t.Iterator[KT]:
- fwdm_inv = self._fwdm.inverse
- for node in self._sntl._iter(reverse=reverse):
- yield fwdm_inv[node]
+ return self._iter(reverse=False)
- def __reversed__(self) -> _t.Iterator[KT]:
+ def __reversed__(self: 'OrderedBidictBase[KT, VT]') -> t.Iterator[KT]:
"""Iterator over the contained keys in reverse insertion order."""
- yield from self._iter(reverse=True)
+ return self._iter(reverse=True)
+
+ def _iter(self, *, reverse: bool = False) -> t.Iterator[KT]:
+ nodes = self._sntl.iternodes(reverse=reverse)
+ korv_by_node = self._node_by_korv.inverse
+ if self._bykey:
+ for node in nodes:
+ yield korv_by_node[node]
+ else:
+ key_by_val = self._invm
+ for node in nodes:
+ val = korv_by_node[node]
+ yield key_by_val[val]
# * Code review nav *
diff --git a/libs/bidict/_orderedbidict.py b/libs/bidict/_orderedbidict.py
index f7aed3b65..58e728576 100644
--- a/libs/bidict/_orderedbidict.py
+++ b/libs/bidict/_orderedbidict.py
@@ -1,79 +1,74 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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 *
+# (see comments in __init__.py)
#==============================================================================
-# ← Prev: _frozenordered.py Current: _orderedbidict.py <FIN>
+# ← Prev: _frozenordered.py Current: _orderedbidict.py <FIN>
#==============================================================================
"""Provide :class:`OrderedBidict`."""
-import typing as _t
+from collections.abc import Set
+import typing as t
-from ._mut import MutableBidict
+from ._base import BidictKeysView
+from ._bidict import MutableBidict
from ._orderedbase import OrderedBidictBase
from ._typing import KT, VT
+# pyright: reportPrivateUsage=false
+
+
class OrderedBidict(OrderedBidictBase[KT, VT], MutableBidict[KT, VT]):
"""Mutable bidict type that maintains items in insertion order."""
- __slots__ = ()
-
- if _t.TYPE_CHECKING:
+ if t.TYPE_CHECKING:
@property
def inverse(self) -> 'OrderedBidict[VT, KT]': ...
def clear(self) -> None:
"""Remove all items."""
- self._fwdm.clear()
- self._invm.clear()
+ super().clear()
+ self._node_by_korv.clear()
self._sntl.nxt = self._sntl.prv = self._sntl
- def popitem(self, last: bool = True) -> _t.Tuple[KT, VT]:
- """*x.popitem() → (k, v)*
+ def _pop(self, key: KT) -> VT:
+ val = super()._pop(key)
+ node = self._node_by_korv[key if self._bykey else val]
+ self._dissoc_node(node)
+ return val
- Remove and return the most recently added item as a (key, value) pair
- if *last* is True, else the least recently added item.
+ def popitem(self, last: bool = True) -> t.Tuple[KT, VT]:
+ """*b.popitem() → (k, v)*
- :raises KeyError: if *x* is empty.
+ If *last* is true,
+ remove and return the most recently added item as a (key, value) pair.
+ Otherwise, remove and return the least recently added item.
+
+ :raises KeyError: if *b* is empty.
"""
if not self:
- raise KeyError('mapping is empty')
- itfn: _t.Callable = reversed if last else iter # type: ignore [assignment]
- it = itfn(self)
- key = next(it)
- val = self._pop(key)
- return key, val
+ raise KeyError('OrderedBidict is empty')
+ node = getattr(self._sntl, 'prv' if last else 'nxt')
+ korv = self._node_by_korv.inverse[node]
+ if self._bykey:
+ return korv, self._pop(korv)
+ return self.inverse._pop(korv), korv # pyright: ignore [reportGeneralTypeIssues]
def move_to_end(self, key: KT, last: bool = True) -> None:
- """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.
+ """Move the item with the given key to the end if *last* is true, else to the beginning.
- :raises KeyError: if the key does not exist
+ :raises KeyError: if *key* is missing
"""
- node = self._fwdm[key]
+ korv = key if self._bykey else self._fwdm[key]
+ node = self._node_by_korv[korv]
node.prv.nxt = node.nxt
node.nxt.prv = node.prv
sntl = self._sntl
@@ -88,8 +83,79 @@ class OrderedBidict(OrderedBidictBase[KT, VT], MutableBidict[KT, VT]):
node.nxt = firstnode
sntl.nxt = firstnode.prv = node
+ # Override the keys() and items() implementations inherited from BidictBase,
+ # which may delegate to the backing _fwdm dict, since this is a mutable ordered bidict,
+ # and therefore the ordering of items can get out of sync with the backing mappings
+ # after mutation. (Need not override values() because it delegates to .inverse.keys().)
+ def keys(self) -> t.KeysView[KT]:
+ """A set-like object providing a view on the contained keys."""
+ return _OrderedBidictKeysView(self)
+
+ def items(self) -> t.ItemsView[KT, VT]:
+ """A set-like object providing a view on the contained items."""
+ return _OrderedBidictItemsView(self)
+
+
+# The following MappingView implementations use the __iter__ implementations
+# inherited from their superclass counterparts in collections.abc, so they
+# continue to yield items in the correct order even after an OrderedBidict
+# is mutated. They also provide a __reversed__ implementation, which is not
+# provided by the collections.abc superclasses.
+class _OrderedBidictKeysView(BidictKeysView[KT]):
+ _mapping: OrderedBidict[KT, t.Any]
+
+ def __reversed__(self) -> t.Iterator[KT]:
+ return reversed(self._mapping)
+
+
+class _OrderedBidictItemsView(t.ItemsView[KT, VT]):
+ _mapping: OrderedBidict[KT, VT]
+
+ def __reversed__(self) -> t.Iterator[t.Tuple[KT, VT]]:
+ ob = self._mapping
+ for key in reversed(ob):
+ yield key, ob[key]
+
+
+# For better performance, make _OrderedBidictKeysView and _OrderedBidictItemsView delegate
+# to backing dicts for the methods they inherit from collections.abc.Set. (Cannot delegate
+# for __iter__ and __reversed__ since they are order-sensitive.) See also: https://bugs.python.org/issue46713
+def _override_set_methods_to_use_backing_dict(
+ cls: t.Union[t.Type[_OrderedBidictKeysView[KT]], t.Type[_OrderedBidictItemsView[KT, t.Any]]],
+ viewname: str,
+ _setmethodnames: t.Iterable[str] = (
+ '__lt__', '__le__', '__gt__', '__ge__', '__eq__', '__ne__', '__sub__', '__rsub__',
+ '__or__', '__ror__', '__xor__', '__rxor__', '__and__', '__rand__', 'isdisjoint',
+ )
+) -> None:
+ def make_proxy_method(methodname: str) -> t.Any:
+ def method(self: t.Union[_OrderedBidictKeysView[KT], _OrderedBidictItemsView[KT, t.Any]], *args: t.Any) -> t.Any:
+ fwdm = self._mapping._fwdm
+ if not isinstance(fwdm, dict): # dict view speedup not available, fall back to Set's implementation.
+ return getattr(Set, methodname)(self, *args)
+ fwdm_dict_view = getattr(fwdm, viewname)()
+ fwdm_dict_view_method = getattr(fwdm_dict_view, methodname)
+ if len(args) != 1 or not isinstance(args[0], self.__class__) or not isinstance(args[0]._mapping._fwdm, dict):
+ return fwdm_dict_view_method(*args)
+ # self and arg are both _OrderedBidictKeysViews or _OrderedBidictItemsViews whose bidicts are backed by a dict.
+ # Use arg's backing dict's corresponding view instead of arg. Otherwise, e.g. `ob1.keys() < ob2.keys()` would give
+ # "TypeError: '<' not supported between instances of '_OrderedBidictKeysView' and '_OrderedBidictKeysView'", because
+ # both `dict_keys(ob1).__lt__(ob2.keys()) is NotImplemented` and `dict_keys(ob2).__gt__(ob1.keys()) is NotImplemented`.
+ arg_dict_view = getattr(args[0]._mapping._fwdm, viewname)()
+ return fwdm_dict_view_method(arg_dict_view)
+ method.__name__ = methodname
+ method.__qualname__ = f'{cls.__qualname__}.{methodname}'
+ return method
+
+ for name in _setmethodnames:
+ setattr(cls, name, make_proxy_method(name))
+
+
+_override_set_methods_to_use_backing_dict(_OrderedBidictKeysView, 'keys')
+_override_set_methods_to_use_backing_dict(_OrderedBidictItemsView, 'items')
+
# * Code review nav *
#==============================================================================
-# ← Prev: _frozenordered.py Current: _orderedbidict.py <FIN>
+# ← Prev: _frozenordered.py Current: _orderedbidict.py <FIN>
#==============================================================================
diff --git a/libs/bidict/_typing.py b/libs/bidict/_typing.py
index cb10baae3..8e3ec6010 100644
--- a/libs/bidict/_typing.py
+++ b/libs/bidict/_typing.py
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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
@@ -8,26 +7,28 @@
"""Provide typing-related objects."""
-import typing as _t
+import typing as t
+from enum import Enum
-KT = _t.TypeVar('KT')
-VT = _t.TypeVar('VT')
-IterItems = _t.Iterable[_t.Tuple[KT, VT]]
-MapOrIterItems = _t.Union[_t.Mapping[KT, VT], IterItems[KT, VT]]
+KT = t.TypeVar('KT')
+VT = t.TypeVar('VT')
+IterItems = t.Iterable[t.Tuple[KT, VT]]
+MapOrIterItems = t.Union[t.Mapping[KT, VT], IterItems[KT, VT]]
-DT = _t.TypeVar('DT') #: for default arguments
-VDT = _t.Union[VT, DT]
+class MissingT(Enum):
+ """Sentinel used to represent none/missing when None itself can't be used."""
-class _BareReprMeta(type):
- def __repr__(cls) -> str:
- return f'<{cls.__name__}>'
+ MISSING = 'MISSING'
+ def __repr__(self) -> str:
+ return '<MISSING>'
-class _NONE(metaclass=_BareReprMeta):
- """Sentinel type used to represent 'missing'."""
+MISSING = MissingT.MISSING
+OKT = t.Union[KT, MissingT] #: optional key type
+OVT = t.Union[VT, MissingT] #: optional value type
-OKT = _t.Union[KT, _NONE] #: optional key type
-OVT = _t.Union[VT, _NONE] #: optional value type
+DT = t.TypeVar('DT') #: for default arguments
+ODT = t.Union[DT, MissingT]
diff --git a/libs/bidict/metadata.py b/libs/bidict/metadata.py
index 733bffe93..c84e4209a 100644
--- a/libs/bidict/metadata.py
+++ b/libs/bidict/metadata.py
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-# Copyright 2009-2021 Joshua Bronson. All Rights Reserved.
+# Copyright 2009-2022 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
@@ -8,22 +7,23 @@
"""Define bidict package metadata."""
-__version__ = '0.21.4'
+__version__ = '0.22.0'
__author__ = 'Joshua Bronson'
__maintainer__ = 'Joshua Bronson'
-__copyright__ = 'Copyright 2009-2021 Joshua Bronson'
+__copyright__ = 'Copyright 2009-2022 Joshua Bronson'
__email__ = '[email protected]'
-
-# See: ../docs/thanks.rst
-__credits__ = [i.strip() for i in """
-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(',')]
-
__description__ = 'The bidirectional mapping library for Python.'
__keywords__ = 'dict dictionary mapping datastructure bimap bijection bijective ' \
'injective inverse reverse bidirectional two-way 2-way'
-
__license__ = 'MPL 2.0'
__status__ = 'Beta'
__url__ = 'https://bidict.readthedocs.io'
+__project_urls__ = {
+ 'Donate': 'https://github.com/sponsors/jab',
+ 'Documentation': 'https://bidict.readthedocs.io',
+ 'Enterprise Support': 'https://bidict.readthedocs.io/#enterprise-support',
+ 'Changelog': 'https://bidict.readthedocs.io/changelog.html',
+ 'Source Code': 'https://github.com/jab/bidict',
+ 'Issue Tracker': 'https://github.com/jab/bidict/issues',
+ 'Chat': 'https://gitter.im/jab/bidict',
+}
diff --git a/libs/bidict/py.typed b/libs/bidict/py.typed
index e69de29bb..342ea76ea 100644
--- a/libs/bidict/py.typed
+++ b/libs/bidict/py.typed
@@ -0,0 +1 @@
+PEP-561 marker.