Skip to content

Instantly share code, notes, and snippets.

@Veedrac
Last active August 29, 2015 14:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Veedrac/d915c15bcd90c6f09fcd to your computer and use it in GitHub Desktop.
Save Veedrac/d915c15bcd90c6f09fcd to your computer and use it in GitHub Desktop.
A little module to make a little thing a little easier... OK a lot easier, but whatever!
"""
A small 1-to-1 dictionary library. Shares inspiration from bidict:
https://pypi.python.org/pypi/bidict/0.1.1
Implementation similarities are mostly coincidental. The ordered variant
was an idea stolen from
https://bitbucket.org/jab/bidict/issue/1/ordereddict-integration
I'm not sure what you'd want one for, but it's there if you need it.
The advantage of this package is the simplicity of implementation and
usage. There are no hacks. You just get a forward view and a backwards
view. Both act just like injections, and the isomorphism is transparent.
This is also correct with initialisation and updating, although that may
come at a speed loss. Because the implementation is simplified, a lot of
internal and external magic is now gone. This is also doesn't compare
equal to non-Injections by default, much like `[] != ()` by default.
Getting the inverse of a pre-existing Injection is purposefully not
supported. If you want such a thing, build a relevant namedtuple of the
two directions and pass that around instead. If you really don't want
to do that (you're insane, perhaps), fork the code and add an inverse
property yourself.
The OrderedInjection prevents use of pair methods, because it complicates
equality. The expected behaviour for equality is unclear as it's not
obvious when order is relevant. With single-direction injections, only
forward order exists. Further, __setitem__ needs additional checking for
bidirectionally ordered variants, to prevent deletions from removing order.
Despite the copied ideas, the code is my own. Thought credit goes to
everyone on the mailing list from
https://mail.python.org/pipermail/python-list/2009-November/558834.html
For reasons of sanity, all code is public domain. Seriously, do
anything you want with it.
"""
from collections import OrderedDict
from collections.abc import KeysView, MutableMapping
class Injection(MutableMapping):
"""
Create an injection from a mapping or iterable, and keyword
arguments. An injection is like a dictionary except that
values must be unique.
To use the injection in either direction, use Injection.pair.
Like with dictionaries, setting an item can remove an old pair.
Unlike with dictionaries, this happens in both directions:
print(inj)
#>>> {0 → 0, 1 → 1}
# Shares start with 0 → 0 and end with 1 → 1,
# so both paths are collapsed into 0 → 1.
inj[0] = 1
print(inj)
#>>> {0 → 1}
"""
def __init__(self, mapping_or_iterable={}, **kwargs):
self._forward = {}
self._backward = {}
# The trick in bidict wrecks the order, so don't use it.
self.update(mapping_or_iterable, **kwargs)
@classmethod
def pair(cls, *args, **kwargs):
"""
Create and return a (forward, backward) tuple of
Injections, with backward being the inverse of
forward.
Mutations to either object affect both Injections.
"""
forward = cls(*args, **kwargs)
backward = cls()
backward._forward = forward._backward
backward._backward = forward._forward
return forward, backward
def __setitem__(self, item, complement):
# Make sure they're hashable before
# destroying things
{item, complement}
# If we point to something, there's
# something poining back to us. Remove it.
if item in self._forward:
del self._backward[self._forward[item]]
# If our target is pointing to something,
# we are pointing to it. Remove that.
if complement in self._backward:
del self._forward[self._backward[complement]]
self._forward[item] = complement
self._backward[complement] = item
def __delitem__(self, item):
del self._backward[self._forward.pop(item)]
# Shalow wrappers
def __getitem__(self, item):
return self._forward[item]
def __iter__(self):
return iter(self._forward)
def __len__(self):
return len(self._forward)
# KeysView > ValuesView, so override Injection.values
# to return a KeysView from the inverted dictionary.
#
# Also override `keys` to return a view directly on the
# underlying dictionary, for prettiness and speed.
def keys(self):
return KeysView(self._forward)
def values(self):
return KeysView(self._backward)
# Printing routines
def __str__(self):
if not self:
return "{→}"
pairstrings = ("{!r} → {!r}".format(*pair) for pair in self.items())
return "{{{}}}".format(", ".join(pairstrings))
def __repr__(self):
return "{}({})".format(type(self).__name__, self._forward)
# More type-strict than default
def __eq__(self, other):
if not isinstance(other, Injection):
return NotImplemented
return self._forward == other._forward
# Faster than default
def clear(self):
self._forward.clear()
self._backward.clear()
clear.__doc__ = MutableMapping.clear.__doc__
# Not in ABC, but dict has it
def copy(self):
new = type(self)()
new._forward = self._forward.copy()
new._backward = self._backward.copy()
return new
class OrderedInjection(Injection):
def __init__(self, mapping_or_iterable={}, **kwargs):
self._forward = OrderedDict()
self._backward = {}
# The trick in bidict *still* wrecks the order
self.update(mapping_or_iterable, **kwargs)
@classmethod
def pair(cls, *args, **kwargs):
raise AttributeError("{} does not support 'pair' method".format(cls.__name__))
from injections import Injection, OrderedInjection
def test_initialisation():
ij = Injection()
assert isinstance(ij, Injection)
assert ij == Injection({}) == Injection(())
def test_pair_initialisation():
left, right = Injection.pair()
assert isinstance(left, Injection)
assert isinstance(right, Injection)
def test_initialisation_dict():
ij = Injection({0: 0, 1: 1})
assert ij[0] == 0
assert ij[1] == 1
assert len(ij) == 2
def test_delete():
ij = Injection({0: 0, 1: 1})
del ij[1]
assert ij[0] == 0
assert 1 not in ij
def test_iteration():
ij = Injection({0: 0, 1: 1})
assert sorted(ij) == [0, 1]
def test_keys():
d = {0: 0, 1: 1}
ij = Injection(d)
assert ij.keys() == d.keys()
def test_values():
d = {0: 0, 1: 1}
ij = Injection(d)
assert ij.values() == {v: k for k, v in d.items()}.keys()
def test_empty_str():
ij = Injection()
assert str(ij) == "{→}"
def test_length_one_str():
ij = Injection({'x': 'y'})
assert str(ij) == "{'x' → 'y'}"
def test_length_two_str():
ij = Injection({'x': 'y', 1: 1})
assert str(ij) in ["{'x' → 'y', 1 → 1}", "{1 → 1, 'x' → 'y'}"]
def test_empty_repr():
ij = Injection()
assert repr(ij) == "Injection({})"
def test_length_one_repr():
ij = Injection({'x': 'y'})
assert repr(ij) == "Injection({'x': 'y'})"
def test_length_two_repr():
ij = Injection({'x': 'y', 1: 1})
assert repr(ij) in ["Injection({'x': 'y', 1: 1})", "Injection({1: 1, 'x': 'y'})"]
def test_equality():
ij = Injection({0: 1})
ij2 = Injection({0: 1})
assert ij == ij
assert ij == ij2
assert ij2 == ij
assert ij != {0: 1}
assert {0: 1} != ij
def test_clear():
ij = Injection({0: 1})
ij.clear()
assert ij == Injection()
def test_copy():
ij = Injection({0: 1})
ij2 = ij.copy()
assert ij == ij2
assert ij is not ij2
def test_set():
ij = Injection()
ij[0] = 0
assert ij[0] == 0
assert 1 not in ij
ij[1] = 1
assert ij[0] == 0
assert ij[1] == 1
ij[0] = 1
assert ij[0] == 1
assert 1 not in ij
ij[1] = 0
assert ij[0] == 1
assert ij[1] == 0
ij[1] = 1
assert 0 not in ij
assert ij[1] == 1
import pytest
from injections import OrderedInjection
def test_initialisation():
ij = OrderedInjection()
assert isinstance(ij, OrderedInjection)
assert ij == OrderedInjection({}) == OrderedInjection(())
def test_pair_initialisation_fail():
with pytest.raises(AttributeError):
OrderedInjection.pair()
def test_initialisation_dict():
ij = OrderedInjection({0: 0, 1: 1})
assert ij[0] == 0
assert ij[1] == 1
assert len(ij) == 2
def test_delete():
ij = OrderedInjection({0: 0, 1: 1})
del ij[1]
assert ij[0] == 0
assert 1 not in ij
def test_iteration():
ij = OrderedInjection({0: 0, 1: 1})
assert sorted(ij) == [0, 1]
def test_keys():
d = {0: 0, 1: 1}
ij = OrderedInjection(d)
assert ij.keys() == d.keys()
def test_values():
d = {0: 0, 1: 1}
ij = OrderedInjection(d)
assert ij.values() == {v: k for k, v in d.items()}.keys()
def test_empty_str():
ij = OrderedInjection()
assert str(ij) == "{→}"
def test_length_one_str():
ij = OrderedInjection({'x': 'y'})
assert str(ij) == "{'x' → 'y'}"
def test_length_two_str():
ij = OrderedInjection({'x': 'y', 1: 1})
assert str(ij) in ["{'x' → 'y', 1 → 1}", "{1 → 1, 'x' → 'y'}"]
def test_empty_repr():
ij = OrderedInjection()
assert repr(ij) == "OrderedInjection(OrderedDict())"
def test_length_one_repr():
ij = OrderedInjection({'x': 'y'})
assert repr(ij) == "OrderedInjection(OrderedDict([('x', 'y')]))"
def test_length_two_repr():
ij = OrderedInjection({'x': 'y', 1: 1})
assert repr(ij) in ["OrderedInjection(OrderedDict([('x', 'y'), (1, 1)]))", "OrderedInjection(OrderedDict([(1, 1), ('x', 'y')]))"]
def test_equality():
ij = OrderedInjection({0: 1})
ij2 = OrderedInjection({0: 1})
assert ij == ij
assert ij == ij2
assert ij2 == ij
assert ij != {0: 1}
assert {0: 1} != ij
def test_clear():
ij = OrderedInjection({0: 1})
ij.clear()
assert ij == OrderedInjection()
def test_copy():
ij = OrderedInjection({0: 1})
ij2 = ij.copy()
assert ij == ij2
assert ij is not ij2
def test_set():
ij = OrderedInjection()
ij[0] = 0
assert ij[0] == 0
assert 1 not in ij
ij[1] = 1
assert ij[0] == 0
assert ij[1] == 1
ij[0] = 1
assert ij[0] == 1
assert 1 not in ij
ij[1] = 0
assert ij[0] == 1
assert ij[1] == 0
ij[1] = 1
assert 0 not in ij
assert ij[1] == 1
def test_order():
ij = OrderedInjection()
ij[0] = 0
assert list(ij) == [0]
ij[1] = 1
assert list(ij) == [0, 1]
ij[0] = 1
assert list(ij) == [0]
ij[1] = 0
assert list(ij) == [0, 1]
ij[1] = 1
assert list(ij) == [1]
ij[1] = 0
assert list(ij) == [1]
ij[0] = 1
assert list(ij) == [1, 0]
ij[1.0] = 0
assert list(ij) == [1, 0]
del ij[0]
assert list(ij) == [1]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment