From 8334993d96ee30bb2cddbadc7393382bf46988f6 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Mon, 14 Oct 2019 14:47:11 +0200 Subject: [PATCH] Added query-string I/O support. --- benedict/dicts/__init__.py | 5 ++ benedict/dicts/io.py | 50 +++++++-------- benedict/utils/io_util.py | 23 +++++++ tests/input/invalid-content.qs | 2 + tests/input/valid-content.qs | 1 + tests/test_benedict.py | 12 ++++ tests/test_io_dict.py | 108 ++++++++++++++++++++++++++++++++- 7 files changed, 175 insertions(+), 26 deletions(-) create mode 100644 tests/input/invalid-content.qs create mode 100644 tests/input/valid-content.qs diff --git a/benedict/dicts/__init__.py b/benedict/dicts/__init__.py index 313700f..38567cf 100644 --- a/benedict/dicts/__init__.py +++ b/benedict/dicts/__init__.py @@ -60,6 +60,11 @@ class benedict(IODict, KeypathDict, ParseDict): def from_json(s, **kwargs): return IODict.from_json(s, **kwargs) + @staticmethod + @benediction + def from_query_string(s, **kwargs): + return IODict.from_query_string(s, **kwargs) + @staticmethod @benediction def from_toml(s, **kwargs): diff --git a/benedict/dicts/io.py b/benedict/dicts/io.py index 6a5dc74..6ea6bc4 100644 --- a/benedict/dicts/io.py +++ b/benedict/dicts/io.py @@ -49,31 +49,21 @@ class IODict(dict): @staticmethod def _from_any_data_string(s, **kwargs): - try: - d = IODict.from_base64(s, **kwargs) - return d - except ValueError: - pass - try: - d = IODict.from_json(s, **kwargs) - return d - except ValueError: - pass - try: - d = IODict.from_toml(s, **kwargs) - return d - except ValueError: - pass - try: - d = IODict.from_xml(s, **kwargs) - return d - except ValueError: - pass - try: - d = IODict.from_yaml(s, **kwargs) - return d - except ValueError: - pass + funcs = [ + IODict.from_base64, + IODict.from_json, + IODict.from_query_string, + IODict.from_toml, + IODict.from_xml, + IODict.from_yaml, + ] + for f in funcs: + try: + options = kwargs.copy() + d = f(s, **options) + return d + except ValueError: + pass @staticmethod def from_base64(s, format='json', **kwargs): @@ -86,6 +76,11 @@ class IODict(dict): return IODict._decode(s, decoder=io_util.decode_json, **kwargs) + @staticmethod + def from_query_string(s, **kwargs): + return IODict._decode(s, + decoder=io_util.decode_query_string, **kwargs) + @staticmethod def from_toml(s, **kwargs): return IODict._decode(s, @@ -112,6 +107,11 @@ class IODict(dict): encoder=io_util.encode_json, filepath=filepath, **kwargs) + def to_query_string(self, filepath=None, **kwargs): + return IODict._encode(self, + encoder=io_util.encode_query_string, + filepath=filepath, **kwargs) + def to_toml(self, filepath=None, **kwargs): return IODict._encode(self, encoder=io_util.encode_toml, diff --git a/benedict/utils/io_util.py b/benedict/utils/io_util.py index 7da77ea..c73b0d7 100644 --- a/benedict/utils/io_util.py +++ b/benedict/utils/io_util.py @@ -6,6 +6,7 @@ import base64 import errno import json import os +import re import requests import xmltodict import toml @@ -14,9 +15,13 @@ import yaml try: # python 3 from urllib.parse import unquote_plus as urldecode + from urllib.parse import urlencode + from urllib.parse import parse_qs except ImportError: # python 2 from urllib import unquote_plus as urldecode + from urllib import urlencode + from urlparse import parse_qs def decode_base64(s, **kwargs): @@ -48,6 +53,19 @@ def decode_json(s, **kwargs): return data +def decode_query_string(s, **kwargs): + flat = kwargs.pop('flat', True) + qs_re = r'^(([\w\-\%\+]+\=[\w\-\%\+]*)+([\&]{1})?)+' + qs_pattern = re.compile(qs_re) + if qs_pattern.match(s): + data = parse_qs(s) + if flat: + data = { key:value[0] for key, value in data.items() } + return data + else: + raise ValueError('Invalid query string: {}'.format(s)) + + def decode_xml(s, **kwargs): kwargs.setdefault('dict_constructor', dict) data = xmltodict.parse(s, **kwargs) @@ -89,6 +107,11 @@ def encode_json(d, **kwargs): return data +def encode_query_string(d, **kwargs): + data = urlencode(d, **kwargs) + return data + + def encode_toml(d, **kwargs): data = toml.dumps(d, **kwargs) return data diff --git a/tests/input/invalid-content.qs b/tests/input/invalid-content.qs new file mode 100644 index 0000000..7728a3a --- /dev/null +++ b/tests/input/invalid-content.qs @@ -0,0 +1,2 @@ +Lorem ipsum consectetur sint id aute officia sed excepteur consectetur labore laboris dolore in labore consequat ut in eu ut deserunt. +Elit aliqua velit aliquip voluptate consequat reprehenderit occaecat dolor ut esse aute laboris cillum fugiat esse est laborum. \ No newline at end of file diff --git a/tests/input/valid-content.qs b/tests/input/valid-content.qs new file mode 100644 index 0000000..7ba0574 --- /dev/null +++ b/tests/input/valid-content.qs @@ -0,0 +1 @@ +ok=1&test=2&page=3&lib=python%20benedict&author=Fabio+Caccamo&author=Fabio%20Caccamo \ No newline at end of file diff --git a/tests/test_benedict.py b/tests/test_benedict.py index dba3f99..76a2058 100644 --- a/tests/test_benedict.py +++ b/tests/test_benedict.py @@ -483,6 +483,18 @@ class BenedictTestCase(unittest.TestCase): self.assertTrue(isinstance(d, benedict)) self.assertEqual(d, { 'a': 1, 'b': 2, 'c': 3, }) + def test_from_query_string_with_valid_data(self): + s = 'ok=1&test=2&page=3&lib=python%20benedict&author=Fabio+Caccamo&author=Fabio%20Caccamo' + r = { 'ok': '1', 'test': '2', 'page': '3', 'lib':'python benedict', 'author':'Fabio Caccamo' } + # static method + d = benedict.from_query_string(s) + self.assertTrue(isinstance(d, benedict)) + self.assertEqual(d, r) + # constructor + d = benedict(s) + self.assertTrue(isinstance(d, benedict)) + self.assertEqual(d, r) + def test_from_toml(self): j = """ a = 1 diff --git a/tests/test_io_dict.py b/tests/test_io_dict.py index 489e6f1..3961303 100644 --- a/tests/test_io_dict.py +++ b/tests/test_io_dict.py @@ -263,7 +263,113 @@ class IODictTestCase(unittest.TestCase): self.assertTrue(d, os.path.isfile(filepath)) self.assertEqual(d, IODict.from_json(filepath)) - # YAML +# QUERY STRING + + def test_from_query_string_with_valid_data(self): + s = 'ok=1&test=2&page=3&lib=python%20benedict&author=Fabio+Caccamo&author=Fabio%20Caccamo' + r = { 'ok': '1', 'test': '2', 'page': '3', 'lib':'python benedict', 'author':'Fabio Caccamo' } + # static method + d = IODict.from_query_string(s) + self.assertTrue(isinstance(d, dict)) + self.assertEqual(d, r) + # constructor + d = IODict(s) + self.assertTrue(isinstance(d, dict)) + self.assertEqual(d, r) + + def test_from_query_string_with_invalid_data(self): + s = 'Lorem ipsum est in ea occaecat nisi officia.' + # static method + with self.assertRaises(ValueError): + IODict.from_query_string(s) + # constructor + with self.assertRaises(ValueError): + IODict(s) + + def test_from_query_string_with_valid_file_valid_content(self): + filepath = self.input_path('valid-content.qs') + # static method + d = IODict.from_query_string(filepath) + self.assertTrue(isinstance(d, dict)) + # constructor + d = IODict(filepath) + self.assertTrue(isinstance(d, dict)) + + def test_from_query_string_with_valid_file_valid_content_invalid_format(self): + filepath = self.input_path('valid-content.base64') + with self.assertRaises(ValueError): + IODict.from_query_string(filepath) + filepath = self.input_path('valid-content.json') + with self.assertRaises(ValueError): + IODict.from_query_string(filepath) + filepath = self.input_path('valid-content.toml') + with self.assertRaises(ValueError): + IODict.from_query_string(filepath) + filepath = self.input_path('valid-content.xml') + with self.assertRaises(ValueError): + IODict.from_query_string(filepath) + filepath = self.input_path('valid-content.yml') + with self.assertRaises(ValueError): + IODict.from_query_string(filepath) + + def test_from_query_string_with_valid_file_invalid_content(self): + filepath = self.input_path('invalid-content.qs') + # static method + with self.assertRaises(ValueError): + IODict.from_query_string(filepath) + # constructor + with self.assertRaises(ValueError): + IODict(filepath) + + def test_from_query_string_with_invalid_file(self): + filepath = self.input_path('invalid-file.qs') + # static method + with self.assertRaises(ValueError): + IODict.from_query_string(filepath) + # constructor + with self.assertRaises(ValueError): + IODict(filepath) + + # def test_from_query_string_with_valid_url_valid_content(self): + # url = 'https://raw.githubusercontent.com/fabiocaccamo/python-benedict/master/tests/input/valid-content.qs' + # # static method + # d = IODict.from_query_string(url) + # self.assertTrue(isinstance(d, dict)) + # # constructor + # d = IODict(url) + # self.assertTrue(isinstance(d, dict)) + + def test_from_query_string_with_valid_url_invalid_content(self): + url = 'https://github.com/fabiocaccamo/python-benedict' + # static method + with self.assertRaises(ValueError): + IODict.from_query_string(url) + # constructor + with self.assertRaises(ValueError): + IODict(url) + + def test_from_query_string_with_invalid_url(self): + url = 'https://github.com/fabiocaccamo/python-benedict-invalid' + # static method + with self.assertRaises(ValueError): + IODict.from_query_string(url) + # constructor + with self.assertRaises(ValueError): + IODict(url) + + def test_to_query_string(self): + data = { 'ok': '1', 'test': '2', 'page': '3', 'lib':'python benedict', 'author':'Fabio Caccamo' } + d = IODict({ 'ok': '1', 'test': '2', 'page': '3', 'lib':'python benedict', 'author':'Fabio Caccamo' }) + s = d.to_query_string() + self.assertEqual(d, IODict.from_query_string(s)) + + def test_to_query_string_file(self): + d = IODict({ 'ok': '1', 'test': '2', 'page': '3', 'lib':'python benedict', 'author':'Fabio Caccamo' }) + filepath = self.output_path('test_to_query_string_file.qs') + d.to_query_string(filepath=filepath) + self.assertTrue(d, os.path.isfile(filepath)) + self.assertEqual(d, IODict.from_query_string(filepath)) + # TOML def test_from_toml_with_valid_data(self):