Repository: isi-vista/immutablecollections

Issue:
Get inverted copy of ImmutableMultiDicts

Comments:

Patch:
diff --git a/immutablecollections/immutablemultidict.py b/immutablecollections/immutablemultidict.py
index 2a9ec3c..f22ee91 100644
--- a/immutablecollections/immutablemultidict.py
+++ b/immutablecollections/immutablemultidict.py
@@ -1,4 +1,4 @@
-from abc import ABCMeta, abstractmethod
+from abc import ABCMeta, abstractmethod, ABC
 from collections import defaultdict
 from typing import AbstractSet, Any, Callable, Collection, Generic, Iterable, Iterator, Mapping, \
     MutableMapping, Optional, Tuple, TypeVar, Union, \
@@ -80,6 +80,44 @@ class ImmutableMultiDict(ImmutableCollection[KT], Generic[KT, VT], metaclass=ABC
     def __iter__(self) -> Iterator[KT]:
         return self.as_dict().__iter__()
 
+    def invert_to_list_multidict(self) -> 'ImmutableListMultiDict[VT, KT]':
+        """
+        Get the inverse of this multidict as a list multidict.
+
+        The returned ``ImmutableListMultiDict`` will contain `(v, k)` one time for each
+        key-value pair `(k, v)` in this multidict.
+        """
+        sink: ImmutableListMultiDict.Builder[VT, KT] = ImmutableListMultiDict.builder()
+        self._invert_to(sink)
+        return sink.build()
+
+    def invert_to_set_multidict(self) -> 'ImmutableSetMultiDict[VT, KT]':
+        """
+        Get the inverse of this multidict as a set multidict.
+
+        The returned ``ImmutableSetMultiDict`` will contain `(v, k)` if and only if
+        `(k, v)` is a key-value pair in this multidict.
+        """
+        sink: ImmutableSetMultiDict.Builder[VT, KT] = ImmutableSetMultiDict.builder()
+        self._invert_to(sink)
+        return sink.build()
+
+    def _invert_to(self, sink: 'ImmutableMultiDict.Builder[VT, KT]') -> None:
+        for (k, v) in self.items():
+            sink.put(v, k)
+
+    class Builder(ABC, Generic[KT2, VT2]):
+        @abstractmethod
+        def put(self: SelfType, key: KT2, value: VT2) -> SelfType:
+            """
+            Add a mapping from ``key`` to ``value``
+            """
+
+        def build(self) -> 'ImmutableMultiDict[KT2, VT2]':
+            """
+            Get a multidict with all key-value pairs given to this builder.
+            """
+
 
 # needs tests: issue #127
 class ImmutableSetMultiDict(ImmutableMultiDict[KT, VT], metaclass=ABCMeta):
@@ -177,7 +215,7 @@ class ImmutableSetMultiDict(ImmutableMultiDict[KT, VT], metaclass=ABCMeta):
         # need the "i" prefix they add with repr
         return "{%s}" % ", ".join("%r: %s" % item for item in self.as_dict().items())
 
-    class Builder(Generic[KT2, VT2]):
+    class Builder(Generic[KT2, VT2], ImmutableMultiDict.Builder[KT2, VT2]):
         def __init__(self, *, source: Optional['ImmutableMultiDict[KT2,VT2]'] = None,
                      order_key: Callable[[VT2], Any] = None) -> None:
             self._dict: MutableMapping[KT2, ImmutableSet.Builder[VT2]] = defaultdict(
@@ -351,7 +389,7 @@ class ImmutableListMultiDict(ImmutableMultiDict[KT, VT], metaclass=ABCMeta):
         # need the "i" prefix they add with repr
         return "{%s}" % ", ".join("%r: %s" % item for item in self.as_dict().items())
 
-    class Builder(Generic[KT2, VT2]):
+    class Builder(Generic[KT2, VT2], ImmutableMultiDict.Builder[KT2, VT2]):
         def __init__(self, *, source: Optional['ImmutableMultiDict[KT2,VT2]'] = None) -> None:
             self._dict: MutableMapping[KT2, ImmutableList.Builder[VT2]] = defaultdict(
                 ImmutableList.builder)
diff --git a/immutablecollections/newsfragments/8.feature b/immutablecollections/newsfragments/8.feature
new file mode 100644
index 0000000..5f2d9e0
--- /dev/null
+++ b/immutablecollections/newsfragments/8.feature
@@ -0,0 +1,1 @@
+Support inversion of MultiDicts


Test patch:
diff --git a/tests/test_immutablemultidict.py b/tests/test_immutablemultidict.py
index d3c1baf..41e6c5d 100644
--- a/tests/test_immutablemultidict.py
+++ b/tests/test_immutablemultidict.py
@@ -67,6 +67,20 @@ class TestImmutableSetMultiDict(TestCase):
         # len's implementation often does caching, so test it works twice
         self.assertEqual(4, len(x))
 
+    def test_inversion(self):
+        x = ImmutableSetMultiDict.of({1: [2, 2, 3, 6], 4: [5, 6]})
+        # when you start from a set multidict, your inverses as a list
+        # and set multidict both contain the same items, since there
+        # are no duplicate mappings in the source
+        reference_set_based = ImmutableSetMultiDict.of({
+            2: [1], 3: [1], 5: [4], 6: [1, 4]
+        })
+        reference_list_based = ImmutableListMultiDict.of({
+            2: [1], 3: [1], 5: [4], 6: [1, 4]
+        })
+        self.assertEqual(reference_set_based, x.invert_to_set_multidict())
+        self.assertEqual(reference_list_based, x.invert_to_list_multidict())
+
 
 class TestImmutableListMultiDict(TestCase):
     def test_empty(self):
@@ -164,3 +178,16 @@ class TestImmutableListMultiDict(TestCase):
         # len's implementation often does caching, so test it works twice
         self.assertEqual(5, len(x))
 
+    def test_inversion(self):
+        x = ImmutableListMultiDict.of({1: [2, 2, 3, 6], 4: [5, 6]})
+        reference_set_based = ImmutableSetMultiDict.of({
+            2: [1], 3: [1], 5: [4], 6: [1, 4]
+        })
+        # 2->1 appears twice because 1->2 appears twice in the source
+        reference_list_based = ImmutableListMultiDict.of({
+            2: [1, 1], 3: [1], 5: [4], 6: [1, 4]
+        })
+        self.assertEqual(reference_set_based, x.invert_to_set_multidict())
+        self.assertEqual(reference_list_based, x.invert_to_list_multidict())
+
+