Extending ``bidict``
--------------------

Although :mod:`bidict` provides the various bidirectional mapping types covered already,
it's possible that some use case might require something more than what's provided.
For this reason,
:mod:`bidict` was written with extensibility in mind.

Let's look at some examples.


``YoloBidict`` Recipe
#####################

If you'd like
:attr:`~bidict.ON_DUP_DROP_OLD`
to be the default :class:`~bidict.bidict.on_dup` behavior
(for :meth:`~bidict.bidict.__init__`,
:meth:`~bidict.bidict.__setitem__`, and
:meth:`~bidict.bidict.update`),
you can use the following recipe:

.. doctest::

   >>> from bidict import bidict, ON_DUP_DROP_OLD

   >>> class YoloBidict(bidict):
   ...     on_dup = ON_DUP_DROP_OLD

   >>> b = YoloBidict({'one': 1})
   >>> b['two'] = 1  # succeeds, no ValueDuplicationError
   >>> b
   YoloBidict({'two': 1})

   >>> b.update({'three': 1})  # ditto
   >>> b
   YoloBidict({'three': 1})

Of course, ``YoloBidict``'s inherited
:meth:`~bidict.bidict.put` and
:meth:`~bidict.bidict.putall` methods
still allow specifying a custom :class:`~bidict.OnDup`
per call via the *on_dup* argument,
and will both still default to raising for all duplication types.

Further demonstrating :mod:`bidict`'s extensibility,
to make an ``OrderedYoloBidict``,
simply have the subclass above inherit from
:class:`bidict.OrderedBidict`
rather than :class:`bidict.bidict`.


Beware of ``ON_DUP_DROP_OLD``
:::::::::::::::::::::::::::::

There's a good reason that :mod:`bidict` does not provide a ``YoloBidict`` out of the box.

Before you decide to use a ``YoloBidict`` in your own code,
beware of the following potentially unexpected, dangerous behavior:

.. doctest::

   >>> b = YoloBidict({'one': 1, 'two': 2})  # contains two items
   >>> b['one'] = 2                          # update one of the items
   >>> b                                     # now only has one item!
   YoloBidict({'one': 2})

As covered in :ref:`basic-usage:Key and Value Duplication`,
setting an existing key to the value of a different existing item
causes both existing items to quietly collapse into a single new item.

A safer example of this type of customization would be something like:

.. doctest::

   >>> from bidict import ON_DUP_RAISE

   >>> class YodoBidict(bidict):  # Note, "Yodo" with a "d"
   ...     on_dup = ON_DUP_RAISE

   >>> b = YodoBidict({'one': 1})
   >>> b['one'] = 2  # Works with a regular bidict, but Yodo plays it safe.
   Traceback (most recent call last):
       ...
   bidict.KeyDuplicationError: one
   >>> b
   YodoBidict({'one': 1})
   >>> b.forceput('one', 2)  # Any destructive change requires more force.
   >>> b
   YodoBidict({'one': 2})


``WeakrefBidict`` Recipe
########################

Suppose you need a custom bidict type that only retains weakrefs
to some objects whose refcounts you're trying not increment.

With :class:`~bidict.BidictBase`\'s
:attr:`~bidict.BidictBase._fwdm_cls` (forward mapping class) and
:attr:`~bidict.BidictBase._invm_cls` (inverse mapping class) attributes,
accomplishing this is as simple as:

.. doctest::

   >>> from bidict import MutableBidict
   >>> from weakref import WeakKeyDictionary, WeakValueDictionary

   >>> class WeakrefBidict(MutableBidict):
   ...     _fwdm_cls = WeakKeyDictionary
   ...     _invm_cls = WeakValueDictionary

Now you can insert items into *WeakrefBidict* without incrementing any refcounts:

.. doctest::

   >>> id_by_obj = WeakrefBidict()

   >>> class MyObj:
   ...     def __init__(self, id):
   ...         self.id = id
   ...     def __repr__(self):
   ...         return f'<MyObj id={self.id}>'

   >>> o1, o2 = MyObj(1), MyObj(2)
   >>> id_by_obj[o1] = o1.id
   >>> id_by_obj[o2] = o2.id
   >>> id_by_obj
   WeakrefBidict({<MyObj id=1>: 1, <MyObj id=2>: 2})
   >>> id_by_obj.inverse
   WeakrefBidictInv({1: <MyObj id=1>, 2: <MyObj id=2>})

If you drop your references to your objects,
you can see that they get deallocated on CPython right away,
since your *WeakrefBidict* isn't holding on to them:

.. doctest::
   :skipif: not_cpython

   >>> del o1, o2
   >>> id_by_obj
   WeakrefBidict()


``SortedBidict`` Recipes
########################

Suppose you need a bidict that maintains its items in sorted order.
The Python standard library does not include any sorted dict types,
but the excellent
`sortedcontainers <http://www.grantjenks.com/docs/sortedcontainers/>`__ and
`sortedcollections <http://www.grantjenks.com/docs/sortedcollections/>`__
libraries do.

Armed with these, along with :class:`~bidict.BidictBase`'s
:attr:`~bidict.BidictBase._fwdm_cls` (forward mapping class) and
:attr:`~bidict.BidictBase._invm_cls` (inverse mapping class) attributes,
creating a sorted bidict is simple:

.. doctest::

   >>> from sortedcontainers import SortedDict

   >>> class SortedBidict(MutableBidict):
   ...     """A sorted bidict whose forward items stay sorted by their keys,
   ...     and whose inverse items stay sorted by *their* keys.
   ...     Note: As a result, an instance and its inverse yield their items
   ...     in different orders.
   ...     """
   ...     _fwdm_cls = SortedDict
   ...     _invm_cls = SortedDict
   ...     _repr_delegate = list  # only used for list-style repr

   >>> b = SortedBidict({'Tokyo': 'Japan', 'Cairo': 'Egypt'})
   >>> b
   SortedBidict([('Cairo', 'Egypt'), ('Tokyo', 'Japan')])

   >>> b['Lima'] = 'Peru'

   >>> list(b.items())  # stays sorted by key
   [('Cairo', 'Egypt'), ('Lima', 'Peru'), ('Tokyo', 'Japan')]

   >>> list(b.inverse.items())  # .inverse stays sorted by *its* keys (b's values)
   [('Egypt', 'Cairo'), ('Japan', 'Tokyo'), ('Peru', 'Lima')]


Here's a recipe for a sorted bidict whose forward items stay sorted by their keys,
and whose inverse items stay sorted by their values. i.e. An instance and its inverse
will yield their items in *the same* order:

.. doctest::

   >>> from sortedcollections import ValueSortedDict

   >>> class KeySortedBidict(MutableBidict):
   ...     _fwdm_cls = SortedDict
   ...     _invm_cls = ValueSortedDict
   ...     _repr_delegate = list

   >>> elem_by_atomicnum = KeySortedBidict({
   ...     6: 'carbon', 1: 'hydrogen', 2: 'helium'})

   >>> list(elem_by_atomicnum.items())  # stays sorted by key
   [(1, 'hydrogen'), (2, 'helium'), (6, 'carbon')]

   >>> list(elem_by_atomicnum.inverse.items())  # .inverse stays sorted by value
   [('hydrogen', 1), ('helium', 2), ('carbon', 6)]

   >>> elem_by_atomicnum[4] = 'beryllium'

   >>> list(elem_by_atomicnum.inverse.items())
   [('hydrogen', 1), ('helium', 2), ('beryllium', 4), ('carbon', 6)]


Automatic "Get Attribute" Pass-Through
######################################

Python makes it easy to customize a class's "get attribute" behavior.
You can take advantage of this to pass attribute access
through to the backing ``_fwdm`` mapping, for example,
when an attribute is not provided by the bidict class itself:

   >>> def __getattribute__(self, name):
   ...     try:
   ...         return object.__getattribute__(self, name)
   ...     except AttributeError:
   ...         return getattr(self._fwdm, name)

   >>> KeySortedBidict.__getattribute__ = __getattribute__

Now, even though this ``KeySortedBidict`` itself provides no ``peekitem`` attribute,
the following call still succeeds
because it's passed through to the backing ``SortedDict``:

   >>> elem_by_atomicnum.peekitem()
   (6, 'carbon')


Dynamic Inverse Class Generation
################################

When a bidict class's
:attr:`~bidict.BidictBase._fwdm_cls` and
:attr:`~bidict.BidictBase._invm_cls`
are the same,
the bidict class is its own inverse class.
(This is the case for all the
:ref:`bidict classes <other-bidict-types:Bidict Types Diagram>`
that come with :mod:`bidict`.)

However, when a bidict's
:attr:`~bidict.BidictBase._fwdm_cls` and
:attr:`~bidict.BidictBase._invm_cls` differ,
as in the ``KeySortedBidict`` and ``WeakrefBidict`` recipes above,
the inverse class of the bidict
needs to have its
:attr:`~bidict.BidictBase._fwdm_cls` and
:attr:`~bidict.BidictBase._invm_cls` swapped.

:class:`~bidict.BidictBase` detects this
and dynamically computes the correct inverse class for you automatically.

You can see this if you inspect ``KeySortedBidict``'s inverse bidict:

   >>> elem_by_atomicnum.inverse.__class__.__name__
   'KeySortedBidictInv'

Notice that :class:`~bidict.BidictBase` automatically created a
``KeySortedBidictInv`` class and used it for the inverse bidict.

As expected, ``KeySortedBidictInv``'s
:attr:`~bidict.BidictBase._fwdm_cls` and
:attr:`~bidict.BidictBase._invm_cls`
are the opposite of ``KeySortedBidict``'s:

   >>> elem_by_atomicnum.inverse._fwdm_cls.__name__
   'ValueSortedDict'
   >>> elem_by_atomicnum.inverse._invm_cls.__name__
   'SortedDict'

:class:`~bidict.BidictBase` also ensures that round trips work as expected:

   >>> KeySortedBidictInv = elem_by_atomicnum.inverse.__class__  # i.e. a value-sorted bidict
   >>> atomicnum_by_elem = KeySortedBidictInv(elem_by_atomicnum.inverse)
   >>> atomicnum_by_elem
   KeySortedBidictInv([('hydrogen', 1), ('helium', 2), ('beryllium', 4), ('carbon', 6)])
   >>> KeySortedBidict(atomicnum_by_elem.inverse) == elem_by_atomicnum
   True


-----

This all goes to show how simple it can be
to compose your own bidirectional mapping types
out of the building blocks that :mod:`bidict` provides.
