Skip to content

Commit faf346f

Browse files
authored
Merge pull request #9 from remigermain/#8-feat--empty-dict/list
#8 feat empty dict/list
2 parents 1a92c49 + e68e987 commit faf346f

File tree

15 files changed

+893
-926
lines changed

15 files changed

+893
-926
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
jobs:
99
build:
1010
name: Python ${{ matrix.python-version }}
11-
runs-on: ubuntu-latest
11+
runs-on: ubuntu-20.04
1212

1313
strategy:
1414
matrix:
@@ -35,7 +35,6 @@ jobs:
3535
- name: Install dependencies
3636
run: |
3737
python -m pip install --upgrade pip
38-
pip install flake8 pytest
3938
if [ -f requirements/dev.txt ]; then pip install -r requirements/dev.txt; fi
4039
4140
- name: Lint with flake8
@@ -47,4 +46,5 @@ jobs:
4746
4847
- name: Test with pytest
4948
run: |
50-
pytest
49+
python -m pytest -v -s --cov=nested_multipart_parser --cov-report=xml --capture=tee-sys ./tests
50+
python -m coverage report -m

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ For this to work perfectly, you must follow the following rules:
169169
- Don't put spaces between separators.
170170

171171
- By default, you can't set set duplicates keys (see options)
172+
173+
- You can set empty dict/list:
174+
for empty list: `"article.authors[]": None` -> `{"article": {"authors": [] }}`
175+
for empty dict: `"article.": None` -> `{"article": {} }`
176+
`.` last dot for empty dict (availables in `dot`, `mixed` and `mixed-dot` options)
177+
`[]` brackets empty for empty list (availables in `brackets`, `mixed` and `mixed-dot` options)
172178

173179

174180

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
from .parser import NestedParser
22

3-
__all__ = [
4-
'NestedParser'
5-
]
3+
__all__ = ["NestedParser"]

nested_multipart_parser/declare.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
class NestedDeclare:
2+
"""Create ditc/list wihout order"""
3+
4+
def __init__(self, _type=None, options=None):
5+
self._elements = {}
6+
self._options = options or {}
7+
self.set_type(_type)
8+
9+
def __repr__(self):
10+
return f"{type(self).__name__}({self._type.__name__})"
11+
12+
def set_type(self, _type):
13+
self._type = _type
14+
self._is_dict = _type is dict
15+
self._is_list = _type is list
16+
self._is_none = _type is None
17+
18+
def get_type(self):
19+
return self._type
20+
21+
def set_type_from_key(self, key):
22+
self.set_type(list if isinstance(key, int) else dict)
23+
24+
def conv_value(self, value):
25+
if isinstance(value, type(self)):
26+
value = value.convert()
27+
return value
28+
29+
def __setitem__(self, key, value):
30+
if self._is_none:
31+
self.set_type_from_key(key)
32+
if isinstance(key, int) and not self._is_list:
33+
raise ValueError("int key cant be integer for dict object")
34+
if not isinstance(key, int) and self._is_list:
35+
raise ValueError("need integer key for list elements")
36+
37+
if key in self._elements:
38+
if (
39+
isinstance(value, type(self))
40+
and isinstance(self._elements[key], type(self))
41+
and self._elements[key].get_type() == value.get_type()
42+
):
43+
return
44+
45+
if self._options.get("raise_duplicate"):
46+
raise ValueError("key is already set")
47+
48+
if not self._options.get("assign_duplicate"):
49+
return
50+
51+
self._elements[key] = value
52+
53+
def __getitem__(self, key):
54+
if key not in self._elements:
55+
self[key] = type(self)(options=self._options)
56+
return self._elements[key]
57+
58+
def _convert_list(self):
59+
keys = sorted(self._elements.keys())
60+
if keys != list(range(len(keys))):
61+
raise ValueError("invalid format list keys")
62+
63+
return [self.conv_value(self._elements[key]) for key in keys]
64+
65+
def _convert_dict(self):
66+
return {key: self.conv_value(value) for key, value in self._elements.items()}
67+
68+
def convert(self):
69+
if self._is_none:
70+
return None
71+
if self._is_list:
72+
return self._convert_list()
73+
return self._convert_dict()

nested_multipart_parser/drf.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,46 @@
11
from .parser import NestedParser as NestPars
2-
from rest_framework.parsers import MultiPartParser, DataAndFiles
2+
from rest_framework.parsers import MultiPartParser
33
from rest_framework.exceptions import ParseError
44
from django.http import QueryDict
55
from django.conf import settings
66

7-
DRF_OPTIONS = {
8-
"querydict": True
9-
}
7+
DRF_OPTIONS = {"querydict": True}
108

11-
class NestedParser(NestPars):
129

10+
class NestedParser(NestPars):
1311
def __init__(self, data):
1412
# merge django settings to default DRF_OPTIONS ( special parser options in on parser)
1513
options = {
1614
**DRF_OPTIONS,
17-
**getattr(settings, "DRF_NESTED_MULTIPART_PARSER", {})
15+
**getattr(settings, "DRF_NESTED_MULTIPART_PARSER", {}),
1816
}
1917
super().__init__(data, options)
2018

21-
def convert_value(self, data, key):
22-
# all value in querydict as set in list
23-
value = data[key]
24-
if isinstance(value, list):
19+
def convert_value(self, value):
20+
if isinstance(value, list) and len(value) > 0:
2521
return value[0]
2622
return value
2723

2824
@property
2925
def validate_data(self):
3026
data = super().validate_data
31-
27+
3228
# return dict ( not conver to querydict)
3329
if not self._options["querydict"]:
3430
return data
35-
31+
3632
dtc = QueryDict(mutable=True)
3733
dtc.update(data)
3834
dtc.mutable = False
3935
return dtc
4036

41-
class DrfNestedParser(MultiPartParser):
4237

38+
class DrfNestedParser(MultiPartParser):
4339
def parse(self, stream, media_type=None, parser_context=None):
4440
clsDataAndFile = super().parse(stream, media_type, parser_context)
4541

4642
data = clsDataAndFile.data.dict()
47-
data.update(clsDataAndFile.files.dict()) # add files to data
43+
data.update(clsDataAndFile.files.dict()) # add files to data
4844

4945
parser = NestedParser(data)
5046
if parser.is_valid():

nested_multipart_parser/options.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import re
2+
3+
4+
class InvalidFormat(Exception):
5+
"""key is invalid formated"""
6+
7+
def __init__(self, key):
8+
super().__init__(f"invaid key format: {key}")
9+
10+
11+
class NestedParserOptionsType(type):
12+
def __new__(cls, cls_name, ns, childs):
13+
if cls_name != "NestedParserOptionsAbstract" and cls_name:
14+
if "sanitize" not in childs:
15+
raise ValueError("you need to define sanitize methods")
16+
return super().__new__(cls, cls_name, ns, childs)
17+
18+
19+
TOKEN_PARSER = ("[", "]", ".")
20+
21+
22+
class NestedParserOptionsAbstract(metaclass=NestedParserOptionsType):
23+
def check(self, key, keys):
24+
if len(keys) == 0:
25+
raise InvalidFormat(key)
26+
27+
first = keys[0]
28+
for token in TOKEN_PARSER:
29+
if token in first:
30+
raise InvalidFormat(key)
31+
32+
for key in keys:
33+
if not isinstance(key, str):
34+
continue
35+
for c in key:
36+
if c.isspace():
37+
raise InvalidFormat(key)
38+
39+
def split(self, key):
40+
contents = list(filter(None, self._reg_spliter.split(key)))
41+
if not contents:
42+
raise ValueError(f"invalid form key: {key}")
43+
44+
lst = [contents[0]]
45+
if len(contents) >= 2:
46+
lst.extend(self._reg_options.split(contents[1]))
47+
if len(contents) == 3:
48+
lst.append(contents[2])
49+
50+
return list(filter(None, lst))
51+
52+
53+
REGEX_SEPARATOR = {
54+
"dot": r"(\.[^\.]+)",
55+
"bracket": r"([^\[\]]+)",
56+
"mixed": r"(\[\d+\])|([^\[\]]+)",
57+
"mixed-dot": r"(\[\d+\])|(\.[^\[\]\.]+)",
58+
}
59+
60+
61+
class NestedParserOptionsDot(NestedParserOptionsAbstract):
62+
def __init__(self):
63+
self._reg_spliter = re.compile(r"^([^\.]+)(.*?)(\.)?$")
64+
self._reg_options = re.compile(r"(\.[^\.]+)")
65+
66+
def sanitize(self, key, value):
67+
contents = self.split(key)
68+
lst = contents[1:]
69+
keys = [contents[0]]
70+
for idx, k in enumerate(lst):
71+
if k.startswith("."):
72+
k = k[1:]
73+
if not k:
74+
if len(lst) != idx + 1:
75+
raise InvalidFormat(key)
76+
value = {}
77+
break
78+
try:
79+
k = int(k)
80+
except Exception:
81+
pass
82+
else:
83+
raise InvalidFormat(key)
84+
keys.append(k)
85+
86+
return keys, value
87+
88+
89+
class NestedParserOptionsBracket(NestedParserOptionsAbstract):
90+
def __init__(self):
91+
self._reg_spliter = re.compile(r"^([^\[\]]+)(.*?)(\[\])?$")
92+
self._reg_options = re.compile(r"(\[[^\[\]]+\])")
93+
94+
def sanitize(self, key, value):
95+
first, *lst = self.split(key)
96+
keys = [first]
97+
98+
for idx, k in enumerate(lst):
99+
if k.startswith("[") or k.endswith("]"):
100+
if not k.startswith("[") or not k.endswith("]"):
101+
raise InvalidFormat(key)
102+
k = k[1:-1]
103+
if not k:
104+
if len(lst) != idx + 1:
105+
raise InvalidFormat(key)
106+
value = []
107+
break
108+
try:
109+
k = int(k)
110+
except Exception:
111+
pass
112+
else:
113+
raise InvalidFormat(key)
114+
keys.append(k)
115+
return keys, value
116+
117+
118+
class NestedParserOptionsMixedDot(NestedParserOptionsAbstract):
119+
def __init__(self):
120+
self._reg_spliter = re.compile(r"^([^\[\]\.]+)(.*?)((?:\.)|(?:\[\]))?$")
121+
self._reg_options = re.compile(r"(\[\d+\])|(\.[^\[\]\.]+)")
122+
123+
def sanitize(self, key, value):
124+
first, *lst = self.split(key)
125+
keys = [first]
126+
127+
for idx, k in enumerate(lst):
128+
if k.startswith("."):
129+
k = k[1:]
130+
# empty dict
131+
if not k:
132+
if len(lst) != idx + 1:
133+
raise InvalidFormat(key)
134+
value = {}
135+
break
136+
elif k.startswith("[") or k.endswith("]"):
137+
if not k.startswith("[") or not k.endswith("]"):
138+
raise InvalidFormat(key)
139+
k = k[1:-1]
140+
if not k:
141+
if len(lst) != idx + 1:
142+
raise InvalidFormat(key)
143+
value = []
144+
break
145+
k = int(k)
146+
else:
147+
raise InvalidFormat(key)
148+
keys.append(k)
149+
150+
return keys, value
151+
152+
153+
class NestedParserOptionsMixed(NestedParserOptionsMixedDot):
154+
def __init__(self):
155+
self._reg_spliter = re.compile(r"^([^\[\]\.]+)(.*?)((?:\.)|(?:\[\]))?$")
156+
self._reg_options = re.compile(r"(\[\d+\])|(\.?[^\[\]\.]+)")
157+
158+
def sanitize(self, key, value):
159+
first, *lst = self.split(key)
160+
keys = [first]
161+
162+
for idx, k in enumerate(lst):
163+
if k.startswith("."):
164+
k = k[1:]
165+
# empty dict
166+
if not k:
167+
if len(lst) != idx + 1:
168+
raise InvalidFormat(key)
169+
value = {}
170+
break
171+
elif k.startswith("[") or k.endswith("]"):
172+
if not k.startswith("[") or not k.endswith("]"):
173+
raise InvalidFormat(key)
174+
k = k[1:-1]
175+
if not k:
176+
if len(lst) != idx + 1:
177+
raise InvalidFormat(key)
178+
value = []
179+
break
180+
k = int(k)
181+
keys.append(k)
182+
183+
return keys, value

0 commit comments

Comments
 (0)