1
0
Fork 0
forked from bton/matekasse

tests versuch 2

This commit is contained in:
2000-Trek 2023-07-28 23:30:45 +02:00
parent fdf385fe06
commit c88f7df83a
2363 changed files with 408191 additions and 0 deletions

View file

@ -0,0 +1,100 @@
# 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 *
#==============================================================================
# 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.
#
# 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 →
#==============================================================================
"""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-2022 Joshua Bronson.
.. :license: MPLv2. See LICENSE for details.
"""
# Use private aliases to not re-export these publicly (for Sphinx automodule with imported-members).
from __future__ import annotations as _annotations
from sys import version_info as _version_info
if _version_info < (3, 7): # pragma: no cover
raise ImportError('Python 3.7+ is required.')
from contextlib import suppress as _suppress
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__ as __author__, __copyright__ as __copyright__, __description__ as __description__,
__license__ as __license__, __url__ as __url__, __version__ as __version__,
)
#: 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
with _suppress(AttributeError):
_obj.__module__ = 'bidict'
# * Code review nav *
#==============================================================================
# Current: __init__.py Next: _abc.py →
#==============================================================================

View file

@ -0,0 +1,77 @@
# 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/.
# * Code review nav *
# (see comments in __init__.py)
#==============================================================================
# ← Prev: __init__.py Current: _abc.py Next: _base.py →
#==============================================================================
"""Provide the :class:`BidirectionalMapping` abstract base class."""
from __future__ import annotations
from abc import abstractmethod
import typing as t
from ._typing import KT, VT
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,
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 @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[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())
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 →
#==============================================================================

View file

@ -0,0 +1,552 @@
# 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/.
# * Code review nav *
# (see comments in __init__.py)
#==============================================================================
# ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py →
#==============================================================================
"""Provide :class:`BidictBase`."""
from __future__ import annotations
from functools import partial
from itertools import starmap
from operator import eq
from types import MappingProxyType
import typing as t
import weakref
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, inverted
from ._typing import KT, VT, MISSING, OKT, OVT, Items, MapOrItems, TypeAlias
OldKV: TypeAlias = 'tuple[OKT[KT], OVT[VT]]'
DedupResult: TypeAlias = 'OldKV[KT, VT] | None'
Write: TypeAlias = 'list[t.Callable[[], None]]'
Unwrite: TypeAlias = Write
PreparedWrite: TypeAlias = 'tuple[Write, Unwrite]'
BT = t.TypeVar('BT', bound='BidictBase[t.Any, t.Any]')
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*.
"""
def get_arg(*args: MapOrItems[KT, VT]) -> MapOrItems[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`."""
#: 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` (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: t.MutableMapping[KT, VT] #: the backing forward mapping (*key* → *val*)
_invm: t.MutableMapping[VT, KT] #: the backing inverse mapping (*val* → *key*)
# 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
#: The class of the inverse bidict instance.
_inv_cls: t.ClassVar[t.Type[BidictBase[t.Any, t.Any]]]
#: 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) -> dict[str, t.Any]:
return {
'_fwdm_cls': cls._invm_cls,
'_invm_cls': cls._fwdm_cls,
}
@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: Items[KT, VT], **kw: VT) -> None: ...
def __init__(self, *args: MapOrItems[KT, VT], **kw: VT) -> None:
"""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.
"""
self._fwdm = self._fwdm_cls()
self._invm = self._invm_cls()
if args or kw:
self._update(get_arg(*args), kw, rbof=False)
# 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: BidictBase[VT, KT] | None = 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: BidictBase[VT, KT] | None = inv
self._invweak: weakref.ReferenceType[BidictBase[VT, KT]] | None = 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
return inv
@property
def inv(self) -> BidictBase[VT, KT]:
"""Alias for :attr:`inverse`."""
return self.inverse
def __repr__(self) -> str:
"""See :func:`repr`."""
clsname = self.__class__.__name__
items = self._repr_delegate(self.items()) if self else ''
return f'{clsname}({items})'
def values(self) -> BidictKeysView[VT]:
"""A set-like object providing a view on the contained values.
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 :meth:`keys` for more information.
"""
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*.
"""
fwdm = self._fwdm
kv = fwdm.keys() if isinstance(fwdm, dict) else BidictKeysView(self)
return kv
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
- 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*
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>`
(https://bidict.rtfd.io/other-bidict-types.html#eq-is-order-insensitive).
*See also* :meth:`equals_order_sensitive`
"""
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)
"""
if not isinstance(other, t.Mapping) or len(self) != len(other):
return False
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*.
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 exception.
If duplication is found and the corresponding :class:`~bidict.OnDupAction` is
:attr:`~bidict.DROP_OLD`, or if no duplication is found,
return *(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 key == oldkey:
assert val == 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 oldkey, oldval
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: list[t.Callable[[], None]] = [
partial(fwdm.__setitem__, newkey, newval),
partial(invm.__setitem__, newval, newkey),
]
unwrite: 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: MapOrItems[KT, VT],
kw: t.Mapping[str, VT] = MappingProxyType({}),
*,
rbof: bool | None = None,
on_dup: OnDup | None = 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
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
# 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: list[Unwrite] = []
append_unwrite = unwrites.append
prep_write = self._prep_write
for (key, val) in iteritems(arg, **kw):
try:
dedup_result = self._dedup(key, val, on_dup)
except DuplicationError:
if rbof:
while unwrites: # apply saved unwrites
unwrite = unwrites.pop()
for unwriteop in unwrite:
unwriteop()
raise
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:
"""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: MapOrItems[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: MapOrItems[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)
#: 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]:
"""Iterator over the contained keys."""
return iter(self._fwdm)
def __getitem__(self, key: KT) -> VT:
"""*x.__getitem__(key) ⟺ x[key]*"""
return self._fwdm[key]
def __reduce__(self) -> 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."""
# * Code review nav *
#==============================================================================
# ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py →
#==============================================================================

View file

@ -0,0 +1,198 @@
# 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/.
# * Code review nav *
# (see comments in __init__.py)
#==============================================================================
# ← Prev: _frozenbidict.py Current: _bidict.py Next: _orderedbase.py →
#==============================================================================
"""Provide :class:`MutableBidict`."""
from __future__ import annotations
import typing as t
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, Items, MapOrItems
class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]):
"""Base class for mutable bidirectional mappings."""
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 = ...) -> VT | DT: ...
def pop(self, key: KT, default: ODT[DT] = MISSING) -> 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) -> 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: Items[KT, VT], **kw: VT) -> None: ...
@t.overload
def update(self, **kw: VT) -> None: ...
def update(self, *args: MapOrItems[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: Items[KT, VT], **kw: VT) -> None: ...
@t.overload
def forceupdate(self, **kw: VT) -> None: ...
def forceupdate(self, *args: MapOrItems[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: Items[KT, VT], on_dup: OnDup = ...) -> None: ...
def putall(self, items: MapOrItems[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:
@property
def inverse(self) -> bidict[VT, KT]: ...
# * Code review nav *
#==============================================================================
# ← Prev: _frozenbidict.py Current: _bidict.py Next: _orderedbase.py →
#==============================================================================

View file

@ -0,0 +1,59 @@
# 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/.
"""Provide :class:`OnDup` and related functionality."""
from __future__ import annotations
from enum import Enum
import typing as t
class OD(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.__class__.__name__}.{self.name}'
RAISE: t.Final[OD] = OD.RAISE
DROP_OLD: t.Final[OD] = OD.DROP_OLD
DROP_NEW: t.Final[OD] = OD.DROP_NEW
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: OD = DROP_OLD, val: OD = RAISE, kv: OD | None = None) -> 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: t.Final[OnDup] = OnDup(key=DROP_OLD, val=RAISE, kv=RAISE)
#: An :class:`OnDup` whose members are all :obj:`RAISE`.
ON_DUP_RAISE: t.Final[OnDup] = OnDup(key=RAISE, val=RAISE, kv=RAISE)
#: An :class:`OnDup` whose members are all :obj:`DROP_OLD`.
ON_DUP_DROP_OLD: t.Final[OnDup] = OnDup(key=DROP_OLD, val=DROP_OLD, kv=DROP_OLD)

View file

@ -0,0 +1,36 @@
# 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/.
"""Provide all bidict exceptions."""
from __future__ import annotations
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,46 @@
# 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/.
# * Code review nav *
# (see comments in __init__.py)
#==============================================================================
# ← Prev: _base.py Current: _frozenbidict.py Next: _bidict.py →
#==============================================================================
"""Provide :class:`frozenbidict`, an immutable, hashable bidirectional mapping type."""
from __future__ import annotations
import typing as t
from ._base import BidictBase
from ._typing import KT, VT
class frozenbidict(BidictBase[KT, VT]):
"""Immutable, hashable bidict type."""
_hash: int
# Work around lack of support for higher-kinded types in Python.
# Ref: https://github.com/python/typing/issues/548#issuecomment-621571821
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:
# 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()
return self._hash
# * Code review nav *
#==============================================================================
# ← Prev: _base.py Current: _frozenbidict.py Next: _bidict.py →
#==============================================================================

View file

@ -0,0 +1,50 @@
# 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/.
# * 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."""
from __future__ import annotations
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.
Like a hashable :class:`bidict.OrderedBidict`
without the mutating APIs, or like a
reversible :class:`bidict.frozenbidict` even on Python < 3.8.
(All bidicts are order-preserving when never mutated, so frozenbidict is
already order-preserving, but only on Python 3.8+, where dicts are
reversible, are all bidicts (including frozenbidict) also reversible.)
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.
"""
__hash__: t.Callable[[t.Any], int] = frozenbidict.__hash__
if t.TYPE_CHECKING:
@property
def inverse(self) -> FrozenOrderedBidict[VT, KT]: ...
# * Code review nav *
#==============================================================================
#← Prev: _orderedbase.py Current: _frozenordered.py Next: _orderedbidict.py →
#==============================================================================

View file

@ -0,0 +1,46 @@
# 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/.
"""Functions for iterating over items in a mapping."""
from __future__ import annotations
from operator import itemgetter
import typing as t
from ._typing import KT, VT, ItemsIter, MapOrItems
def iteritems_mapping_or_iterable(arg: MapOrItems[KT, VT]) -> ItemsIter[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
def iteritems(__arg: MapOrItems[KT, VT], **kw: VT) -> ItemsIter[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]
swap = itemgetter(1, 0)
def inverted(arg: MapOrItems[KT, VT]) -> ItemsIter[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__`
"""
invattr = getattr(arg, '__inverted__', None)
if callable(invattr):
inv: ItemsIter[VT, KT] = invattr()
return inv
return map(swap, iteritems_mapping_or_iterable(arg))

View file

@ -0,0 +1,97 @@
# 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/.
"""Provide :func:`bidict.namedbidict`."""
from __future__ import annotations
from sys import _getframe
import typing as t
from ._base import BidictBase
from ._bidict import bidict
from ._typing import KT, VT
class NamedBidictBase:
"""Base class that namedbidicts derive from."""
def namedbidict(
typename: str,
keyname: str,
valname: str,
*,
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.
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\`>`
(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:`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, 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)
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) -> 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

View file

@ -0,0 +1,238 @@
# 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/.
# * Code review nav *
# (see comments in __init__.py)
#==============================================================================
# ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py →
#==============================================================================
"""Provide :class:`OrderedBidictBase`."""
from __future__ import annotations
from functools import partial
from weakref import ref as weakref
import typing as t
from ._base import BidictBase, PreparedWrite
from ._bidict import bidict
from ._iter import iteritems
from ._typing import KT, VT, OKT, OVT, MISSING, Items, MapOrItems
IT = t.TypeVar('IT') # instance type
AT = t.TypeVar('AT') # attr type
class WeakAttr(t.Generic[IT, AT]):
"""Descriptor to automatically manage (de)referencing the given slot as a weakref.
See https://docs.python.org/3/howto/descriptor.html#managed-attributes
for an intro to using descriptors like this for managed attributes.
"""
def __init__(self, *, slot: str) -> None:
self.slot = slot
def __set__(self, instance: IT, value: AT) -> None:
setattr(instance, self.slot, weakref(value))
def __get__(self, instance: IT, owner: t.Any) -> AT:
return getattr(instance, self.slot)() # type: ignore [no-any-return]
class Node:
"""A node in a circular doubly-linked list
used to encode the order of items in an ordered bidict.
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`.
"""
prv: WeakAttr[Node, Node] = WeakAttr(slot='_prv_weak')
__slots__ = ('_prv_weak', 'nxt', '__weakref__')
def __init__(self, prv: Node, nxt: Node) -> None:
self.prv = prv
self.nxt = nxt
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).
"""
self.prv.nxt = self.nxt
self.nxt.prv = self.prv
def relink(self) -> None:
"""Restore self between prv and nxt after unlinking (see above)."""
self.prv.nxt = self.nxt.prv = self
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.
"""
nxt: WeakAttr['SentinelNode', Node] = WeakAttr(slot='_nxt_weak') # type: ignore [assignment]
__slots__ = ('_nxt_weak',)
def __init__(self) -> None:
super().__init__(self, self)
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 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`."""
_repr_delegate: t.ClassVar[t.Any] = list
_node_by_korv: bidict[t.Any, Node]
_bykey: bool
@t.overload
def __init__(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ...
@t.overload
def __init__(self, __i: Items[KT, VT], **kw: VT) -> None: ...
@t.overload
def __init__(self, **kw: VT) -> None: ...
def __init__(self, *args: MapOrItems[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()
self._node_by_korv = bidict()
self._bykey = True
super().__init__(*args, **kw)
if t.TYPE_CHECKING:
@property
def inverse(self) -> OrderedBidictBase[VT, 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: MapOrItems[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(reverse=False)
def __reversed__(self) -> t.Iterator[KT]:
"""Iterator over the contained keys in reverse insertion order."""
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 *
#==============================================================================
# ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py →
#==============================================================================

View file

@ -0,0 +1,160 @@
# 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/.
# * Code review nav *
# (see comments in __init__.py)
#==============================================================================
# ← Prev: _frozenordered.py Current: _orderedbidict.py <FIN>
#==============================================================================
"""Provide :class:`OrderedBidict`."""
from __future__ import annotations
from collections.abc import Set
import typing as t
from ._base import BidictKeysView
from ._bidict 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."""
if t.TYPE_CHECKING:
@property
def inverse(self) -> OrderedBidict[VT, KT]: ...
def clear(self) -> None:
"""Remove all items."""
super().clear()
self._node_by_korv.clear()
self._sntl.nxt = self._sntl.prv = self._sntl
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
def popitem(self, last: bool = True) -> tuple[KT, VT]:
"""*b.popitem() → (k, v)*
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('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
def move_to_end(self, key: KT, last: bool = True) -> None:
"""Move the item with the given key to the end if *last* is true, else to the beginning.
:raises KeyError: if *key* is missing
"""
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
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
# 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[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.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: _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 = args[0]._mapping._fwdm
arg_dict_view = getattr(arg_dict, 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>
#==============================================================================

View file

@ -0,0 +1,43 @@
# 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/.
"""Provide typing-related objects."""
from __future__ import annotations
from enum import Enum
import typing as t
if t.TYPE_CHECKING:
from typing_extensions import TypeAlias as TypeAlias
else:
TypeAlias = 'TypeAlias'
KT = t.TypeVar('KT')
VT = t.TypeVar('VT')
Items: TypeAlias = 't.Iterable[tuple[KT, VT]]'
MapOrItems: TypeAlias = 't.Mapping[KT, VT] | Items[KT, VT]'
ItemsIter: TypeAlias = 't.Iterator[tuple[KT, VT]]'
class MissingT(Enum):
"""Sentinel used to represent none/missing when None itself can't be used."""
MISSING = 'MISSING'
def __repr__(self) -> str:
return '<MISSING>'
MISSING: t.Final[MissingT] = MissingT.MISSING
OKT: TypeAlias = 'KT | MissingT' #: optional key type
OVT: TypeAlias = 'VT | MissingT' #: optional value type
DT = t.TypeVar('DT') #: for default arguments
ODT: TypeAlias = 'DT | MissingT' #: optional default arg type

View file

@ -0,0 +1,15 @@
# 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/.
"""Define bidict package metadata."""
__version__ = '0.22.1'
__author__ = {'name': 'Joshua Bronson', 'email': 'jabronson@gmail.com'}
__copyright__ = '© 2009-2022 Joshua Bronson'
__description__ = 'The bidirectional mapping library for Python.'
__license__ = 'MPL 2.0'
__url__ = 'https://bidict.readthedocs.io'

View file

@ -0,0 +1 @@
PEP-561 marker.