Repository: HypothesisWorks/hypothesis

Issue:
Wrong data generation using dataclasses, Sequence and from_type
Hypothesis `from_type` sometimes generates bytes when the dataclass has an attribute of type `Sequence[X]`:

```python
>>> from dataclasses import dataclass
>>> from typing import Sequence
>>> @dataclass
... class Item:
...     name: str
...     

>>> @dataclass
... class Row:
...     name: str
...     items: Sequence[Item]
...     

>>> strategies.from_type(Row).example()   # <--- Wrong data type
Row(name='', items=b'\x00')

>>> strategies.from_type(Row).example()
Row(name='\x13', items=[])

>>> strategies.from_type(Row).example()
Row(name='', items=[Item(name='')])

```


Discussion:
I'm using hypothesis 4.50.6.  But the issue is reproducible in master, commit 89d494dcb7ba4368b4dd9e78fdb8480efc080005
The issue seems to be in the implementation of `from_typing_thing`:

```python
>>> from_typing_type(Sequence[Item])                                                                                                                                                          
one_of(binary(), lists(elements=builds(Item)))
```
which includes `binary()`.
@Zac-HD 

I found the underlying cause is that:

```
>>> try_issubclass(typing.ByteString, Sequence[Item])                                                                                                                                         
True
```
I think `typing.ByteString` should not be a subclass of `Sequence`.  There are other things to consider:

```python
>>> from_typing_type(Sequence)                                                                                                                                                                
one_of(binary(), lists(elements=text()))
```
One may argue that if `binary()` is valid, so is `text()`, and the result should be `one_of(binary(), text(), lists(elements=text()))`.

---

In Python we don't have a type for a single char and or byte; so it's difficult to express `Sequence[Byte]` or `Sequence[Char]`; and make `ByteString` a subclass of the first and `str` and subclass of the last.
@Zac-HD 

I've created a PR #2258 but I feel it deserves a tests.  However, I'm not sure if it's ok to depends of `dataclasses` for tests.

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/types.py b/hypothesis-python/src/hypothesis/strategies/_internal/types.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/types.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/types.py
@@ -189,6 +189,28 @@ def from_typing_type(thing):
         # such as Container
         for t in (typing.KeysView, typing.ValuesView, typing.ItemsView):
             mapping.pop(t, None)
+    if len(mapping) > 1:
+        # issubclass treats bytestring as a kind of sequence, which it is,
+        # but treating it as such breaks everything else when it is presumed
+        # to be a generic sequence or container that could hold any item.
+        # Except for sequences of integers, or unions which include integer!
+        # See https://github.com/HypothesisWorks/hypothesis/issues/2257
+        #
+        # This block drops ByteString from the types that can be generated
+        # if there is more than one allowed type, and the element type is
+        # not either `int` or a Union with `int` as one of its elements.
+        elem_type = (getattr(thing, "__args__", None) or ["not int"])[0]
+        if getattr(elem_type, "__origin__", None) is typing.Union:
+            union_elems = elem_type.__args__
+        elif hasattr(elem_type, "__union_params__"):  # pragma: no cover
+            union_elems = elem_type.__union_params__  # python 3.5 only
+        else:
+            union_elems = ()
+        if PY2 or not any(
+            isinstance(T, type) and issubclass(int, T)
+            for T in list(union_elems) + [elem_type]
+        ):
+            mapping.pop(typing.ByteString, None)
     strategies = [
         v if isinstance(v, st.SearchStrategy) else v(thing)
         for k, v in mapping.items()
@@ -306,7 +328,7 @@ def from_typing_type(thing):
 else:
     _global_type_lookup.update(
         {
-            typing.ByteString: st.binary(),
+            typing.ByteString: st.binary() | st.binary().map(bytearray),
             # Reversible is somehow a subclass of Hashable, so we tuplize it.
             # See also the discussion at https://bugs.python.org/issue39046
             typing.Reversible: st.lists(st.integers()).map(tuple),  # type: ignore


Test patch:
diff --git a/hypothesis-python/tests/py3/test_lookup.py b/hypothesis-python/tests/py3/test_lookup.py
--- a/hypothesis-python/tests/py3/test_lookup.py
+++ b/hypothesis-python/tests/py3/test_lookup.py
@@ -23,6 +23,8 @@
 import io
 import string
 import sys
+import typing
+from numbers import Real
 
 import pytest
 
@@ -40,7 +42,6 @@
 from tests.common.debug import find_any, minimal
 from tests.common.utils import fails_with
 
-typing = pytest.importorskip("typing")
 sentinel = object()
 generics = sorted(
     (t for t in types._global_type_lookup if isinstance(t, typing_root_type)), key=str
@@ -574,3 +575,23 @@ def test_inference_on_generic_collections_abc_aliases(typ, data):
     # see https://github.com/HypothesisWorks/hypothesis/issues/2272
     value = data.draw(st.from_type(typ))
     assert isinstance(value, typ)
+
+
+@given(st.from_type(typing.Sequence[set]))
+def test_bytestring_not_treated_as_generic_sequence(val):
+    # Check that we don't fall into the specific problem from
+    # https://github.com/HypothesisWorks/hypothesis/issues/2257
+    assert not isinstance(val, typing.ByteString)
+    # Check it hasn't happened again from some other non-generic sequence type.
+    for x in val:
+        assert isinstance(x, set)
+
+
+@pytest.mark.parametrize(
+    "type_", [int, Real, object, typing.Union[int, str], typing.Union[Real, str]]
+)
+def test_bytestring_is_valid_sequence_of_int_and_parent_classes(type_):
+    find_any(
+        st.from_type(typing.Sequence[type_]),
+        lambda val: isinstance(val, typing.ByteString),
+    )