Repository: marshmallow-code/marshmallow

Issue:
Uncaught error when passing non-list validators to Email and URL fields
`fields.Email` and `fields.URL` error if a non-list is passed to `validate`.

```python
from marshmallow import Schema, fields, validate

class UserSchema(Schema):
    email = fields.Email(validate=(validate.Length(min=5), ))
    url = fields.URL(validate=(validate.Length(min=5), ))
```

```
AttributeError: 'tuple' object has no attribute 'insert'
```

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 150127b5..9a31971a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,3 +73,8 @@ venv
 # Other
 .directory
 
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ae3914e2..a9a07809 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,13 @@
 Changelog
 ---------
 
+2.20.5 (unreleased)
++++++++++++++++++++
+
+Bug fixes:
+
+- Fix behavior when a non-list collection is passed to the ``validate`` argument of ``fields.Email`` and ``fields.URL`` (:issue:`1400`).
+
 2.20.4 (2019-09-11)
 +++++++++++++++++++
 
diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py
index 5187f91c..bf2e9f8a 100755
--- a/src/marshmallow/fields.py
+++ b/src/marshmallow/fields.py
@@ -1128,13 +1128,14 @@ class Url(ValidatedField, String):
         String.__init__(self, **kwargs)
 
         self.relative = relative
-        # Insert validation into self.validators so that multiple errors can be
-        # stored.
-        self.validators.insert(0, validate.URL(
-            relative=self.relative,
-            schemes=schemes,
-            error=self.error_messages['invalid']
-        ))
+        # Insert validation into self.validators so that multiple errors can be stored.
+        self.validators = (
+            [validate.URL(
+                relative=self.relative,
+                schemes=schemes,
+                error=self.error_messages['invalid']
+            )] + list(self.validators)
+        )
 
     def _validated(self, value):
         if value is None:
@@ -1156,9 +1157,10 @@ class Email(ValidatedField, String):
 
     def __init__(self, *args, **kwargs):
         String.__init__(self, *args, **kwargs)
-        # Insert validation into self.validators so that multiple errors can be
-        # stored.
-        self.validators.insert(0, validate.Email(error=self.error_messages['invalid']))
+        # Insert validation into self.validators so that multiple errors can be stored.
+        self.validators = (
+            [validate.Email(error=self.error_messages['invalid'])] + list(self.validators)
+        )
 
     def _validated(self, value):
         if value is None:


Test patch:
diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py
index bd7afc27..dd0aa280 100644
--- a/tests/test_deserialization.py
+++ b/tests/test_deserialization.py
@@ -475,6 +475,12 @@ class TestFieldDeserialization:
             field.deserialize('/foo/bar')
         assert excinfo.value.args[0][0] == 'Not a valid URL.'
 
+    # regression test for https://github.com/marshmallow-code/marshmallow/issues/1400
+    def test_url_field_non_list_validators(self):
+        field = fields.Url(validate=(validate.Length(min=16), ))
+        with pytest.raises(ValidationError, match="Shorter than minimum length"):
+            field.deserialize('https://abc.def')
+
     def test_relative_url_field_deserialization(self):
         field = fields.Url(relative=True)
         assert field.deserialize('/foo/bar') == '/foo/bar'
@@ -494,6 +500,12 @@ class TestFieldDeserialization:
             field.deserialize('invalidemail')
         assert excinfo.value.args[0][0] == 'Not a valid email address.'
 
+    # regression test for https://github.com/marshmallow-code/marshmallow/issues/1400
+    def test_email_field_non_list_validators(self):
+        field = fields.Email(validate=(validate.Length(min=9), ))
+        with pytest.raises(ValidationError, match="Shorter than minimum length"):
+            field.deserialize('a@bc.com')
+
     def test_function_field_deserialization_is_noop_by_default(self):
         field = fields.Function(lambda x: None)
         # Default is noop