Repository: bloomberg/attrs-strict

Issue:
Support for Tuple is incorrect/incomplete
When declaring an attribute as a Tuple, there are 2 possible forms:

Simple form, like `Tuple[str]` or `Tuple[int, int]`, should be used for fixed size tuple declarations, which seems to be what attrs-strict currently supports. But there is also the form with an ellipsis to declare a  variable-length tuple of homogeneous type (see https://docs.python.org/3.7/library/typing.html#typing.Tuple )

It seems to me that this should at the very least be mentioned in the documentation (in the "What is currently supported ?" section), or be fixed to have proper support for this pattern.

eg.

```
@attr.s()
class Model:
    item = attr.ib(type=Tuple[int, ...], validator=type_validator())

obj = Model(item=(1, 2, 3))
```

will raise:
```
TupleError: Element (1, 2, 3) has more elements than types specified in typing.Tuple[int, ...]. Expected 2 received 3
```


Discussion:
erikseulean: This looks good, thanks for fixing it. I'll accept it when the CI passes.
douardda: Should be ok now.

Patch:
diff --git a/attrs_strict/_type_validation.py b/attrs_strict/_type_validation.py
index fce8bc5..2dc75d2 100644
--- a/attrs_strict/_type_validation.py
+++ b/attrs_strict/_type_validation.py
@@ -105,6 +105,9 @@ def _handle_dict(attribute, container, expected_type):
 
 def _handle_tuple(attribute, container, expected_type):
     tuple_types = expected_type.__args__
+    if len(tuple_types) == 2 and tuple_types[1] == Ellipsis:
+        element_type = tuple_types[0]
+        tuple_types = (element_type, ) * len(container)
 
     if len(container) != len(tuple_types):
         raise TupleError(container, attribute.type, tuple_types)


Test patch:
diff --git a/tests/test_tuple.py b/tests/test_tuple.py
index a3428ab..765ac07 100644
--- a/tests/test_tuple.py
+++ b/tests/test_tuple.py
@@ -45,3 +45,45 @@ def test_tuple_of_tuple_raises():
         "in typing.Tuple[typing.Tuple[int, int], typing.Tuple[int, int]]. "
         "Expected 2 received 3 in ((1, 2), (3, 4, 5))>"
     ) == repr(error.value)
+
+
+def test_variable_length_tuple():
+    element = (1, 2, 3, 4)
+
+    attr = MagicMock()
+    attr.name = "foo"
+    attr.type = Tuple[int, ...]
+
+    validator = type_validator()
+
+    validator(None, attr, element)
+
+
+def test_variable_length_tuple_empty():
+    element = ()
+
+    attr = MagicMock()
+    attr.name = "foo"
+    attr.type = Tuple[int, ...]
+
+    validator = type_validator()
+
+    validator(None, attr, element)
+
+
+def test_variable_length_tuple_raises():
+    element = (1, 2, 3, "4")
+
+    attr = MagicMock()
+    attr.name = "foo"
+    attr.type = Tuple[int, ...]
+
+    validator = type_validator()
+
+    with pytest.raises(ValueError) as error:
+        validator(None, attr, element)
+
+    assert (
+        "<foo must be typing.Tuple[int, ...] (got 4 that is a {}) "
+        "in (1, 2, 3, '4')>"
+    ).format(str) == repr(error.value)