Repository: python-attrs/attrs

Problem statement:
attr default based on other attributes
Hi!

I have unsucessfully tried to define a default value by referencing other attributes. I'm sure the code below doesnt' work for some obvious or fundamental reason, but I would be grateful for comments on how to do something like it:

```python
import attr
from attr.validators import instance_of
import datetime

@attr.s
class Something:
    some_date = attr.ib(validator=instance_of(datetime.date))
    some_number = attr.ib(convert=float)
    name = attr.ib(validator=instance_of(str),
                   default="Generic Name {0} - {1}%".format(
                       some_date.strftime("%d-%b-%Y"),
                       some_number * 100)
                   )

s = Something(some_date=datetime.date.today(), some_number=0.375)
```

I included the `.strftime()` conversion to highlight that `name` doesn't see a float and a date, but a `_CountingAttr` object, hence I get an AttributeError (and a TypeError for `some_number * 100`). Since I can't reference self either, what would be the correct way to do this?


Hints:
Hi,

for now I suggest using `__attrs_post_init__` (http://attrs.readthedocs.io/en/stable/examples.html?highlight=attrs_post_init#other-goodies). The linked example is basically what you want.
I have wanted to do this several times. For example, you have a test of a function which takes many inputs. You want to test this function with many different cases. So (of course) you create a quick e.g. `@attr.s class TestInput:...` for that. In many cases you want some attribute to have a default depending on other attributes, but still be able to override it. The existing options are not so good for this:

- I think you can use `these` for this but it's cumbersome.
- `__attrs_post_init__` is not good because you can't override it.

What I'd want, following the ad-hoc validators example, is something like this:

```
@attr.s
class C:
    x = attr.ib()
    y = attr.ib()
    z = attr.ib()

    @z.default
    def z_default(self, attribute):
        return self.x + self.y
```

This is sensitive to the initialization order. However, if you have a stateful factory, the order is already important, so this is not new. And the natural order is the definition order which is intuitive.

Another issue is that this makes it possible to specify multiple defaults. I would just raise an error if this is detected.

Final issue I can think of is that an occasional user might confuse this with a property, while it does not behave like a property:

- The value is persisted.
- In mutable objects, the value doesn't change if the dependencies change, unlike a dynamically-computed property.

But I think "default" is clear on the behavior it has.
At first glance I like the proposed API :)
I guess we could have a lot fun with decorators based on `_CountingAttr`.
I *think* I want this in 17.1 but it entirely depends on the goodwill of reviewers (most likely @Tinche – I *gotta* shanghai some innocent souls at PyCon).

Patch:
diff --git a/src/attr/validators.py b/src/attr/validators.py
--- a/src/attr/validators.py
+++ b/src/attr/validators.py
@@ -48,9 +48,9 @@ def instance_of(type):
     :param type: The type to check for.
     :type type: type or tuple of types
 
-    The :exc:`TypeError` is raised with a human readable error message, the
-    attribute (of type :class:`attr.Attribute`), the expected type, and the
-    value it got.
+    :raises TypeError: With a human readable error message, the attribute
+        (of type :class:`attr.Attribute`), the expected type, and the value it
+        got.
     """
     return _InstanceOfValidator(type)
 
@@ -87,9 +87,9 @@ def provides(interface):
 
     :param zope.interface.Interface interface: The interface to check for.
 
-    The :exc:`TypeError` is raised with a human readable error message, the
-    attribute (of type :class:`attr.Attribute`), the expected interface, and
-    the value it got.
+    :raises TypeError: With a human readable error message, the attribute
+        (of type :class:`attr.Attribute`), the expected interface, and the
+        value it got.
     """
     return _ProvidesValidator(interface)
 
@@ -127,3 +127,39 @@ def optional(validator):
     if isinstance(validator, list):
         return _OptionalValidator(_AndValidator(validator))
     return _OptionalValidator(validator)
+
+
+@attributes(repr=False, slots=True)
+class _InValidator(object):
+    options = attr()
+
+    def __call__(self, inst, attr, value):
+        if value not in self.options:
+            raise ValueError(
+                "'{name}' must be in {options!r} (got {value!r})"
+                .format(name=attr.name, options=self.options, value=value)
+            )
+
+    def __repr__(self):
+        return (
+            "<in_ validator with options {options!r}>"
+            .format(options=self.options)
+        )
+
+
+def in_(options):
+    """
+    A validator that raises a :exc:`ValueError` if the initializer is called
+    with a value that does not belong in the options provided.  The check is
+    performed using ``value in options``.
+
+    :param options: Allowed options.
+    :type options: list, tuple, :class:`enum.Enum`, ...
+
+    :raises ValueError: With a human readable error message, the attribute (of
+       type :class:`attr.Attribute`), the expected options, and the value it
+       got.
+
+    .. versionadded:: 17.1.0
+    """
+    return _InValidator(options)


Test patch:
diff --git a/tests/test_validators.py b/tests/test_validators.py
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -7,7 +7,7 @@
 import pytest
 import zope.interface
 
-from attr.validators import and_, instance_of, provides, optional
+from attr.validators import and_, instance_of, provides, optional, in_
 from attr._compat import TYPE
 from attr._make import attributes, attr
 
@@ -214,3 +214,37 @@ def test_repr(self, validator):
                  "<{type} 'int'>> or None>")
                 .format(type=TYPE)
             ) == repr(v)
+
+
+class TestIn_(object):
+    """
+    Tests for `in_`.
+    """
+    def test_success_with_value(self):
+        """
+        If the value is in our options, nothing happens.
+        """
+        v = in_([1, 2, 3])
+        a = simple_attr("test")
+        v(1, a, 3)
+
+    def test_fail(self):
+        """
+        Raise ValueError if the value is outside our options.
+        """
+        v = in_([1, 2, 3])
+        a = simple_attr("test")
+        with pytest.raises(ValueError) as e:
+            v(None, a, None)
+        assert (
+            "'test' must be in [1, 2, 3] (got None)",
+        ) == e.value.args
+
+    def test_repr(self):
+        """
+        Returned validator has a useful `__repr__`.
+        """
+        v = in_([3, 4, 5])
+        assert(
+            ("<in_ validator with options [3, 4, 5]>")
+        ) == repr(v)