Repository: python-attrs/attrs (python)

Issue:
Poor error message/type for validators.in_ with a string
```python
import attr

@attr.s
class C:
   s = attr.ib(validator=attr.validators.in_('abc'))

C(s=1)  # TypeError: 'in <string>' requires string as left operand, not int
```

`__contains__` behaves a little weirdly for strings, but I still think this error could be improved.

Obvious options would be to catch exceptions and re-raise something with a clearer error message, and/or to deprecate use of strings as the collection here.

Found in attrs=18.1.0 while working on HypothesisWorks/hypothesis#954.

Pull request description:
Improve error for in_(a_string) with a non-string value
- [x] Added **tests** for changed code.
- [x] Changes (and possible deprecations) have news fragments in [`changelog.d`](https://github.com/python-attrs/attrs/blob/master/changelog.d).
- [x] Is my first attrs PR :heart_eyes:

Closes #382.

Patch:
diff --git a/changelog.d/383.change.rst b/changelog.d/383.change.rst
new file mode 100644
index 0000000..6600b5b
--- /dev/null
+++ b/changelog.d/383.change.rst
@@ -0,0 +1,2 @@
+``attr.validators.in_()`` now raises a ``ValueError`` with a useful message even if the options are a string and the value is not a string.
+This previously raised a ``TypeError``, like ``1 in "abc"``.
diff --git a/src/attr/validators.py b/src/attr/validators.py
index f8892fc..4904e61 100644
--- a/src/attr/validators.py
+++ b/src/attr/validators.py
@@ -135,7 +135,12 @@ class _InValidator(object):
     options = attrib()
 
     def __call__(self, inst, attr, value):
-        if value not in self.options:
+        try:
+            in_options = value in self.options
+        except TypeError as e:  # e.g. `1 in "abc"`
+            in_options = False
+
+        if not in_options:
             raise ValueError(
                 "'{name}' must be in {options!r} (got {value!r})"
                 .format(name=attr.name, options=self.options, value=value)


Test patch:
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 0e67f99..4d24ce9 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -243,6 +243,19 @@ class TestIn_(object):
             "'test' must be in [1, 2, 3] (got None)",
         ) == e.value.args
 
+    def test_fail_with_string(self):
+        """
+        Raise ValueError if the value is outside our options when the
+        options are specified as a string and the value is not a string.
+        """
+        v = in_("abc")
+        a = simple_attr("test")
+        with pytest.raises(ValueError) as e:
+            v(None, a, None)
+        assert (
+            "'test' must be in 'abc' (got None)",
+        ) == e.value.args
+
     def test_repr(self):
         """
         Returned validator has a useful `__repr__`.