Repository: isi-vista/immutablecollections

Issue:
Cannot initialize an `immutableset` from a `KeysView`
Because it fails on the check for `isinstance(AbstractSet)`

Comments:

Patch:
diff --git a/immutablecollections/_immutableset.py b/immutablecollections/_immutableset.py
index 877f774..4ed06f3 100644
--- a/immutablecollections/_immutableset.py
+++ b/immutablecollections/_immutableset.py
@@ -32,6 +32,7 @@ T = TypeVar("T")  # pylint:disable=invalid-name
 # necessary because inner classes cannot share typevars
 T2 = TypeVar("T2")  # pylint:disable=invalid-name
 SelfType = TypeVar("SelfType")  # pylint:disable=invalid-name
+ViewTypes = (KeysView, ValuesView, ItemsView)  # pylint:disable=invalid-name
 
 
 def immutableset(
@@ -61,30 +62,31 @@ def immutableset(
         return iterable
 
     if not disable_order_check:
-        # case where iterable is an ImmutableSet (which is order-safe)
-        # is already checked above
-        if isinstance(iterable, AbstractSet):
-            raise ValueError(
-                "Attempting to initialize an ImmutableSet from "
-                "a non-ImmutableSet set. This probably loses "
-                "determinism in iteration order.  If you don't care "
-                "or are otherwise sure your input has determinstic "
-                "iteration order, specify disable_order_check=True"
-            )
-        # we put this check on the outside to avoid isinstance checks on newer Python versions
         if not DICT_ITERATION_IS_DETERMINISTIC:
-            if (
-                isinstance(iterable, KeysView)
-                or isinstance(iterable, ValuesView)
-                or isinstance(iterable, ItemsView)
-            ):
+            # See https://github.com/isi-vista/immutablecollections/pull/36
+            # for benchmarks as to why the split conditional is faster even
+            # with short-circuiting.
+            if isinstance(iterable, ViewTypes):
                 raise ValueError(
                     "Attempting to initialize an ImmutableSet from "
                     "a dict view. On this Python version, this probably loses "
                     "determinism in iteration order.  If you don't care "
-                    "or are otherwise sure your input has determinstic "
+                    "or are otherwise sure your input has deterministic "
                     "iteration order, specify disable_order_check=True"
                 )
+        if isinstance(iterable, AbstractSet) and not isinstance(iterable, ViewTypes):
+            # dict order is deterministic in this interpreter, so order
+            # of KeysView and ItemsView from standard dicts will be as well.
+            # These could be user implementations of this interface which are
+            # non-deterministic, but this check is just a courtesy to catch the
+            # most common cases anyway.
+            raise ValueError(
+                "Attempting to initialize an ImmutableSet from "
+                "a non-ImmutableSet set. This probably loses "
+                "determinism in iteration order.  If you don't care "
+                "or are otherwise sure your input has deterministic "
+                "iteration order, specify disable_order_check=True"
+            )
 
     iteration_order = []
     containment_set: MutableSet[T] = set()
diff --git a/immutablecollections/newsfragments/35.bugfix b/immutablecollections/newsfragments/35.bugfix
new file mode 100644
index 0000000..cc498a1
--- /dev/null
+++ b/immutablecollections/newsfragments/35.bugfix
@@ -0,0 +1,1 @@
+immutableset can now be created from KeysView, ItemsView if dict iteration is deterministic.
diff --git a/pyproject.toml b/pyproject.toml
index eeb030c..4e83571 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,6 +18,11 @@
         name = "New Features"
         showcontent = true
 
+    [[tool.towncrier.type]]
+        directory = "Bugfix"
+        name = "Bug Fixes"
+        showcontent = true
+
 [tool.black]
 line-length = 90
 


Test patch:
diff --git a/tests/test_immutableset.py b/tests/test_immutableset.py
index 8fd8d59..6b54fc4 100644
--- a/tests/test_immutableset.py
+++ b/tests/test_immutableset.py
@@ -1,7 +1,8 @@
 from collections.abc import Set
 from unittest import TestCase
+from unittest.mock import patch
 
-from immutablecollections import ImmutableList, ImmutableSet, immutableset
+from immutablecollections import ImmutableSet, immutableset
 
 
 class TestImmutableSet(TestCase):
@@ -224,3 +225,34 @@ class TestImmutableSet(TestCase):
         source = (1, 2, 3)
         dict1 = immutableset(source)
         return next(iter(dict1))
+
+    # A reminder that when patching, it must be done where used, not where it is defined.
+    @patch("immutablecollections._immutableset.DICT_ITERATION_IS_DETERMINISTIC", False)
+    def test_dict_iteration_is_not_deterministic(self):
+        source = {"b": 2, "c": 3, "a": 1}
+        value_error_regex = r"Attempting to initialize an ImmutableSet from a dict view\."
+        with self.assertRaisesRegex(ValueError, value_error_regex):
+            immutableset(source.keys())
+        with self.assertRaisesRegex(ValueError, value_error_regex):
+            immutableset(source.values())
+        with self.assertRaisesRegex(ValueError, value_error_regex):
+            immutableset(source.items())
+
+        with self.assertRaisesRegex(
+            ValueError,
+            r"Attempting to initialize an ImmutableSet from a non-ImmutableSet set\.",
+        ):
+            immutableset({"b", "c", "a"})
+
+    @patch("immutablecollections._immutableset.DICT_ITERATION_IS_DETERMINISTIC", True)
+    def test_dict_iteration_is_deterministic(self):
+        source = {"b": 2, "c": 3, "a": 1}
+        immutableset(source.keys())
+        immutableset(source.values())
+        immutableset(source.items())
+
+        with self.assertRaisesRegex(
+            ValueError,
+            r"Attempting to initialize an ImmutableSet from a non-ImmutableSet set\.",
+        ):
+            immutableset({"b", "c", "a"})