Repository: HypothesisWorks/hypothesis

Issue:
`strategy.choices` should support picking items from a set.
I'd like to write code something like

``` python
ALL = {'a', 'b', 'c'}
ALLOWED = {
    'x': {'a'},
    'y': {'a','b'},
    'z': {'a,'c'},
}

@st.given(st.sample_from(ALLOWED.keys()), st.choices())
def test(which, choice):
    disallowed = choice(ALL - ALLOWED[which])
    ...
```



Discussion:
But choices strategy is similar to random.choice, which cannot work with sets as well...

That doesn't mean that `strategy.choices` can't.

Sorry for the delay on replying to this one too.

I'm not _intrinsically_ against this, but I don't currently have good ideas about how it could be possible.

The problem is that there's no actually stable way of choosing an element from a set because sets aren't ordered. This makes things like minimization and saving of the generated examples nearly impossible to do correctly.

I'll leave this open as it's not an unreasonable thing to want, but I wouldn't hold your breath. I'd recommend using lists or other indexable sequences if you want to be able to choose from them.


Resolving patch:
diff --git a/src/hypothesis/internal/conjecture/utils.py b/src/hypothesis/internal/conjecture/utils.py
--- a/src/hypothesis/internal/conjecture/utils.py
+++ b/src/hypothesis/internal/conjecture/utils.py
@@ -18,7 +18,9 @@
 from __future__ import division, print_function, absolute_import
 
 import math
+from collections import Sequence
 
+from hypothesis._settings import note_deprecation
 from hypothesis.internal.compat import hbytes, bit_length, int_to_bytes, \
     int_from_bytes
 
@@ -101,6 +103,22 @@ def centered_integer_range(data, lower, upper, center):
     )
 
 
+def check_sample(values):
+    if not isinstance(values, Sequence):
+        note_deprecation(
+            ('Cannot sample from %r, not a sequence.  ' % (values,)) +
+            'Hypothesis goes to some length to ensure that sampling an '
+            'element from a collection (with `sampled_from` or `choices`) is '
+            'replayable and can be minimised.  To replay a saved example, '
+            'the sampled values must have the same iteration order on every '
+            'run - ruling out sets, dicts, etc due to hash randomisation.  '
+            'Most cases can simply use `sorted(values)`, but mixed types or '
+            'special values such as math.nan require careful handling - and '
+            'note that when simplifying an example, Hypothesis treats '
+            'earlier values as simpler.')
+    return tuple(values)
+
+
 def choice(data, values):
     return values[integer_range(data, 0, len(values) - 1)]
 
diff --git a/src/hypothesis/searchstrategy/misc.py b/src/hypothesis/searchstrategy/misc.py
--- a/src/hypothesis/searchstrategy/misc.py
+++ b/src/hypothesis/searchstrategy/misc.py
@@ -77,7 +77,7 @@ class SampledFromStrategy(SearchStrategy):
 
     def __init__(self, elements):
         SearchStrategy.__init__(self)
-        self.elements = tuple(elements)
+        self.elements = d.check_sample(elements)
         assert self.elements
 
     def do_draw(self, data):
diff --git a/src/hypothesis/strategies.py b/src/hypothesis/strategies.py
--- a/src/hypothesis/strategies.py
+++ b/src/hypothesis/strategies.py
@@ -412,13 +412,13 @@ def sampled_from(elements):
 
     from hypothesis.searchstrategy.misc import SampledFromStrategy, \
         JustStrategy
-    elements = tuple(iter(elements))
+    from hypothesis.internal.conjecture.utils import check_sample
+    elements = check_sample(elements)
     if not elements:
         return nothing()
     if len(elements) == 1:
         return JustStrategy(elements[0])
-    else:
-        return SampledFromStrategy(elements)
+    return SampledFromStrategy(elements)
 
 
 @cacheable
@@ -955,7 +955,7 @@ def choices():
 
     """
     from hypothesis.control import note, current_build_context
-    from hypothesis.internal.conjecture.utils import choice
+    from hypothesis.internal.conjecture.utils import choice, check_sample
 
     class Chooser(object):
 
@@ -967,7 +967,7 @@ def __init__(self, build_context, data):
         def __call__(self, values):
             if not values:
                 raise IndexError('Cannot choose from empty sequence')
-            result = choice(self.data, values)
+            result = choice(self.data, check_sample(values))
             with self.build_context.local():
                 self.choice_count += 1
                 note('Choice #%d: %r' % (self.choice_count, result))


Test patch:
diff --git a/tests/cover/test_sampled_from.py b/tests/cover/test_sampled_from.py
--- a/tests/cover/test_sampled_from.py
+++ b/tests/cover/test_sampled_from.py
@@ -18,6 +18,8 @@
 from __future__ import division, print_function, absolute_import
 
 from hypothesis import given, settings
+from hypothesis.errors import HypothesisDeprecationWarning
+from tests.common.utils import fails_with
 from hypothesis.strategies import sampled_from
 
 
@@ -25,3 +27,14 @@
 @settings(min_satisfying_examples=10)
 def test_can_handle_sampling_from_fewer_than_min_satisfying(v):
     pass
+
+
+@fails_with(HypothesisDeprecationWarning)
+def test_cannot_sample_sets_in_strict_mode():
+    with settings(strict=True):
+        sampled_from(set('abc')).example()
+
+
+def test_can_sample_sets_while_deprecated():
+    with settings(strict=False):
+        assert sampled_from(set('abc')).example() in 'abc'
diff --git a/tests/nocover/test_descriptortests.py b/tests/nocover/test_descriptortests.py
--- a/tests/nocover/test_descriptortests.py
+++ b/tests/nocover/test_descriptortests.py
@@ -18,7 +18,7 @@
 from __future__ import division, print_function, absolute_import
 
 import math
-from collections import namedtuple
+from collections import Container, namedtuple
 
 from hypothesis.strategies import just, none, sets, text, lists, binary, \
     builds, floats, one_of, tuples, randoms, booleans, decimals, \
@@ -173,12 +173,20 @@ def integers_from(x):
             dictionaries(text(), js, average_size=2),
         max_leaves=10))
 
+
+def sort_nested(v):
+    if not isinstance(v, Container):
+        return v
+    v = tuple(sort_nested(e) for e in v)
+    return tuple([e for e in v if not isinstance(v, Container)] +
+                 [e for e in v if isinstance(v, Container)])
+
+
 TestWayTooClever = strategy_test_suite(
     recursive(
         frozensets(integers(), min_size=1, average_size=2.0),
-        lambda x: frozensets(x, min_size=2, max_size=4)).flatmap(
-        sampled_from
-    )
+        lambda x: frozensets(x, min_size=2, max_size=4)
+    ).map(sort_nested).flatmap(sampled_from)
 )