Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Bug Fixes
::

* Adjust field tests for Python 3.14 (``staticmethod`` around ``functools.partial`` used as a class attribute). [python-restx]
* Keep later wildcard keys when a mapped value is null or an empty list (#409, #283) [Cherith1222]

.. _section-1.3.1:
1.3.1
Expand Down
10 changes: 10 additions & 0 deletions flask_restx/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,9 +819,13 @@ class Wildcard(Raw):
_obj = None
_cache = set()
_last = None
# True once output() has no further matching key. A mapped value of
# None or [] is still a real value and must not set this.
_exhausted = False

def __init__(self, cls_or_instance, **kwargs):
super(Wildcard, self).__init__(**kwargs)
self._exhausted = False
error_msg = "The type of the wildcard elements must be a subclass of fields.Raw"
if isinstance(cls_or_instance, type):
if not issubclass(cls_or_instance, Raw):
Expand Down Expand Up @@ -866,9 +870,11 @@ def reset(self):
self._obj = None
self._cache = set()
self._last = None
self._exhausted = False

def output(self, key, obj, ordered=False):
value = None
found = False
reg = fnmatch.translate(key)

if self._flatten(obj):
Expand All @@ -889,12 +895,16 @@ def output(self, key, obj, ordered=False):
and re.match(reg, objkey, re.IGNORECASE)
):
value = val
found = True
self._cache.add(objkey)
self._last = objkey
break
except IndexError:
break

# None and [] are valid mapped values. Only "no remaining key"
# ends the wildcard scan (see marshalling.marshal).
self._exhausted = not found
if value is None:
if self.default is not None:
return self.container.format(self.default)
Expand Down
6 changes: 2 additions & 4 deletions flask_restx/marshalling.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,9 @@ def _append(k, v):

key = field.key or dkey
_append(key, value)
while True:
while not field._exhausted:
value = field.output(dkey, data, ordered=ordered)
if value is None or value == field.container.format(
field.default
):
if field._exhausted:
break
key = field.key
_append(key, value)
Expand Down
45 changes: 45 additions & 0 deletions tests/test_marshalling.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,51 @@ def test_marshal_wildcard_with_skip_none(self):
output = marshal(marshal_dict, model, skip_none=True)
assert output == {"baz": "biz"}

def test_marshal_wildcard_keeps_null_and_later_keys(self):
# A null value used to end the scan, dropping every key not yet visited.
wild = fields.Wildcard(fields.String)
model = {"*": wild}
data = {"a": 1, "b": "2", "c": None, "d": 20, "e": "e"}
assert marshal(data, model) == {
"a": "1",
"b": "2",
"c": None,
"d": "20",
"e": "e",
}
assert marshal(data, model, skip_none=True) == {
"a": "1",
"b": "2",
"d": "20",
"e": "e",
}
ordered = marshal(data, model, ordered=True)
assert list(ordered) == ["a", "b", "c", "d", "e"]

def test_marshal_wildcard_keeps_empty_lists(self):
# List.format(None) is [], so an empty list compared equal to the
# "no more keys" sentinel and stopped the scan.
wild = fields.Wildcard(fields.List(fields.String))
model = {"*": wild}
data = {"a": ["1"], "b": [], "c": ["3"]}
assert marshal(data, model) == {"a": ["1"], "b": [], "c": ["3"]}

nested = {"id": fields.String(), "name": fields.String()}
wild_list = fields.Wildcard(fields.List(fields.Nested(nested)))
payload = {
"listMap": {
"key7": [{"id": "1", "name": "Guido"}],
"key8": [],
"key9": [{"id": "3", "name": "Linus"}],
}
}
output = marshal(payload, {"listMap": fields.Nested({"*": wild_list})})
assert output["listMap"] == {
"key7": [{"id": "1", "name": "Guido"}],
"key8": [],
"key9": [{"id": "3", "name": "Linus"}],
}

def test_marshal_decorator(self):
model = OrderedDict([("foo", fields.Raw)])

Expand Down