Skip to content

Commit 79ae789

Browse files
committed
add mixed separator
1 parent a375968 commit 79ae789

File tree

7 files changed

+405
-126
lines changed

7 files changed

+405
-126
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ Attributes where sub keys are other than full numbers are converted into Python
140140
data = {
141141
'the.0.chained.key.0.are.awesome.0.0': 'im here !!'
142142
}
143+
# with "mixed" separator option:
144+
data = {
145+
'the[0].chained.key[0].are.awesome[0][0]': 'im here !!'
146+
}
143147
```
144148

145149

@@ -150,6 +154,8 @@ For this to work perfectly, you must follow the following rules:
150154

151155
- Each sub key need to be separate by brackets `[ ]` or dot `.` (depends of your options)
152156

157+
- For `mixed` options, brackets `[]` is for list, and dot `.` is for object
158+
153159
- Don't put spaces between separators.
154160

155161
- By default, you can't set set duplicates keys (see options)
@@ -163,7 +169,7 @@ For this to work perfectly, you must follow the following rules:
163169
# Separators:
164170
# with bracket: article[title][authors][0]: "jhon doe"
165171
# with dot: article.title.authors.0: "jhon doe"
166-
'separator': 'bracket' or 'dot', # default is bracket
172+
'separator': 'bracket' or 'dot' or 'mixed', # default is bracket
167173

168174

169175
# raise a expections when you have duplicate keys

nested_multipart_parser/parser.py

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,85 @@ def _merge_options(self, options):
1919
options = {**DEFAULT_OPTIONS, **options}
2020
self._options = options
2121

22-
assert self._options.get("separator", "dot") in ["dot", "bracket"]
22+
assert self._options.get("separator", "dot") in [
23+
"dot", "bracket", "mixed"]
2324
assert isinstance(self._options.get("raise_duplicate", False), bool)
2425
assert isinstance(self._options.get("assign_duplicate", False), bool)
2526

26-
self._is_dot = self._options["separator"] == "dot"
27-
if not self.is_dot:
27+
self.__is_dot = False
28+
self.__is_mixed = False
29+
self.__is_bracket = False
30+
if self._options["separator"] == "dot":
31+
self.__is_dot = True
32+
elif self._options["separator"] == "mixed":
33+
self.__is_mixed = True
34+
else:
35+
self.__is_bracket = True
2836
self._reg = re.compile(r"\[|\]")
2937

30-
@property
31-
def is_dot(self):
32-
return self._is_dot
38+
def mixed_split(self, key):
39+
def span(key, i):
40+
old = i
41+
while i != len(key):
42+
if key[i] in ".[]":
43+
break
44+
i += 1
45+
if old == i:
46+
raise ValueError(
47+
f"invalid format key '{full_keys}', empty key value at position {i + pos}")
48+
return i
49+
50+
full_keys = key
51+
idx = span(key, 0)
52+
pos = idx
53+
keys = [key[:idx]]
54+
key = key[idx:]
55+
56+
i = 0
57+
while i < len(key):
58+
if key[i] == '.':
59+
i += 1
60+
idx = span(key, i)
61+
keys.append(key[i: idx])
62+
i = idx
63+
elif key[i] == '[':
64+
i += 1
65+
idx = span(key, i)
66+
if key[idx] != ']':
67+
raise ValueError(
68+
f"invalid format key '{full_keys}', not end with bracket at position {i + pos}")
69+
sub = key[i: idx]
70+
if not sub.isdigit():
71+
raise ValueError(
72+
f"invalid format key '{full_keys}', list key is not a valid number at position {i + pos}")
73+
keys.append(int(key[i: idx]))
74+
i = idx + 1
75+
elif key[i] == ']':
76+
raise ValueError(
77+
f"invalid format key '{full_keys}', not start with bracket at position {i + pos}")
78+
else:
79+
raise ValueError(
80+
f"invalid format key '{full_keys}', invalid char at position {i + pos}")
81+
return keys
3382

3483
def split_key(self, key):
3584
# remove space
3685
k = key.replace(" ", "")
86+
if len(k) != len(key):
87+
raise Exception(f"invalid format from key {key}, no space allowed")
3788

3889
# remove empty string and count key length for check is a good format
3990
# reduce + filter are a hight cost so do manualy with for loop
4091

4192
# optimize by split with string func
42-
if self.is_dot:
93+
if self.__is_mixed:
94+
return self.mixed_split(key)
95+
if self.__is_dot:
4396
length = 1
44-
splitter = k.split(".")
97+
splitter = key.split(".")
4598
else:
4699
length = 2
47-
splitter = self._reg.split(k)
100+
splitter = self._reg.split(key)
48101

49102
check = -length
50103

@@ -54,7 +107,7 @@ def split_key(self, key):
54107
results.append(select)
55108
check += len(select) + length
56109

57-
if len(k) != check:
110+
if len(key) != check:
58111
raise Exception(f"invalid format from key {key}")
59112
return results
60113

@@ -79,8 +132,10 @@ def set_type(self, dtc, key, value, full_keys, prev=None, last=False):
79132
return self.set_type(dtc[prev['key']], key, value, full_keys, prev, last)
80133
return key
81134

82-
def get_next_type(self, keys):
83-
return [] if keys.isdigit() else {}
135+
def get_next_type(self, key):
136+
if self.__is_mixed:
137+
return [] if isinstance(key, int) else {}
138+
return [] if key.isdigit() else {}
84139

85140
def convert_value(self, data, key):
86141
return data[key]

tests/test_drf.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,13 @@ def parser_boundary(self, data):
8585
factory = APIRequestFactory()
8686
content = encode_multipart('BoUnDaRyStRiNg', data)
8787
content_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
88-
request = factory.put('/notes/547/', content, content_type=content_type)
88+
request = factory.put('/notes/547/', content,
89+
content_type=content_type)
8990
return Request(request, parsers=[DrfNestedParser()])
9091

9192
def test_views(self):
92-
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER', {"separator": "bracket"})
93+
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER',
94+
{"separator": "bracket"})
9395
data = {
9496
"dtc[key]": 'value',
9597
"dtc[vla]": "value2",
@@ -149,7 +151,8 @@ def test_views_options(self):
149151
self.assertFalse(results.data.mutable)
150152

151153
def test_views_invalid(self):
152-
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER', {"separator": "bracket"})
154+
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER',
155+
{"separator": "bracket"})
153156
data = {
154157
"dtc[key": 'value',
155158
"dtc[hh][oo]": "sub",
@@ -161,7 +164,8 @@ def test_views_invalid(self):
161164
results.data
162165

163166
def test_views_invalid_options(self):
164-
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER', {"separator": "invalid"})
167+
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER',
168+
{"separator": "invalid"})
165169
data = {
166170
"dtc[key]": 'value',
167171
"dtc[hh][oo]": "sub",
@@ -171,3 +175,38 @@ def test_views_invalid_options(self):
171175

172176
with self.assertRaises(AssertionError):
173177
results.data
178+
179+
def test_views_options_mixed_invalid(self):
180+
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER',
181+
{"separator": "mixed"})
182+
data = {
183+
"dtc[key]": 'value',
184+
"dtc[hh][oo]": "sub",
185+
"dtc[hh][aa]": "sub2"
186+
}
187+
results = self.parser_boundary(data)
188+
189+
with self.assertRaises(ParseError):
190+
results.data
191+
192+
def test_views_options_mixed_valid(self):
193+
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER',
194+
{"separator": "mixed"})
195+
data = {
196+
"dtc.key": 'value',
197+
"dtc.hh.oo": "sub",
198+
"dtc.hh.aa": "sub2"
199+
}
200+
results = self.parser_boundary(data)
201+
202+
expected = {
203+
"dtc": {
204+
"key": "value",
205+
"hh": {
206+
"aa": "sub2",
207+
"oo": "sub"
208+
}
209+
}
210+
}
211+
212+
self.assertEqual(results.data, toQueryDict(expected))

0 commit comments

Comments
 (0)