first commit

This commit is contained in:
root
2023-06-22 15:30:22 +02:00
commit 4d9979354c
3294 changed files with 445677 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#==============================================================================
# * Welcome to the bidict source code *
#==============================================================================
# Doing a code review? You'll find a "Code review nav" comment like the one
# below at the top and bottom of the most important source files. This provides
# a suggested initial path through the source when reviewing.
#
# Note: If you aren't reading this on https://github.com/jab/bidict, you may be
# viewing an outdated version of the code. Please head to GitHub to review the
# latest version, which contains important improvements over older versions.
#
# Thank you for reading and for any feedback you provide.
# * Code review nav *
#==============================================================================
# Current: __init__.py Next: _abc.py →
#==============================================================================
"""The bidirectional mapping library for Python.
bidict by example:
.. code-block:: python
>>> from bidict import bidict
>>> element_by_symbol = bidict({'H': 'hydrogen'})
>>> element_by_symbol['H']
'hydrogen'
>>> element_by_symbol.inverse['hydrogen']
'H'
Please see https://github.com/jab/bidict for the most up-to-date code and
https://bidict.readthedocs.io for the most up-to-date documentation
if you are reading this elsewhere.
.. :copyright: (c) 2009-2020 Joshua Bronson.
.. :license: MPLv2. See LICENSE for details.
"""
# Use private aliases to not re-export these publicly (for Sphinx automodule with imported-members).
from sys import version_info as _version_info
if _version_info < (3, 6): # pragma: no cover
raise ImportError('Python 3.6+ is required.')
# The rest of this file only collects functionality implemented in the rest of the
# source for the purposes of exporting it under the `bidict` module namespace.
# flake8: noqa: F401 (imported but unused)
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
from .metadata import (
__author__, __maintainer__, __copyright__, __email__, __credits__, __url__,
__license__, __status__, __description__, __keywords__, __version__, __version_info__,
)
# 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
if not getattr(_obj, '__module__', '').startswith('bidict.'):
continue
try:
_obj.__module__ = 'bidict'
except AttributeError as exc: # raised when __module__ is read-only (as in OnDup)
pass
# * Code review nav *
#==============================================================================
# Current: __init__.py Next: _abc.py →
#==============================================================================

View File

@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#==============================================================================
# * Welcome to the bidict source code *
#==============================================================================
# Doing a code review? You'll find a "Code review nav" comment like the one
# below at the top and bottom of the most important source files. This provides
# a suggested initial path through the source when reviewing.
#
# Note: If you aren't reading this on https://github.com/jab/bidict, you may be
# viewing an outdated version of the code. Please head to GitHub to review the
# latest version, which contains important improvements over older versions.
#
# Thank you for reading and for any feedback you provide.
# * Code review nav *
#==============================================================================
# ← Prev: __init__.py Current: _abc.py Next: _base.py →
#==============================================================================
"""Provide the :class:`BidirectionalMapping` abstract base class."""
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.
Extends :class:`collections.abc.Mapping` primarily by adding the
(abstract) :attr:`inverse` property,
which implementors of :class:`BidirectionalMapping`
should override to return a reference to the inverse
:class:`BidirectionalMapping` instance.
"""
__slots__ = ()
@property
@abstractmethod
def inverse(self) -> 'BidirectionalMapping[VT, KT]':
"""The inverse of this bidirectional mapping instance.
*See also* :attr:`bidict.BidictBase.inverse`, :attr:`bidict.BidictBase.inv`
:raises NotImplementedError: Meant to be overridden in subclasses.
"""
# The @abstractproperty decorator prevents BidirectionalMapping subclasses from being
# instantiated unless they override this method. So users shouldn't be able to get to the
# point where they can unintentionally call this implementation of .inverse on something
# anyway. Could leave the method body empty, but raise NotImplementedError so it's extra
# clear there's no reason to call this implementation (e.g. via super() after overriding).
raise NotImplementedError
def __inverted__(self) -> _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
forward mapping and inverting each one on the fly, but this provides a
more efficient implementation: Assuming the already-inverted items
are stored in :attr:`inverse`, just return an iterator over them directly.
Providing this default implementation enables external functions,
particularly :func:`~bidict.inverted`, to use this optimized
implementation when available, instead of having to invert on the fly.
*See also* :func:`bidict.inverted`
"""
return iter(self.inverse.items())
def values(self) -> _t.AbstractSet[VT]: # type: ignore # 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()
class MutableBidirectionalMapping(BidirectionalMapping[KT, VT], _t.MutableMapping[KT, VT]):
"""Abstract base class (ABC) for mutable bidirectional mapping types."""
__slots__ = ()
# * Code review nav *
#==============================================================================
# ← Prev: __init__.py Current: _abc.py Next: _base.py →
#==============================================================================

View File

@@ -0,0 +1,383 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#==============================================================================
# * Welcome to the bidict source code *
#==============================================================================
# Doing a code review? You'll find a "Code review nav" comment like the one
# below at the top and bottom of the most important source files. This provides
# a suggested initial path through the source when reviewing.
#
# Note: If you aren't reading this on https://github.com/jab/bidict, you may be
# viewing an outdated version of the code. Please head to GitHub to review the
# latest version, which contains important improvements over older versions.
#
# Thank you for reading and for any feedback you provide.
# * Code review nav *
#==============================================================================
# ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py →
#==============================================================================
"""Provide :class:`BidictBase`."""
import typing as _t
from collections import namedtuple
from copy import copy
from weakref import ref
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
_WriteResult = namedtuple('_WriteResult', 'key val oldkey oldval')
_DedupResult = namedtuple('_DedupResult', 'isdupkey isdupval invbyval fwdbykey')
_NODUP = _DedupResult(False, False, _NONE, _NONE)
BT = _t.TypeVar('BT', bound='BidictBase') # typevar for BidictBase.copy
class BidictBase(BidirectionalMapping[KT, VT]):
"""Base class implementing :class:`BidirectionalMapping`."""
__slots__ = ['_fwdm', '_invm', '_inv', '_invweak', '_hash', '__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`
on_dup = ON_DUP_DEFAULT
_fwdm_cls = dict #: class of the backing forward mapping
_invm_cls = dict #: class of the backing inverse mapping
#: The object used by :meth:`__repr__` for printing the contained items.
_repr_delegate = dict
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,
'_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
def __init__(self, **kw: VT) -> None: ...
def __init__(self, *args: MapOrIterItems[KT, VT], **kw: VT) -> None:
"""Make a new bidirectional dictionary.
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.Dict[KT, VT] = self._fwdm_cls()
#: The backing :class:`~collections.abc.Mapping`
#: storing the inverse mapping data (*value* → *key*).
self._invm: _t.Dict[VT, KT] = self._invm_cls()
self._init_inv()
if args or kw:
self._update(True, self.on_dup, *args, **kw)
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) # type: ignore
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
@property
def _isinv(self) -> bool:
return self._inv is None
@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 # type: ignore
# 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
#: Alias for :attr:`inverse`.
inv = inverse
def __getstate__(self) -> dict:
"""Needed to enable pickling due to use of :attr:`__slots__` and weakrefs.
*See also* :meth:`object.__getstate__`
"""
state = {}
for cls in self.__class__.__mro__:
slots = getattr(cls, '__slots__', ())
for slot in slots:
if hasattr(self, slot):
state[slot] = getattr(self, slot)
# weakrefs can't be pickled.
state.pop('_invweak', None) # Added back in __setstate__ via _init_inv call.
state.pop('__weakref__', None) # Not added back in __setstate__. Python manages this one.
return state
def __setstate__(self, state: dict) -> None:
"""Implemented because use of :attr:`__slots__` would prevent unpickling otherwise.
*See also* :meth:`object.__setstate__`
"""
for slot, value in state.items():
setattr(self, slot, value)
self._init_inv()
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())})'
# The inherited Mapping.__eq__ implementation would work, but it's implemented in terms of an
# inefficient ``dict(self.items()) == dict(other.items())`` comparison, so override it with a
# more efficient implementation.
def __eq__(self, other: object) -> bool:
"""*x.__eq__(other)  x == other*
Equivalent to *dict(x.items()) == dict(other.items())*
but more efficient.
Note that :meth:`bidict's __eq__() <bidict.bidict.__eq__>` implementation
is inherited by subclasses,
in particular by the ordered bidict subclasses,
so even with ordered bidicts,
:ref:`== comparison is order-insensitive <eq-order-insensitive>`.
*See also* :meth:`bidict.FrozenOrderedBidict.equals_order_sensitive`
"""
if not isinstance(other, _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
# 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]:
"""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 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.
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)*.
"""
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)
if isdupkey and isdupval:
if self._already_have(key, val, oldkey, oldval):
# (key, val) duplicates an existing item -> no-op.
return None
# key and val each duplicate a different existing item.
if on_dup.kv is RAISE:
raise KeyAndValueDuplicationError(key, val)
if on_dup.kv is DROP_NEW:
return None
assert on_dup.kv is DROP_OLD
# Fall through to the return statement on the last line.
elif isdupkey:
if on_dup.key is RAISE:
raise KeyDuplicationError(key)
if on_dup.key is DROP_NEW:
return None
assert on_dup.key is DROP_OLD
# Fall through to the return statement on the last line.
elif isdupval:
if on_dup.val is RAISE:
raise ValueDuplicationError(val)
if on_dup.val is DROP_NEW:
return None
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
@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:
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
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):
try:
dedup_result = dedup_item(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)
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]
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 = self.__class__.__new__(self.__class__)
cp._fwdm = copy(self._fwdm)
cp._invm = copy(self._invm)
cp._init_inv()
return cp # type: ignore
#: Used for the copy protocol.
#: *See also* the :mod:`copy` module
__copy__ = copy
def __len__(self) -> int:
"""The number of contained items."""
return len(self._fwdm)
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]*"""
return self._fwdm[key]
# 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 →
#==============================================================================

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#==============================================================================
# * Welcome to the bidict source code *
#==============================================================================
# Doing a code review? You'll find a "Code review nav" comment like the one
# below at the top and bottom of the most important source files. This provides
# a suggested initial path through the source when reviewing.
#
# Note: If you aren't reading this on https://github.com/jab/bidict, you may be
# viewing an outdated version of the code. Please head to GitHub to review the
# latest version, which contains important improvements over older versions.
#
# Thank you for reading and for any feedback you provide.
# * Code review nav *
#==============================================================================
# ← Prev: _mut.py Current: _bidict.py Next: _orderedbase.py →
#==============================================================================
"""Provide :class:`bidict`."""
import typing as _t
from ._delegating import _DelegatingBidict
from ._mut import MutableBidict
from ._typing import KT, VT
class bidict(_DelegatingBidict[KT, VT], MutableBidict[KT, VT]):
"""Base class for mutable bidirectional mappings."""
__slots__ = ()
if _t.TYPE_CHECKING:
@property
def inverse(self) -> 'bidict[VT, KT]': ...
# * Code review nav *
#==============================================================================
# ← Prev: _mut.py Current: _bidict.py Next: _orderedbase.py →
#==============================================================================

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 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()
def values(self) -> _t.KeysView[VT]: # type: ignore # https://github.com/python/typeshed/issues/4435
"""A set-like object providing a view on the contained values."""
return self._invm.keys()
def items(self) -> _t.ItemsView[KT, VT]:
"""A set-like object providing a view on the contained items."""
return self._fwdm.items()

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 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:`OnDup` and related functionality."""
from collections import namedtuple
from enum import Enum
class OnDupAction(Enum):
"""An action to take to prevent duplication from occurring."""
#: Raise a :class:`~bidict.DuplicationError`.
RAISE = 'RAISE'
#: Overwrite existing items with new items.
DROP_OLD = 'DROP_OLD'
#: Keep existing items and drop new items.
DROP_NEW = 'DROP_NEW'
def __repr__(self) -> str:
return f'<{self.name}>'
RAISE = OnDupAction.RAISE
DROP_OLD = OnDupAction.DROP_OLD
DROP_NEW = OnDupAction.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.
*See also* :ref:`basic-usage: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':
"""Override to provide user-friendly default values."""
return super().__new__(cls, key, val, kv or val)
#: Default :class:`OnDup` used for the
#: :meth:`~bidict.bidict.__init__`,
#: :meth:`~bidict.bidict.__setitem__`, and
#: :meth:`~bidict.bidict.update` methods.
ON_DUP_DEFAULT = OnDup()
#: 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`.
ON_DUP_DROP_OLD = OnDup(key=DROP_OLD, val=DROP_OLD, kv=DROP_OLD)

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 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 all bidict exceptions."""
class BidictException(Exception):
"""Base class for bidict exceptions."""
class DuplicationError(BidictException):
"""Base class for exceptions raised when uniqueness is violated
as per the :attr:~bidict.RAISE` :class:`~bidict.OnDupAction`.
"""
class KeyDuplicationError(DuplicationError):
"""Raised when a given key is not unique."""
class ValueDuplicationError(DuplicationError):
"""Raised when a given value is not unique."""
class KeyAndValueDuplicationError(KeyDuplicationError, ValueDuplicationError):
"""Raised when a given item's key and value are not unique.
That is, its key duplicates that of another item,
and its value duplicates that of a different other item.
"""

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#==============================================================================
# * Welcome to the bidict source code *
#==============================================================================
# Doing a code review? You'll find a "Code review nav" comment like the one
# below at the top and bottom of the most important source files. This provides
# a suggested initial path through the source when reviewing.
#
# Note: If you aren't reading this on https://github.com/jab/bidict, you may be
# viewing an outdated version of the code. Please head to GitHub to review the
# latest version, which contains important improvements over older versions.
#
# Thank you for reading and for any feedback you provide.
# * Code review nav *
#==============================================================================
# ← Prev: _base.py Current: _frozenbidict.py Next: _mut.py →
#==============================================================================
"""Provide :class:`frozenbidict`, an immutable, hashable bidirectional mapping type."""
import typing as _t
from ._delegating import _DelegatingBidict
from ._typing import KT, VT
class frozenbidict(_DelegatingBidict[KT, VT]):
"""Immutable, hashable bidict type."""
__slots__ = ()
# Work around lack of support for higher-kinded types in mypy.
# 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:
@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
return self._hash # type: ignore
# * Code review nav *
#==============================================================================
# ← Prev: _base.py Current: _frozenbidict.py Next: _mut.py →
#==============================================================================

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#==============================================================================
# * Welcome to the bidict source code *
#==============================================================================
# Doing a code review? You'll find a "Code review nav" comment like the one
# below at the top and bottom of the most important source files. This provides
# a suggested initial path through the source when reviewing.
#
# Note: If you aren't reading this on https://github.com/jab/bidict, you may be
# viewing an outdated version of the code. Please head to GitHub to review the
# latest version, which contains important improvements over older versions.
#
# Thank you for reading and for any feedback you provide.
# * Code review nav *
#==============================================================================
#← Prev: _orderedbase.py Current: _frozenordered.py Next: _orderedbidict.py →
#==============================================================================
"""Provide :class:`FrozenOrderedBidict`, an immutable, hashable, ordered bidict."""
import typing as _t
from ._frozenbidict import frozenbidict
from ._orderedbase import OrderedBidictBase
from ._typing import KT, VT
class FrozenOrderedBidict(OrderedBidictBase[KT, VT]):
"""Hashable, immutable, ordered bidict type."""
__slots__ = ()
__hash__ = frozenbidict.__hash__
if _t.TYPE_CHECKING:
@property
def inverse(self) -> 'FrozenOrderedBidict[VT, KT]': ...
# Assume the Python implementation's dict type is ordered (e.g. PyPy or CPython >= 3.6), so we
# can delegate to `_fwdm` and `_invm` for faster implementations of several methods. Both
# `_fwdm` and `_invm` will always be initialized with the provided items in the correct order,
# and since `FrozenOrderedBidict` is immutable, their respective orders can't get out of sync
# after a mutation.
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]:
if reverse:
return super()._iter(reverse=True)
return iter(self._fwdm._fwdm)
def keys(self) -> _t.KeysView[KT]:
"""A set-like object providing a view on the contained keys."""
return self._fwdm._fwdm.keys()
def values(self) -> _t.KeysView[VT]: # type: ignore
"""A set-like object providing a view on the contained values."""
return self._invm._fwdm.keys()
# We can't delegate for items because values in `_fwdm` are nodes.
# * Code review nav *
#==============================================================================
#← Prev: _orderedbase.py Current: _frozenordered.py Next: _orderedbidict.py →
#==============================================================================

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 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/.
"""Functions for iterating over items in a mapping."""
import typing as _t
from collections.abc import Mapping
from itertools import chain, repeat
from ._typing import KT, VT, IterItems, MapOrIterItems
_NULL_IT = repeat(None, 0) # repeat 0 times -> raise StopIteration from the start
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_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}')
itemchain = None
if args:
arg = args[0]
if arg:
itemchain = _iteritems_mapping_or_iterable(arg)
if kw:
iterkw = iter(kw.items())
itemchain = chain(itemchain, iterkw) if itemchain else iterkw # type: ignore
return itemchain or _NULL_IT # type: ignore
@_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.
If *arg* has a :func:`callable` ``__inverted__`` attribute,
return the result of calling it.
Otherwise, return an iterator over the items in `arg`,
inverting each item on the fly.
*See also* :attr:`bidict.BidirectionalMapping.__inverted__`
"""
inv = getattr(arg, '__inverted__', None)
if callable(inv):
return inv() # type: ignore
return ((val, key) for (key, val) in _iteritems_mapping_or_iterable(arg))

View File

@@ -0,0 +1,188 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 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 →
#==============================================================================

View File

@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 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 :func:`bidict.namedbidict`."""
import typing as _t
from sys import _getframe
from ._abc import BidirectionalMapping, KT, VT
from ._bidict import bidict
def namedbidict(
typename: str,
keyname: str,
valname: str,
*,
base_type: _t.Type[BidirectionalMapping[KT, VT]] = bidict,
) -> _t.Type[BidirectionalMapping[KT, VT]]:
r"""Create a new subclass of *base_type* with custom accessors.
Like :func:`collections.namedtuple` for bidicts.
The new class's ``__name__`` and ``__qualname__`` will be set to *typename*,
and its ``__module__`` will be set to the caller's module.
Instances of the new class will provide access to their
:attr:`inverse <BidirectionalMapping.inverse>` instances
via the custom *keyname*\_for property,
and access to themselves
via the custom *valname*\_for property.
*See also* the :ref:`namedbidict usage documentation
<other-bidict-types:\:func\:\`~bidict.namedbidict\`>`
:raises ValueError: if any of the *typename*, *keyname*, or *valname*
strings is not a valid Python identifier, or if *keyname == valname*.
:raises TypeError: if *base_type* is not a :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`.
"""
if not issubclass(base_type, BidirectionalMapping) or not all(hasattr(base_type, i) for i in ('_isinv', '__getstate__')):
raise TypeError(base_type)
names = (typename, keyname, valname)
if not all(map(str.isidentifier, names)) or keyname == valname:
raise ValueError(names)
class _Named(base_type): # type: ignore
__slots__ = ()
def _getfwd(self) -> '_Named':
return self.inverse if self._isinv else self # type: ignore
def _getinv(self) -> '_Named':
return self if self._isinv else self.inverse # type: ignore
@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
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()

View File

@@ -0,0 +1,314 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#==============================================================================
# * Welcome to the bidict source code *
#==============================================================================
# Doing a code review? You'll find a "Code review nav" comment like the one
# below at the top and bottom of the most important source files. This provides
# a suggested initial path through the source when reviewing.
#
# Note: If you aren't reading this on https://github.com/jab/bidict, you may be
# viewing an outdated version of the code. Please head to GitHub to review the
# latest version, which contains important improvements over older versions.
#
# Thank you for reading and for any feedback you provide.
# * Code review nav *
#==============================================================================
# ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py →
#==============================================================================
"""Provide :class:`OrderedBidictBase`."""
import typing as _t
from copy import copy
from weakref import ref
from ._base import _NONE, _DedupResult, _WriteResult, BidictBase, BT
from ._bidict import bidict
from ._typing import KT, VT, IterItems, MapOrIterItems
class _Node:
"""A node in a circular doubly-linked list
used to encode the order of items in an ordered bidict.
Only weak references to the next and previous nodes
are held to avoid creating strong reference cycles.
Because an ordered bidict retains two strong references
to each node instance (one from its backing `_fwdm` mapping
and one from its `_invm` mapping), a node's refcount will not
drop to zero (and so will not be garbage collected) as long as
the ordered bidict that contains it is still alive.
Because nodes don't have strong reference cycles,
once their containing bidict is freed,
they too are immediately freed.
"""
__slots__ = ('_prv', '_nxt', '__weakref__')
def __init__(self, prv: '_Node' = None, nxt: '_Node' = None) -> None:
self._setprv(prv)
self._setnxt(nxt)
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 _getprv(self) -> '_t.Optional[_Node]':
return self._prv() if isinstance(self._prv, ref) else self._prv
def _setprv(self, prv: '_t.Optional[_Node]') -> None:
self._prv = prv and ref(prv)
prv = property(_getprv, _setprv)
def _getnxt(self) -> '_t.Optional[_Node]':
return self._nxt() if isinstance(self._nxt, ref) else self._nxt
def _setnxt(self, nxt: '_t.Optional[_Node]') -> None:
self._nxt = nxt and ref(nxt)
nxt = property(_getnxt, _setnxt)
def __getstate__(self) -> dict:
"""Return the instance state dictionary
but with weakrefs converted to strong refs
so that it can be pickled.
*See also* :meth:`object.__getstate__`
"""
return dict(_prv=self.prv, _nxt=self.nxt)
def __setstate__(self, state: dict) -> None:
"""Set the instance state from *state*."""
self._setprv(state['_prv'])
self._setnxt(state['_nxt'])
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)
def __repr__(self) -> str:
return '<SNTL>'
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.
"""
attr = 'prv' if reverse else 'nxt'
node = getattr(self, attr)
while node:
yield node
node = getattr(node, attr)
class OrderedBidictBase(BidictBase[KT, VT]):
"""Base class implementing an ordered :class:`BidirectionalMapping`."""
__slots__ = ('_sntl',)
_fwdm_cls = bidict # type: ignore
_invm_cls = bidict # type: ignore
#: The object used by :meth:`__repr__` for printing the contained items.
_repr_delegate = list # type: ignore
@_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
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`.
Items passed in are added in the order they are passed,
respecting the :attr:`on_dup` class attribute in the process.
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.
super().__init__(*args, **kw)
if _t.TYPE_CHECKING:
@property
def inverse(self) -> 'OrderedBidictBase[VT, KT]': ...
_fwdm: bidict[KT, _Node] # type: ignore
_invm: bidict[VT, _Node] # type: ignore
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 = 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
cp._fwdm = fwdm
cp._invm = invm
cp._init_inv()
return cp # type: ignore
__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
# 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 = oldval = _NONE
elif isdupkey and isdupval:
# Key and value duplication across two different nodes.
assert nodefwd is not nodeinv
oldval = invm.inverse[nodefwd] # type: ignore
oldkey = fwdm.inverse[nodeinv] # type: ignore
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) # type: ignore
assert tmp is nodeinv
tmp = invm.pop(oldval) # type: ignore
assert tmp is nodefwd
fwdm[key] = invm[val] = nodefwd
elif isdupkey:
oldval = invm.inverse[nodefwd] # type: ignore
oldkey = _NONE
oldnodeinv = invm.pop(oldval) # type: ignore
assert oldnodeinv is nodefwd
invm[val] = nodefwd
else: # isdupval
oldkey = fwdm.inverse[nodeinv] # type: ignore
oldval = _NONE
oldnodefwd = fwdm.pop(oldkey) # type: ignore
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]:
"""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]
def __reversed__(self) -> _t.Iterator[KT]:
"""Iterator over the contained keys in reverse insertion order."""
yield from self._iter(reverse=True)
def equals_order_sensitive(self, other: object) -> bool:
"""Order-sensitive equality check.
*See also* :ref:`eq-order-insensitive`
"""
# Same short-circuit as BidictBase.__eq__. Factoring out not worth function call overhead.
if not isinstance(other, _t.Mapping) or len(self) != len(other):
return False
return all(i == j for (i, j) in zip(self.items(), other.items()))
# * Code review nav *
#==============================================================================
# ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py →
#==============================================================================

View File

@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#==============================================================================
# * Welcome to the bidict source code *
#==============================================================================
# Doing a code review? You'll find a "Code review nav" comment like the one
# below at the top and bottom of the most important source files. This provides
# a suggested initial path through the source when reviewing.
#
# Note: If you aren't reading this on https://github.com/jab/bidict, you may be
# viewing an outdated version of the code. Please head to GitHub to review the
# latest version, which contains important improvements over older versions.
#
# Thank you for reading and for any feedback you provide.
# * Code review nav *
#==============================================================================
# ← Prev: _frozenordered.py Current: _orderedbidict.py <FIN>
#==============================================================================
"""Provide :class:`OrderedBidict`."""
import typing as _t
from ._mut import MutableBidict
from ._orderedbase import OrderedBidictBase
from ._typing import KT, VT
class OrderedBidict(OrderedBidictBase[KT, VT], MutableBidict[KT, VT]):
"""Mutable bidict type that maintains items in insertion order."""
__slots__ = ()
if _t.TYPE_CHECKING:
@property
def inverse(self) -> 'OrderedBidict[VT, KT]': ...
def clear(self) -> None:
"""Remove all items."""
self._fwdm.clear()
self._invm.clear()
self._sntl.nxt = self._sntl.prv = self._sntl
def popitem(self, last: bool = True) -> _t.Tuple[KT, VT]:
"""*x.popitem() → (k, v)*
Remove and return the most recently added item as a (key, value) pair
if *last* is True, else the least recently added item.
:raises KeyError: if *x* is empty.
"""
if not self:
raise KeyError('mapping is empty')
key = next((reversed if last else iter)(self)) # type: ignore
val = self._pop(key)
return key, val
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.
:raises KeyError: if the key does not exist
"""
node = self._fwdm[key]
node.prv.nxt = node.nxt
node.nxt.prv = node.prv
sntl = self._sntl
if last:
lastnode = sntl.prv
node.prv = lastnode
node.nxt = sntl
sntl.prv = lastnode.nxt = node
else:
firstnode = sntl.nxt
node.prv = sntl
node.nxt = firstnode
sntl.nxt = firstnode.prv = node
# * Code review nav *
#==============================================================================
# ← Prev: _frozenordered.py Current: _orderedbidict.py <FIN>
#==============================================================================

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 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 typing-related objects."""
import typing as _t
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 _BareReprMeta(type):
def __repr__(cls) -> str:
return f'<{cls.__name__}>'
class _NONE(metaclass=_BareReprMeta):
"""Sentinel type used to represent 'missing'."""
OKT = _t.Union[KT, _NONE] #: optional key type
OVT = _t.Union[VT, _NONE] #: optional value type

View File

@@ -0,0 +1,4 @@
# coding: utf-8
# file generated by setuptools_scm
# don't change, don't track in version control
version = '0.21.2'

View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Copyright 2009-2020 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""Define bidict package metadata."""
# _version.py is generated by setuptools_scm (via its `write_to` param, see setup.py)
try:
from ._version import version
except (ImportError, ValueError, SystemError): # pragma: no cover
try:
import pkg_resources
except ImportError:
__version__ = '0.0.0.VERSION_NOT_FOUND'
else:
try:
__version__ = pkg_resources.get_distribution('bidict').version
except pkg_resources.DistributionNotFound:
__version__ = '0.0.0.VERSION_NOT_FOUND'
else: # pragma: no cover
__version__ = version
try:
__version_info__ = tuple(int(p) if i < 3 else p for (i, p) in enumerate(__version__.split('.')))
except Exception: # pragma: no cover
__vesion_info__ = (0, 0, 0, f'PARSE FAILURE: __version__={__version__!r}')
__author__ = 'Joshua Bronson'
__maintainer__ = 'Joshua Bronson'
__copyright__ = 'Copyright 2009-2020 Joshua Bronson'
__email__ = 'jabronson@gmail.com'
# 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'