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