#!/usr/bin/env python # -*- coding: utf-8 -*- ''' Overview -------- :mod:`bidict` provides a bidirectional mapping data structure and related functionality to naturally work with one-to-one relations in Python. Unlike alternative implementations, ``bidict`` builds on top of the dict API and supports the familiar ``__getitem__`` syntax. It also supports a convenient slice syntax to express an inverse mapping:: >>> element_by_symbol = bidict(H='hydrogen') >>> element_by_symbol['H'] # forward mapping works just like with dict 'hydrogen' >>> element_by_symbol[:'hydrogen'] # use slice for the inverse mapping 'H' Syntax hacks ftw. Motivation ---------- Python's built-in dict lets us associate unique keys with arbitrary values. Because keys must be hashable, values can be looked up by key in constant time. Different keys can map to the same value, but a single key cannot map to two different values. For instance, ``{-1: 1, 0: 0, 1: 1}`` is a dict with three unique keys and two unique values, because the keys -1 and 1 both map to 1. If you try to write its inverse ``{1: -1, 0: 0, 1: 1}``, the dict that results has only two mappings, one for key 1 and one for key 0; since key 1 is not allowed to map to both -1 and 1, one of these mappings is discarded. Sometimes the relation we're modeling will only ever have a single key mapping to a single value, as in the relation of chemical elements and their symbols. This is called a one-to-one (or injective) mapping (see https://en.wikipedia.org/wiki/Injective_mapping). In this case we can be sure that the inverse mapping has the same number of items as the forward mapping, and moreover that if key k maps to value v in the forward mapping, then v maps to k in the inverse. It would be useful then to be able to look up, in constant time, keys by value, in addition to being able to look up values by key. With the additional constraint that values must also be hashable as well as keys, we can get constant-time forward and inverse lookups -- with a convenient syntax to boot -- with ``bidict``. More Examples ------------- Expanding on the previous example, anywhere the ``__getitem__`` syntax can be used to reference a forward mapping, slice syntax can be used too:: >>> element_by_symbol['H'] = 'Hydrogen' >>> element_by_symbol['H':] 'Hydrogen' Including setting and deleting items in either direction:: >>> element_by_symbol['He':] = 'helium' >>> element_by_symbol[:'lithium'] = 'Li' >>> del element_by_symbol['H':] >>> del element_by_symbol[:'lithium'] >>> element_by_symbol bidict({'He': 'helium'}) The rest of the ``MutableMapping`` interface is also supported:: >>> 'C' in element_by_symbol False >>> element_by_symbol.get('C', 'carbon') 'carbon' >>> element_by_symbol.pop('He') 'helium' >>> element_by_symbol bidict({}) >>> element_by_symbol.update(Hg='mercury') >>> element_by_symbol bidict({'Hg': 'mercury'}) You can also use the unary inverse operator ~ on a ``bidict`` to get the inverse mapping:: >>> ~element_by_symbol bidict({'mercury': 'Hg'}) This is especially handy for composing with other operations:: >>> 'mercury' in ~element_by_symbol True >>> (~element_by_symbol).pop('mercury') 'Hg' Because ``bidict`` maintains inverse mappings alongside forward mappings, getting the inverse or composing an operation with the inverse costs only constant time; the inverse does not have to be computed from scratch. See the :class:`bidict.bidict` class for more documentation. The ``inverted`` iterator is also provided in the spirit of the built-in function ``reversed``. Pass in a mapping to get the inverse mapping, an iterable of pairs to get the pairs' inverses, or any object implementing an ``__inverted__`` method. See the :class:`bidict.inverted` class for more documentation. ``frozenbidict`` provides a hashable, immutable version of ``bidict`` making it possible to insert into sets or mappings. See the :class:`bidict.frozenbidict` class for more documentation. The ``namedbidict`` class factory can be used to create a bidirectional mapping with customized names for the forward and the inverse mappings accessible via attributes. See :attr:`bidict.namedbidict` for more documentation. Notes ----- * It is intentional that the term "inverse" is used rather than "reverse". Consider a collection of (k, v) pairs. Taking the reverse of the collection can only be done if it is ordered, and (as the name says) reverses the order of the pairs in the collection. But, each original (k, v) pair remains in the resulting collection. By contrast, taking the inverse of such a collection does not require an original ordering, and does not make any claims about the ordering of the result, but rather just replaces every (k, v) pair with the inverse pair (v, k). * "keys" and "values" could perhaps more properly be called "primary keys" and "secondary keys" (as in a database), or even "forward keys" and "inverse keys", respectively. ``bidict`` sticks with the terms "keys" and "values" for the sake of familiarity and to avoid potential confusion, but it's worth noting that values are also keys themselves. This allows us to return a set-like object for :attr:`bidict.BidirectionalMapping.values` (Python 3) / :attr:`bidict.BidirectionalMapping.viewvalues` (Python 2), for example. * The built-in ``htmlentitydefs`` module provides an example of where ``bidict`` could be used in the Python standard library instead of having to maintain the two ``name2codepoint`` and ``codepoint2name`` dictionaries separately by hand. Caveats ------- * Because ``bidict`` is a bidirectional dict, values as well as keys must be hashable. Attempting to insert an unhashable value will result in an error:: >>> anagrams_by_alphagram = bidict(opt=['opt', 'pot', 'top']) # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError:...unhashable... >>> bidict(opt=('opt', 'pot', 'top')) bidict({'opt': ('opt', 'pot', 'top')}) * When instantiating or updating a ``bidict``, remember that mappings for like values with differing keys will be silently dropped (just as the dict literal ``{1: 'one', 1: 'uno'}`` silently drops a mapping), to maintain bidirectionality:: >>> nils = bidict(zero=0, zilch=0, zip=0) >>> len(nils) 1 >>> nils.update(nix=0, nada=0) >>> len(nils) 1 * When mapping the key of one existing mapping to the value of another (or vice versa), the two mappings silently collapse into one:: >>> b = bidict({1: 'one', 2: 'two'}) >>> b[1] = 'two' >>> b bidict({1: 'two'}) >>> b = bidict({1: 'one', 2: 'two'}) >>> b[:'two'] = 1 >>> b bidict({1: 'two'}) Links ----- * Documentation: https://bidict.readthedocs.org * Development: https://github.com/jab/bidict * License: http://choosealicense.com/licenses/isc/ Credits ------- * Thanks to Terry Reedy for the idea for the slice syntax. * Thanks to Raymond Hettinger for the idea for namedbidict and pointing out various caveats. * Thanks to Francis Carr for the idea of storing the inverse bidict. See the rest of the bidict module for further documentation. ------------------------------------------------------------ ''' from sys import version_info PY2 = version_info[0] == 2 if PY2: assert version_info[1] > 6, 'Python >= 2.7 required' from collections import Hashable, Iterator, Mapping, MutableMapping from re import compile from sys import version_info if PY2: iteritems = lambda x: x.iteritems() viewitems = lambda x: x.viewitems() else: iteritems = lambda x: iter(x.items()) viewitems = lambda x: x.items() def fancy_iteritems(*map_or_it, **kw): ''' Generator yielding the mappings provided. Abstracts differences between Python 2 and 3:: >>> it = fancy_iteritems({1: 2}) >>> next(it) (1, 2) >>> next(it) Traceback (most recent call last): ... StopIteration Accepts zero or one positional argument which it first tries iterating over as a mapping (as above), and if that fails, falls back to iterating over as a sequence, yielding items two at a time:: >>> it = fancy_iteritems([(1, 2), (3, 4)]) >>> next(it) (1, 2) >>> next(it) (3, 4) >>> next(it) Traceback (most recent call last): ... StopIteration >>> list(fancy_iteritems()) [] Mappings may also be passed as keyword arguments, which will be yielded after any mappings passed via positional argument:: >>> list(sorted(fancy_iteritems(a=1, b=2))) [('a', 1), ('b', 2)] >>> list(sorted(fancy_iteritems({'a': 1}, b=2, c=3))) [('a', 1), ('b', 2), ('c', 3)] >>> list(sorted(fancy_iteritems([('a', 1)], b=2, c=3))) [('a', 1), ('b', 2), ('c', 3)] In other words, this is like a generator analog of the dict constructor. Note that if any mappings from a sequence or keyword argument repeat an earlier mapping in the positional argument, repeat mappings will still be yielded, whereas with ``dict`` the last repeat clobbers earlier ones:: >>> dict([('a', 1), ('a', 2)]) {'a': 2} >>> list(fancy_iteritems([('a', 1), ('a', 2)])) [('a', 1), ('a', 2)] >>> dict([('a', 1), ('a', 2)], a=3) {'a': 3} >>> list(fancy_iteritems([('a', 1), ('a', 2)], a=3)) [('a', 1), ('a', 2), ('a', 3)] ''' if map_or_it: l = len(map_or_it) if l != 1: raise TypeError('expected at most 1 argument, got %d' % l) map_or_it = map_or_it[0] try: it = iteritems(map_or_it) # mapping? except AttributeError: # no for (k, v) in map_or_it: # -> treat as sequence yield (k, v) else: # yes for (k, v) in it: # -> treat as mapping yield (k, v) for (k, v) in iteritems(kw): yield (k, v) class inverted(Iterator): ''' An iterator in the spirit of ``reversed``. Useful for inverting a mapping:: >>> keys = (1, 2, 3) >>> vals = ('one', 'two', 'three') >>> fwd = dict(zip(keys, vals)) >>> inv = dict(inverted(fwd)) >>> inv == dict(zip(vals, keys)) True Passing an iterable of pairs produces an iterable of the pairs' inverses:: >>> seq = [(1, 'one'), (2, 'two'), (3, 'three')] >>> list(inverted(seq)) [('one', 1), ('two', 2), ('three', 3)] Passing an ``inverted`` object back into ``inverted`` produces the original sequence of pairs:: >>> seq == list(inverted(inverted(seq))) True Under the covers, ``inverted`` first tries to call ``__inverted__`` on the wrapped object and returns the result if the call succeeded, creating synergy with :attr:`bidict.BidirectionalMapping.__inverted__` (effectively delegating to the class if it supports inverting natively). If the call fails, ``inverted`` falls back on calling its own ``__next__`` method, which in turn calls :attr:`bidict.fancy_iteritems` on the wrapped object, yielding the inverse of the pairs that fancy_iteritems yields. Be careful with passing the inverse of a non-injective mapping into ``dict``; mappings for like values with differing keys will be dropped silently, just as ``{1: 'one', 1: 'uno'}`` silently drops a mapping:: >>> squares = {-2: 4, -1: 1, 0: 0, 1: 1, 2: 4} >>> len(squares) 5 >>> len(dict(inverted(squares))) 3 ''' def __init__(self, data): self._data = data def __iter__(self): try: it = self._data.__inverted__ except AttributeError: it = self.__next__ return it() def __next__(self): for (k, v) in fancy_iteritems(self._data): yield (v, k) # compat if PY2: next = __next__ class BidirectionalMapping(Mapping): ''' The read-only functionality of ``bidict`` is implemented in this base class. :class:`bidict.bidict` and :class:`bidict.frozenbidict` both extend this. ''' def __init__(self, *args, **kw): self._fwd = {} self._bwd = {} for (k, v) in fancy_iteritems(*args, **kw): self._set(k, v) inv = object.__new__(self.__class__) inv._fwd = self._bwd inv._bwd = self._fwd inv._inv = self self._inv = inv self._hash = None def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self._fwd) __str__ = __repr__ def __eq__(self, other): ''' Supports equality testing with another mapping:: >>> d = dict(a=1) >>> b = bidict(a=1) >>> b == d True >>> f = frozenbidict(a=1) >>> f == d True >>> ElementMap = namedbidict('ElementMap', 'symbol', 'element') >>> noble_gases = ElementMap(He='helium') >>> noble_gases == dict(He='helium') True Even works with nan:: >>> nan = float('nan') >>> bidict({nan: 1}) == {nan: 1} True Comparing with a non-mapping returns False: >>> bidict(a=1) == [('a', 1)] False ''' try: return viewitems(self) == viewitems(other) except: return False def __ne__(self, other): return not self.__eq__(other) def __invert__(self): ''' Called when the unary inverse operator (~) is applied. ''' return self._inv inv = property(__invert__, doc='Property providing access to the inverse '\ 'bidict. Can be chained as in: ``B.inv.inv is B``') def __inverted__(self): return iteritems(self._bwd) @staticmethod def _fwd_slice(slice): ''' Raises ``TypeError`` if the given slice does not have either only its start or only its stop set to a non-None value. Returns True if only its start is not None and False if only its stop is not None. ''' if slice.step is not None or \ (not ((slice.start is None) ^ (slice.stop is None))): raise TypeError('Slice must specify only either start or stop') return slice.start is not None def __getitem__(self, keyorslice): if isinstance(keyorslice, slice): # forward lookup (by key): b[key:] if self._fwd_slice(keyorslice): return self._fwd[keyorslice.start] else: # inverse lookup (by val): b[:val] return self._bwd[keyorslice.stop] else: # keyorslice is a key: b[key] return self._fwd[keyorslice] def _set(self, key, val): try: oldkey = self._bwd[val] except KeyError: pass else: del self._fwd[oldkey] try: oldval = self._fwd[key] except KeyError: pass else: del self._bwd[oldval] self._fwd[key] = val self._bwd[val] = key get = lambda self, k, *args: self._fwd.get(k, *args) copy = lambda self: self.__class__(self._fwd) get.__doc__ = dict.get.__doc__ copy.__doc__ = dict.copy.__doc__ __len__ = lambda self: len(self._fwd) __iter__ = lambda self: iter(self._fwd) __contains__ = lambda self, x: x in self._fwd __len__.__doc__ = dict.__len__.__doc__ __iter__.__doc__ = dict.__iter__.__doc__ __contains__.__doc__ = dict.__contains__.__doc__ keys = lambda self: self._fwd.keys() items = lambda self: self._fwd.items() keys.__doc__ = dict.keys.__doc__ items.__doc__ = dict.items.__doc__ values = lambda self: self._bwd.keys() values.__doc__ = \ "D.values() -> a set-like object providing a view on D's values. " \ "Note that because values of a BidirectionalMapping are also keys, " \ "this returns a ``dict_keys`` object rather than a ``dict_values`` " \ "object." if PY2: iterkeys = lambda self: self._fwd.iterkeys() viewkeys = lambda self: self._fwd.viewkeys() iteritems = lambda self: self._fwd.iteritems() viewitems = lambda self: self._fwd.viewitems() itervalues = lambda self: self._bwd.iterkeys() viewvalues = lambda self: self._bwd.viewkeys() iterkeys.__doc__ = dict.iterkeys.__doc__ viewkeys.__doc__ = dict.viewkeys.__doc__ iteritems.__doc__ = dict.iteritems.__doc__ viewitems.__doc__ = dict.viewitems.__doc__ itervalues.__doc__ = dict.itervalues.__doc__ viewvalues.__doc__ = values.__doc__.replace('values()', 'viewvalues()') values.__doc__ = dict.values.__doc__ class frozenbidict(BidirectionalMapping, Hashable): ''' Extends ``BidirectionalMapping`` and ``Hashable`` to provide an immutable, hashable bidict. It's immutable simply because it doesn't implement any mutating methods:: >>> f = frozenbidict() >>> f.update(H='hydrogen') # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError... >>> f['C'] = 'carbon' # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError... And, unlike ``BidirectionalMapping`` and ``bidict``, it's hashable simply because it implements ``__hash__``:: >>> b = bidict(H='hydrogen') >>> h = hash(b) # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError... >>> f = frozenbidict(b) >>> hash(f) is not 'an exception' True Hashability allows for insertion into sets, mappings, etc. and associated hashing semantics:: >>> d = {} >>> d[f] = 'hashable!' >>> g = frozenbidict(H='hydrogen') >>> hash(f) == hash(g) True >>> f is g False To compute a frozenbidict's hash value, a temporary frozenset is constructed out of the frozenbidict's items, and the hash of the frozenset is returned. So be aware that computing a frozenbidict's hash is not a constant-time or -space operation. To mitigate this, the hash is computed lazily, only when ``__hash__`` is first called, and is then cached so that future calls take constant time. ''' def __hash__(self): if self._hash is None: self._hash = hash(frozenset(viewitems(self))) return self._hash class bidict(BidirectionalMapping, MutableMapping): ''' Extends :class:`bidict.BidirectionalMapping` to implement the ``MutableMapping`` interface. The API is a superset of the ``dict`` API minus the ``fromkeys`` method, which doesn't make sense for a bidirectional mapping because keys *and* values must be unique. Examples:: >>> keys = (1, 2, 3) >>> vals = ('one', 'two', 'three') >>> bi = bidict(zip(keys, vals)) >>> bi == bidict({1: 'one', 2: 'two', 3: 'three'}) True >>> bidict(inverted(bi)) == bidict(zip(vals, keys)) True You can use standard subscripting syntax with a key to get or set a forward mapping:: >>> bi[2] 'two' >>> bi[2] = 'twain' >>> bi[2] 'twain' >>> bi[4] Traceback (most recent call last): ... KeyError: 4 Or use a slice with only a ``start``:: >>> bi[2:] 'twain' >>> bi[0:] = 'naught' >>> bi[0:] 'naught' Use a slice with only a ``stop`` to get or set an inverse mapping:: >>> bi[:'one'] 1 >>> bi[:'aught'] = 1 >>> bi[:'aught'] 1 >>> bi[1] 'aught' >>> bi[:'one'] Traceback (most recent call last): ... KeyError: 'one' Deleting items from the bidict works the same way:: >>> del bi[0] >>> del bi[2:] >>> del bi[:'three'] >>> bi bidict({1: 'aught'}) bidicts maintain references to their inverses via the ``inv`` property, which can also be used to access or modify them:: >>> bi.inv bidict({'aught': 1}) >>> bi.inv['aught'] 1 >>> bi.inv[:1] 'aught' >>> bi.inv[:1] = 'one' >>> bi.inv bidict({'one': 1}) >>> bi bidict({1: 'one'}) >>> bi.inv.inv is bi True >>> bi.inv.inv.inv is bi.inv True A ``bidict``’s inverse can also be accessed via the unary ~ operator, by analogy to the unary bitwise inverse operator:: >>> ~bi bidict({'one': 1}) >>> ~bi is bi.inv True Because ~ binds less tightly than brackets, parentheses are necessary for something like:: >>> (~bi)['one'] 1 bidicts work with ``inverted`` as expected:: >>> biinv = bidict(inverted(bi)) >>> biinv bidict({'one': 1}) This of course creates a new object (equivalent but not identical):: >>> biinv == bi.inv True >>> biinv is bi.inv False Notice that ``__eq__`` has been implemented to make == work as expected. Inverting the inverse should round-trip:: >>> bi == bidict(inverted(inverted(bi))) True Use ``invert`` to invert the mapping in place, in constant time:: >>> bi.invert() >>> bi bidict({'one': 1}) The rest of the ``MutableMapping`` interface is supported too:: >>> bi.get('one') 1 >>> bi.get('zero') >>> bi.get('zero', 0) 0 >>> list(bi.keys()) ['one'] >>> list(bi.values()) [1] >>> list(bi.items()) [('one', 1)] >>> bi.setdefault('one', 2) 1 >>> bi.setdefault('two', 2) 2 >>> bi.pop('one') 1 >>> bi.popitem() ('two', 2) >>> bi.inv.setdefault(3, 'three') 'three' >>> bi bidict({'three': 3}) >>> len(bi) # calls __len__ 1 >>> [key for key in bi] # calls __iter__, returns keys like dict ['three'] >>> 'three' in bi # calls __contains__ True >>> list(bi.keys()) ['three'] >>> list(bi.values()) [3] >>> bi.update([('four', 4)]) >>> bi.update({'five': 5}, six=6, seven=7) >>> sorted(bi.items(), key=lambda x: x[1]) [('three', 3), ('four', 4), ('five', 5), ('six', 6), ('seven', 7)] When instantiating or updating a ``bidict``, remember that mappings for like values with differing keys will be silently dropped (just as the literal ``{1: 'one', 1: 'uno'}`` silently drops a mapping), to maintain bidirectionality:: >>> nils = bidict(zero=0, zilch=0, zip=0) >>> len(nils) 1 >>> nils.update(nix=0, nada=0) >>> len(nils) 1 Another caveat: when mapping the key of one existing mapping to the value of another (or vice versa), the two mappings collapse into one:: >>> b = bidict({1: 'one', 2: 'two'}) >>> b[1] = 'two' >>> b bidict({1: 'two'}) >>> b = bidict({1: 'one', 2: 'two'}) >>> b[:'two'] = 1 >>> b bidict({1: 'two'}) ''' def __del(self, key): val = self._fwd[key] del self._fwd[key] del self._bwd[val] def __delitem__(self, keyorslice): if isinstance(keyorslice, slice): # delete by key: del b[key:] if self._fwd_slice(keyorslice): self.__del(keyorslice.start) else: # delete by value: del b[:val] self.__del(self._bwd[keyorslice.stop]) else: # keyorslice is a key: del b[key] self.__del(keyorslice) def __setitem__(self, keyorslice, keyorval): if isinstance(keyorslice, slice): # keyorslice.start is key, keyorval is val: b[key:] = val if self._fwd_slice(keyorslice): self._set(keyorslice.start, keyorval) else: # keyorval is key, keyorslice.stop is val: b[:val] = key self._set(keyorval, keyorslice.stop) else: # keyorslice is a key, keyorval is a val: b[key] = val self._set(keyorslice, keyorval) def clear(self): self._fwd.clear() self._bwd.clear() def invert(self): self._fwd, self._bwd = self._bwd, self._fwd self._inv._fwd, self._inv._bwd = self._inv._bwd, self._inv._fwd def pop(self, key, *args): val = self._fwd.pop(key, *args) del self._bwd[val] return val def popitem(self): if not self._fwd: raise KeyError key, val = self._fwd.popitem() del self._bwd[val] return key, val def setdefault(self, key, default=None): val = self._fwd.setdefault(key, default) self._bwd[val] = key return val def update(self, *args, **kw): for k, v in fancy_iteritems(*args, **kw): self[k] = v _LEGALNAMEPAT = '^[a-zA-Z][a-zA-Z0-9_]*$' _LEGALNAMERE = compile(_LEGALNAMEPAT) def _empty_namedbidict(mapname, fwdname, invname): ''' Create an empty instance of a custom bidict (namedbidict). This method is used to make ``namedbidict`` instances picklable. ''' return namedbidict(mapname, fwdname, invname)() def namedbidict(mapname, fwdname, invname, bidict_type=bidict): ''' Generate a custom bidict class in the spirit of ``namedtuple`` with custom attribute-based access to forward and inverse mappings:: >>> ElementMap = namedbidict('ElementMap', 'symbol', 'element') >>> noble_gases = ElementMap(He='helium') >>> noble_gases.element_for['He'] 'helium' >>> noble_gases.symbol_for['helium'] 'He' >>> noble_gases.element_for['Ne'] = 'neon' >>> del noble_gases.symbol_for['helium'] >>> noble_gases ElementMap({'Ne': 'neon'}) Pass to ``bidict`` to get back a regular ``bidict``:: >>> bidict(noble_gases) bidict({'Ne': 'neon'}) Comparison works as expected:: >>> noble_gases2 = ElementMap({'Ne': 'neon'}) >>> noble_gases2 == noble_gases True >>> noble_gases2 == bidict(noble_gases) True >>> noble_gases2 == dict(noble_gases) True >>> noble_gases2['Rn'] = 'radon' >>> noble_gases2 == noble_gases False >>> noble_gases2 != noble_gases True >>> noble_gases2 != bidict(noble_gases) True >>> noble_gases2 != dict(noble_gases) True The ``bidict_type`` keyword arg allows overriding the BidirectionalMapping type used, useful for creating e.g. a namedfrozenbidict:: >>> ElMap = namedbidict('ElMap', 'sym', 'el', bidict_type=frozenbidict) >>> noble = ElMap(He='helium') >>> hash(noble) is not 'an exception' True >>> noble['C'] = 'carbon' # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError... ''' for name in mapname, fwdname, invname: if _LEGALNAMERE.match(name) is None: raise ValueError('"%s" does not match pattern %s' % (name, _LEGALNAMEPAT)) for_fwd = invname + '_for' for_inv = fwdname + '_for' __dict__ = {for_fwd: property(lambda self: self), for_inv: bidict_type.inv} custombidict = type(mapname, (bidict_type,), __dict__) # support pickling custombidict.__reduce__ = lambda self: \ (_empty_namedbidict, (mapname, fwdname, invname), self.__dict__) return custombidict if __name__ == '__main__': from doctest import testmod testmod()