Last active
August 29, 2015 14:01
-
-
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!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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__)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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