Skip to content

Commit a8f3582

Browse files
authored
Merge pull request #255 from ziirish/master
add: Wildcard fields
2 parents ce223cf + d64f1c6 commit a8f3582

File tree

5 files changed

+425
-9
lines changed

5 files changed

+425
-9
lines changed

doc/marshalling.rst

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,63 @@ You can also unmarshal fields as lists ::
214214
>>> json.dumps(marshal(data, resource_fields))
215215
>>> '{"first_names": ["Emile", "Raoul"], "name": "Bougnazal"}'
216216

217+
.. _wildcard-field:
218+
219+
Wildcard Field
220+
--------------
221+
222+
If you don't know the name(s) of the field(s) you want to unmarshall, you can
223+
use :class:`~fields.Wildcard` ::
224+
225+
>>> from flask_restplus import fields, marshal
226+
>>> import json
227+
>>>
228+
>>> wild = fields.Wildcard(fields.String)
229+
>>> wildcard_fields = {'*': wild}
230+
>>> data = {'John': 12, 'bob': 42, 'Jane': '68'}
231+
>>> json.dumps(marshal(data, wildcard_fields))
232+
>>> '{"Jane": "68", "bob": "42", "John": "12"}'
233+
234+
The name you give to your :class:`~fields.Wildcard` acts as a real glob as
235+
shown bellow ::
236+
237+
>>> from flask_restplus import fields, marshal
238+
>>> import json
239+
>>>
240+
>>> wild = fields.Wildcard(fields.String)
241+
>>> wildcard_fields = {'j*': wild}
242+
>>> data = {'John': 12, 'bob': 42, 'Jane': '68'}
243+
>>> json.dumps(marshal(data, wildcard_fields))
244+
>>> '{"Jane": "68", "John": "12"}'
245+
246+
.. note ::
247+
It is important you define your :class:`~fields.Wildcard` **outside** your
248+
model (ie. you **cannot** use it like this:
249+
``res_fields = {'*': fields.Wildcard(fields.String)}``) because it has to be
250+
stateful to keep a track of what fields it has already treated.
251+
252+
.. note ::
253+
The glob is not a regex, it can only treat simple wildcards like '*' or '?'.
254+
255+
In order to avoid unexpected behavior, when mixing :class:`~fields.Wildcard`
256+
with other fields, you may want to use an ``OrderedDict`` and use the
257+
:class:`~fields.Wildcard` as the last field ::
258+
259+
>>> from flask_restplus import fields, marshal
260+
>>> from collections import OrderedDict
261+
>>> import json
262+
>>>
263+
>>> wild = fields.Wildcard(fields.Integer)
264+
>>> mod = OrderedDict()
265+
>>> mod['zoro'] = fields.String
266+
>>> mod['*'] = wild
267+
>>> # you can use it in api.model like this:
268+
>>> # some_fields = api.model('MyModel', mod)
269+
>>>
270+
>>> data = {'John': 12, 'bob': 42, 'Jane': '68', 'zoro': 72}
271+
>>> json.dumps(marshal(data, mod))
272+
>>> '{"zoro": "72", "Jane": 68, "bob": 42, "John": 12}'
273+
217274
.. _nested-field:
218275

219276
Nested Field

flask_restplus/fields.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import unicode_literals
33

4+
import re
5+
import fnmatch
6+
import inspect
7+
48
from calendar import timegm
59
from datetime import date, datetime
610
from decimal import Decimal, ROUND_HALF_EVEN
@@ -20,7 +24,7 @@
2024

2125
__all__ = ('Raw', 'String', 'FormattedString', 'Url', 'DateTime', 'Date',
2226
'Boolean', 'Integer', 'Float', 'Arbitrary', 'Fixed',
23-
'Nested', 'List', 'ClassName', 'Polymorph',
27+
'Nested', 'List', 'ClassName', 'Polymorph', 'Wildcard',
2428
'StringMixin', 'MinMaxMixin', 'NumberMixin', 'MarshallingError')
2529

2630

@@ -702,3 +706,102 @@ def clone(self, mask=None):
702706

703707
data['mask'] = mask
704708
return Polymorph(mapping, **data)
709+
710+
711+
class Wildcard(Raw):
712+
'''
713+
Field for marshalling list of "unkown" fields.
714+
715+
:param cls_or_instance: The field type the list will contain.
716+
'''
717+
exclude = set()
718+
# cache the flat object
719+
_flat = None
720+
_obj = None
721+
_cache = set()
722+
_last = None
723+
724+
def __init__(self, cls_or_instance, **kwargs):
725+
super(Wildcard, self).__init__(**kwargs)
726+
error_msg = 'The type of the wildcard elements must be a subclass of fields.Raw'
727+
if isinstance(cls_or_instance, type):
728+
if not issubclass(cls_or_instance, Raw):
729+
raise MarshallingError(error_msg)
730+
self.container = cls_or_instance()
731+
else:
732+
if not isinstance(cls_or_instance, Raw):
733+
raise MarshallingError(error_msg)
734+
self.container = cls_or_instance
735+
736+
def _flatten(self, obj):
737+
if obj is None:
738+
return None
739+
if obj == self._obj and self._flat is not None:
740+
return self._flat
741+
if isinstance(obj, dict):
742+
self._flat = [x for x in iteritems(obj)]
743+
else:
744+
745+
def __match_attributes(attribute):
746+
attr_name, attr_obj = attribute
747+
if inspect.isroutine(attr_obj) or \
748+
(attr_name.startswith('__') and attr_name.endswith('__')):
749+
return False
750+
return True
751+
752+
attributes = inspect.getmembers(obj)
753+
self._flat = [x for x in attributes if __match_attributes(x)]
754+
755+
self._cache = set()
756+
self._obj = obj
757+
return self._flat
758+
759+
@property
760+
def key(self):
761+
return self._last
762+
763+
def reset(self):
764+
self.exclude = set()
765+
self._flat = None
766+
self._obj = None
767+
self._cache = set()
768+
self._last = None
769+
770+
def output(self, key, obj, ordered=False):
771+
value = None
772+
reg = fnmatch.translate(key)
773+
774+
if self._flatten(obj):
775+
while True:
776+
try:
777+
# we are using pop() so that we don't
778+
# loop over the whole object every time dropping the
779+
# complexity to O(n)
780+
(objkey, val) = self._flat.pop()
781+
if objkey not in self._cache and \
782+
objkey not in self.exclude and \
783+
re.match(reg, objkey, re.IGNORECASE):
784+
value = val
785+
self._cache.add(objkey)
786+
self._last = objkey
787+
break
788+
except IndexError:
789+
break
790+
791+
if value is None:
792+
if self.default is not None:
793+
return self.container.format(self.default)
794+
return None
795+
796+
return self.container.format(value)
797+
798+
def schema(self):
799+
schema = super(Wildcard, self).schema()
800+
schema['type'] = 'object'
801+
schema['additionalProperties'] = self.container.__schema__
802+
return schema
803+
804+
def clone(self):
805+
kwargs = self.__dict__.copy()
806+
model = kwargs.pop('container')
807+
return self.__class__(model, **kwargs)

flask_restplus/marshalling.py

Lines changed: 115 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
from .utils import unpack
1212

1313

14+
def make(cls):
15+
if isinstance(cls, type):
16+
return cls()
17+
return cls
18+
19+
1420
def marshal(data, fields, envelope=None, skip_none=False, mask=None, ordered=False):
1521
"""Takes raw data (in the form of a dict, list, object) and a dict of
1622
fields to output and filters the data based on those fields.
@@ -49,11 +55,103 @@ def marshal(data, fields, envelope=None, skip_none=False, mask=None, ordered=Fal
4955
OrderedDict([('a', 100)])
5056
5157
"""
58+
out, has_wildcards = _marshal(data, fields, envelope, skip_none, mask, ordered)
59+
60+
if has_wildcards:
61+
# ugly local import to avoid dependency loop
62+
from .fields import Wildcard
63+
64+
items = []
65+
keys = []
66+
for dkey, val in fields.items():
67+
key = dkey
68+
if isinstance(val, dict):
69+
value = marshal(data, val, skip_none=skip_none, ordered=ordered)
70+
else:
71+
field = make(val)
72+
is_wildcard = isinstance(field, Wildcard)
73+
# exclude already parsed keys from the wildcard
74+
if is_wildcard:
75+
field.reset()
76+
if keys:
77+
field.exclude |= set(keys)
78+
keys = []
79+
value = field.output(dkey, data)
80+
if is_wildcard:
81+
82+
def _append(k, v):
83+
if skip_none and (v is None or v == OrderedDict() or v == {}):
84+
return
85+
items.append((k, v))
86+
87+
key = field.key or dkey
88+
_append(key, value)
89+
while True:
90+
value = field.output(dkey, data, ordered=ordered)
91+
if value is None or \
92+
value == field.container.format(field.default):
93+
break
94+
key = field.key
95+
_append(key, value)
96+
continue
97+
98+
keys.append(key)
99+
if skip_none and (value is None or value == OrderedDict() or value == {}):
100+
continue
101+
items.append((key, value))
102+
103+
items = tuple(items)
104+
105+
out = OrderedDict(items) if ordered else dict(items)
106+
107+
if envelope:
108+
out = OrderedDict([(envelope, out)]) if ordered else {envelope: out}
109+
110+
return out
111+
112+
return out
113+
114+
115+
def _marshal(data, fields, envelope=None, skip_none=False, mask=None, ordered=False):
116+
"""Takes raw data (in the form of a dict, list, object) and a dict of
117+
fields to output and filters the data based on those fields.
118+
119+
:param data: the actual object(s) from which the fields are taken from
120+
:param fields: a dict of whose keys will make up the final serialized
121+
response output
122+
:param envelope: optional key that will be used to envelop the serialized
123+
response
124+
:param bool skip_none: optional key will be used to eliminate fields
125+
which value is None or the field's key not
126+
exist in data
127+
:param bool ordered: Wether or not to preserve order
128+
129+
130+
>>> from flask_restplus import fields, marshal
131+
>>> data = { 'a': 100, 'b': 'foo', 'c': None }
132+
>>> mfields = { 'a': fields.Raw, 'c': fields.Raw, 'd': fields.Raw }
133+
134+
>>> marshal(data, mfields)
135+
{'a': 100, 'c': None, 'd': None}
136+
137+
>>> marshal(data, mfields, envelope='data')
138+
{'data': {'a': 100, 'c': None, 'd': None}}
139+
140+
>>> marshal(data, mfields, skip_none=True)
141+
{'a': 100}
142+
143+
>>> marshal(data, mfields, ordered=True)
144+
OrderedDict([('a', 100), ('c', None), ('d', None)])
52145
53-
def make(cls):
54-
if isinstance(cls, type):
55-
return cls()
56-
return cls
146+
>>> marshal(data, mfields, envelope='data', ordered=True)
147+
OrderedDict([('data', OrderedDict([('a', 100), ('c', None), ('d', None)]))])
148+
149+
>>> marshal(data, mfields, skip_none=True, ordered=True)
150+
OrderedDict([('a', 100)])
151+
152+
"""
153+
# ugly local import to avoid dependency loop
154+
from .fields import Wildcard
57155

58156
mask = mask or getattr(fields, '__mask__', None)
59157
fields = getattr(fields, 'resolved', fields)
@@ -64,12 +162,21 @@ def make(cls):
64162
out = [marshal(d, fields, skip_none=skip_none, ordered=ordered) for d in data]
65163
if envelope:
66164
out = OrderedDict([(envelope, out)]) if ordered else {envelope: out}
67-
return out
165+
return out, False
166+
167+
has_wildcards = {'present': False}
168+
169+
def __format_field(key, val):
170+
field = make(val)
171+
if isinstance(field, Wildcard):
172+
has_wildcards['present'] = True
173+
value = field.output(key, data, ordered=ordered)
174+
return (key, value)
68175

69176
items = (
70-
(k, marshal(data, v, skip_none=skip_none, ordered=ordered)
177+
(k, marshal(data, v, skip_none=skip_none, ordered=ordered))
71178
if isinstance(v, dict)
72-
else make(v).output(k, data, ordered=ordered))
179+
else __format_field(k, v)
73180
for k, v in iteritems(fields)
74181
)
75182

@@ -82,7 +189,7 @@ def make(cls):
82189
if envelope:
83190
out = OrderedDict([(envelope, out)]) if ordered else {envelope: out}
84191

85-
return out
192+
return out, has_wildcards['present']
86193

87194

88195
class marshal_with(object):

0 commit comments

Comments
 (0)