Skip to content

Commit f1b7c61

Browse files
committed
add options in parser
1 parent fe17693 commit f1b7c61

File tree

8 files changed

+622
-23
lines changed

8 files changed

+622
-23
lines changed

README.md

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ exemple:
9393
for this working perfectly you need to follow this rules:
9494

9595
- a first key need to be set ex: 'title[0]' or 'title', in both the first key is 'title'
96-
- each sub key need to enclose by brackets "[--your-key--]"
96+
- each sub key need to seperate by brackets "[--your-key--]" or dot "." (depends of your options)
9797
- if sub key are a full number, is converted to list
9898
- if sub key is Not a number is converted to dictionary
99-
- the key can't be rewite
99+
- by default,the duplicate keys can't be set (see options to override that)
100100
ex:
101101

102102
```python
@@ -117,7 +117,7 @@ for this working perfectly you need to follow this rules:
117117
# ERROR , you set a number is upper thans actual list
118118

119119

120-
# wrong format
120+
# wrong format if separator is brackets (see options)
121121
data = {
122122
'title[0]]]': 'my-value',
123123
'title[0': 'my-value',
@@ -162,7 +162,12 @@ for this working perfectly you need to follow this rules:
162162
data = {
163163
'the[0][chained][key][0][are][awesome][0][0]': 'im here !!'
164164
}
165-
# output
165+
# with "dot" separator in options is ;look like that
166+
data = {
167+
'the.0.chained.key.0.are.awesome.0.0': 'im here !!'
168+
}
169+
170+
# the output
166171
output: {
167172
'the': [
168173
{
@@ -185,6 +190,88 @@ for this working perfectly you need to follow this rules:
185190
}
186191
```
187192

193+
# How to use it
194+
195+
## for every framwork
196+
197+
```python
198+
from nested_multipart_parser import NestedParser
199+
200+
options = {
201+
"separator": "bracket"
202+
}
203+
204+
def my_view():
205+
# options is optional
206+
parser = NestedParser(data, options)
207+
if parser.is_valid():
208+
validate_data = parser.validate_data
209+
...
210+
else:
211+
print(parser.errors)
212+
213+
```
214+
215+
## for django rest framwork
216+
217+
```python
218+
from nested_multipart_parser.drf import DrfNestedParser
219+
...
220+
221+
class YourViewSet(viewsets.ViewSet):
222+
parser_classes = (DrfNestedParser,)
223+
```
224+
225+
## options
226+
227+
```python
228+
{
229+
# the separator
230+
# with bracket: article[title][authors][0]: "jhon doe"
231+
# with dot: article.title.authors.0: "jhon doe"
232+
'separator': 'bracket' or 'dot', # default is bracket
233+
234+
# raise a expections when you have duplicate keys
235+
# ex :
236+
# {
237+
# "article": 42,
238+
# "article[title]": 42,
239+
# }
240+
'raise_duplicate': True,
241+
242+
# overide the duplicate keys, you need to set "raise_duplicate" to False
243+
# ex :
244+
# {
245+
# "article": 42,
246+
# "article[title]": 42,
247+
# }
248+
# the out is
249+
# ex :
250+
# {
251+
# "article"{
252+
# "title": 42,
253+
# }
254+
# }
255+
'assign_duplicate': False
256+
}
257+
```
258+
259+
## options with django rest framwork
260+
261+
In your settings.py, add "DRF_NESTED_MULTIPART_PARSER"
262+
263+
```python
264+
#settings.py
265+
...
266+
267+
DRF_NESTED_MULTIPART_PARSER = {
268+
"separator": "bracket",
269+
"raise_duplicate": True,
270+
"assign_duplicate": False
271+
272+
}
273+
```
274+
275+
## for frontend javscript
188276

189-
## Javscript
190277
You can use this [multipart-object](https://github.com/remigermain/multipart-object) library to easy convert object to flat nested object formated for this library

nested_multipart_parser/drf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
from rest_framework.parsers import MultiPartParser
33
from django.http.multipartparser import MultiPartParserError
44
from django.http import QueryDict
5+
from django.conf import settings
56

67

78
class NestedParser(NestPars):
89

10+
def __init__(self, data):
11+
super().__init__(data, getattr(settings, "DRF_NESTED_MULTIPART_PARSER", {}))
12+
913
@property
1014
def validate_data(self):
1115
dtc = QueryDict(mutable=True)

nested_multipart_parser/parser.py

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,61 +4,110 @@
44
class NestedParser:
55
_valid = None
66
errors = None
7-
_reg = re.compile(r"\[|\]")
87

9-
def __init__(self, data):
8+
def __init__(self, data, options={}):
109
self.data = data
10+
self._merge_options(options)
11+
12+
def _merge_options(self, options):
13+
DEFAULT_OPTIONS = {
14+
"separator": "bracket",
15+
"raise_duplicate": True,
16+
"assign_duplicate": False
17+
}
18+
19+
options = {**DEFAULT_OPTIONS, **options}
20+
self._options = options
21+
22+
assert self._options.get("separator", "dot") in ["dot", "bracket"]
23+
assert isinstance(self._options.get("raise_duplicate", False), bool)
24+
assert isinstance(self._options.get("assign_duplicate", False), bool)
25+
26+
self._is_dot = self._options["separator"] == "dot"
27+
if not self.is_dot:
28+
self._reg = re.compile(r"\[|\]")
29+
30+
@property
31+
def is_dot(self):
32+
return self._is_dot
1133

1234
def split_key(self, key):
1335
# remove space
1436
k = key.replace(" ", "")
37+
1538
# remove empty string and count key length for check is a good format
1639
# reduce + filter are a hight cost so do manualy with for loop
17-
results = []
18-
check = -2
1940

20-
for select in self._reg.split(k):
41+
# optimize by split with string func
42+
if self.is_dot:
43+
length = 1
44+
splitter = k.split(".")
45+
else:
46+
splitter = self._reg.split(k)
47+
length = 2
48+
49+
check = -length
50+
51+
results = []
52+
for select in splitter:
2153
if select:
2254
results.append(select)
23-
check += len(select) + 2
55+
check += len(select) + length
2456

2557
if len(k) != check:
2658
raise Exception(f"invalid format from key {key}")
2759
return results
2860

29-
def set_type(self, dtc, key, value, full_keys):
61+
def set_type(self, dtc, key, value, full_keys, prev=None, last=False):
3062
if isinstance(dtc, list):
3163
key = int(key)
3264
if len(dtc) < key:
3365
raise ValueError(
3466
f"key \"{full_keys}\" is upper than actual list")
3567
if len(dtc) == key:
3668
dtc.append(value)
37-
return key
3869
elif isinstance(dtc, dict):
39-
if key not in dtc:
70+
if key not in dtc or self._options["assign_duplicate"] and last:
4071
dtc[key] = value
4172
else:
42-
raise ValueError(
43-
f"invalid rewrite key from \"{full_keys}\" to \"{dtc}\"")
73+
if self._options["raise_duplicate"]:
74+
raise ValueError(
75+
f"invalid rewrite key from \"{full_keys}\" to \"{dtc}\"")
76+
elif self._options["assign_duplicate"]:
77+
dtc = prev['dtc']
78+
dtc[prev['key']] = prev['type']
79+
return self.set_type(dtc[prev['key']], key, value, full_keys, prev, last)
4480
return key
4581

82+
def get_next_type(self, keys):
83+
return [] if keys.isdigit() else {}
84+
4685
def construct(self, data):
4786
dictionary = {}
4887

4988
for key in data:
5089
keys = self.split_key(key)
5190
tmp = dictionary
91+
prev = {
92+
'key': keys[0],
93+
'dtc': tmp,
94+
'type': None
95+
}
5296

5397
# optimize with while loop instend of for in with zip function
5498
i = 0
5599
lenght = len(keys) - 1
56100
while i < lenght:
57-
set_type = [] if keys[i+1].isdigit() else {}
58-
tmp = tmp[self.set_type(tmp, keys[i], set_type, key)]
101+
set_type = self.get_next_type(keys[i+1])
102+
index = self.set_type(
103+
tmp, keys[i], set_type, key, prev=prev)
104+
prev['dtc'] = tmp
105+
prev['key'] = index
106+
prev['type'] = set_type
107+
tmp = tmp[index]
59108
i += 1
60109

61-
self.set_type(tmp, keys[-1], data[key], key)
110+
self.set_type(tmp, keys[-1], data[key], key, prev=prev, last=True)
62111
return dictionary
63112

64113
def is_valid(self):

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
setuptools.setup(
2525
name="nested-multipart-parser",
26-
version="0.1.0",
26+
version="0.2.0",
2727
author="Example Author",
2828
license='MIT',
2929
author_email='contact@germainremi.fr',

tests/test_drf.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
from django.http import QueryDict
33

44

5+
def toQueryDict(data):
6+
q = QueryDict(mutable=True)
7+
q.update(data)
8+
q._mutable = False
9+
return q
10+
11+
512
class TestDrfParser(unittest.TestCase):
613

714
@classmethod
@@ -24,8 +31,7 @@ def test_querydict_mutable(self):
2431
}
2532
)
2633
self.assertTrue(parser.is_valid())
27-
q = QueryDict(mutable=True)
28-
q.update({
34+
q = toQueryDict({
2935
"dtc": {
3036
"key": "value",
3137
"vla": "value2",
@@ -40,6 +46,34 @@ def test_querydict_mutable(self):
4046
],
4147
"string": "value",
4248
})
43-
q.mutable = False
4449
self.assertEqual(parser.validate_data, q)
4550
self.assertFalse(parser.validate_data.mutable)
51+
52+
def test_settings(self):
53+
from nested_multipart_parser.drf import NestedParser
54+
55+
data = {
56+
"article.title": "youpi"
57+
}
58+
p = NestedParser(data)
59+
self.assertTrue(p.is_valid())
60+
expected = toQueryDict({
61+
"article.title": "youpi"
62+
})
63+
self.assertEqual(p.validate_data, expected)
64+
65+
# set settings
66+
from django.conf import settings
67+
options = {
68+
"separator": "dot"
69+
}
70+
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER', options)
71+
72+
p = NestedParser(data)
73+
self.assertTrue(p.is_valid())
74+
expected = toQueryDict({
75+
"article": {
76+
"title": "youpi"
77+
}
78+
})
79+
self.assertEqual(p.validate_data, expected)

tests/test_parser.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,20 @@ def test_parser_rewrite_key_boject(self):
295295
}
296296
parser = NestedParser(data)
297297
self.assertFalse(parser.is_valid())
298+
299+
def test_wrong_settings(self):
300+
301+
data = {"data": "data"}
302+
303+
with self.assertRaises(AssertionError):
304+
NestedParser(data, options={
305+
"separator": "worng"
306+
})
307+
with self.assertRaises(AssertionError):
308+
NestedParser(data, options={
309+
"raise_duplicate": "need_boolean"
310+
})
311+
with self.assertRaises(AssertionError):
312+
NestedParser(data, options={
313+
"assign_duplicate": "need_boolean"
314+
})

0 commit comments

Comments
 (0)