Repository: isi-vista/immutablecollections

Issue:
Update allowed input types for ImmutableDict creation
`Mapping`s were previously not allowed in the module-level constructor. Replace `dict` with `Mapping`, but still perform ordering checks internally.

Constantine says:
> we are ok with taking the risk that the user can pass a non-deterministically ordered `Mapping` to `immutabledict` as long as it isn't a known-non-deterministic `dict`

Comments:

Patch:
diff --git a/immutablecollections/__init__.py b/immutablecollections/__init__.py
index a3c516f..af96c53 100644
--- a/immutablecollections/__init__.py
+++ b/immutablecollections/__init__.py
@@ -27,7 +27,11 @@ from attr.exceptions import FrozenInstanceError
 
 from immutablecollections.immutablecollection import ImmutableCollection
 from immutablecollections._immutabledict import immutabledict, ImmutableDict
-from immutablecollections._immutableset import immutableset, ImmutableSet
+from immutablecollections._immutableset import (
+    immutableset,
+    immutableset_from_unique_elements,
+    ImmutableSet,
+)
 from immutablecollections._immutablemultidict import (
     ImmutableSetMultiDict,
     ImmutableListMultiDict,
diff --git a/immutablecollections/_immutabledict.py b/immutablecollections/_immutabledict.py
index 36a2907..924c46a 100644
--- a/immutablecollections/_immutabledict.py
+++ b/immutablecollections/_immutabledict.py
@@ -23,18 +23,16 @@ IT = Tuple[KT, VT]
 # cannot share type variables between outer and inner classes
 KT2 = TypeVar("KT2")
 VT2 = TypeVar("VT2")
-IT2 = Tuple[KT, VT]
+IT2 = Tuple[KT2, VT2]
 
 SelfType = TypeVar("SelfType")  # pylint:disable=invalid-name
 
-AllowableDictType = Union[  # pylint:disable=invalid-name
-    "ImmutableDict[KT, VT]", Dict[KT, VT]
-]
+AllowableSourceType = Union[Iterable[IT], Mapping[KT, VT], "ImmutableDict[KT, VT]"]
 InstantiationTypes = (Mapping, Iterable)  # pylint:disable=invalid-name
 
 
 def immutabledict(
-    iterable: Optional[Union[Iterable[Tuple[KT, VT]], AllowableDictType]] = None
+    iterable: Optional[AllowableSourceType] = None
 ) -> "ImmutableDict[KT, VT]":
     """
     Create an immutable dictionary with the given mappings.
@@ -88,7 +86,7 @@ class ImmutableDict(ImmutableCollection[KT], Mapping[KT, VT], metaclass=ABCMeta)
     # Signature of the of method varies by collection
     # pylint: disable = arguments-differ
     @staticmethod
-    def of(dict_: Union[Mapping[KT, VT], Iterable[IT]]) -> "ImmutableDict[KT, VT]":
+    def of(dict_: AllowableSourceType) -> "ImmutableDict[KT, VT]":
         """
         Deprecated - prefer ``immutabledict`` module-level factory
         """
diff --git a/immutablecollections/_immutableset.py b/immutablecollections/_immutableset.py
index 73920ae..593b362 100644
--- a/immutablecollections/_immutableset.py
+++ b/immutablecollections/_immutableset.py
@@ -34,7 +34,10 @@ ViewTypes = (KeysView, ValuesView, ItemsView)  # pylint:disable=invalid-name
 
 
 def immutableset(
-    iterable: Optional[Iterable[T]] = None, *, disable_order_check: bool = False
+    iterable: Optional[Iterable[T]] = None,
+    *,
+    disable_order_check: bool = False,
+    forbid_duplicate_elements: bool = False,
 ) -> "ImmutableSet[T]":
     """
     Create an immutable set with the given contents.
@@ -48,6 +51,9 @@ def immutableset(
     could be removed in the future and should not be relied upon.  This check may be disabled
     by setting *disable_order_check* to ``True``.
 
+    If *forbid_duplicate_elements* is ``True`` and one item occurs twice in *iterable*, then
+    a ``ValueError`` will be thrown.
+
     If *iterable* is already an ``ImmutableSet``, *iterable* itself will be returned.
     """
     # immutableset() should return an empty set
@@ -92,6 +98,10 @@ def immutableset(
         if value not in containment_set:
             containment_set.add(value)
             iteration_order.append(value)
+        elif forbid_duplicate_elements:
+            raise ValueError(
+                "Input collection has duplicate items and forbid_duplicate_elements=False"
+            )
 
     if iteration_order:
         if len(iteration_order) == 1:
@@ -102,6 +112,18 @@ def immutableset(
         return _EMPTY
 
 
+def immutableset_from_unique_elements(
+    iterable: Optional[Iterable[T]] = None, *, disable_order_check: bool = False
+):
+    """
+    Create an immutableset from *iterable*. If one item occurs twice in *iterable*, then
+    a ``ValueError`` will be thrown. More information in ``immutablecollections.immutableset``.
+    """
+    return immutableset(
+        iterable, disable_order_check=disable_order_check, forbid_duplicate_elements=True
+    )
+
+
 # typing.AbstractSet matches collections.abc.Set
 class ImmutableSet(  # pylint: disable=duplicate-bases
     Generic[T],


Test patch:
diff --git a/tests/test_immutableset.py b/tests/test_immutableset.py
index 2cc00a7..ead7932 100644
--- a/tests/test_immutableset.py
+++ b/tests/test_immutableset.py
@@ -3,7 +3,11 @@ from collections.abc import Set
 from unittest import TestCase
 from unittest.mock import patch
 
-from immutablecollections import ImmutableSet, immutableset
+from immutablecollections import (
+    ImmutableSet,
+    immutableset,
+    immutableset_from_unique_elements,
+)
 
 
 class TestImmutableSet(TestCase):
@@ -280,3 +284,13 @@ class TestImmutableSet(TestCase):
         # frozensets on LHS
         self.assertEqual({5, 6}, frozenset([4, 5, 6]) - immutableset([2, 3, 4]))
         self.assertEqual({"a"}, frozenset(["a", "b"]) - immutableset(["b", "c"]))
+
+    def test_forbid_duplicate_elements(self):
+        with self.assertRaises(ValueError):
+            immutableset([4, 6, 7, 9, 7], forbid_duplicate_elements=True)
+        immutableset([3, 7, 5, 9], forbid_duplicate_elements=True)
+
+    def test_immutableset_from_unique_elements(self):
+        with self.assertRaises(ValueError):
+            immutableset_from_unique_elements([4, 6, 7, 9, 7])
+        immutableset_from_unique_elements([3, 7, 5, 9])