Skip to content
Merged
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ Attributes where sub keys are other than full numbers are converted into Python
data = {
'the.0.chained.key.0.are.awesome.0.0': 'im here !!'
}
# with "mixed" separator option:
data = {
'the[0].chained.key[0].are.awesome[0][0]': 'im here !!'
}
```


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

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

- For `mixed` options, brackets `[]` is for list, and dot `.` is for object

- Don't put spaces between separators.

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


# raise a expections when you have duplicate keys
Expand Down
79 changes: 67 additions & 12 deletions nested_multipart_parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,85 @@ def _merge_options(self, options):
options = {**DEFAULT_OPTIONS, **options}
self._options = options

assert self._options.get("separator", "dot") in ["dot", "bracket"]
assert self._options.get("separator", "dot") in [
"dot", "bracket", "mixed"]
assert isinstance(self._options.get("raise_duplicate", False), bool)
assert isinstance(self._options.get("assign_duplicate", False), bool)

self._is_dot = self._options["separator"] == "dot"
if not self.is_dot:
self.__is_dot = False
self.__is_mixed = False
self.__is_bracket = False
if self._options["separator"] == "dot":
self.__is_dot = True
elif self._options["separator"] == "mixed":
self.__is_mixed = True
else:
self.__is_bracket = True
self._reg = re.compile(r"\[|\]")

@property
def is_dot(self):
return self._is_dot
def mixed_split(self, key):
def span(key, i):
old = i
while i != len(key):
if key[i] in ".[]":
break
i += 1
if old == i:
raise ValueError(
f"invalid format key '{full_keys}', empty key value at position {i + pos}")
return i

full_keys = key
idx = span(key, 0)
pos = idx
keys = [key[:idx]]
key = key[idx:]

i = 0
while i < len(key):
if key[i] == '.':
i += 1
idx = span(key, i)
keys.append(key[i: idx])
i = idx
elif key[i] == '[':
i += 1
idx = span(key, i)
if key[idx] != ']':
raise ValueError(
f"invalid format key '{full_keys}', not end with bracket at position {i + pos}")
sub = key[i: idx]
if not sub.isdigit():
raise ValueError(
f"invalid format key '{full_keys}', list key is not a valid number at position {i + pos}")
keys.append(int(key[i: idx]))
i = idx + 1
elif key[i] == ']':
raise ValueError(
f"invalid format key '{full_keys}', not start with bracket at position {i + pos}")
else:
raise ValueError(
f"invalid format key '{full_keys}', invalid char at position {i + pos}")
return keys

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

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

# optimize by split with string func
if self.is_dot:
if self.__is_mixed:
return self.mixed_split(key)
if self.__is_dot:
length = 1
splitter = k.split(".")
splitter = key.split(".")
else:
length = 2
splitter = self._reg.split(k)
splitter = self._reg.split(key)

check = -length

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

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

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

def get_next_type(self, keys):
return [] if keys.isdigit() else {}
def get_next_type(self, key):
if self.__is_mixed:
return [] if isinstance(key, int) else {}
return [] if key.isdigit() else {}

def convert_value(self, data, key):
return data[key]
Expand Down
47 changes: 43 additions & 4 deletions tests/test_drf.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,13 @@ def parser_boundary(self, data):
factory = APIRequestFactory()
content = encode_multipart('BoUnDaRyStRiNg', data)
content_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
request = factory.put('/notes/547/', content, content_type=content_type)
request = factory.put('/notes/547/', content,
content_type=content_type)
return Request(request, parsers=[DrfNestedParser()])

def test_views(self):
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER', {"separator": "bracket"})
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER',
{"separator": "bracket"})
data = {
"dtc[key]": 'value',
"dtc[vla]": "value2",
Expand Down Expand Up @@ -149,7 +151,8 @@ def test_views_options(self):
self.assertFalse(results.data.mutable)

def test_views_invalid(self):
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER', {"separator": "bracket"})
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER',
{"separator": "bracket"})
data = {
"dtc[key": 'value',
"dtc[hh][oo]": "sub",
Expand All @@ -161,7 +164,8 @@ def test_views_invalid(self):
results.data

def test_views_invalid_options(self):
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER', {"separator": "invalid"})
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER',
{"separator": "invalid"})
data = {
"dtc[key]": 'value',
"dtc[hh][oo]": "sub",
Expand All @@ -171,3 +175,38 @@ def test_views_invalid_options(self):

with self.assertRaises(AssertionError):
results.data

def test_views_options_mixed_invalid(self):
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER',
{"separator": "mixed"})
data = {
"dtc[key]": 'value',
"dtc[hh][oo]": "sub",
"dtc[hh][aa]": "sub2"
}
results = self.parser_boundary(data)

with self.assertRaises(ParseError):
results.data

def test_views_options_mixed_valid(self):
setattr(settings, 'DRF_NESTED_MULTIPART_PARSER',
{"separator": "mixed"})
data = {
"dtc.key": 'value',
"dtc.hh.oo": "sub",
"dtc.hh.aa": "sub2"
}
results = self.parser_boundary(data)

expected = {
"dtc": {
"key": "value",
"hh": {
"aa": "sub2",
"oo": "sub"
}
}
}

self.assertEqual(results.data, toQueryDict(expected))
Loading