Repository: HypothesisWorks/hypothesis

Issue:
Generating mixed value for enum.Flag causes pretty-printer exception
A self-contained reproduction:

```
import enum

from hypothesis import example, given
from hypothesis import strategies as st


class MyFlag(enum.Flag):
    ONE = 1
    TWO = 2


@given(st.sampled_from(MyFlag))
@example(MyFlag.ONE | MyFlag.TWO)
def test_pretty_printing(flag: MyFlag):
    assert isinstance(flag, MyFlag)
```

Tests fail with the following error message:

```
test_hypothesis_bug.py::test_pretty_printing Failed: [undefined]TypeError: can only concatenate str (not "NoneType") to str
@given(st.sampled_from(MyFlag))
>   @example(MyFlag.ONE | MyFlag.TWO)

tests/unit/test_hypothesis_bug.py:13: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.10/site-packages/hypothesis/core.py:1056: in _raise_to_user
    raise the_error_hypothesis_found
.venv/lib/python3.10/site-packages/hypothesis/core.py:470: in execute_explicit_examples
    bits = ", ".join(nicerepr(x) for x in arguments) + ", ".join(
.venv/lib/python3.10/site-packages/hypothesis/core.py:471: in <genexpr>
    f"{k}={nicerepr(v)}" for k, v in example_kwargs.items()
.venv/lib/python3.10/site-packages/hypothesis/internal/reflection.py:452: in nicerepr
    return re.sub(r"(\[)~([A-Z][a-z]*\])", r"\g<1>\g<2>", pretty(v))
.venv/lib/python3.10/site-packages/hypothesis/vendor/pretty.py:101: in pretty
    printer.pretty(obj)
.venv/lib/python3.10/site-packages/hypothesis/vendor/pretty.py:199: in pretty
    return printer(obj, self, cycle)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

obj = <MyFlag.TWO|ONE: 3>
p = <hypothesis.vendor.pretty.RepresentationPrinter object at 0x7fc633f41090>
cycle = False

    def _repr_enum(obj, p, cycle):
>       p.text(type(obj).__name__ + "." + obj.name)
E       TypeError: can only concatenate str (not "NoneType") to str

.venv/lib/python3.10/site-packages/hypothesis/vendor/pretty.py:804: TypeError
```
Root cause: mixed-value flags don't have a name.

Python 3.10
hypothesis 6.82.0
6.82.0 does not build with Python 3.8
Hypothesis 6.82.0 does not build with Python 3.8:

```
Traceback (most recent call last):
  File "setup.py", line 31, in <module>
    SOURCE = str(local_file("src"))
  File "setup.py", line 28, in local_file
    return Path(__file__).parent.joinpath(name).relative_to(Path.cwd())
  File "/usr/pkg/lib/python3.8/pathlib.py", line 908, in relative_to
    raise ValueError("{!r} does not start with {!r}"
ValueError: 'src' does not start with '/tmp/devel/py-hypothesis/work/hypothesis-6.82.0'
```

That's because Python 3.8 sets `__file__` as a relative path.

Here is my fix:

```
--- setup.py.orig	2023-07-29 15:30:00.000000000 +0000
+++ setup.py
@@ -25,7 +25,7 @@ if sys.version_info[:2] < (3, 8):
 
 
 def local_file(name):
-    return Path(__file__).parent.joinpath(name).relative_to(Path.cwd())
+    return Path(__file__).absolute().parent.joinpath(name).relative_to(Path.cwd())
 
 
 SOURCE = str(local_file("src"))
```


Discussion:
Thanks for the report!  This is a neat little bug, fortunately the solution should be pretty self-contained in that `_repr_enum()` function for whoever gets to it first.


Resolving patch:
diff --git a/hypothesis-python/setup.py b/hypothesis-python/setup.py
--- a/hypothesis-python/setup.py
+++ b/hypothesis-python/setup.py
@@ -25,7 +25,7 @@
 
 
 def local_file(name):
-    return Path(__file__).parent.joinpath(name).relative_to(Path.cwd())
+    return Path(__file__).absolute().parent.joinpath(name).relative_to(Path.cwd())
 
 
 SOURCE = str(local_file("src"))
diff --git a/hypothesis-python/src/hypothesis/vendor/pretty.py b/hypothesis-python/src/hypothesis/vendor/pretty.py
--- a/hypothesis-python/src/hypothesis/vendor/pretty.py
+++ b/hypothesis-python/src/hypothesis/vendor/pretty.py
@@ -69,6 +69,7 @@ def _repr_pretty_(self, p, cycle):
 import warnings
 from collections import defaultdict, deque
 from contextlib import contextmanager
+from enum import Flag
 from io import StringIO
 from math import copysign, isnan
 
@@ -801,7 +802,11 @@ def _repr_dataframe(obj, p, cycle):  # pragma: no cover
 
 
 def _repr_enum(obj, p, cycle):
-    p.text(type(obj).__name__ + "." + obj.name)
+    tname = type(obj).__name__
+    if isinstance(obj, Flag):
+        p.text(" | ".join(f"{tname}.{x.name}" for x in type(obj) if x & obj == x))
+    else:
+        p.text(f"{tname}.{obj.name}")
 
 
 for_type_by_name("collections", "defaultdict", _defaultdict_pprint)


Test patch:
diff --git a/hypothesis-python/tests/cover/test_pretty.py b/hypothesis-python/tests/cover/test_pretty.py
--- a/hypothesis-python/tests/cover/test_pretty.py
+++ b/hypothesis-python/tests/cover/test_pretty.py
@@ -50,7 +50,7 @@
 import re
 import warnings
 from collections import Counter, OrderedDict, defaultdict, deque
-from enum import Enum
+from enum import Enum, Flag
 
 import pytest
 
@@ -628,5 +628,40 @@ class AnEnum(Enum):
     SOME_MEMBER = 1
 
 
-def test_pretty_prints_enums_as_code():
-    assert pretty.pretty(AnEnum.SOME_MEMBER) == "AnEnum.SOME_MEMBER"
+class Options(Flag):
+    A = 1
+    B = 2
+    C = 4
+
+
+class EvilReprOptions(Flag):
+    A = 1
+    B = 2
+
+    def __repr__(self):
+        return "can't parse this nonsense"
+
+
+class LyingReprOptions(Flag):
+    A = 1
+    B = 2
+
+    def __repr__(self):
+        return "LyingReprOptions.A|B|C"
+
+
+@pytest.mark.parametrize(
+    "rep",
+    [
+        "AnEnum.SOME_MEMBER",
+        "Options.A",
+        "Options.A | Options.B",
+        "Options.A | Options.B | Options.C",
+        "EvilReprOptions.A",
+        "LyingReprOptions.A",
+        "EvilReprOptions.A | EvilReprOptions.B",
+        "LyingReprOptions.A | LyingReprOptions.B",
+    ],
+)
+def test_pretty_prints_enums_as_code(rep):
+    assert pretty.pretty(eval(rep)) == rep