Repository: HypothesisWorks/hypothesis

Issue:
Invalid failing example description with `hypothesis.stateful.multiple` containing single value
On my machine with `Python 3.9.5` and `hypothesis==6.37.1` (latest at the moment of writing) test case
```python
from hypothesis.stateful import (Bundle,
                                 RuleBasedStateMachine,
                                 multiple,
                                 rule)


class State(RuleBasedStateMachine):
    values = Bundle('values')

    @rule(target=values)
    def create_single_multiple(self):
        return multiple(42)

    @rule(value=values)
    def check_value(self, value):
        assert value != 42


TestCase = State.TestCase
```
gives
```bash
Falsifying example:
    state = State()
    v1 = state.create_single_multiple()
    state.check_value(value=v1)
    state.teardown()
```
which will succeed because `v1` is `MultipleResults(values=(42,))` but I think it should be `42`, so somewhere in depths of generating failing example for `RuleBasedStateMachine` we need to handle special case with single-value `MultipleResults` so the output will be
```bash
Falsifying example:
    state = State()
    v1, = state.create_single_multiple()
    # ^ note this comma
    state.check_value(value=v1)
    state.teardown()
```



Discussion:
I found a place in code which responsible for this behavior
https://github.com/HypothesisWorks/hypothesis/blob/887e227003e93710f037632c58de71e183bf1930/hypothesis-python/src/hypothesis/stateful.py#L343

I've managed to fix this locally by changing it to
```python
            output_vars = self._last_names(n_output_vars)
            output_assignment = (output_vars[0] + ","
                                 if (isinstance(result, MultipleResults)
                                     and n_output_vars == 1)
                                 else ", ".join(output_vars)) + " = "
```


Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/stateful.py b/hypothesis-python/src/hypothesis/stateful.py
--- a/hypothesis-python/src/hypothesis/stateful.py
+++ b/hypothesis-python/src/hypothesis/stateful.py
@@ -333,16 +333,16 @@ def invariants(cls):
 
     def _print_step(self, rule, data, result):
         self.step_count = getattr(self, "step_count", 0) + 1
-        # If the step has target bundles, and the result is a MultipleResults
-        # then we want to assign to multiple variables.
-        if isinstance(result, MultipleResults):
-            n_output_vars = len(result.values)
-        else:
-            n_output_vars = 1
-        if rule.targets and n_output_vars >= 1:
-            output_assignment = ", ".join(self._last_names(n_output_vars)) + " = "
-        else:
-            output_assignment = ""
+        output_assignment = ""
+        if rule.targets:
+            if isinstance(result, MultipleResults):
+                if len(result.values) == 1:
+                    output_assignment = f"({self._last_names(1)[0]},) = "
+                elif result.values:
+                    output_names = self._last_names(len(result.values))
+                    output_assignment = ", ".join(output_names) + " = "
+            else:
+                output_assignment = self._last_names(1)[0] + " = "
         report(
             "{}state.{}({})".format(
                 output_assignment,


Test patch:
diff --git a/hypothesis-python/tests/cover/test_stateful.py b/hypothesis-python/tests/cover/test_stateful.py
--- a/hypothesis-python/tests/cover/test_stateful.py
+++ b/hypothesis-python/tests/cover/test_stateful.py
@@ -228,6 +228,32 @@ def fail_fast(self):
         state.fail_fast()
 
 
+def test_multiple_variables_printed_single_element():
+    # https://github.com/HypothesisWorks/hypothesis/issues/3236
+    class ProducesMultiple(RuleBasedStateMachine):
+        b = Bundle("b")
+
+        @initialize(target=b)
+        def populate_bundle(self):
+            return multiple(1)
+
+        @rule(b=b)
+        def fail_fast(self, b):
+            assert b != 1
+
+    with capture_out() as o, raises(AssertionError):
+        run_state_machine_as_test(ProducesMultiple)
+
+    assignment_line = o.getvalue().split("\n")[2]
+    assert assignment_line == "(v1,) = state.populate_bundle()"
+
+    state = ProducesMultiple()
+    (v1,) = state.populate_bundle()
+    state.fail_fast((v1,))  # passes if tuple not unpacked
+    with raises(AssertionError):
+        state.fail_fast(v1)
+
+
 def test_no_variables_printed():
     class ProducesNoVariables(RuleBasedStateMachine):
         b = Bundle("b")