Repository: marshmallow-code/webargs

Issue:
Add marshmallow.fields.Tuple to detected `is_multiple` fields
I noticed this while working on #584 . Currently, `is_multiple(ma.fields.List(...)) == True`, but `is_multiple(ma.fields.Tuple(...)) == False`.
We should add `Tuple` so that `is_multiple` returns true.

It is possible that users have subclassed `fields.Tuple` or done something else which could make that a breaking change. I don't think that's very likely, but we are already queuing things up for 8.0 . I'm going to put this in the 8.0 milestone. We could probably do it sooner if there's a reason to do so.


I don't think using `Tuple` for a query param is a good idea. Supporting `...?foo=1&foo=2` but *not* `...?foo=1` or `...?foo=1&foo=2&foo=3` is unusual and therefore likely to be confusing. I'd advise webargs users not to do that. However! It's surprising that `Tuple` is not treated as `is_multiple=True`, and I'm just aiming for the least surprising behavior for webargs here.

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 703bfe6..f03f463 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,20 +1,20 @@
 Changelog
 ---------
 
-7.1.0 (Unreleased)
+8.0.0 (Unreleased)
 ******************
 
 Features:
 
 * Detection of fields as "multi-value" for unpacking lists from multi-dict
-  types is now extensible with the `is_multiple` attribute. If a field sets
-  `is_multiple = True` it will be detected as a multi-value field.
+  types is now extensible with the ``is_multiple`` attribute. If a field sets
+  ``is_multiple = True`` it will be detected as a multi-value field.
   (:issue:`563`)
 
-* If `is_multiple` is not set or is set to `None`, webargs will check if the
-  field is an instance of `List`.
+* If ``is_multiple`` is not set or is set to ``None``, webargs will check if the
+  field is an instance of ``List`` or ``Tuple``.
 
-* A new attribute on `Parser` objects, ``Parser.KNOWN_MULTI_FIELDS`` can be
+* A new attribute on ``Parser`` objects, ``Parser.KNOWN_MULTI_FIELDS`` can be
   used to set fields which should be detected as ``is_multiple=True`` even when
   the attribute is not set.
 
diff --git a/src/webargs/core.py b/src/webargs/core.py
index de8953d..e53d1c9 100644
--- a/src/webargs/core.py
+++ b/src/webargs/core.py
@@ -142,9 +142,8 @@ class Parser:
     DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS
     #: Default error message for validation errors
     DEFAULT_VALIDATION_MESSAGE: str = "Invalid value."
-    # TODO: add ma.fields.Tuple in v8.0
     #: field types which should always be treated as if they set `is_multiple=True`
-    KNOWN_MULTI_FIELDS: typing.List[typing.Type] = [ma.fields.List]
+    KNOWN_MULTI_FIELDS: typing.List[typing.Type] = [ma.fields.List, ma.fields.Tuple]
 
     #: Maps location => method name
     __location_map__: typing.Dict[str, typing.Union[str, typing.Callable]] = {
diff --git a/src/webargs/multidictproxy.py b/src/webargs/multidictproxy.py
index 7017d96..a277178 100644
--- a/src/webargs/multidictproxy.py
+++ b/src/webargs/multidictproxy.py
@@ -18,7 +18,10 @@ class MultiDictProxy(Mapping):
         self,
         multidict,
         schema: ma.Schema,
-        known_multi_fields: typing.Tuple[typing.Type, ...] = (ma.fields.List,),
+        known_multi_fields: typing.Tuple[typing.Type, ...] = (
+            ma.fields.List,
+            ma.fields.Tuple,
+        ),
     ):
         self.data = multidict
         self.known_multi_fields = known_multi_fields


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index d7b35fb..1219659 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -35,7 +35,7 @@ class MockRequestParser(Parser):
     """A minimal parser implementation that parses mock requests."""
 
     def load_querystring(self, req, schema):
-        return MultiDictProxy(req.query, schema)
+        return self._makeproxy(req.query, schema)
 
     def load_json(self, req, schema):
         return req.json
@@ -1040,6 +1040,7 @@ def test_type_conversion_with_multiple_required(web_request, parser):
         "is_multiple_false",
         "is_multiple_notset",
         "list_field",
+        "tuple_field",
         "added_to_known",
     ],
 )
@@ -1103,13 +1104,20 @@ def test_is_multiple_detection(web_request, parser, input_dict, setting):
         args = {"foos": fields.List(fields.Str())}
         result = parser.parse(args, web_request, location="query")
         assert result["foos"] in (["a", "b"], ["b", "a"])
+    # case 5: the field is a Tuple (special case)
+    elif setting == "tuple_field":
+        # this should behave like the is_multiple=True case and produce a tuple
+        args = {"foos": fields.Tuple((fields.Str, fields.Str))}
+        result = parser.parse(args, web_request, location="query")
+        assert result["foos"] in (("a", "b"), ("b", "a"))
+    # case 6: the field is custom, but added to the known fields of the parser
     elif setting == "added_to_known":
         # if it's included in the known multifields and is_multiple is not set, behave
         # like is_multiple=True
         parser.KNOWN_MULTI_FIELDS.append(CustomMultiplexingField)
         args = {"foos": CustomMultiplexingField()}
         result = parser.parse(args, web_request, location="query")
-        assert result["foos"] in ("a", "b")
+        assert result["foos"] in (["a", "b"], ["b", "a"])
     else:
         raise NotImplementedError