diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 08313fe..7b7b133 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ on: jobs: build: name: Python ${{ matrix.python-version }} - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: @@ -35,7 +35,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest if [ -f requirements/dev.txt ]; then pip install -r requirements/dev.txt; fi - name: Lint with flake8 @@ -47,4 +46,5 @@ jobs: - name: Test with pytest run: | - pytest + python -m pytest -v -s --cov=nested_multipart_parser --cov-report=xml --capture=tee-sys ./tests + python -m coverage report -m diff --git a/README.md b/README.md index 154a960..d492b2c 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,12 @@ For this to work perfectly, you must follow the following rules: - Don't put spaces between separators. - By default, you can't set set duplicates keys (see options) + +- You can set empty dict/list: + for empty list: `"article.authors[]": None` -> `{"article": {"authors": [] }}` + for empty dict: `"article.": None` -> `{"article": {} }` + `.` last dot for empty dict (availables in `dot`, `mixed` and `mixed-dot` options) + `[]` brackets empty for empty list (availables in `brackets`, `mixed` and `mixed-dot` options) diff --git a/nested_multipart_parser/__init__.py b/nested_multipart_parser/__init__.py index 9477144..2a0c204 100644 --- a/nested_multipart_parser/__init__.py +++ b/nested_multipart_parser/__init__.py @@ -1,5 +1,3 @@ from .parser import NestedParser -__all__ = [ - 'NestedParser' -] +__all__ = ["NestedParser"] diff --git a/nested_multipart_parser/declare.py b/nested_multipart_parser/declare.py new file mode 100644 index 0000000..db2c95a --- /dev/null +++ b/nested_multipart_parser/declare.py @@ -0,0 +1,73 @@ +class NestedDeclare: + """Create ditc/list wihout order""" + + def __init__(self, _type=None, options=None): + self._elements = {} + self._options = options or {} + self.set_type(_type) + + def __repr__(self): + return f"{type(self).__name__}({self._type.__name__})" + + def set_type(self, _type): + self._type = _type + self._is_dict = _type is dict + self._is_list = _type is list + self._is_none = _type is None + + def get_type(self): + return self._type + + def set_type_from_key(self, key): + self.set_type(list if isinstance(key, int) else dict) + + def conv_value(self, value): + if isinstance(value, type(self)): + value = value.convert() + return value + + def __setitem__(self, key, value): + if self._is_none: + self.set_type_from_key(key) + if isinstance(key, int) and not self._is_list: + raise ValueError("int key cant be integer for dict object") + if not isinstance(key, int) and self._is_list: + raise ValueError("need integer key for list elements") + + if key in self._elements: + if ( + isinstance(value, type(self)) + and isinstance(self._elements[key], type(self)) + and self._elements[key].get_type() == value.get_type() + ): + return + + if self._options.get("raise_duplicate"): + raise ValueError("key is already set") + + if not self._options.get("assign_duplicate"): + return + + self._elements[key] = value + + def __getitem__(self, key): + if key not in self._elements: + self[key] = type(self)(options=self._options) + return self._elements[key] + + def _convert_list(self): + keys = sorted(self._elements.keys()) + if keys != list(range(len(keys))): + raise ValueError("invalid format list keys") + + return [self.conv_value(self._elements[key]) for key in keys] + + def _convert_dict(self): + return {key: self.conv_value(value) for key, value in self._elements.items()} + + def convert(self): + if self._is_none: + return None + if self._is_list: + return self._convert_list() + return self._convert_dict() diff --git a/nested_multipart_parser/drf.py b/nested_multipart_parser/drf.py index 3454398..0ebb336 100644 --- a/nested_multipart_parser/drf.py +++ b/nested_multipart_parser/drf.py @@ -1,50 +1,46 @@ from .parser import NestedParser as NestPars -from rest_framework.parsers import MultiPartParser, DataAndFiles +from rest_framework.parsers import MultiPartParser from rest_framework.exceptions import ParseError from django.http import QueryDict from django.conf import settings -DRF_OPTIONS = { - "querydict": True -} +DRF_OPTIONS = {"querydict": True} -class NestedParser(NestPars): +class NestedParser(NestPars): def __init__(self, data): # merge django settings to default DRF_OPTIONS ( special parser options in on parser) options = { **DRF_OPTIONS, - **getattr(settings, "DRF_NESTED_MULTIPART_PARSER", {}) + **getattr(settings, "DRF_NESTED_MULTIPART_PARSER", {}), } super().__init__(data, options) - def convert_value(self, data, key): - # all value in querydict as set in list - value = data[key] - if isinstance(value, list): + def convert_value(self, value): + if isinstance(value, list) and len(value) > 0: return value[0] return value @property def validate_data(self): data = super().validate_data - + # return dict ( not conver to querydict) if not self._options["querydict"]: return data - + dtc = QueryDict(mutable=True) dtc.update(data) dtc.mutable = False return dtc -class DrfNestedParser(MultiPartParser): +class DrfNestedParser(MultiPartParser): def parse(self, stream, media_type=None, parser_context=None): clsDataAndFile = super().parse(stream, media_type, parser_context) data = clsDataAndFile.data.dict() - data.update(clsDataAndFile.files.dict()) # add files to data + data.update(clsDataAndFile.files.dict()) # add files to data parser = NestedParser(data) if parser.is_valid(): diff --git a/nested_multipart_parser/options.py b/nested_multipart_parser/options.py new file mode 100644 index 0000000..6a6e6d5 --- /dev/null +++ b/nested_multipart_parser/options.py @@ -0,0 +1,183 @@ +import re + + +class InvalidFormat(Exception): + """key is invalid formated""" + + def __init__(self, key): + super().__init__(f"invaid key format: {key}") + + +class NestedParserOptionsType(type): + def __new__(cls, cls_name, ns, childs): + if cls_name != "NestedParserOptionsAbstract" and cls_name: + if "sanitize" not in childs: + raise ValueError("you need to define sanitize methods") + return super().__new__(cls, cls_name, ns, childs) + + +TOKEN_PARSER = ("[", "]", ".") + + +class NestedParserOptionsAbstract(metaclass=NestedParserOptionsType): + def check(self, key, keys): + if len(keys) == 0: + raise InvalidFormat(key) + + first = keys[0] + for token in TOKEN_PARSER: + if token in first: + raise InvalidFormat(key) + + for key in keys: + if not isinstance(key, str): + continue + for c in key: + if c.isspace(): + raise InvalidFormat(key) + + def split(self, key): + contents = list(filter(None, self._reg_spliter.split(key))) + if not contents: + raise ValueError(f"invalid form key: {key}") + + lst = [contents[0]] + if len(contents) >= 2: + lst.extend(self._reg_options.split(contents[1])) + if len(contents) == 3: + lst.append(contents[2]) + + return list(filter(None, lst)) + + +REGEX_SEPARATOR = { + "dot": r"(\.[^\.]+)", + "bracket": r"([^\[\]]+)", + "mixed": r"(\[\d+\])|([^\[\]]+)", + "mixed-dot": r"(\[\d+\])|(\.[^\[\]\.]+)", +} + + +class NestedParserOptionsDot(NestedParserOptionsAbstract): + def __init__(self): + self._reg_spliter = re.compile(r"^([^\.]+)(.*?)(\.)?$") + self._reg_options = re.compile(r"(\.[^\.]+)") + + def sanitize(self, key, value): + contents = self.split(key) + lst = contents[1:] + keys = [contents[0]] + for idx, k in enumerate(lst): + if k.startswith("."): + k = k[1:] + if not k: + if len(lst) != idx + 1: + raise InvalidFormat(key) + value = {} + break + try: + k = int(k) + except Exception: + pass + else: + raise InvalidFormat(key) + keys.append(k) + + return keys, value + + +class NestedParserOptionsBracket(NestedParserOptionsAbstract): + def __init__(self): + self._reg_spliter = re.compile(r"^([^\[\]]+)(.*?)(\[\])?$") + self._reg_options = re.compile(r"(\[[^\[\]]+\])") + + def sanitize(self, key, value): + first, *lst = self.split(key) + keys = [first] + + for idx, k in enumerate(lst): + if k.startswith("[") or k.endswith("]"): + if not k.startswith("[") or not k.endswith("]"): + raise InvalidFormat(key) + k = k[1:-1] + if not k: + if len(lst) != idx + 1: + raise InvalidFormat(key) + value = [] + break + try: + k = int(k) + except Exception: + pass + else: + raise InvalidFormat(key) + keys.append(k) + return keys, value + + +class NestedParserOptionsMixedDot(NestedParserOptionsAbstract): + def __init__(self): + self._reg_spliter = re.compile(r"^([^\[\]\.]+)(.*?)((?:\.)|(?:\[\]))?$") + self._reg_options = re.compile(r"(\[\d+\])|(\.[^\[\]\.]+)") + + def sanitize(self, key, value): + first, *lst = self.split(key) + keys = [first] + + for idx, k in enumerate(lst): + if k.startswith("."): + k = k[1:] + # empty dict + if not k: + if len(lst) != idx + 1: + raise InvalidFormat(key) + value = {} + break + elif k.startswith("[") or k.endswith("]"): + if not k.startswith("[") or not k.endswith("]"): + raise InvalidFormat(key) + k = k[1:-1] + if not k: + if len(lst) != idx + 1: + raise InvalidFormat(key) + value = [] + break + k = int(k) + else: + raise InvalidFormat(key) + keys.append(k) + + return keys, value + + +class NestedParserOptionsMixed(NestedParserOptionsMixedDot): + def __init__(self): + self._reg_spliter = re.compile(r"^([^\[\]\.]+)(.*?)((?:\.)|(?:\[\]))?$") + self._reg_options = re.compile(r"(\[\d+\])|(\.?[^\[\]\.]+)") + + def sanitize(self, key, value): + first, *lst = self.split(key) + keys = [first] + + for idx, k in enumerate(lst): + if k.startswith("."): + k = k[1:] + # empty dict + if not k: + if len(lst) != idx + 1: + raise InvalidFormat(key) + value = {} + break + elif k.startswith("[") or k.endswith("]"): + if not k.startswith("[") or not k.endswith("]"): + raise InvalidFormat(key) + k = k[1:-1] + if not k: + if len(lst) != idx + 1: + raise InvalidFormat(key) + value = [] + break + k = int(k) + keys.append(k) + + return keys, value diff --git a/nested_multipart_parser/parser.py b/nested_multipart_parser/parser.py index 8925e37..743310e 100644 --- a/nested_multipart_parser/parser.py +++ b/nested_multipart_parser/parser.py @@ -1,185 +1,66 @@ -import re +from nested_multipart_parser.declare import NestedDeclare +from nested_multipart_parser.options import ( + NestedParserOptionsMixedDot, + NestedParserOptionsMixed, + NestedParserOptionsBracket, + NestedParserOptionsDot, +) + + DEFAULT_OPTIONS = { "separator": "mixed-dot", "raise_duplicate": True, - "assign_duplicate": False + "assign_duplicate": False, +} + +REGEX_SEPARATOR = { + "bracket": NestedParserOptionsBracket, + "dot": NestedParserOptionsDot, + "mixed": NestedParserOptionsMixed, + "mixed-dot": NestedParserOptionsMixedDot, } + class NestedParser: _valid = None errors = None def __init__(self, data, options={}): self.data = data - self._merge_options(options) - - def _merge_options(self, options): - options = {**DEFAULT_OPTIONS, **options} - self._options = options - - assert self._options.get("separator", "mixed-dot") in [ - "dot", "bracket", "mixed", "mixed-dot"] - assert isinstance(self._options.get("raise_duplicate", False), bool) - assert isinstance(self._options.get("assign_duplicate", False), bool) - - self.__is_dot = False - self.__is_mixed = False - self.__is_bracket = False - self.__is_mixed_dot = False - if self._options["separator"] == "dot": - self.__is_dot = True - elif self._options["separator"] == "mixed": - self.__is_mixed = True - elif self._options["separator"] == "mixed-dot": - self.__is_mixed_dot = True - else: - self.__is_bracket = True - self._reg = re.compile(r"\[|\]") - - 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 - last_is_list = False - while i < len(key): - if 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 - last_is_list = True - elif key[i] == ']': - raise ValueError( - f"invalid format key '{full_keys}', not start with bracket at position {i + pos}") - elif (key[i] == '.' and self.__is_mixed_dot) or ( - not self.__is_mixed_dot and ( - (key[i] != '.' and last_is_list) or - (key[i] == '.' and not last_is_list) - ) - ): - if self.__is_mixed_dot or not last_is_list: - i += 1 - idx = span(key, i) - keys.append(key[i: idx]) - i = idx - last_is_list = False - 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_mixed or self.__is_mixed_dot: - return self.mixed_split(key) - if self.__is_dot: - length = 1 - splitter = key.split(".") - else: - length = 2 - splitter = self._reg.split(key) - - check = -length - - results = [] - for select in splitter: - if select: - results.append(select) - check += len(select) + length - - if len(key) != check: - raise Exception(f"invalid format from key {key}") - return results - - def set_type(self, dtc, key, value, full_keys, prev=None, last=False): - if isinstance(dtc, list): - key = int(key) - if len(dtc) < key: - raise ValueError( - f"key \"{full_keys}\" is upper than actual list") - if len(dtc) == key: - dtc.append(value) - elif isinstance(dtc, dict): - if key not in dtc or last and self._options["assign_duplicate"]: - dtc[key] = value - else: - if self._options["raise_duplicate"]: - raise ValueError( - f"invalid rewrite key from \"{full_keys}\" to \"{dtc}\"") - elif self._options["assign_duplicate"]: - dtc = prev['dtc'] - dtc[prev['key']] = prev['type'] - return self.set_type(dtc[prev['key']], key, value, full_keys, prev, last) - return key - - def get_next_type(self, key): - if self.__is_mixed or self.__is_mixed_dot: - return [] if isinstance(key, int) else {} - return [] if key.isdigit() else {} - - def convert_value(self, data, key): - return data[key] + self._options = {**DEFAULT_OPTIONS, **options} - def construct(self, data): - dictionary = {} - prev = {} + assert self._options["separator"] in ["dot", "bracket", "mixed", "mixed-dot"] + assert isinstance(self._options["raise_duplicate"], bool) + assert isinstance(self._options["assign_duplicate"], bool) - for key in data: - keys = self.split_key(key) - tmp = dictionary + self._cls_options = REGEX_SEPARATOR[self._options["separator"]] + + def _split_keys(self, data): + for key, value in data.items(): + checker = self._cls_options() + keys, value = checker.sanitize(key, value) + checker.check(key, keys) - # need it for duplicate assignement - prev['key'] = keys[0] - prev['dtc'] = tmp - prev['type'] = None + yield keys, value - # optimize with while loop instend of for in with zip function - i = 0 - lenght = len(keys) - 1 - while i < lenght: - set_type = self.get_next_type(keys[i+1]) - index = self.set_type(tmp, keys[i], set_type, key, prev) + def convert_value(self, value): + return value - prev['dtc'] = tmp - prev['key'] = index - prev['type'] = set_type + def construct(self, data): + dictionary = NestedDeclare(dict, self._options) + + for keys, value in self._split_keys(data): + tmp = dictionary - tmp = tmp[index] - i += 1 + for actual_key, next_key in zip(keys, keys[1:]): + if isinstance(next_key, int): + tmp[actual_key] = NestedDeclare(list, self._options) + else: + tmp[actual_key] = NestedDeclare(dict, self._options) + tmp = tmp[actual_key] - value = self.convert_value(data, key) - self.set_type(tmp, keys[-1], value, key, prev, True) - return dictionary + tmp[keys[-1]] = self.convert_value(value) + return dictionary.convert() def is_valid(self): self._valid = False @@ -194,7 +75,8 @@ def is_valid(self): def validate_data(self): if self._valid is None: raise ValueError( - "You need to be call is_valid() before access validate_data") + "You need to be call is_valid() before access validate_data" + ) if self._valid is False: raise ValueError("You can't get validate data") return self.__validate_data diff --git a/requirements/dev.txt b/requirements/dev.txt index 0c03206..b6779cd 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,3 +1,7 @@ -r common.txt Django -djangorestframework \ No newline at end of file +djangorestframework + +pytest +pytest-cov +flake8 \ No newline at end of file diff --git a/setup.py b/setup.py index 474311b..418c7aa 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ import sys import subprocess -version = "1.4.1" +version = "1.5.0" if sys.argv[-1] == 'publish': if os.system("pip freeze | grep twine"): @@ -28,8 +28,8 @@ name="nested-multipart-parser", version=version, author="rgermain", - license='MIT', - author_email='contact@germainremi.fr', + license="MIT", + author_email="contact@germainremi.fr", description="A parser for nested data in multipart form", long_description=long_description, long_description_content_type="text/markdown", @@ -38,23 +38,23 @@ "Bug Tracker": "https://github.com/remigermain/nested-multipart-parser/issues", }, classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Framework :: Django :: 3.1', - 'Framework :: Django :: 3.2', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3 :: Only', - 'Topic :: Internet :: WWW/HTTP', + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 2.2", + "Framework :: Django :: 3.0", + "Framework :: Django :: 3.1", + "Framework :: Django :: 3.2", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Internet :: WWW/HTTP", ], packages=["nested_multipart_parser"], python_requires=">=3.6", diff --git a/tests/test_drf.py b/tests/test_drf.py index 5f4f859..86d1aa3 100644 --- a/tests/test_drf.py +++ b/tests/test_drf.py @@ -1,16 +1,21 @@ import unittest -from django.http import QueryDict from django.conf import settings +from django.http import QueryDict + settings.configure() -# need to be after settings configure -from rest_framework.test import APIRequestFactory # noqa: E402 +from django.core.files.uploadedfile import (InMemoryUploadedFile, + SimpleUploadedFile) from django.test.client import encode_multipart # noqa: E402 -from nested_multipart_parser.drf import DrfNestedParser, NestedParser # noqa: E402 -from rest_framework.request import Request # noqa: E402 from rest_framework.exceptions import ParseError # noqa: E402 -from django.core.files.uploadedfile import SimpleUploadedFile, InMemoryUploadedFile +from rest_framework.request import Request # noqa: E402 +# need to be after settings configure +from rest_framework.test import APIRequestFactory # noqa: E402 + +from nested_multipart_parser.drf import (DrfNestedParser, # noqa: E402 + NestedParser) + def toQueryDict(data): q = QueryDict(mutable=True) @@ -27,228 +32,176 @@ def setUp(self): def test_querydict_mutable(self): parser = NestedParser( { - "dtc.key": 'value', + "dtc.key": "value", "dtc.vla": "value2", "list[0]": "value1", "list[1]": "value2", "string": "value", "dtc.hh.oo": "sub", - "dtc.hh.aa": "sub2" + "dtc.hh.aa": "sub2", }, ) self.assertTrue(parser.is_valid()) - expected = toQueryDict({ - "dtc": { - "key": "value", - "vla": "value2", - "hh": { - "oo": "sub", - "aa": "sub2" - } - }, - "list": [ - "value1", - "value2", - ], - "string": "value", - }) + expected = toQueryDict( + { + "dtc": { + "key": "value", + "vla": "value2", + "hh": {"oo": "sub", "aa": "sub2"}, + }, + "list": [ + "value1", + "value2", + ], + "string": "value", + } + ) self.assertEqual(parser.validate_data, expected) self.assertFalse(parser.validate_data.mutable) def test_settings(self): from nested_multipart_parser.drf import NestedParser - data = { - "article.title": "youpi" - } + data = {"article.title": "youpi"} p = NestedParser(data) self.assertTrue(p.is_valid()) - expected = toQueryDict({ - "article": { - "title": "youpi" - } - }) + expected = toQueryDict({"article": {"title": "youpi"}}) self.assertEqual(p.validate_data, expected) # set settings from django.conf import settings - options = { - "separator": "dot" - } - setattr(settings, 'DRF_NESTED_MULTIPART_PARSER', options) + + options = {"separator": "dot"} + setattr(settings, "DRF_NESTED_MULTIPART_PARSER", options) p = NestedParser(data) self.assertTrue(p.is_valid()) - expected = toQueryDict({ - "article": { - "title": "youpi" - } - }) + expected = toQueryDict({"article": {"title": "youpi"}}) self.assertEqual(p.validate_data, expected) 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) + content = encode_multipart("BoUnDaRyStRiNg", data) + content_type = "multipart/form-data; boundary=BoUnDaRyStRiNg" + 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[key]": "value", "dtc[vla]": "value2", "list[0]": "value1", "list[1]": "value2", "string": "value", "dtc[hh][oo]": "sub", - "dtc[hh][aa]": "sub2" + "dtc[hh][aa]": "sub2", } results = self.parser_boundary(data) - expected = toQueryDict({ - "dtc": { - "key": "value", - "vla": "value2", - "hh": { - "oo": "sub", - "aa": "sub2" - } - }, - "list": [ - "value1", - "value2", - ], - "string": "value", - }) + expected = toQueryDict( + { + "dtc": { + "key": "value", + "vla": "value2", + "hh": {"oo": "sub", "aa": "sub2"}, + }, + "list": [ + "value1", + "value2", + ], + "string": "value", + } + ) self.assertEqual(results.data, expected) self.assertFalse(results.data.mutable) def test_views_options(self): - setattr(settings, 'DRF_NESTED_MULTIPART_PARSER', {"separator": "dot"}) + setattr(settings, "DRF_NESTED_MULTIPART_PARSER", {"separator": "dot"}) data = { - "dtc.key": 'value', + "dtc.key": "value", "dtc.vla": "value2", "list.0": "value1", "list.1": "value2", "string": "value", "dtc.hh.oo": "sub", - "dtc.hh.aa": "sub2" + "dtc.hh.aa": "sub2", } results = self.parser_boundary(data) - expected = toQueryDict({ - "dtc": { - "key": "value", - "vla": "value2", - "hh": { - "oo": "sub", - "aa": "sub2" - } - }, - "list": [ - "value1", - "value2", - ], - "string": "value", - }) + expected = toQueryDict( + { + "dtc": { + "key": "value", + "vla": "value2", + "hh": {"oo": "sub", "aa": "sub2"}, + }, + "list": [ + "value1", + "value2", + ], + "string": "value", + } + ) self.assertEqual(results.data, expected) self.assertFalse(results.data.mutable) def test_views_invalid(self): - setattr(settings, 'DRF_NESTED_MULTIPART_PARSER', - {"separator": "bracket"}) - data = { - "dtc[key": 'value', - "dtc[hh][oo]": "sub", - "dtc[hh][aa]": "sub2" - } + setattr(settings, "DRF_NESTED_MULTIPART_PARSER", {"separator": "bracket"}) + 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_invalid_options(self): - setattr(settings, 'DRF_NESTED_MULTIPART_PARSER', - {"separator": "invalid"}) - data = { - "dtc[key]": 'value', - "dtc[hh][oo]": "sub", - "dtc[hh][aa]": "sub2" - } + setattr(settings, "DRF_NESTED_MULTIPART_PARSER", {"separator": "invalid"}) + data = {"dtc[key]": "value", "dtc[hh][oo]": "sub", "dtc[hh][aa]": "sub2"} results = self.parser_boundary(data) 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" - } + 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" - } + 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" - } - } - } + expected = {"dtc": {"key": "value", "hh": {"aa": "sub2", "oo": "sub"}}} self.assertEqual(results.data, toQueryDict(expected)) - + def test_output_querydict(self): - setattr(settings, 'DRF_NESTED_MULTIPART_PARSER', - {"separator": "mixed", "querydict": False}) - data = { - "dtc.key": 'value', - "dtc.hh.oo": "sub", - "dtc.hh.aa": "sub2" - } + setattr( + settings, + "DRF_NESTED_MULTIPART_PARSER", + {"separator": "mixed", "querydict": False}, + ) + 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" - } - } - } + expected = {"dtc": {"key": "value", "hh": {"aa": "sub2", "oo": "sub"}}} self.assertDictEqual(results.data, expected) - def test_nested_files(self): - file = SimpleUploadedFile("file.png", b"file_content", content_type="image/png") - file1 = SimpleUploadedFile("file.pdf", b"file_content", content_type="application/pdf") + file = SimpleUploadedFile("file.png", b"file_content", content_type="image/png") + file1 = SimpleUploadedFile( + "file.pdf", b"file_content", content_type="application/pdf" + ) data = { "file": file, "title": "title", - 'files[0].description': 'description', - 'files[1].file': file1, - 'files[1].description': 'description2', + "files[0].description": "description", + "files[1].file": file1, + "files[1].description": "description2", } results = self.parser_boundary(data) @@ -263,7 +216,7 @@ def test_nested_files(self): { "file": file1, "description": "description2", - } + }, ], } data = results.data.dict() @@ -283,3 +236,32 @@ def test_nested_files(self): self.assertEqual(len(data["files"][1]), 2) self.assertEqual(data["files"][1]["description"], "description2") self.assertIsInstance(data["files"][1]["file"], InMemoryUploadedFile) + + def test_nested_files_index_not_order(self): + file = SimpleUploadedFile("file.png", b"file_content", content_type="image/png") + file1 = SimpleUploadedFile("file.pdf", b"file_content", content_type="application/pdf") + + data = { + "files[2]": file1, + "files[1].description": "description2", + "files[1].file": file, + "files[0].description": "description", + } + results = self.parser_boundary(data) + + data = results.data.dict() + self.assertEqual(len(data), 1) + + self.assertEqual(len(data["files"]), 3) + self.assertIsInstance(data["files"], list) + + self.assertIsInstance(data["files"][0], dict) + self.assertEqual(len(data["files"][0]), 1) + self.assertEqual(data["files"][0]["description"], "description") + + self.assertIsInstance(data["files"][1], dict) + self.assertEqual(len(data["files"][1]), 2) + self.assertEqual(data["files"][1]["description"], "description2") + self.assertIsInstance(data["files"][1]["file"], InMemoryUploadedFile) + + self.assertIsInstance(data["files"][2], InMemoryUploadedFile) \ No newline at end of file diff --git a/tests/test_mixed_dot_separator.py b/tests/test_mixed_dot_separator.py index 665f264..fed44c0 100644 --- a/tests/test_mixed_dot_separator.py +++ b/tests/test_mixed_dot_separator.py @@ -3,18 +3,18 @@ class TestSettingsSeparatorMixedDot(TestCase): - def test_assign_duplicate_list(self): - data = { - "title": 42, - "title[0]": 101 - } + data = {"title": 42, "title[0]": 101} p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed-dot"}) + data, + { + "raise_duplicate": False, + "assign_duplicate": True, + "separator": "mixed-dot", + }, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [101] - } + expected = {"title": [101]} self.assertEqual(p.validate_data, expected) def test_assign_duplicate_number_after_list(self): @@ -23,11 +23,15 @@ def test_assign_duplicate_number_after_list(self): "title": 42, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed-dot"}) + data, + { + "raise_duplicate": False, + "assign_duplicate": True, + "separator": "mixed-dot", + }, + ) self.assertTrue(p.is_valid()) - expected = { - "title": 42 - } + expected = {"title": 42} self.assertEqual(p.validate_data, expected) def test_assign_nested_duplicate_number_after_list(self): @@ -36,15 +40,15 @@ def test_assign_nested_duplicate_number_after_list(self): "title[0].sub": 42, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed-dot"}) + data, + { + "raise_duplicate": False, + "assign_duplicate": True, + "separator": "mixed-dot", + }, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [ - { - "sub": 42 - } - ] - } + expected = {"title": [{"sub": 42}]} self.assertEqual(p.validate_data, expected) def test_assign_nested_duplicate_number_after_list2(self): @@ -53,15 +57,15 @@ def test_assign_nested_duplicate_number_after_list2(self): "title[0].sub[0]": 101, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed-dot"}) + data, + { + "raise_duplicate": False, + "assign_duplicate": True, + "separator": "mixed-dot", + }, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [ - { - "sub": [101] - } - ] - } + expected = {"title": [{"sub": [101]}]} self.assertEqual(p.validate_data, expected) def test_assign_nested_duplicate_number_after_dict(self): @@ -70,17 +74,15 @@ def test_assign_nested_duplicate_number_after_dict(self): "title[0].sub.title": 101, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed-dot"}) + data, + { + "raise_duplicate": False, + "assign_duplicate": True, + "separator": "mixed-dot", + }, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [ - { - "sub": { - "title": 101 - } - } - ] - } + expected = {"title": [{"sub": {"title": 101}}]} self.assertEqual(p.validate_data, expected) def test_assign_nested_duplicate_number_after_dict2(self): @@ -89,197 +91,186 @@ def test_assign_nested_duplicate_number_after_dict2(self): "title[0].sub": 42, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed-dot"}) + data, + { + "raise_duplicate": False, + "assign_duplicate": True, + "separator": "mixed-dot", + }, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [ - { - "sub": 42 - } - ] - } + expected = {"title": [{"sub": 42}]} self.assertEqual(p.validate_data, expected) def test_mixed_spearator(self): data = { - 'title': 'lalal', - 'article.object': 'lalal', + "title": "lalal", + "article.object": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertTrue(parser.is_valid()) - expected = { - "title": 'lalal', - "article": { - "object": "lalal" - } - } + expected = {"title": "lalal", "article": {"object": "lalal"}} self.assertEqual(expected, parser.validate_data) def test_mixed_int_object(self): data = { - 'title': 'lalal', - 'article.0': 'lalal', + "title": "lalal", + "article.0": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertTrue(parser.is_valid()) - expected = { - "title": 'lalal', - "article": { - "0": "lalal" - } - } + expected = {"title": "lalal", "article": {"0": "lalal"}} self.assertEqual(expected, parser.validate_data) def test_mixed_int_list(self): data = { - 'title': 'lalal', - 'article[0]': 'lalal', + "title": "lalal", + "article[0]": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertTrue(parser.is_valid()) - expected = { - "title": 'lalal', - "article": [ - "lalal" - ] - } + expected = {"title": "lalal", "article": ["lalal"]} self.assertEqual(expected, parser.validate_data) def test_real(self): data = { - 'title': 'title', - 'date': "time", - 'langs[0].id': "id", - 'langs[0].title': 'title', - 'langs[0].description': 'description', - 'langs[0].language': "language", - 'langs[1].id': "id1", - 'langs[1].title': 'title1', - 'langs[1].description': 'description1', - 'langs[1].language': "language1" + "title": "title", + "date": "time", + "langs[0].id": "id", + "langs[0].title": "title", + "langs[0].description": "description", + "langs[0].language": "language", + "langs[1].id": "id1", + "langs[1].title": "title1", + "langs[1].description": "description1", + "langs[1].language": "language1", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertTrue(parser.is_valid()) expected = { - 'title': 'title', - 'date': "time", - 'langs': [ + "title": "title", + "date": "time", + "langs": [ { - 'id': 'id', - 'title': 'title', - 'description': 'description', - 'language': 'language' + "id": "id", + "title": "title", + "description": "description", + "language": "language", }, { - 'id': 'id1', - 'title': 'title1', - 'description': 'description1', - 'language': 'language1' - } - ] + "id": "id1", + "title": "title1", + "description": "description1", + "language": "language1", + }, + ], } self.assertDictEqual(parser.validate_data, expected) def test_mixed_invalid_list_index(self): data = { - 'title': 'lalal', - 'article[0f]': 'lalal', + "title": "lalal", + "article[0f]": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_list_empty_index(self): data = { - 'title': 'lalal', - 'article[]': 'lalal', + "title": "lalal", + "article[]": None, } parser = NestedParser(data, {"separator": "mixed-dot"}) - self.assertFalse(parser.is_valid()) + self.assertTrue(parser.is_valid()) + expected = {"title": "lalal", "article": []} + self.assertDictEqual(parser.validate_data, expected) def test_mixed_invalid_bracket(self): data = { - 'title': 'lalal', - 'article[': 'lalal', + "title": "lalal", + "article[": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_bracket2(self): data = { - 'title': 'lalal', - 'article]': 'lalal', + "title": "lalal", + "article]": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_list_dot(self): data = { - 'title': 'lalal', - 'article[3.]': 'lalal', + "title": "lalal", + "article[3.]": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_list_negative_index(self): data = { - 'title': 'lalal', - 'article[-3]': 'lalal', + "title": "lalal", + "article[-3]": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_object(self): data = { - 'title': 'lalal', - 'article..op': 'lalal', + "title": "lalal", + "article..op": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_object2(self): data = { - 'title': 'lalal', - 'article.op.': 'lalal', + "title": "lalal", + "article.op.": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) - self.assertFalse(parser.is_valid()) + self.assertTrue(parser.is_valid()) + expected = {"title": "lalal", "article": {"op": {}}} + self.assertDictEqual(parser.validate_data, expected) def test_mixed_invalid_object3(self): data = { - 'title': 'lalal', - 'article.op..': 'lalal', + "title": "lalal", + "article.op..": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_object4(self): data = { - 'title': 'lalal', - 'article[0]op': 'lalal', + "title": "lalal", + "article[0]op": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_list_with_object_dot(self): data = { - 'title': 'lalal', - 'article[0].op..': 'lalal', + "title": "lalal", + "article[0].op..": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertFalse(parser.is_valid()) - def test_mixed_invalid_list_with_object_dot2(self): + def test_mixed_list_with_object_dot2(self): data = { - 'title': 'lalal', - 'article[0]op[0]e.': 'lalal', + "title": "lalal", + "article[0]op[0]e.": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_list_with_object_dot3(self): data = { - 'title': 'lalal', - 'article.op.[0]': 'lalal', + "title": "lalal", + "article.op.[0]": "lalal", } parser = NestedParser(data, {"separator": "mixed-dot"}) self.assertFalse(parser.is_valid()) diff --git a/tests/test_mixed_separator.py b/tests/test_mixed_separator.py index dafa115..6926500 100644 --- a/tests/test_mixed_separator.py +++ b/tests/test_mixed_separator.py @@ -3,18 +3,14 @@ class TestSettingsSeparatorMixed(TestCase): - def test_assign_duplicate_list(self): - data = { - "title": 42, - "title[0]": 101 - } + data = {"title": 42, "title[0]": 101} p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed"}) + data, + {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed"}, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [101] - } + expected = {"title": [101]} self.assertEqual(p.validate_data, expected) def test_assign_duplicate_number_after_list(self): @@ -23,11 +19,11 @@ def test_assign_duplicate_number_after_list(self): "title": 42, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed"}) + data, + {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed"}, + ) self.assertTrue(p.is_valid()) - expected = { - "title": 42 - } + expected = {"title": 42} self.assertEqual(p.validate_data, expected) def test_assign_nested_duplicate_number_after_list(self): @@ -36,15 +32,11 @@ def test_assign_nested_duplicate_number_after_list(self): "title[0]sub": 42, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed"}) + data, + {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed"}, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [ - { - "sub": 42 - } - ] - } + expected = {"title": [{"sub": 42}]} self.assertEqual(p.validate_data, expected) def test_assign_nested_duplicate_number_after_list2(self): @@ -53,15 +45,11 @@ def test_assign_nested_duplicate_number_after_list2(self): "title[0]sub[0]": 101, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed"}) + data, + {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed"}, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [ - { - "sub": [101] - } - ] - } + expected = {"title": [{"sub": [101]}]} self.assertEqual(p.validate_data, expected) def test_assign_nested_duplicate_number_after_dict(self): @@ -70,17 +58,11 @@ def test_assign_nested_duplicate_number_after_dict(self): "title[0]sub.title": 101, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed"}) + data, + {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed"}, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [ - { - "sub": { - "title": 101 - } - } - ] - } + expected = {"title": [{"sub": {"title": 101}}]} self.assertEqual(p.validate_data, expected) def test_assign_nested_duplicate_number_after_dict2(self): @@ -89,197 +71,186 @@ def test_assign_nested_duplicate_number_after_dict2(self): "title[0]sub": 42, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed"}) + data, + {"raise_duplicate": False, "assign_duplicate": True, "separator": "mixed"}, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [ - { - "sub": 42 - } - ] - } + expected = {"title": [{"sub": 42}]} self.assertEqual(p.validate_data, expected) def test_mixed_spearator(self): data = { - 'title': 'lalal', - 'article.object': 'lalal', + "title": "lalal", + "article.object": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) self.assertTrue(parser.is_valid()) - expected = { - "title": 'lalal', - "article": { - "object": "lalal" - } - } + expected = {"title": "lalal", "article": {"object": "lalal"}} self.assertEqual(expected, parser.validate_data) def test_mixed_int_object(self): data = { - 'title': 'lalal', - 'article.0': 'lalal', + "title": "lalal", + "article.0": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) self.assertTrue(parser.is_valid()) - expected = { - "title": 'lalal', - "article": { - "0": "lalal" - } - } + expected = {"title": "lalal", "article": {"0": "lalal"}} self.assertEqual(expected, parser.validate_data) def test_mixed_int_list(self): data = { - 'title': 'lalal', - 'article[0]': 'lalal', + "title": "lalal", + "article[0]": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) self.assertTrue(parser.is_valid()) - expected = { - "title": 'lalal', - "article": [ - "lalal" - ] - } + expected = {"title": "lalal", "article": ["lalal"]} self.assertEqual(expected, parser.validate_data) def test_real(self): data = { - 'title': 'title', - 'date': "time", - 'langs[0]id': "id", - 'langs[0]title': 'title', - 'langs[0]description': 'description', - 'langs[0]language': "language", - 'langs[1]id': "id1", - 'langs[1]title': 'title1', - 'langs[1]description': 'description1', - 'langs[1]language': "language1" + "title": "title", + "date": "time", + "langs[0]id": "id", + "langs[0]title": "title", + "langs[0]description": "description", + "langs[0]language": "language", + "langs[1]id": "id1", + "langs[1]title": "title1", + "langs[1]description": "description1", + "langs[1]language": "language1", } parser = NestedParser(data, {"separator": "mixed"}) self.assertTrue(parser.is_valid()) expected = { - 'title': 'title', - 'date': "time", - 'langs': [ + "title": "title", + "date": "time", + "langs": [ { - 'id': 'id', - 'title': 'title', - 'description': 'description', - 'language': 'language' + "id": "id", + "title": "title", + "description": "description", + "language": "language", }, { - 'id': 'id1', - 'title': 'title1', - 'description': 'description1', - 'language': 'language1' - } - ] + "id": "id1", + "title": "title1", + "description": "description1", + "language": "language1", + }, + ], } self.assertDictEqual(parser.validate_data, expected) def test_mixed_invalid_list_index(self): data = { - 'title': 'lalal', - 'article[0f]': 'lalal', + "title": "lalal", + "article[0f]": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) self.assertFalse(parser.is_valid()) - def test_mixed_invalid_list_empty_index(self): + def test_mixed_list_empty_index(self): data = { - 'title': 'lalal', - 'article[]': 'lalal', + "title": "lalal", + "article[]": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) - self.assertFalse(parser.is_valid()) + self.assertTrue(parser.is_valid()) + expected = {"title": "lalal", "article": []} + self.assertDictEqual(parser.validate_data, expected) def test_mixed_invalid_bracket(self): data = { - 'title': 'lalal', - 'article[': 'lalal', + "title": "lalal", + "article[": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_bracket2(self): data = { - 'title': 'lalal', - 'article]': 'lalal', + "title": "lalal", + "article]": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_list_dot(self): data = { - 'title': 'lalal', - 'article[3.]': 'lalal', + "title": "lalal", + "article[3.]": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_list_negative_index(self): data = { - 'title': 'lalal', - 'article[-3]': 'lalal', + "title": "lalal", + "article[-3]": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_object(self): data = { - 'title': 'lalal', - 'article..op': 'lalal', + "title": "lalal", + "article..op": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) self.assertFalse(parser.is_valid()) - def test_mixed_invalid_object2(self): + def test_mixed_empty_obj(self): data = { - 'title': 'lalal', - 'article.op.': 'lalal', + "title": "lalal", + "article.op.": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) - self.assertFalse(parser.is_valid()) + self.assertTrue(parser.is_valid()) + expected = {"title": "lalal", "article": {"op": {}}} + self.assertDictEqual(parser.validate_data, expected) - def test_mixed_invalid_object3(self): + def test_mixed_empty_obj_2(self): data = { - 'title': 'lalal', - 'article[0].op': 'lalal', + "title": "lalal", + "article[0].op": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) - self.assertFalse(parser.is_valid()) + self.assertTrue(parser.is_valid()) + expected = {"title": "lalal", "article": [{"op": "lalal"}]} + self.assertDictEqual(parser.validate_data, expected) def test_mixed_invalid_object4(self): data = { - 'title': 'lalal', - 'article.op..': 'lalal', + "title": "lalal", + "article.op..": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) self.assertFalse(parser.is_valid()) def test_mixed_invalid_list_with_object_dot(self): data = { - 'title': 'lalal', - 'article[0].op..': 'lalal', + "title": "lalal", + "article[0].op..": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) self.assertFalse(parser.is_valid()) - def test_mixed_invalid_list_with_object_dot2(self): + def test_mixed_empty_object_dot2(self): data = { - 'title': 'lalal', - 'article[0]op[0]e.': 'lalal', + "title": "lalal", + "article[0]op[0]e.": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) - self.assertFalse(parser.is_valid()) + self.assertTrue(parser.is_valid()) + expected = {"title": "lalal", "article": [{"op": [{"e": {}}]}]} + self.assertDictEqual(parser.validate_data, expected) def test_mixed_invalid_list_with_object_dot3(self): data = { - 'title': 'lalal', - 'article.op.[0]': 'lalal', + "title": "lalal", + "article.op.[0]": "lalal", } parser = NestedParser(data, {"separator": "mixed"}) self.assertFalse(parser.is_valid()) diff --git a/tests/test_parser.py b/tests/test_parser.py index a9abf43..c1953fa 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -3,7 +3,6 @@ class TestParser(TestCase): - def setUp(self): self.parser = NestedParser("") @@ -21,233 +20,180 @@ def test_is_valid_wrong(self): self.assertIsInstance(ctx.exception, ValueError) def test_parser_object(self): - data = { - 'title[id][length]': 'lalal' - } + data = {"title[id][length]": "lalal"} parser = NestedParser(data, {"separator": "bracket"}) self.assertTrue(parser.is_valid()) - expected = { - 'title': { - 'id': { - 'length': 'lalal' - } - } - } + expected = {"title": {"id": {"length": "lalal"}}} self.assertEqual(expected, parser.validate_data) def test_parser_object2(self): - data = { - 'title[id][length]': 'lalal', - 'title[id][value]': 'lalal' - } + data = {"title[id][length]": "lalal", "title[id][value]": "lalal"} parser = NestedParser(data, {"separator": "bracket"}) self.assertTrue(parser.is_valid()) - expected = { - 'title': { - 'id': { - 'length': 'lalal', - 'value': 'lalal' - } - } - } + expected = {"title": {"id": {"length": "lalal", "value": "lalal"}}} self.assertEqual(expected, parser.validate_data) def test_parser_object3(self): data = { - 'title[id][length]': 'lalal', - 'title[id][value]': 'lalal', - 'title[id][value]': 'lalal', - 'title[value]': 'lalal' + "title[id][length]": "lalal", + "title[id][value]": "lalal", + "title[id][value]": "lalal", + "title[value]": "lalal", } parser = NestedParser(data, {"separator": "bracket"}) self.assertTrue(parser.is_valid()) expected = { - 'title': { - 'id': { - 'length': 'lalal', - 'value': 'lalal' - }, - 'value': 'lalal' - } + "title": {"id": {"length": "lalal", "value": "lalal"}, "value": "lalal"} } self.assertEqual(expected, parser.validate_data) def test_parser_object4(self): data = { - 'title[id][length]': 'lalal', - 'title[id][value]': 'lalal', - 'title[id][value]': 'lalal', - 'title[value]': 'lalal', - 'sub': 'lalal', - 'title[id][recusrive][only][field]': 'icci' + "title[id][length]": "lalal", + "title[id][value]": "lalal", + "title[id][value]": "lalal", + "title[value]": "lalal", + "sub": "lalal", + "title[id][recusrive][only][field]": "icci", } parser = NestedParser(data, {"separator": "bracket"}) self.assertTrue(parser.is_valid()) expected = { - 'title': { - 'id': { - 'length': 'lalal', - 'value': 'lalal', - 'recusrive': { - 'only': { - 'field': 'icci' - } - } + "title": { + "id": { + "length": "lalal", + "value": "lalal", + "recusrive": {"only": {"field": "icci"}}, }, - 'value': 'lalal' + "value": "lalal", }, - 'sub': 'lalal' + "sub": "lalal", } self.assertEqual(expected, parser.validate_data) + def test_parser_empty_object(self): + data = {"title[id][]": "lalal"} + parser = NestedParser(data, {"separator": "bracket"}) + self.assertTrue(parser.is_valid()) + expected = {"title": {"id": []}} + self.assertEqual(expected, parser.validate_data) + def test_parser_object_reasing2(self): data = { - 'title[id][length]': 'lalal', - 'title[value]': 'lalal', - 'sub': 'lalal', - 'title[id][recusrive][only][field]': 'icci', + "title[id][length]": "lalal", + "title[value]": "lalal", + "sub": "lalal", + "title[id][recusrive][only][field]": "icci", } parser = NestedParser(data, {"separator": "bracket"}) self.assertTrue(parser.is_valid()) expected = { - 'title': { - 'id': { - 'length': 'lalal', - 'recusrive': { - 'only': { - 'field': 'icci' - }, + "title": { + "id": { + "length": "lalal", + "recusrive": { + "only": {"field": "icci"}, }, }, - 'value': 'lalal', + "value": "lalal", }, - 'sub': 'lalal', + "sub": "lalal", } self.assertEqual(expected, parser.validate_data) def test_parser_classic(self): - data = { - 'title': 'lalal' - } + data = {"title": "lalal"} parser = NestedParser(data) self.assertTrue(parser.is_valid()) - expected = { - 'title': 'lalal' - } + expected = {"title": "lalal"} self.assertDictEqual(expected, parser.validate_data) def test_parser_list_out_index(self): data = { - 'title': 'dddddddddddddd', - 'tist[0]': 'lalal', - 'tist[2]': 'lalal', + "title": "dddddddddddddd", + "tist[0]": "lalal", + "tist[2]": "lalal", } parser = NestedParser(data) self.assertFalse(parser.is_valid()) def test_parser_empty_list_out_index(self): data = { - 'title': 'dddddddddddddd', - 'tist[0]': 'lalal', - 'tist[]': 'lalal', + "title": "dddddddddddddd", + "tist[0]": "lalal", + "tist[]": "lalal", } parser = NestedParser(data) self.assertFalse(parser.is_valid()) def test_parser_list(self): - data = { - 'title': 'lalal', - 'list[0]': 'icicici' - } + data = {"title": "lalal", "list[0]": "icicici"} parser = NestedParser(data) - expected = { - 'title': 'lalal', - 'list': [ - 'icicici' - ] - } + expected = {"title": "lalal", "list": ["icicici"]} self.assertTrue(parser.is_valid()) self.assertEqual(expected, parser.validate_data) def test_parser_list_index_out_of_range(self): - data = { - 'title': 'lalal', - 'list[0]': 'icicici' - } + data = {"title": "lalal", "list[0]": "icicici"} parser = NestedParser(data) self.assertTrue(parser.is_valid()) - expected = { - 'title': 'lalal', - 'list': [ - "icicici" - ] - } + expected = {"title": "lalal", "list": ["icicici"]} self.assertEqual(expected, parser.validate_data) def test_parser_list_object_index(self): - data = { - 'title': 'lalal', - 'list[length][0]': 'icicici' - } + data = {"title": "lalal", "list[length][0]": "icicici"} parser = NestedParser(data, {"separator": "bracket"}) - expected = { - 'title': 'lalal', - 'list': { - 'length': [ - 'icicici' - ] - } - } + expected = {"title": "lalal", "list": {"length": ["icicici"]}} self.assertTrue(parser.is_valid()) self.assertEqual(expected, parser.validate_data) def test_real(self): data = { - 'title': 'title', - 'date': "time", - 'langs[0][id]': "id", - 'langs[0][title]': 'title', - 'langs[0][description]': 'description', - 'langs[0][language]': "language", - 'langs[1][id]': "id1", - 'langs[1][title]': 'title1', - 'langs[1][description]': 'description1', - 'langs[1][language]': "language1" + "title": "title", + "date": "time", + "langs[0][id]": "id", + "langs[0][title]": "title", + "langs[0][description]": "description", + "langs[0][language]": "language", + "langs[1][id]": "id1", + "langs[1][title]": "title1", + "langs[1][description]": "description1", + "langs[1][language]": "language1", } parser = NestedParser(data, {"separator": "bracket"}) self.assertTrue(parser.is_valid()) expected = { - 'title': 'title', - 'date': "time", - 'langs': [ + "title": "title", + "date": "time", + "langs": [ { - 'id': 'id', - 'title': 'title', - 'description': 'description', - 'language': 'language' + "id": "id", + "title": "title", + "description": "description", + "language": "language", }, { - 'id': 'id1', - 'title': 'title1', - 'description': 'description1', - 'language': 'language1' - } - ] + "id": "id1", + "title": "title1", + "description": "description1", + "language": "language1", + }, + ], } self.assertDictEqual(parser.validate_data, expected) def test_parser_rewrite_key_list(self): data = { - 'title': 'lalal', - 'title[0]': 'lalal', + "title": "lalal", + "title[0]": "lalal", } parser = NestedParser(data) self.assertFalse(parser.is_valid()) def test_parser_rewrite_key_boject(self): data = { - 'title': 'lalal', - 'title[object]': 'lalal', + "title": "lalal", + "title[object]": "lalal", } parser = NestedParser(data) self.assertFalse(parser.is_valid()) @@ -257,14 +203,8 @@ def test_wrong_settings(self): data = {"data": "data"} with self.assertRaises(AssertionError): - NestedParser(data, options={ - "separator": "worng" - }) + NestedParser(data, options={"separator": "worng"}) with self.assertRaises(AssertionError): - NestedParser(data, options={ - "raise_duplicate": "need_boolean" - }) + NestedParser(data, options={"raise_duplicate": "need_boolean"}) with self.assertRaises(AssertionError): - NestedParser(data, options={ - "assign_duplicate": "need_boolean" - }) + NestedParser(data, options={"assign_duplicate": "need_boolean"}) diff --git a/tests/test_parser_assign.py b/tests/test_parser_assign.py index 59a713c..4bd8fc3 100644 --- a/tests/test_parser_assign.py +++ b/tests/test_parser_assign.py @@ -3,18 +3,18 @@ class TestSettingsSeparator(TestCase): - def test_assign_duplicate_list(self): - data = { - "title": 42, - "title[0]": 101 - } + data = {"title": 42, "title[0]": 101} p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "bracket"}) + data, + { + "raise_duplicate": False, + "assign_duplicate": True, + "separator": "bracket", + }, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [101] - } + expected = {"title": [101]} self.assertEqual(p.validate_data, expected) def test_assign_duplicate_number_after_list(self): @@ -23,11 +23,15 @@ def test_assign_duplicate_number_after_list(self): "title": 42, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "bracket"}) + data, + { + "raise_duplicate": False, + "assign_duplicate": True, + "separator": "bracket", + }, + ) self.assertTrue(p.is_valid()) - expected = { - "title": 42 - } + expected = {"title": 42} self.assertEqual(p.validate_data, expected) def test_assign_nested_duplicate_number_after_list(self): @@ -36,15 +40,15 @@ def test_assign_nested_duplicate_number_after_list(self): "title[0][sub]": 42, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "bracket"}) + data, + { + "raise_duplicate": False, + "assign_duplicate": True, + "separator": "bracket", + }, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [ - { - "sub": 42 - } - ] - } + expected = {"title": [{"sub": 42}]} self.assertEqual(p.validate_data, expected) def test_assign_nested_duplicate_number_after_list2(self): @@ -53,15 +57,15 @@ def test_assign_nested_duplicate_number_after_list2(self): "title[0][sub][0]": 101, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "bracket"}) + data, + { + "raise_duplicate": False, + "assign_duplicate": True, + "separator": "bracket", + }, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [ - { - "sub": [101] - } - ] - } + expected = {"title": [{"sub": [101]}]} self.assertEqual(p.validate_data, expected) def test_assign_nested_duplicate_number_after_dict(self): @@ -70,17 +74,15 @@ def test_assign_nested_duplicate_number_after_dict(self): "title[0][sub][title]": 101, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "bracket"}) + data, + { + "raise_duplicate": False, + "assign_duplicate": True, + "separator": "bracket", + }, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [ - { - "sub": { - "title": 101 - } - } - ] - } + expected = {"title": [{"sub": {"title": 101}}]} self.assertEqual(p.validate_data, expected) def test_assign_nested_duplicate_number_after_dict2(self): @@ -89,13 +91,13 @@ def test_assign_nested_duplicate_number_after_dict2(self): "title[0][sub]": 42, } p = NestedParser( - data, {"raise_duplicate": False, "assign_duplicate": True, "separator": "bracket"}) + data, + { + "raise_duplicate": False, + "assign_duplicate": True, + "separator": "bracket", + }, + ) self.assertTrue(p.is_valid()) - expected = { - "title": [ - { - "sub": 42 - } - ] - } + expected = {"title": [{"sub": 42}]} self.assertEqual(p.validate_data, expected) diff --git a/tests/test_parser_separator.py b/tests/test_parser_separator.py index 73a5545..63aa2b8 100644 --- a/tests/test_parser_separator.py +++ b/tests/test_parser_separator.py @@ -3,242 +3,181 @@ class TestSettingsSeparator(TestCase): - def test_parser_object(self): - data = { - 'title.id.length': 'lalal' - } + data = {"title.id.length": "lalal"} parser = NestedParser(data, {"separator": "dot"}) self.assertTrue(parser.is_valid()) - expected = { - 'title': { - 'id': { - 'length': 'lalal' - } - } - } + expected = {"title": {"id": {"length": "lalal"}}} self.assertEqual(expected, parser.validate_data) def test_parser_object2(self): - data = { - 'title.id.length': 'lalal', - 'title.id.value': 'lalal' - } + data = {"title.id.length": "lalal", "title.id.value": "lalal"} parser = NestedParser(data, {"separator": "dot"}) self.assertTrue(parser.is_valid()) - expected = { - 'title': { - 'id': { - 'length': 'lalal', - 'value': 'lalal' - } - } - } + expected = {"title": {"id": {"length": "lalal", "value": "lalal"}}} self.assertEqual(expected, parser.validate_data) def test_parser_object3(self): data = { - 'title.id.length': 'lalal', - 'title.id.value': 'lalal', - 'title.id.value': 'lalal', - 'title.value': 'lalal' + "title.id.length": "lalal", + "title.id.value": "lalal", + "title.id.value": "lalal", + "title.value": "lalal", } parser = NestedParser(data, {"separator": "dot"}) self.assertTrue(parser.is_valid()) expected = { - 'title': { - 'id': { - 'length': 'lalal', - 'value': 'lalal' - }, - 'value': 'lalal' - } + "title": {"id": {"length": "lalal", "value": "lalal"}, "value": "lalal"} } self.assertEqual(expected, parser.validate_data) def test_parser_object4(self): data = { - 'title.id.length': 'lalal', - 'title.id.value': 'lalal', - 'title.id.value': 'lalal', - 'title.value': 'lalal', - 'sub': 'lalal', - 'title.id.recusrive.only.field': 'icci' + "title.id.length": "lalal", + "title.id.value": "lalal", + "title.id.value": "lalal", + "title.value": "lalal", + "sub": "lalal", + "title.id.recusrive.only.field": "icci", } parser = NestedParser(data, {"separator": "dot"}) self.assertTrue(parser.is_valid()) expected = { - 'title': { - 'id': { - 'length': 'lalal', - 'value': 'lalal', - 'recusrive': { - 'only': { - 'field': 'icci' - } - } + "title": { + "id": { + "length": "lalal", + "value": "lalal", + "recusrive": {"only": {"field": "icci"}}, }, - 'value': 'lalal' + "value": "lalal", }, - 'sub': 'lalal' + "sub": "lalal", } self.assertEqual(expected, parser.validate_data) def test_parser_object_reasing2(self): data = { - 'title.id.length': 'lalal', - 'title.value': 'lalal', - 'sub': 'lalal', - 'title.id.recusrive.only.field': 'icci', + "title.id.length": "lalal", + "title.value": "lalal", + "sub": "lalal", + "title.id.recusrive.only.field": "icci", } parser = NestedParser(data, {"separator": "dot"}) self.assertTrue(parser.is_valid()) expected = { - 'title': { - 'id': { - 'length': 'lalal', - 'recusrive': { - 'only': { - 'field': 'icci' - }, + "title": { + "id": { + "length": "lalal", + "recusrive": { + "only": {"field": "icci"}, }, }, - 'value': 'lalal', + "value": "lalal", }, - 'sub': 'lalal', + "sub": "lalal", } self.assertEqual(expected, parser.validate_data) def test_parser_classic(self): - data = { - 'title': 'lalal' - } + data = {"title": "lalal"} parser = NestedParser(data, {"separator": "dot"}) self.assertTrue(parser.is_valid()) - expected = { - 'title': 'lalal' - } + expected = {"title": "lalal"} self.assertDictEqual(expected, parser.validate_data) def test_parser_list_out_index(self): data = { - 'title': 'dddddddddddddd', - 'tist.0': 'lalal', - 'tist.2': 'lalal', + "title": "dddddddddddddd", + "tist.0": "lalal", + "tist.2": "lalal", } parser = NestedParser(data, {"separator": "dot"}) self.assertFalse(parser.is_valid()) def test_parser_empty_list_out_index(self): data = { - 'title': 'dddddddddddddd', - 'tist.0': 'lalal', - 'tist.': 'lalal', + "title": "dddddddddddddd", + "tist.0": "lalal", + "tist.": "lalal", } parser = NestedParser(data, {"separator": "dot"}) self.assertFalse(parser.is_valid()) def test_parser_list(self): - data = { - 'title': 'lalal', - 'list.0': 'icicici' - } + data = {"title": "lalal", "list.0": "icicici"} parser = NestedParser(data, {"separator": "dot"}) - expected = { - 'title': 'lalal', - 'list': [ - 'icicici' - ] - } + expected = {"title": "lalal", "list": ["icicici"]} self.assertTrue(parser.is_valid()) self.assertEqual(expected, parser.validate_data) def test_parser_list_index_out_of_range(self): - data = { - 'title': 'lalal', - 'list.0': 'icicici' - } + data = {"title": "lalal", "list.0": "icicici"} parser = NestedParser(data, {"separator": "dot"}) self.assertTrue(parser.is_valid()) - expected = { - 'title': 'lalal', - 'list': [ - "icicici" - ] - } + expected = {"title": "lalal", "list": ["icicici"]} self.assertEqual(expected, parser.validate_data) def test_parser_list_object_index(self): - data = { - 'title': 'lalal', - 'list.length.0': 'icicici' - } + data = {"title": "lalal", "list.length.0": "icicici"} parser = NestedParser(data, {"separator": "dot"}) - expected = { - 'title': 'lalal', - 'list': { - 'length': [ - 'icicici' - ] - } - } + expected = {"title": "lalal", "list": {"length": ["icicici"]}} self.assertTrue(parser.is_valid()) self.assertEqual(expected, parser.validate_data) def test_parser_space_key(self): data = { - 'title ': 'lalal', + "title ": "lalal", } parser = NestedParser(data, {"separator": "dot"}) self.assertFalse(parser.is_valid()) def test_real(self): data = { - 'title': 'title', - 'date': "time", - 'langs.0.id': "id", - 'langs.0.title': 'title', - 'langs.0.description': 'description', - 'langs.0.language': "language", - 'langs.1.id': "id1", - 'langs.1.title': 'title1', - 'langs.1.description': 'description1', - 'langs.1.language': "language1" + "title": "title", + "date": "time", + "langs.0.id": "id", + "langs.0.title": "title", + "langs.0.description": "description", + "langs.0.language": "language", + "langs.1.id": "id1", + "langs.1.title": "title1", + "langs.1.description": "description1", + "langs.1.language": "language1", } parser = NestedParser(data, {"separator": "dot"}) self.assertTrue(parser.is_valid()) expected = { - 'title': 'title', - 'date': "time", - 'langs': [ + "title": "title", + "date": "time", + "langs": [ { - 'id': 'id', - 'title': 'title', - 'description': 'description', - 'language': 'language' + "id": "id", + "title": "title", + "description": "description", + "language": "language", }, { - 'id': 'id1', - 'title': 'title1', - 'description': 'description1', - 'language': 'language1' - } - ] + "id": "id1", + "title": "title1", + "description": "description1", + "language": "language1", + }, + ], } self.assertDictEqual(parser.validate_data, expected) def test_parser_rewrite_key_list(self): data = { - 'title': 'lalal', - 'title.0': 'lalal', + "title": "lalal", + "title.0": "lalal", } parser = NestedParser(data, {"separator": "dot"}) self.assertFalse(parser.is_valid()) def test_parser_rewrite_key_boject(self): data = { - 'title': 'lalal', - 'title.object': 'lalal', + "title": "lalal", + "title.object": "lalal", } parser = NestedParser(data, {"separator": "dot"}) self.assertFalse(parser.is_valid())