Skip to content

Commit

Permalink
Merge pull request #832 from marshmallow-code/delimited-empty
Browse files Browse the repository at this point in the history
Add `empty` as a parameter to DelimitedFieldMixin
  • Loading branch information
sirosen committed Jul 11, 2023
2 parents 44e2037 + be8461c commit 8e902ae
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 2 deletions.
27 changes: 26 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
Changelog
---------

8.3.1 (Unreleased)
8.4.0 (Unreleased)
******************

Features:

* Add a new class attribute, ``empty_value`` to ``DelimitedList`` and
``DelimitedTuple``, with a default of ``""``.
This controls the value deserialized when an empty string is seen.

``empty_value`` can be used to handle types other than strings more gracefully, e.g.

.. code-block:: python
from webargs import fields
class IntList(fields.DelimitedList):
empty_value = 0
myfield = IntList(fields.Int())
.. note::

``empty_value`` will be changing in webargs v9.0 to be ``missing`` by
default. This will allow use of fields with ``load_default`` to specify
handling of the empty value.

Changes:

* Type annotations for ``FlaskParser`` have been improved
Expand Down
14 changes: 13 additions & 1 deletion src/webargs/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"""
from __future__ import annotations

import typing

import marshmallow as ma

# Expose all fields from marshmallow.fields.
Expand Down Expand Up @@ -64,6 +66,8 @@ class DelimitedFieldMixin:
delimiter: str = ","
# delimited fields set is_multiple=False for webargs.core.is_multiple
is_multiple: bool = False
# NOTE: in 8.x this defaults to "" but in 9.x it will be 'missing'
empty_value: typing.Any = ""

def _serialize(self, value, attr, obj, **kwargs):
# serializing will start with parent-class serialization, so that we correctly
Expand All @@ -77,6 +81,8 @@ def _deserialize(self, value, attr, data, **kwargs):
if not isinstance(value, (str, bytes)):
raise self.make_error("invalid")
values = value.split(self.delimiter) if value else []
# convert empty strings to the empty value; typically "" and therefore a no-op
values = [v or self.empty_value for v in values]
return super()._deserialize(values, attr, data, **kwargs)


Expand Down Expand Up @@ -117,6 +123,12 @@ class DelimitedTuple(DelimitedFieldMixin, ma.fields.Tuple):

default_error_messages = {"invalid": "Not a valid delimited tuple."}

def __init__(self, tuple_fields, *, delimiter: str | None = None, **kwargs):
def __init__(
self,
tuple_fields,
*,
delimiter: str | None = None,
**kwargs,
):
self.delimiter = delimiter or self.delimiter
super().__init__(tuple_fields, **kwargs)
42 changes: 42 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
INCLUDE,
RAISE,
Schema,
missing,
post_load,
pre_load,
validates_schema,
Expand Down Expand Up @@ -1106,6 +1107,47 @@ def test_delimited_tuple_passed_invalid_type(web_request, parser):
assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited tuple."]}}


def test_delimited_list_custom_empty_value(web_request, parser):
class ZeroList(fields.DelimitedList):
empty_value = 0

web_request.json = {"ids": "1,,3"}
schema_cls = Schema.from_dict({"ids": ZeroList(fields.Int())})
schema = schema_cls()

parsed = parser.parse(schema, web_request)
assert parsed["ids"] == [1, 0, 3]


def test_delimited_tuple_custom_empty_value(web_request, parser):
class ZeroTuple(fields.DelimitedTuple):
empty_value = 0

web_request.json = {"ids": "1,,3"}
schema_cls = Schema.from_dict(
{"ids": ZeroTuple((fields.Int, fields.Int, fields.Int))}
)
schema = schema_cls()

parsed = parser.parse(schema, web_request)
assert parsed["ids"] == (1, 0, 3)


def test_delimited_list_using_missing_for_empty(web_request, parser):
# this is "future" because we plan to make this the default for webargs v9.0
class FutureList(fields.DelimitedList):
empty_value = missing

web_request.json = {"ids": "foo,,bar"}
schema_cls = Schema.from_dict(
{"ids": FutureList(fields.String(load_default="nil"))}
)
schema = schema_cls()

parsed = parser.parse(schema, web_request)
assert parsed["ids"] == ["foo", "nil", "bar"]


def test_missing_list_argument_not_in_parsed_result(web_request, parser):
# arg missing in request
web_request.json = {}
Expand Down

0 comments on commit 8e902ae

Please sign in to comment.