Repository: marshmallow-code/webargs

Issue:
RFC: Only accept delimited string in DelimitedList
`DelimitedList` accepts either a list or a delimited string (e.g. "foo,bar,baz").

I'd like to make it more strict by only accepting a delimited list. Rather than adding a `strict` parameter, I'm thinking of dropping the whole "also accept a list" feature.

Any reason to support both?

I understand it inherits from `List` because once the string is parsed, it can be deserialized as a normal list. But are there cases where you'd expect either a list or a delimited string?

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 8c56899..a88f6bc 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,16 @@
 Changelog
 ---------
 
+6.0.0b5 (Unreleased)
+********************
+
+Refactoring:
+
+* *Backwards-incompatible*: `DelimitedList` now requires that its input be a
+  string and always serializes as a string. It can still serialize and deserialize
+  using another field, e.g. `DelimitedList(Int())` is still valid and requires
+  that the values in the list parse as ints.
+
 6.0.0b4 (2020-01-28)
 ********************
 
diff --git a/src/webargs/fields.py b/src/webargs/fields.py
index 0d8d46b..fcdf05f 100644
--- a/src/webargs/fields.py
+++ b/src/webargs/fields.py
@@ -44,37 +44,35 @@ class Nested(ma.fields.Nested):
 
 
 class DelimitedList(ma.fields.List):
-    """Same as `marshmallow.fields.List`, except can load from either a list or
-    a delimited string (e.g. "foo,bar,baz").
+    """A field which is similar to a List, but takes its input as a delimited
+    string (e.g. "foo,bar,baz").
+
+    Like List, it can be given a nested field type which it will use to
+    de/serialize each element of the list.
 
     :param Field cls_or_instance: A field class or instance.
     :param str delimiter: Delimiter between values.
-    :param bool as_string: Dump values to string.
     """
 
+    default_error_messages = {"invalid": "Not a valid delimited list."}
     delimiter = ","
 
-    def __init__(self, cls_or_instance, delimiter=None, as_string=False, **kwargs):
+    def __init__(self, cls_or_instance, delimiter=None, **kwargs):
         self.delimiter = delimiter or self.delimiter
-        self.as_string = as_string
         super().__init__(cls_or_instance, **kwargs)
 
     def _serialize(self, value, attr, obj):
-        ret = super()._serialize(value, attr, obj)
-        if self.as_string:
-            return self.delimiter.join(format(each) for each in ret)
-        return ret
+        # serializing will start with List serialization, so that we correctly
+        # output lists of non-primitive types, e.g. DelimitedList(DateTime)
+        return self.delimiter.join(
+            format(each) for each in super()._serialize(value, attr, obj)
+        )
 
     def _deserialize(self, value, attr, data, **kwargs):
-        try:
-            ret = (
-                value
-                if ma.utils.is_iterable_but_not_string(value)
-                else value.split(self.delimiter)
-            )
-        except AttributeError:
+        # attempting to deserialize from a non-string source is an error
+        if not isinstance(value, (str, bytes)):
             if MARSHMALLOW_VERSION_INFO[0] < 3:
                 self.fail("invalid")
             else:
                 raise self.make_error("invalid")
-        return super()._deserialize(ret, attr, data, **kwargs)
+        return super()._deserialize(value.split(self.delimiter), attr, data, **kwargs)


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 4ff341b..feef0c1 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -835,21 +835,6 @@ def test_delimited_list_default_delimiter(web_request, parser):
     parsed = parser.parse(schema, web_request)
     assert parsed["ids"] == [1, 2, 3]
 
-    dumped = schema.dump(parsed)
-    data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped
-    assert data["ids"] == [1, 2, 3]
-
-
-def test_delimited_list_as_string(web_request, parser):
-    web_request.json = {"ids": "1,2,3"}
-    schema_cls = dict2schema(
-        {"ids": fields.DelimitedList(fields.Int(), as_string=True)}
-    )
-    schema = schema_cls()
-
-    parsed = parser.parse(schema, web_request)
-    assert parsed["ids"] == [1, 2, 3]
-
     dumped = schema.dump(parsed)
     data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped
     assert data["ids"] == "1,2,3"
@@ -858,11 +843,7 @@ def test_delimited_list_as_string(web_request, parser):
 def test_delimited_list_as_string_v2(web_request, parser):
     web_request.json = {"dates": "2018-11-01,2018-11-02"}
     schema_cls = dict2schema(
-        {
-            "dates": fields.DelimitedList(
-                fields.DateTime(format="%Y-%m-%d"), as_string=True
-            )
-        }
+        {"dates": fields.DelimitedList(fields.DateTime(format="%Y-%m-%d"))}
     )
     schema = schema_cls()
 
@@ -886,13 +867,17 @@ def test_delimited_list_custom_delimiter(web_request, parser):
     assert parsed["ids"] == [1, 2, 3]
 
 
-def test_delimited_list_load_list(web_request, parser):
+def test_delimited_list_load_list_errors(web_request, parser):
     web_request.json = {"ids": [1, 2, 3]}
     schema_cls = dict2schema({"ids": fields.DelimitedList(fields.Int())})
     schema = schema_cls()
 
-    parsed = parser.parse(schema, web_request)
-    assert parsed["ids"] == [1, 2, 3]
+    with pytest.raises(ValidationError) as excinfo:
+        parser.parse(schema, web_request)
+    exc = excinfo.value
+    assert isinstance(exc, ValidationError)
+    errors = exc.args[0]
+    assert errors["ids"] == ["Not a valid delimited list."]
 
 
 # Regresion test for https://github.com/marshmallow-code/webargs/issues/149
@@ -903,7 +888,7 @@ def test_delimited_list_passed_invalid_type(web_request, parser):
 
     with pytest.raises(ValidationError) as excinfo:
         parser.parse(schema, web_request)
-    assert excinfo.value.messages == {"ids": ["Not a valid list."]}
+    assert excinfo.value.messages == {"ids": ["Not a valid delimited list."]}
 
 
 def test_missing_list_argument_not_in_parsed_result(web_request, parser):