From 78a3f1ce4defd8aeb823811389b374c5845354a9 Mon Sep 17 00:00:00 2001 From: Eduardo Stalinho Date: Tue, 31 May 2016 02:38:21 -0300 Subject: [PATCH 01/58] add compatibility for iteritems --- jsonbender/_compat.py | 8 ++++++++ jsonbender/core.py | 4 +++- jsonbender/string_ops.py | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 jsonbender/_compat.py diff --git a/jsonbender/_compat.py b/jsonbender/_compat.py new file mode 100644 index 0000000..5c4082d --- /dev/null +++ b/jsonbender/_compat.py @@ -0,0 +1,8 @@ +import sys + +PY2 = sys.version_info[0] == 2 + +if not PY2: + iteritems = lambda d: iter(d.items()) +else: + iteritems = lambda d: d.iteritems() diff --git a/jsonbender/core.py b/jsonbender/core.py index 867018e..2e04cb8 100644 --- a/jsonbender/core.py +++ b/jsonbender/core.py @@ -1,5 +1,7 @@ from functools import partial +from jsonbender._compat import iteritems + class Bender(object): @@ -120,7 +122,7 @@ def bend(mapping, source): returns a new dict according to the provided map. """ res = {} - for k, value in mapping.iteritems(): + for k, value in iteritems(mapping): if isinstance(value, Bender): try: newv = value(source) diff --git a/jsonbender/string_ops.py b/jsonbender/string_ops.py index 563b719..4c3a144 100644 --- a/jsonbender/string_ops.py +++ b/jsonbender/string_ops.py @@ -1,4 +1,5 @@ from jsonbender.core import Bender +from jsonbender._compat import iteritems class Format(Bender): @@ -10,6 +11,6 @@ def __init__(self, format_string, *args, **kwargs): def execute(self, source): args = [bender(source) for bender in self._positional_benders] kwargs = {k: bender(source) - for k, bender in self._named_benders.iteritems()} + for k, bender in iteritems(self._named_benders)} return self._format_str.format(*args, **kwargs) From 62ec8d57491214054dc525e542b0732959ac27f4 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Tue, 31 May 2016 10:45:50 -0300 Subject: [PATCH 02/58] Bump version to 0.4 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index afff7f2..ec20b09 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -4,5 +4,5 @@ from jsonbender.selectors import F, K, S -__version__ = '0.3' +__version__ = '0.4' From 750d1d53e2db4094c25855dfbb97682d31833f55 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Tue, 31 May 2016 11:15:30 -0300 Subject: [PATCH 03/58] Package for PyPI --- jsonbender/__init__.py | 2 +- setup.cfg | 2 ++ setup.py | 9 +++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 setup.cfg diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index ec20b09..724cd15 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -4,5 +4,5 @@ from jsonbender.selectors import F, K, S -__version__ = '0.4' +__version__ = '0.4.1' diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..224a779 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py index 8630b51..f6d64fa 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,12 @@ setup( name='JSONBender', version=__version__, - description='Library for transforming dicts.', - packages=find_packages(), + description='Library for transforming JSON data between different formats.', + author='Elias Tandel', + author_email='backend@onyo.com', + url='https://github.com/Onyo/jsonbender', + download_url='https://codeload.github.com/Onyo/jsonbender/tar.gz/' + __version__, + keywords=['dsl', 'edsl', 'json'], + packages=['jsonbender'], ) From 6a8a829f12b62b99a5fa5816f6ce8126842e4b59 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Tue, 31 May 2016 23:02:49 -0300 Subject: [PATCH 04/58] Implement OptS --- jsonbender/__init__.py | 2 +- jsonbender/selectors.py | 29 +++++++++++++++++++++++++++++ tests/test_selectors.py | 17 ++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index 724cd15..527267f 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -1,7 +1,7 @@ from jsonbender.core import Bender, bend, BendingException from jsonbender.list_ops import FlatForall, Forall, Filter, Reduce from jsonbender.string_ops import Format -from jsonbender.selectors import F, K, S +from jsonbender.selectors import F, K, S, OptS __version__ = '0.4.1' diff --git a/jsonbender/selectors.py b/jsonbender/selectors.py index 3bc6fdf..e2e421a 100644 --- a/jsonbender/selectors.py +++ b/jsonbender/selectors.py @@ -28,6 +28,35 @@ def execute(self, source): source = source[key] return source + def optional(self, default=None): + """ + Return an OptS with the same path and with the given `default`. + """ + return OptS(*self._path, default=default) + + +class OptS(S): + """ + Similar to S. However, if any of the keys doesn't exist, returns the + `default` value. + + `default` defaults to None. + Example: + OptS('a', 0, 'b', default=23).execute({'a': []}) -> 23 + """ + + def __init__(self, *path, **kwargs): + self.default = kwargs.get('default') + super(OptS, self).__init__(*path) + + def execute(self, source): + try: + ret = super(OptS, self).execute(source) + except KeyError: + return self.default + else: + return ret + class F(Bender): def __init__(self, func, *args, **kwargs): diff --git a/tests/test_selectors.py b/tests/test_selectors.py index c42ae4f..7a8b6b9 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -1,6 +1,6 @@ import unittest -from jsonbender import F, K, S +from jsonbender.selectors import F, K, S, OptS class TestK(unittest.TestCase): @@ -21,6 +21,21 @@ def test_deep_existing_path(self): self.assertEqual(S('a', 1, 'b')(source), 'ok!') +class TestOptS(unittest.TestCase): + def test_opts(self): + opts = OptS('key', 'missing') + self.assertEqual(opts({'key': {'missing': 23}}), 23) + self.assertEqual(opts({'key': {}}), None) + self.assertEqual(opts({}), None) + + def test_opts_with_default_value(self): + default = 27 + opts = OptS('key', 'missing', default=default) + self.assertEqual(opts({'key': {'missing': 23}}), 23) + self.assertEqual(opts({'key': {}}), default) + self.assertEqual(opts({}), default) + + class TestF(unittest.TestCase): def test_f(self): self.assertEqual(F(len)(range(5)), 5) From 2c58b5ee80394bc2e844155456ab9024c7cb2231 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Tue, 31 May 2016 23:09:15 -0300 Subject: [PATCH 05/58] Unify S and OptS tests --- tests/test_selectors.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/test_selectors.py b/tests/test_selectors.py index 7a8b6b9..88b8149 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -4,24 +4,36 @@ class TestK(unittest.TestCase): + selector_cls = K + def test_k(self): self.assertEqual(K(1)({}), 1) self.assertEqual(K('string')({}), 'string') -class TestS(unittest.TestCase): +class STests(object): def test_no_selector_raises_value_error(self): - self.assertRaises(ValueError, S) + self.assertRaises(ValueError, self.selector_cls) def test_single_existing_field(self): - self.assertEqual(S('a')({'a': 'val'}), 'val') + source = {'a': 'val'} + self.assertEqual(self.selector_cls('a')(source), 'val') def test_deep_existing_path(self): source = {'a': [{}, {'b': 'ok!'}]} - self.assertEqual(S('a', 1, 'b')(source), 'ok!') + self.assertEqual(self.selector_cls('a', 1, 'b')(source), 'ok!') + + +class TestS(unittest.TestCase, STests): + selector_cls = S + + def test_missing_field(self): + self.assertRaises(KeyError, self.selector_cls('k'), {}) + +class TestOptS(unittest.TestCase, STests): + selector_cls = OptS -class TestOptS(unittest.TestCase): def test_opts(self): opts = OptS('key', 'missing') self.assertEqual(opts({'key': {'missing': 23}}), 23) From 0bcab03637e63bcbbb673dd299c648d20164ca14 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Tue, 31 May 2016 23:11:48 -0300 Subject: [PATCH 06/58] Implement ProtectedF --- jsonbender/selectors.py | 36 ++++++++++++++++++++++++++++++++++++ tests/test_selectors.py | 16 +++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/jsonbender/selectors.py b/jsonbender/selectors.py index e2e421a..71274ec 100644 --- a/jsonbender/selectors.py +++ b/jsonbender/selectors.py @@ -67,3 +67,39 @@ def __init__(self, func, *args, **kwargs): def execute(self, value): return self._func(value, *self._args, **self._kwargs) + def protect(self, protect_against=None): + """ + Return a ProtectedF with the same parameters and with the given + `protect_against`. + """ + return ProtectedF(self._func, + *self._args, + protect_against=protect_against, + **self._kwargs) + + +class ProtectedF(F): + """ + Similar to F. + However, if the passing value equals the `protect_against` parameter, + don't execute the function and return the passed value. + + `protect_against` defaults to None. + Example: + ``` + f = ProtectedF(lambda i: 1.0 / i, protect_against=0.0) + f.execute(0) # -> 0 + ``` + + """ + def __init__(self, func, *args, **kwargs): + self._protect_against = kwargs.pop('protect_against', None) + super(ProtectedF, self).__init__(func, *args, **kwargs) + + def execute(self, value): + if value == self._protect_against: + return value + else: + return super(ProtectedF, self).execute(value) + + diff --git a/tests/test_selectors.py b/tests/test_selectors.py index 88b8149..4f10316 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -1,6 +1,6 @@ import unittest -from jsonbender.selectors import F, K, S, OptS +from jsonbender.selectors import F, ProtectedF, K, S, OptS class TestK(unittest.TestCase): @@ -57,6 +57,13 @@ def test_curry_kwargs(self): source = [{'v': 2}, {'v': 3}, {'v': 1}] self.assertEqual(f(source), [{'v': 1}, {'v': 2}, {'v': 3}]) + def test_protect(self): + protected = F(int).protect(protect_against='bad') + self.assertIsInstance(protected, ProtectedF) + self.assertEqual(protected('123'), 123) + self.assertEqual(protected('bad'), 'bad') + + # TODO: move this to a more general Bender test def test_composition(self): s = S('val') f = F(len) @@ -65,6 +72,13 @@ def test_composition(self): self.assertEqual((s >> f)(source), 5) +class TestProtectedF(unittest.TestCase): + def test_protectedf(self): + protected = ProtectedF(int) + self.assertEqual(protected('123'), 123) + self.assertEqual(protected(None), None) + + if __name__ == '__main__': unittest.main() From 51c1dc16d69e9f6be886fa0812771d26514cd232 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Tue, 31 May 2016 23:14:40 -0300 Subject: [PATCH 07/58] Unify F and ProtectedF tests --- tests/test_selectors.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/test_selectors.py b/tests/test_selectors.py index 4f10316..c61148a 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -48,17 +48,17 @@ def test_opts_with_default_value(self): self.assertEqual(opts({}), default) -class TestF(unittest.TestCase): +class FTests(object): def test_f(self): - self.assertEqual(F(len)(range(5)), 5) + self.assertEqual(self.selector_cls(len)(range(5)), 5) def test_curry_kwargs(self): - f = F(sorted, key=lambda d: d['v']) + f = self.selector_cls(sorted, key=lambda d: d['v']) source = [{'v': 2}, {'v': 3}, {'v': 1}] self.assertEqual(f(source), [{'v': 1}, {'v': 2}, {'v': 3}]) def test_protect(self): - protected = F(int).protect(protect_against='bad') + protected = self.selector_cls(int).protect(protect_against='bad') self.assertIsInstance(protected, ProtectedF) self.assertEqual(protected('123'), 123) self.assertEqual(protected('bad'), 'bad') @@ -66,13 +66,19 @@ def test_protect(self): # TODO: move this to a more general Bender test def test_composition(self): s = S('val') - f = F(len) + f = self.selector_cls(len) source = {'val': 'hello'} self.assertEqual((f << s)(source), 5) self.assertEqual((s >> f)(source), 5) -class TestProtectedF(unittest.TestCase): +class TestF(unittest.TestCase, FTests): + selector_cls = F + + +class TestProtectedF(unittest.TestCase, FTests): + selector_cls = ProtectedF + def test_protectedf(self): protected = ProtectedF(int) self.assertEqual(protected('123'), 123) From 1f8cfcb6af8404a05f3430207f4a43637207b53b Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Tue, 31 May 2016 23:43:43 -0300 Subject: [PATCH 08/58] Add docstring to Format --- jsonbender/string_ops.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/jsonbender/string_ops.py b/jsonbender/string_ops.py index 4c3a144..1054e22 100644 --- a/jsonbender/string_ops.py +++ b/jsonbender/string_ops.py @@ -3,6 +3,20 @@ class Format(Bender): + """ + Return a formatted string just like `str.format()`. + However, the values to be formatted are given by benders as positional or + named parameters. + + `format_string` is a template with the same syntax as `str.format()` + + Example: + ``` + fmt = Format('{} {} {last}', S('first'), S('second'), last=S('last')) + source = {'first': 'Edsger', 'second': 'W.', 'last': 'Dijkstra'} + fmt.execute(source) # -> 'Edsger W. Dijkstra' + ``` + """ def __init__(self, format_string, *args, **kwargs): self._format_str = format_string self._positional_benders = args From 47d44d952efd8246eacce427e5d58054666dd908 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Tue, 31 May 2016 23:53:43 -0300 Subject: [PATCH 09/58] Add docstring to F --- jsonbender/selectors.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/jsonbender/selectors.py b/jsonbender/selectors.py index 3bc6fdf..fe83e13 100644 --- a/jsonbender/selectors.py +++ b/jsonbender/selectors.py @@ -30,6 +30,19 @@ def execute(self, source): class F(Bender): + """ + Lifts a python callable into a Bender, so it can be composed. + The extra positional and named parameters are passed to the function at + bending time after the given value. + + `func` is a callable + + Example: + ``` + f = F(sorted, key=lambda d: d['id']) + K([{'id': 3}, {'id': 1}]) >> f # -> [{'id': 1}, {'id': 3}] + ``` + """ def __init__(self, func, *args, **kwargs): self._func = func self._args = args From 41643f50812cfc3cc5d57f41ad7170e7bb6a3671 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Wed, 1 Jun 2016 15:56:28 -0300 Subject: [PATCH 10/58] Rename OptS -> OptionalS --- jsonbender/__init__.py | 2 +- jsonbender/selectors.py | 12 ++++++------ tests/test_selectors.py | 20 ++++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index 527267f..13cb35d 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -1,7 +1,7 @@ from jsonbender.core import Bender, bend, BendingException from jsonbender.list_ops import FlatForall, Forall, Filter, Reduce from jsonbender.string_ops import Format -from jsonbender.selectors import F, K, S, OptS +from jsonbender.selectors import F, K, S, OptionalS __version__ = '0.4.1' diff --git a/jsonbender/selectors.py b/jsonbender/selectors.py index 71274ec..869bc14 100644 --- a/jsonbender/selectors.py +++ b/jsonbender/selectors.py @@ -30,28 +30,28 @@ def execute(self, source): def optional(self, default=None): """ - Return an OptS with the same path and with the given `default`. + Return an OptionalS with the same path and with the given `default`. """ - return OptS(*self._path, default=default) + return OptionalS(*self._path, default=default) -class OptS(S): +class OptionalS(S): """ Similar to S. However, if any of the keys doesn't exist, returns the `default` value. `default` defaults to None. Example: - OptS('a', 0, 'b', default=23).execute({'a': []}) -> 23 + OptionalS('a', 0, 'b', default=23).execute({'a': []}) -> 23 """ def __init__(self, *path, **kwargs): self.default = kwargs.get('default') - super(OptS, self).__init__(*path) + super(OptionalS, self).__init__(*path) def execute(self, source): try: - ret = super(OptS, self).execute(source) + ret = super(OptionalS, self).execute(source) except KeyError: return self.default else: diff --git a/tests/test_selectors.py b/tests/test_selectors.py index c61148a..01f6fdc 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -1,6 +1,6 @@ import unittest -from jsonbender.selectors import F, ProtectedF, K, S, OptS +from jsonbender.selectors import F, ProtectedF, K, S, OptionalS class TestK(unittest.TestCase): @@ -11,7 +11,7 @@ def test_k(self): self.assertEqual(K('string')({}), 'string') -class STests(object): +class STestsMixin(object): def test_no_selector_raises_value_error(self): self.assertRaises(ValueError, self.selector_cls) @@ -24,31 +24,31 @@ def test_deep_existing_path(self): self.assertEqual(self.selector_cls('a', 1, 'b')(source), 'ok!') -class TestS(unittest.TestCase, STests): +class TestS(unittest.TestCase, STestsMixin): selector_cls = S def test_missing_field(self): self.assertRaises(KeyError, self.selector_cls('k'), {}) -class TestOptS(unittest.TestCase, STests): - selector_cls = OptS +class TestOptionalS(unittest.TestCase, STestsMixin): + selector_cls = OptionalS def test_opts(self): - opts = OptS('key', 'missing') + opts = OptionalS('key', 'missing') self.assertEqual(opts({'key': {'missing': 23}}), 23) self.assertEqual(opts({'key': {}}), None) self.assertEqual(opts({}), None) def test_opts_with_default_value(self): default = 27 - opts = OptS('key', 'missing', default=default) + opts = OptionalS('key', 'missing', default=default) self.assertEqual(opts({'key': {'missing': 23}}), 23) self.assertEqual(opts({'key': {}}), default) self.assertEqual(opts({}), default) -class FTests(object): +class FTestsMixin(object): def test_f(self): self.assertEqual(self.selector_cls(len)(range(5)), 5) @@ -72,11 +72,11 @@ def test_composition(self): self.assertEqual((s >> f)(source), 5) -class TestF(unittest.TestCase, FTests): +class TestF(unittest.TestCase, FTestsMixin): selector_cls = F -class TestProtectedF(unittest.TestCase, FTests): +class TestProtectedF(unittest.TestCase, FTestsMixin): selector_cls = ProtectedF def test_protectedf(self): From ec5d9a0df3e42f3c652f6257cf5ba1175ccdc5dc Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Wed, 1 Jun 2016 15:51:46 -0300 Subject: [PATCH 11/58] Add more tests to S --- tests/test_selectors.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_selectors.py b/tests/test_selectors.py index 01f6fdc..22d3732 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -15,7 +15,7 @@ class STestsMixin(object): def test_no_selector_raises_value_error(self): self.assertRaises(ValueError, self.selector_cls) - def test_single_existing_field(self): + def test_shallow_existing_field(self): source = {'a': 'val'} self.assertEqual(self.selector_cls('a')(source), 'val') @@ -27,23 +27,24 @@ def test_deep_existing_path(self): class TestS(unittest.TestCase, STestsMixin): selector_cls = S - def test_missing_field(self): + def test_shallow_missing_field(self): self.assertRaises(KeyError, self.selector_cls('k'), {}) + def test_deep_missing_field(self): + self.assertRaises(KeyError, self.selector_cls('k', 'k2'), {'k': {}}) + class TestOptionalS(unittest.TestCase, STestsMixin): selector_cls = OptionalS - def test_opts(self): + def test_opts_without_default(self): opts = OptionalS('key', 'missing') - self.assertEqual(opts({'key': {'missing': 23}}), 23) self.assertEqual(opts({'key': {}}), None) self.assertEqual(opts({}), None) - def test_opts_with_default_value(self): + def test_opts_with_default(self): default = 27 opts = OptionalS('key', 'missing', default=default) - self.assertEqual(opts({'key': {'missing': 23}}), 23) self.assertEqual(opts({'key': {}}), default) self.assertEqual(opts({}), default) From 86f0a73480684a9212351ae3afca09b1ca0d6da8 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Wed, 1 Jun 2016 19:41:31 -0300 Subject: [PATCH 12/58] Bump version to 0.5.0 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index 13cb35d..4db99b2 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -4,5 +4,5 @@ from jsonbender.selectors import F, K, S, OptionalS -__version__ = '0.4.1' +__version__ = '0.5.0' From 774bae25504095b584c6ec3539f0da1d2a91f948 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Wed, 1 Jun 2016 20:09:26 -0300 Subject: [PATCH 13/58] Make ListOps composable --- jsonbender/list_ops.py | 5 ++--- tests/test_list_ops.py | 7 +++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/jsonbender/list_ops.py b/jsonbender/list_ops.py index 286eb41..d575b98 100644 --- a/jsonbender/list_ops.py +++ b/jsonbender/list_ops.py @@ -10,15 +10,14 @@ class ListOp(Bender): to the operator's __init__(), a list of *values* and should return the desired result. """ - def __init__(self, bender, function): + def __init__(self, function): self._func = function - self._bender = bender def op(self, func, vals): raise NotImplementedError() def execute(self, source): - return self.op(self._func, self._bender(source)) + return self.op(self._func, source) class Forall(ListOp): diff --git a/tests/test_list_ops.py b/tests/test_list_ops.py index 3f73f3a..e67d779 100644 --- a/tests/test_list_ops.py +++ b/tests/test_list_ops.py @@ -8,8 +8,7 @@ class ListOpTestCase(unittest.TestCase): cls = ListOp def assert_list_op(self, the_list, func, expected_value): - bender = self.cls(K(the_list), func) - self.assertEqual(bender({}), expected_value) + self.assertEqual(self.cls(func)(the_list), expected_value) class TestForall(ListOpTestCase): @@ -26,8 +25,8 @@ class TestReduce(ListOpTestCase): cls = Reduce def test_empty_list(self): - bender = Reduce(K([]), lambda acc, i: acc + i) - self.assertRaises(ValueError, bender, {}) + bender = Reduce(lambda acc, i: acc + i) + self.assertRaises(ValueError, bender, []) def test_nonempty_list(self): self.assert_list_op(range(1, 5), lambda acc, i: acc + i, 10) From a50c90da98489b459ad59181400a643affb11cb6 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Wed, 1 Jun 2016 20:31:58 -0300 Subject: [PATCH 14/58] Add compatibility to older ListOps --- jsonbender/list_ops.py | 22 ++++++++++++++++++++-- tests/test_list_ops.py | 25 +++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/jsonbender/list_ops.py b/jsonbender/list_ops.py index d575b98..ad5e4c3 100644 --- a/jsonbender/list_ops.py +++ b/jsonbender/list_ops.py @@ -1,4 +1,5 @@ from itertools import chain +from warnings import warn from jsonbender.core import Bender @@ -10,13 +11,30 @@ class ListOp(Bender): to the operator's __init__(), a list of *values* and should return the desired result. """ - def __init__(self, function): - self._func = function + def __init__(self, *args): + if len(args) == 1: + self._func = args[0] + self._bender = None + # TODO: this is here for compatibility reasons. + elif len(args) == 2: + self._bender, self._func = args + msg = ('Passing a bender to {0} is deprecated.' + 'Please use {0} in a composition chain ' + '(see docs for more details).' + .format(type(self).__name__)) + warn(DeprecationWarning(msg)) + else: + msg = ('{} constructor only takes one parameter, {} given' + .format(type(self).__name__, len(args))) + raise TypeError(msg) def op(self, func, vals): raise NotImplementedError() def execute(self, source): + # TODO: this is here for compatibility reasons + if self._bender: + source = self._bender(source) return self.op(self._func, source) diff --git a/tests/test_list_ops.py b/tests/test_list_ops.py index e67d779..5a55673 100644 --- a/tests/test_list_ops.py +++ b/tests/test_list_ops.py @@ -1,3 +1,4 @@ +from operator import add import unittest from jsonbender import K @@ -20,16 +21,26 @@ def test_empty_list(self): def test_nonempty_list(self): self.assert_list_op(range(1, 5), lambda i: i*2, [2, 4, 6, 8]) + def test_compatibility(self): + # TODO: remove this when compatibility is broken + bender = self.cls(K([1]), lambda i: i) + self.assertEqual(bender({}), [1]) + class TestReduce(ListOpTestCase): cls = Reduce def test_empty_list(self): - bender = Reduce(lambda acc, i: acc + i) + bender = Reduce(add) self.assertRaises(ValueError, bender, []) def test_nonempty_list(self): - self.assert_list_op(range(1, 5), lambda acc, i: acc + i, 10) + self.assert_list_op(range(1, 5), add, 10) + + def test_compatibility(self): + # TODO: remove this when compatibility is broken + bender = self.cls(K([1, 2]), add) + self.assertEqual(bender({}), 3) class TestFilter(ListOpTestCase): @@ -47,6 +58,11 @@ def test_nonempty_list(self): expected = [{'id': 2, 'ignore': False}, {'id': 3, 'ignore': False}] self.assert_list_op(the_list, lambda d: not d['ignore'], expected) + def test_compatibility(self): + # TODO: remove this on next release + bender = self.cls(K([1]), lambda i: True) + self.assertEqual(bender({}), [1]) + class TestFlatForall(ListOpTestCase): cls = FlatForall @@ -59,6 +75,11 @@ def test_nonempty_list(self): lambda d: d['b'], [1, 2, -2, -1]) + def test_compatibility(self): + # TODO: remove this on next release + bender = self.cls(K([1]), lambda i: [i]) + self.assertEqual(bender({}), [1]) + if __name__ == '__main__': unittest.main() From 3e7e97eb2a3ca0853d4574902a44b4e4d4f544da Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Wed, 1 Jun 2016 20:40:29 -0300 Subject: [PATCH 15/58] Update list ops docstrings --- jsonbender/list_ops.py | 43 ++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/jsonbender/list_ops.py b/jsonbender/list_ops.py index ad5e4c3..92a67a4 100644 --- a/jsonbender/list_ops.py +++ b/jsonbender/list_ops.py @@ -8,7 +8,7 @@ class ListOp(Bender): """ Base class for operations on lists. Subclasses must implement the op() method, which takes the function passed - to the operator's __init__(), a list of *values* and should return the + to the operator's __init__(), an iterable, and should return the desired result. """ def __init__(self, *args): @@ -40,22 +40,32 @@ def execute(self, source): class Forall(ListOp): """ + Similar to Python's map(). Builds a new list by applying the given function to each element of the - selected list. Similar to Python's map(). + iterable. + + Example: + ``` + Forall(lambda i: i * 2)(range(5)) # -> [0, 2, 4, 6, 8] + ``` """ op = map class Reduce(ListOp): """ - Reduces a list into a single value by repeatedly applying the given + Similar to Python's reduce(). + Reduces an iterable into a single value by repeatedly applying the given function to the elements. The function must accept two parameters: the first is the accumulator (the value returned from the last call), which defaults to the first element of - the list (hence, the list must be nonempty); the second is the next value fro the list. + the iterable (it must be nonempty); the second is the next value from the + iterable. Example: To sum a given list, - Reduce(K([1, 4, 6], lambda acc, i: acc+i) -> 11 + ``` + Reduce(lambda acc, i: acc + i)([1, 4, 6]) # -> 11 + ``` """ def op(self, func, vals): try: @@ -66,21 +76,30 @@ def op(self, func, vals): class Filter(ListOp): """ - Builds a new list with the elements of the selected list for which the - given function returns True. Similar to Python's filter(). + Similar to Python's filter(). + Builds a new list with the elements of the iterable for which the given + function returns True. + + Example: + ``` + Filter(lambda i: i % 2 == 0)(range(5)) # -> [0, 2, 4] + ``` """ op = filter class FlatForall(ListOp): """ - Similar to Forall, but the given function must return a list for each - element of the selected list, which are than "flattened" into a single + Similar to Forall, but the given function must return an iterable for each + element of the iterable, which are than "flattened" into a single list. - Example: FlatForall(K([1, 10, 100]), lambda x: [x-1, x+1]) -> - [[0, 2], [9, 11], [99, 101]] -> - [0, 1, 9, 11, 99, 101] + Example: + ``` + FlatForall(lambda x: [x-1, x+1])([1, 10, 100]) -> + [[0, 2], [9, 11], [99, 101]] -> + [0, 1, 9, 11, 99, 101] + ``` """ def op(self, func, vals): return list(chain.from_iterable(map(func, vals))) From 874c4db4e33ea5a2440127fbf5142c0294007b9b Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Wed, 1 Jun 2016 20:40:53 -0300 Subject: [PATCH 16/58] Make list ops compatible with py3 --- jsonbender/list_ops.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jsonbender/list_ops.py b/jsonbender/list_ops.py index 92a67a4..6e24b23 100644 --- a/jsonbender/list_ops.py +++ b/jsonbender/list_ops.py @@ -1,3 +1,4 @@ +from functools import reduce from itertools import chain from warnings import warn From e1ac15572237b1fc08647da6e7f80820efd15002 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Thu, 2 Jun 2016 11:46:13 -0300 Subject: [PATCH 17/58] Allow for non-dict and non-list constants to not use K --- jsonbender/core.py | 4 +++- tests/test_core.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/jsonbender/core.py b/jsonbender/core.py index 2e04cb8..a3d1210 100644 --- a/jsonbender/core.py +++ b/jsonbender/core.py @@ -131,8 +131,10 @@ def bend(mapping, source): raise BendingException(m) elif isinstance(value, list): newv = map(lambda v: bend(v, source), value) - else: + elif isinstance(value, dict): newv = bend(value, source) + else: + newv = value res[k] = newv return res diff --git a/tests/test_core.py b/tests/test_core.py index 8d98a7b..830d2d7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -62,6 +62,11 @@ def test_bending_exception_is_raised_when_something_bad_happens(self): source = {} self.assertRaises(BendingException, bend, mapping, source) + def test_constants_without_K(self): + mapping = {'a': 'a const value', 'b': 123} + self.assertDictEqual(bend(mapping, {}), + {'a': 'a const value', 'b': 123}) + class TestOperators(unittest.TestCase): def test_add(self): From 81dc1f56182e16df9ebe6a47e5765c764c37202d Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Fri, 3 Jun 2016 14:38:43 -0300 Subject: [PATCH 18/58] Add contexts --- jsonbender/core.py | 37 ++++++++++++++++++++++++++++++------- tests/test_core.py | 13 ++++++++++++- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/jsonbender/core.py b/jsonbender/core.py index a3d1210..283547c 100644 --- a/jsonbender/core.py +++ b/jsonbender/core.py @@ -17,10 +17,16 @@ class Bender(object): """ def __init__(self, *args, **kwargs): - raise NotImplementedError() + pass def __call__(self, source): - return self.execute(source) + return self.raw_execute(source) + + def raw_execute(self, source): + if isinstance(source, Transport): + return Transport(self.execute(source.value), source.context) + else: + return self.execute(source) def execute(self, source): raise NotImplementedError() @@ -60,7 +66,7 @@ def __init__(self, first, second): self._first = first self._second = second - def execute(self, source): + def raw_execute(self, source): return self._second(self._first(source)) @@ -108,11 +114,22 @@ def op(self, v1, v2): return float(v1) / float(v2) +class Context(Bender): + def raw_execute(self, source): + return Transport(source.context, source.context) + + class BendingException(Exception): pass -def bend(mapping, source): +class Transport(object): + def __init__(self, value, context): + self.value = value + self.context = context + + +def bend(mapping, source, context=None): """ The main bending function. @@ -121,18 +138,24 @@ def bend(mapping, source): returns a new dict according to the provided map. """ + context = {} if context is None else context + transport = Transport(source, context) + return _bend(mapping, transport) + + +def _bend(mapping, transport): res = {} for k, value in iteritems(mapping): if isinstance(value, Bender): try: - newv = value(source) + newv = value(transport).value except Exception as e: m = 'Error for key {}: {}'.format(k, str(e)) raise BendingException(m) elif isinstance(value, list): - newv = map(lambda v: bend(v, source), value) + newv = map(lambda v: _bend(v, transport), value) elif isinstance(value, dict): - newv = bend(value, source) + newv = _bend(value, transport) else: newv = value res[k] = newv diff --git a/tests/test_core.py b/tests/test_core.py index 830d2d7..5c231df 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,7 @@ import unittest -from jsonbender import bend, BendingException, S, K +from jsonbender import S, K +from jsonbender.core import bend, BendingException, Context class TestBend(unittest.TestCase): @@ -67,6 +68,16 @@ def test_constants_without_K(self): self.assertDictEqual(bend(mapping, {}), {'a': 'a const value', 'b': 123}) + def test_context_shallow(self): + mapping = {'a': Context() >> S('b')} + res = bend(mapping, {}, context={'b': 23}) + self.assertDictEqual(res, {'a': 23}) + + def test_context_deep(self): + mapping = {'a': [{'a': Context() >> S('b')}]} + res = bend(mapping, {}, context={'b': 23}) + self.assertDictEqual(res, {'a': [{'a': 23}]}) + class TestOperators(unittest.TestCase): def test_add(self): From c5b1892bd05117cbe93d36f1581202ace0a6f469 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Fri, 3 Jun 2016 15:00:48 -0300 Subject: [PATCH 19/58] Bump version to 0.6.0 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index 4db99b2..da00cae 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -4,5 +4,5 @@ from jsonbender.selectors import F, K, S, OptionalS -__version__ = '0.5.0' +__version__ = '0.6.0' From db78ce862386ed1ef22f3e0ff5118e99a70d31c9 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Fri, 3 Jun 2016 15:12:25 -0300 Subject: [PATCH 20/58] Add Context to jsonbender root module --- jsonbender/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index da00cae..f730962 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -1,8 +1,8 @@ -from jsonbender.core import Bender, bend, BendingException +from jsonbender.core import Bender, Context, bend, BendingException from jsonbender.list_ops import FlatForall, Forall, Filter, Reduce from jsonbender.string_ops import Format from jsonbender.selectors import F, K, S, OptionalS -__version__ = '0.6.0' +__version__ = '0.6.1' From 1927a6c2de64e92b7b4f64dddea614497a164a7b Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Sun, 5 Jun 2016 10:13:01 -0300 Subject: [PATCH 21/58] Fix handling of Transport --- jsonbender/core.py | 18 ++++++++++------- jsonbender/list_ops.py | 2 +- jsonbender/string_ops.py | 12 ++++++----- jsonbender/test.py | 10 ++++++++++ tests/test_core.py | 16 ++++++++------- tests/test_list_ops.py | 13 ++++++------ tests/test_selectors.py | 43 ++++++++++++++++++++-------------------- tests/test_string_ops.py | 12 ++++++++--- 8 files changed, 76 insertions(+), 50 deletions(-) create mode 100644 jsonbender/test.py diff --git a/jsonbender/core.py b/jsonbender/core.py index 283547c..8de15a2 100644 --- a/jsonbender/core.py +++ b/jsonbender/core.py @@ -1,5 +1,3 @@ -from functools import partial - from jsonbender._compat import iteritems @@ -23,10 +21,8 @@ def __call__(self, source): return self.raw_execute(source) def raw_execute(self, source): - if isinstance(source, Transport): - return Transport(self.execute(source.value), source.context) - else: - return self.execute(source) + transport = Transport.from_source(source) + return Transport(self.execute(transport.value), transport.context) def execute(self, source): raise NotImplementedError() @@ -91,7 +87,8 @@ def op(self, v1, v2): raise NotImplementedError() def execute(self, source): - return self.op(self._bender1(source), self._bender2(source)) + return self.op(self._bender1(source).value, + self._bender2(source).value) class Add(BinaryOperator): @@ -128,6 +125,13 @@ def __init__(self, value, context): self.value = value self.context = context + @classmethod + def from_source(cls, source): + if isinstance(source, cls): + return source + else: + return cls(source, {}) + def bend(mapping, source, context=None): """ diff --git a/jsonbender/list_ops.py b/jsonbender/list_ops.py index 6e24b23..ee43100 100644 --- a/jsonbender/list_ops.py +++ b/jsonbender/list_ops.py @@ -35,7 +35,7 @@ def op(self, func, vals): def execute(self, source): # TODO: this is here for compatibility reasons if self._bender: - source = self._bender(source) + source = self._bender(source).value return self.op(self._func, source) diff --git a/jsonbender/string_ops.py b/jsonbender/string_ops.py index 1054e22..ce94338 100644 --- a/jsonbender/string_ops.py +++ b/jsonbender/string_ops.py @@ -1,4 +1,4 @@ -from jsonbender.core import Bender +from jsonbender.core import Bender, Transport from jsonbender._compat import iteritems @@ -22,9 +22,11 @@ def __init__(self, format_string, *args, **kwargs): self._positional_benders = args self._named_benders = kwargs - def execute(self, source): - args = [bender(source) for bender in self._positional_benders] - kwargs = {k: bender(source) + def raw_execute(self, source): + transport = Transport.from_source(source) + args = [bender(transport).value for bender in self._positional_benders] + kwargs = {k: bender(transport).value for k, bender in iteritems(self._named_benders)} - return self._format_str.format(*args, **kwargs) + value = self._format_str.format(*args, **kwargs) + return Transport(value, transport.context) diff --git a/jsonbender/test.py b/jsonbender/test.py new file mode 100644 index 0000000..ffb5074 --- /dev/null +++ b/jsonbender/test.py @@ -0,0 +1,10 @@ +from jsonbender.core import Transport + + +class BenderTestMixin(object): + def assert_bender(self, bender, source, expected_value, + context=None, msg=None): + context = context or {} + got = bender(Transport(source, context)).value + self.assertEqual(got, expected_value, msg) + diff --git a/tests/test_core.py b/tests/test_core.py index 5c231df..58383cf 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2,6 +2,7 @@ from jsonbender import S, K from jsonbender.core import bend, BendingException, Context +from jsonbender.test import BenderTestMixin class TestBend(unittest.TestCase): @@ -79,24 +80,25 @@ def test_context_deep(self): self.assertDictEqual(res, {'a': [{'a': 23}]}) -class TestOperators(unittest.TestCase): +class TestOperators(unittest.TestCase, BenderTestMixin): def test_add(self): - self.assertEqual((S('v1') + K(2))({'v1': 5}), 7) + self.assert_bender(K(5) + K(2), None, 7) def test_sub(self): - self.assertEqual((S('v1') - K(2))({'v1': 5}), 3) + self.assert_bender(K(5) - K(2), None, 3) def test_mul(self): - self.assertEqual((S('v1') * K(2))({'v1': 5}), 10) + self.assert_bender(K(5) * K(2), None, 10) def test_div(self): - self.assertAlmostEqual((S('v1') / K(2))({'v1': 5}), 2.5, 2) + self.assert_bender(K(4) / K(2), None, 2) + self.assertAlmostEqual((K(5) / K(2))(None).value, 2.5, 2) -class TestGetItem(unittest.TestCase): +class TestGetItem(unittest.TestCase, BenderTestMixin): def test_getitem(self): bender = S('val')[2:8:2] - self.assertEqual(bender({'val': range(10)}), [2, 4, 6]) + self.assert_bender(bender, {'val': range(10)}, [2, 4, 6]) if __name__ == '__main__': diff --git a/tests/test_list_ops.py b/tests/test_list_ops.py index 5a55673..aa259b3 100644 --- a/tests/test_list_ops.py +++ b/tests/test_list_ops.py @@ -3,13 +3,14 @@ from jsonbender import K from jsonbender.list_ops import Forall, FlatForall, Filter, ListOp, Reduce +from jsonbender.test import BenderTestMixin -class ListOpTestCase(unittest.TestCase): +class ListOpTestCase(unittest.TestCase, BenderTestMixin): cls = ListOp def assert_list_op(self, the_list, func, expected_value): - self.assertEqual(self.cls(func)(the_list), expected_value) + self.assert_bender(self.cls(func), the_list, expected_value) class TestForall(ListOpTestCase): @@ -24,7 +25,7 @@ def test_nonempty_list(self): def test_compatibility(self): # TODO: remove this when compatibility is broken bender = self.cls(K([1]), lambda i: i) - self.assertEqual(bender({}), [1]) + self.assert_bender(bender, {}, [1]) class TestReduce(ListOpTestCase): @@ -40,7 +41,7 @@ def test_nonempty_list(self): def test_compatibility(self): # TODO: remove this when compatibility is broken bender = self.cls(K([1, 2]), add) - self.assertEqual(bender({}), 3) + self.assert_bender(bender, {}, 3) class TestFilter(ListOpTestCase): @@ -61,7 +62,7 @@ def test_nonempty_list(self): def test_compatibility(self): # TODO: remove this on next release bender = self.cls(K([1]), lambda i: True) - self.assertEqual(bender({}), [1]) + self.assert_bender(bender, {}, [1]) class TestFlatForall(ListOpTestCase): @@ -78,7 +79,7 @@ def test_nonempty_list(self): def test_compatibility(self): # TODO: remove this on next release bender = self.cls(K([1]), lambda i: [i]) - self.assertEqual(bender({}), [1]) + self.assert_bender(bender, {}, [1]) if __name__ == '__main__': diff --git a/tests/test_selectors.py b/tests/test_selectors.py index 22d3732..ef6962a 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -1,27 +1,28 @@ import unittest from jsonbender.selectors import F, ProtectedF, K, S, OptionalS +from jsonbender.test import BenderTestMixin -class TestK(unittest.TestCase): +class TestK(unittest.TestCase, BenderTestMixin): selector_cls = K def test_k(self): - self.assertEqual(K(1)({}), 1) - self.assertEqual(K('string')({}), 'string') + self.assert_bender(K(1), {}, 1) + self.assert_bender(K('string'), {}, 'string') -class STestsMixin(object): +class STestsMixin(BenderTestMixin): def test_no_selector_raises_value_error(self): self.assertRaises(ValueError, self.selector_cls) def test_shallow_existing_field(self): source = {'a': 'val'} - self.assertEqual(self.selector_cls('a')(source), 'val') + self.assert_bender(self.selector_cls('a'), source, 'val') def test_deep_existing_path(self): source = {'a': [{}, {'b': 'ok!'}]} - self.assertEqual(self.selector_cls('a', 1, 'b')(source), 'ok!') + self.assert_bender(self.selector_cls('a', 1, 'b'), source, 'ok!') class TestS(unittest.TestCase, STestsMixin): @@ -38,39 +39,39 @@ class TestOptionalS(unittest.TestCase, STestsMixin): selector_cls = OptionalS def test_opts_without_default(self): - opts = OptionalS('key', 'missing') - self.assertEqual(opts({'key': {}}), None) - self.assertEqual(opts({}), None) + bender = OptionalS('key', 'missing') + self.assert_bender(bender, {'key': {}}, None) + self.assert_bender(bender, {}, None) def test_opts_with_default(self): default = 27 - opts = OptionalS('key', 'missing', default=default) - self.assertEqual(opts({'key': {}}), default) - self.assertEqual(opts({}), default) + bender = OptionalS('key', 'missing', default=default) + self.assert_bender(bender, {'key': {}}, default) + self.assert_bender(bender, {}, default) -class FTestsMixin(object): +class FTestsMixin(BenderTestMixin): def test_f(self): - self.assertEqual(self.selector_cls(len)(range(5)), 5) + self.assert_bender(self.selector_cls(len), range(5), 5) def test_curry_kwargs(self): f = self.selector_cls(sorted, key=lambda d: d['v']) source = [{'v': 2}, {'v': 3}, {'v': 1}] - self.assertEqual(f(source), [{'v': 1}, {'v': 2}, {'v': 3}]) + self.assert_bender(f, source, [{'v': 1}, {'v': 2}, {'v': 3}]) def test_protect(self): protected = self.selector_cls(int).protect(protect_against='bad') self.assertIsInstance(protected, ProtectedF) - self.assertEqual(protected('123'), 123) - self.assertEqual(protected('bad'), 'bad') + self.assert_bender(protected, '123', 123) + self.assert_bender(protected, 'bad', 'bad') # TODO: move this to a more general Bender test def test_composition(self): s = S('val') f = self.selector_cls(len) source = {'val': 'hello'} - self.assertEqual((f << s)(source), 5) - self.assertEqual((s >> f)(source), 5) + self.assert_bender((f << s), source, 5) + self.assert_bender((s >> f), source, 5) class TestF(unittest.TestCase, FTestsMixin): @@ -82,8 +83,8 @@ class TestProtectedF(unittest.TestCase, FTestsMixin): def test_protectedf(self): protected = ProtectedF(int) - self.assertEqual(protected('123'), 123) - self.assertEqual(protected(None), None) + self.assert_bender(protected, '123', 123) + self.assert_bender(protected, None, None) if __name__ == '__main__': diff --git a/tests/test_string_ops.py b/tests/test_string_ops.py index af3282f..3d1dd34 100644 --- a/tests/test_string_ops.py +++ b/tests/test_string_ops.py @@ -1,15 +1,21 @@ import unittest -from jsonbender import K +from jsonbender import Context, K, S from jsonbender.string_ops import Format +from jsonbender.test import BenderTestMixin -class TestFormat(unittest.TestCase): +class TestFormat(unittest.TestCase, BenderTestMixin): def test_format(self): bender = Format('{} {} {} {noun}.', K('This'), K('is'), K('a'), noun=K('test')) - self.assertEqual(bender({}), 'This is a test.') + self.assert_bender(bender, None, 'This is a test.') + + def test_with_context(self): + bender = Format('value: {}', Context() >> S('b')) + self.assert_bender(bender, None, 'value: 23', + context={'b': 23}) if __name__ == '__main__': From ef045726279e64c74b1d898006bee2a324a768b3 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Sun, 5 Jun 2016 10:13:39 -0300 Subject: [PATCH 22/58] Bump version to 0.6.2 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index f730962..fb0af43 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -4,5 +4,5 @@ from jsonbender.selectors import F, K, S, OptionalS -__version__ = '0.6.1' +__version__ = '0.6.2' From 08e7985b41aede08b0aac99a898de9b835a27bc2 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Sun, 5 Jun 2016 11:29:34 -0300 Subject: [PATCH 23/58] Make __call__ return value, not transport --- jsonbender/core.py | 13 +++++++------ jsonbender/list_ops.py | 2 +- jsonbender/string_ops.py | 4 ++-- jsonbender/test.py | 2 +- tests/test_core.py | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/jsonbender/core.py b/jsonbender/core.py index 8de15a2..38134ab 100644 --- a/jsonbender/core.py +++ b/jsonbender/core.py @@ -18,7 +18,7 @@ def __init__(self, *args, **kwargs): pass def __call__(self, source): - return self.raw_execute(source) + return self.raw_execute(source).value def raw_execute(self, source): transport = Transport.from_source(source) @@ -63,7 +63,7 @@ def __init__(self, first, second): self._second = second def raw_execute(self, source): - return self._second(self._first(source)) + return self._second.raw_execute(self._first.raw_execute(source)) class BinaryOperator(Bender): @@ -87,8 +87,8 @@ def op(self, v1, v2): raise NotImplementedError() def execute(self, source): - return self.op(self._bender1(source).value, - self._bender2(source).value) + return self.op(self._bender1(source), + self._bender2(source)) class Add(BinaryOperator): @@ -113,7 +113,8 @@ def op(self, v1, v2): class Context(Bender): def raw_execute(self, source): - return Transport(source.context, source.context) + transport = Transport.from_source(source) + return Transport(transport.context, transport.context) class BendingException(Exception): @@ -152,7 +153,7 @@ def _bend(mapping, transport): for k, value in iteritems(mapping): if isinstance(value, Bender): try: - newv = value(transport).value + newv = value(transport) except Exception as e: m = 'Error for key {}: {}'.format(k, str(e)) raise BendingException(m) diff --git a/jsonbender/list_ops.py b/jsonbender/list_ops.py index ee43100..6e24b23 100644 --- a/jsonbender/list_ops.py +++ b/jsonbender/list_ops.py @@ -35,7 +35,7 @@ def op(self, func, vals): def execute(self, source): # TODO: this is here for compatibility reasons if self._bender: - source = self._bender(source).value + source = self._bender(source) return self.op(self._func, source) diff --git a/jsonbender/string_ops.py b/jsonbender/string_ops.py index ce94338..47ed805 100644 --- a/jsonbender/string_ops.py +++ b/jsonbender/string_ops.py @@ -24,8 +24,8 @@ def __init__(self, format_string, *args, **kwargs): def raw_execute(self, source): transport = Transport.from_source(source) - args = [bender(transport).value for bender in self._positional_benders] - kwargs = {k: bender(transport).value + args = [bender(source) for bender in self._positional_benders] + kwargs = {k: bender(source) for k, bender in iteritems(self._named_benders)} value = self._format_str.format(*args, **kwargs) return Transport(value, transport.context) diff --git a/jsonbender/test.py b/jsonbender/test.py index ffb5074..757bf12 100644 --- a/jsonbender/test.py +++ b/jsonbender/test.py @@ -5,6 +5,6 @@ class BenderTestMixin(object): def assert_bender(self, bender, source, expected_value, context=None, msg=None): context = context or {} - got = bender(Transport(source, context)).value + got = bender(Transport(source, context)) self.assertEqual(got, expected_value, msg) diff --git a/tests/test_core.py b/tests/test_core.py index 58383cf..1e4ba57 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -92,7 +92,7 @@ def test_mul(self): def test_div(self): self.assert_bender(K(4) / K(2), None, 2) - self.assertAlmostEqual((K(5) / K(2))(None).value, 2.5, 2) + self.assertAlmostEqual((K(5) / K(2))(None), 2.5, 2) class TestGetItem(unittest.TestCase, BenderTestMixin): From d647469d9543558f26946845226dfd60419b9a7b Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Sun, 5 Jun 2016 21:22:27 -0300 Subject: [PATCH 24/58] Bump version to 0.6.3 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index fb0af43..55e0bc8 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -4,5 +4,5 @@ from jsonbender.selectors import F, K, S, OptionalS -__version__ = '0.6.2' +__version__ = '0.6.3' From 9dac27910064a4559c8719098dc075fd678a446a Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Tue, 28 Jun 2016 16:50:41 -0300 Subject: [PATCH 25/58] Add Forall.bend() --- jsonbender/list_ops.py | 20 +++++++++++++++++++- tests/test_list_ops.py | 7 ++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/jsonbender/list_ops.py b/jsonbender/list_ops.py index 6e24b23..aee032d 100644 --- a/jsonbender/list_ops.py +++ b/jsonbender/list_ops.py @@ -2,7 +2,7 @@ from itertools import chain from warnings import warn -from jsonbender.core import Bender +from jsonbender.core import Bender, bend class ListOp(Bender): @@ -52,6 +52,24 @@ class Forall(ListOp): """ op = map + @classmethod + def bend(cls, mapping): + """ + Return a Forall instance that bends each element of the list with the + given mapping. + + mapping: a JSONBender mapping as passed to the `bend()` function. + + Example: + ``` + source = [{'a': 23}, {'a': 27}] + bender = Forall.bend({'b': S('a')}) + bender(source) # -> [{'b': 23}, {'b': 27}] + ``` + + """ + return cls(lambda source: bend(mapping, source)) + class Reduce(ListOp): """ diff --git a/tests/test_list_ops.py b/tests/test_list_ops.py index aa259b3..614362a 100644 --- a/tests/test_list_ops.py +++ b/tests/test_list_ops.py @@ -1,7 +1,7 @@ from operator import add import unittest -from jsonbender import K +from jsonbender import K, S from jsonbender.list_ops import Forall, FlatForall, Filter, ListOp, Reduce from jsonbender.test import BenderTestMixin @@ -27,6 +27,11 @@ def test_compatibility(self): bender = self.cls(K([1]), lambda i: i) self.assert_bender(bender, {}, [1]) + def test_bend(self): + self.assert_bender(self.cls.bend({'b': S('a')}), + [{'a': 23}, {'a': 27}], + [{'b': 23}, {'b': 27}]) + class TestReduce(ListOpTestCase): cls = Reduce From 9adca5e5d7c810399945e253e85cab5fe11487cd Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Tue, 28 Jun 2016 16:52:13 -0300 Subject: [PATCH 26/58] Bump version to 0.6.4 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index 55e0bc8..cb02d92 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -4,5 +4,5 @@ from jsonbender.selectors import F, K, S, OptionalS -__version__ = '0.6.3' +__version__ = '0.6.4' From ada2986ece0f3d69fa39a1cff867a35102c6bcdb Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Tue, 28 Jun 2016 16:59:43 -0300 Subject: [PATCH 27/58] Add context to Forall.bend() --- jsonbender/list_ops.py | 4 ++-- tests/test_list_ops.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/jsonbender/list_ops.py b/jsonbender/list_ops.py index aee032d..1c6019b 100644 --- a/jsonbender/list_ops.py +++ b/jsonbender/list_ops.py @@ -53,7 +53,7 @@ class Forall(ListOp): op = map @classmethod - def bend(cls, mapping): + def bend(cls, mapping, context=None): """ Return a Forall instance that bends each element of the list with the given mapping. @@ -68,7 +68,7 @@ def bend(cls, mapping): ``` """ - return cls(lambda source: bend(mapping, source)) + return cls(lambda source: bend(mapping, source, context)) class Reduce(ListOp): diff --git a/tests/test_list_ops.py b/tests/test_list_ops.py index 614362a..0d7b3a5 100644 --- a/tests/test_list_ops.py +++ b/tests/test_list_ops.py @@ -1,7 +1,7 @@ from operator import add import unittest -from jsonbender import K, S +from jsonbender import Context, K, S from jsonbender.list_ops import Forall, FlatForall, Filter, ListOp, Reduce from jsonbender.test import BenderTestMixin @@ -32,6 +32,13 @@ def test_bend(self): [{'a': 23}, {'a': 27}], [{'b': 23}, {'b': 27}]) + def test_bend_with_context(self): + mapping = {'b': Context() >> S('c')} + context = {'c': 42} + self.assert_bender(self.cls.bend(mapping, context), + [{}, {}], + [{'b': 42}, {'b': 42}]) + class TestReduce(ListOpTestCase): cls = Reduce From 737015448d7f991026e10b63e170bdbd3a1d1870 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Tue, 28 Jun 2016 17:00:26 -0300 Subject: [PATCH 28/58] Bump version to 0.7.0 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index cb02d92..7b95511 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -4,5 +4,5 @@ from jsonbender.selectors import F, K, S, OptionalS -__version__ = '0.6.4' +__version__ = '0.7.0' From 108f15865db0015ceb8fc41e749fd6985143bd41 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Thu, 1 Sep 2016 19:21:52 -0300 Subject: [PATCH 29/58] Make BinaryOperator work with contexts --- jsonbender/core.py | 8 +++++--- tests/test_core.py | 9 ++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/jsonbender/core.py b/jsonbender/core.py index 38134ab..3b0e0b1 100644 --- a/jsonbender/core.py +++ b/jsonbender/core.py @@ -86,9 +86,11 @@ def __init__(self, bender1, bender2): def op(self, v1, v2): raise NotImplementedError() - def execute(self, source): - return self.op(self._bender1(source), - self._bender2(source)) + def raw_execute(self, source): + source = Transport.from_source(source) + val = self.op(self._bender1(source), + self._bender2(source)) + return Transport(val, source.context) class Add(BinaryOperator): diff --git a/tests/test_core.py b/tests/test_core.py index 1e4ba57..e0207e7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,7 +1,7 @@ import unittest from jsonbender import S, K -from jsonbender.core import bend, BendingException, Context +from jsonbender.core import bend, BendingException, Context, BinaryOperator from jsonbender.test import BenderTestMixin @@ -94,6 +94,13 @@ def test_div(self): self.assert_bender(K(4) / K(2), None, 2) self.assertAlmostEqual((K(5) / K(2))(None), 2.5, 2) + def test_op_with_context(self): + mapping = {'res': (Context() >> S('b')) - S('a')} + in_ = {'a': 23} + context = {'b': 27} + res = bend(mapping, in_, context=context) + self.assertEqual(res, {'res': 4}) + class TestGetItem(unittest.TestCase, BenderTestMixin): def test_getitem(self): From eedc735af3c13b95948085b61eaaaf18db865dbb Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Thu, 1 Sep 2016 19:23:32 -0300 Subject: [PATCH 30/58] Bump JSONBender to 0.7.1 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index 7b95511..7561357 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -4,5 +4,5 @@ from jsonbender.selectors import F, K, S, OptionalS -__version__ = '0.7.0' +__version__ = '0.7.1' From c7c1b981cd949912fbf072763c22b8d682684a78 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Thu, 1 Sep 2016 23:50:40 -0300 Subject: [PATCH 31/58] Improve docs --- README.md | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 215 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 705020c..a97275e 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,15 @@ Installing --- ```bash -git clone git@github.com:Onyo/jsonbender.git -python setup.py install +pip install JSONBender ``` Usage --- -JSONBender works by calling the `bend()` function with a mapping and the source `dict` as arguments. It raises a `BendingException` if anyting bad happens during the transformation fase. + +JSONBender works by calling the `bend()` function with a mapping and the source `dict` as arguments. It raises a `BendingException` if anyting bad happens during the transformation phase. The mapping itself is a dict whose values are benders, i.e. objects that represent the transformations to be done to the source dict. Ex: @@ -30,7 +30,9 @@ from jsonbender import bend, K, S MAPPING = { - 'fullName': S('customer', 'first_name') + K(' ') + S('customer', 'last_name'), + 'fullName': (S('customer', 'first_name') + + K(' ') + + S('customer', 'last_name')), 'city': S('address', 'city'), } @@ -53,14 +55,19 @@ print(json.dumps(result)) {"city": "Sicily", "fullName": "Inigo Montoya"} ``` -###Benders -####K +### Benders + + +#### Selectors -`K()` is a selector for constant values: It takes any value as a parameter and always returns that value regardless of the source dict. +##### K +`K()` is a selector for constant values: +It takes any value as a parameter and always returns that value regardless of the input. -####S + +##### S `S()` is a selector for accessing keys and indices: It takes a variable number of keys / indices and returns the corresponding value on the source dict: @@ -70,19 +77,215 @@ ret = bend(MAPPING, {'a': {'deeply': {'nested': [{'value': 42}]}}}) assert ret == {'val': 42} ``` -####F -`F()` takes a function and optional args, and applies that function at bending time. It is useful for performing complex operations for which actual python code is necessary. F-benders can be composed with other benders using `<<` and `>>` to make them receive arbitrary values at bending time. Ex: +If any of keys may not exist, `S()` can be "annotated" by calling the `.optional(default)` method, which returns an instance of `OptionalS`. +`.optional()` takes a single parameter which is passed as the `default` value of `OptionalS`; it defaults to `None`. + +##### OptionalS + +`OptionalS` is like `S()` but does not raise errors when any of the keys is not found. Instead, it returns `None` or the `default` value that is passed on its construction. + +```python +source = {'does': {'exist': 23} + +MAPPING_1 = {'val': OptionalS('does', 'not', 'exist')} +ret = bend(MAPPING_1, }, source) +assert ret == {'val': None} + +MAPPING_2 = {'val': OptionalS('does', 'not', 'exist', default=27)} +ret = bend(MAPPING_2, }, source) +assert ret == {'val': 28} +``` + +For readability and reusability, prefer using `S().optional()` instead. + + +##### F +`F()` lifts a python callable into a Bender, so it can be called at bending time. +It is useful for performing more complex operations for which actual python code is necessary. + +The extra optional args and kwargs are passed to the function at +bending time after the given value. ```python MAPPING = { 'total_number_of_keys': F(len), 'number_of_str_keys': F(lambda source: len([k for k in source.iterkeys() if isinstance(k, str)])), - 'price_floor': S('price_as_str') >> F(float) >> F(int), + 'price_truncated': S('price_as_str') >> F(float) >> F(int), } ret = bend(MAPPING, {'price_as_str': '42.2', 'k1': 'v', 1: 'a'}) -assert ret == {'price_floor': 42, +assert ret == {'price_truncated': 42, 'total_number_of_keys': 3, 'number_of_str_keys': 2} ``` +If the function can't take certain values, you can protect it by calling the `.protect()` method. + +```python +import math + +MAPPING_1 = {'sqrt': S('val') >> F(math.sqrt).protect()} +assert bend(MAPPING, {'val': 4}) == {'sqrt': 2} +assert bend(MAPPING, {'val': None}) == {'sqrt': None} + +MAPPING_2 = {'sqrt': S('val') >> F(math.sqrt).protect(-1)} +assert bend(MAPPING, {'val': -1}) == {'sqrt': -1} +``` + + +#### Operators + +Benders implement most of python's binary operators. + +##### Arithmetic + +For the arithmetic `+`, `-`, `*`, `/`, +the behavior is to apply the operator to the bended values of each operand. + +```python +a = S('a') +b = S('b') +MAPPING = {'add': a + b, 'sub': a - b, 'mul': a * b, 'div': a / b} +ret = bend(MAPPING, {'a': 10, 'b': 5}) +assert ret == {'add': 15, 'sub': 5, 'mul': 50, 'div': 2} + +ret = bend({'full_name': S('first_name') + K(' ') + S('last_name')}, + {'first_name': 'John', 'last_name': 'Doe'}) +assert ret == {'full_name': 'John Doe'} +``` + +##### Bitwise + +The bitwise operators are not yet implemented, except for the lshift (`<<`) and rshift (`>>`). +See "Composition" below. + + +#### List ops + +There are 4 benders for working with lists, inspired by the common functional programming operations. + +##### Reduce + +Similar to Python's `reduce()`. +Reduces an iterable into a single value by repeatedly applying the given +function to the elements. +The function must accept two parameters: the first is the accumulator (the +value returned from the last call), which defaults to the first element of +the iterable (it must be nonempty); the second is the next value from the +iterable. + + +```python +MAPPING = {'sum': S('ints') >> Reduce(lambda acc, i: acc + i)} +ret = bend(MAPPING, {'ints': [1, 4, 7, 9]}) +assert ret == {'sum': 21} +``` + +##### Filter + +Similar to Python's `filter()`. +Builds a new list with the elements of the iterable for which the given +function returns True. + +```python +MAPPING = {'even': S('ints') >> Filter(lambda i: i % 2 == 0)} +ret = bend(MAPPING, {'ints': range(5)}) +assert ret == {'even': [0, 2, 4]} +``` + +##### Forall + +Similar to Python's `map()`. +Builds a new list by applying the given function to each element of the +iterable. + + +```python +MAPPING = {'doubles': S('ints') >> Forall(lambda i: i * 2)} +ret = bend(MAPPING, {'ints': range(5)}) +assert ret == {'doubles': [0, 2, 4, 6, 8]} +``` + +For the common case of applying a JSONBender mapping to each element of a list, +the `Forall.bend()` class method is provided. + +```python +MAPPING = {'list_of_bs': S('list_of_as') >> Forall.bend({'b': S('a')})} +source = {'list_of_as': [{'a': 23}, {'a': 27}]} +ret = bend(MAPPING, source) +assert ret == {'list_of_bs': [{'b': 23}, {'b': 27}]} +``` + +##### FlatForall + +Similar to Forall, but the given function must return an iterable for each +element of the iterable, which are than "flattened" into a single +list. + +```python +MAPPING = {'doubles_triples': S('ints') >> FlatForall(lambda x: [x * 1, x * 3])} +source = {'ints': [2, 15, 50]) +ret = bend(MAPPING, source) +assert ret == {'doubles_triples': [4, 6, 30, 45, 100, 150]} +``` + +#### String ops + +JSONBender currently provides only one string-related bender. + +##### Format + +Return a formatted string just like `str.format()`. +Where the values to be formatted are given by benders as positional or +named parameters. + +It uses the same syntax as `str.format()` + +```python +MAPPING = {'formatted': Format('{} {} {last}', + S('first'), + S('second'), + last=S('last'))} +source = {'first': 'Edsger', 'second': 'W.', 'last': 'Dijkstra'} +ret = bend(MAPPING, source) +assert ret == {'formatted': 'Edsger W. Dijkstra'} +``` + + +### Composition + +All JSONBenders can be composed with other benders using `<<` and `>>` +to make them receive previously bended values. + +```python +MAPPING = { + 'name': S('name'), + 'pythonista': S('prog_langs') >> Forall(str.lower) >> F(lambda ls: 'python' in ls), +} +source = { + 'name': 'Mary', + 'prog_lags': ['C', 'Python', 'Lua'], +} +ret = bend(MAPPING, source) +assert ret == {'name': 'Mary', 'pythonista': True} +``` + +### Context + +Sometimes it's necessary to use values at bending time that are not on the +source json and are not known at mapping time. +For these cases there is the optional `context` argument to `bend()` function. +Whatever you pass for the argument is can be used at bending time by the +`Context()` bender. + + +```python +MAPPING = { + 'name': S('name'), + 'age': (Context() >> S('year')) - S('birthyear'), +} +source = {'name': 'Mary', 'birthyear': 1990} +ret = bend(MAPPING, source, context={'year': 2016}) +assert ret == {'name': 'Mary', 'age': 26} +``` + From 6ad0fac9d6d4fbf756f3aa1bd39a8d13e6b5503d Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Thu, 6 Oct 2016 19:58:49 -0300 Subject: [PATCH 32/58] Make Forall.bend() inherit context --- jsonbender/list_ops.py | 34 +++++++++++++++++++++++++++++++--- tests/test_list_ops.py | 10 +++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/jsonbender/list_ops.py b/jsonbender/list_ops.py index 1c6019b..f43aacb 100644 --- a/jsonbender/list_ops.py +++ b/jsonbender/list_ops.py @@ -2,7 +2,7 @@ from itertools import chain from warnings import warn -from jsonbender.core import Bender, bend +from jsonbender.core import Bender, bend, Transport class ListOp(Bender): @@ -55,10 +55,13 @@ class Forall(ListOp): @classmethod def bend(cls, mapping, context=None): """ - Return a Forall instance that bends each element of the list with the + Return a ForallBend instance that bends each element of the list with the given mapping. mapping: a JSONBender mapping as passed to the `bend()` function. + context: optional. the context that will be passed to `bend()`. + Note that if context is not passed, it defaults at bend-time + to the one passed to the outer mapping. Example: ``` @@ -68,7 +71,32 @@ def bend(cls, mapping, context=None): ``` """ - return cls(lambda source: bend(mapping, source, context)) + return ForallBend(mapping, context) + + +class ForallBend(Forall): + """ + Bends each element of the list with given mapping and context. + + mapping: a JSONBender mapping as passed to the `bend()` function. + context: optional. the context that will be passed to `bend()`. + Note that if context is not passed, it defaults at bend-time + to the one passed to the outer mapping. + """ + + def __init__(self, mapping, context=None): + self._mapping = mapping + self._context = context + # TODO this is here for retrocompatibility reasons. + # remove this when ListOp also breaks retrocompatibility + self._bender = None + + def raw_execute(self, source): + transport = Transport.from_source(source) + context = self._context or transport.context + # ListOp.execute assumes the func is saved on self._func + self._func = lambda v: bend(self._mapping, v, context) + return Transport(self.execute(transport.value), transport.context) class Reduce(ListOp): diff --git a/tests/test_list_ops.py b/tests/test_list_ops.py index 0d7b3a5..587c5a3 100644 --- a/tests/test_list_ops.py +++ b/tests/test_list_ops.py @@ -1,7 +1,7 @@ from operator import add import unittest -from jsonbender import Context, K, S +from jsonbender import Context, K, S, bend from jsonbender.list_ops import Forall, FlatForall, Filter, ListOp, Reduce from jsonbender.test import BenderTestMixin @@ -39,6 +39,14 @@ def test_bend_with_context(self): [{}, {}], [{'b': 42}, {'b': 42}]) + def test_bend_inherits_outer_context_by_default(self): + inner_mapping = {'val': Context()} + outer_mapping = {'a': S('items') >> Forall.bend(inner_mapping)} + source = {'items': range(3)} + got = bend(outer_mapping, source, context=27) + expected = {'a': [{'val': 27}, {'val': 27}, {'val': 27}]} + self.assertEqual(got, expected) + class TestReduce(ListOpTestCase): cls = Reduce From 978793225a7a464c7833903f6d8d70064035f75f Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Thu, 6 Oct 2016 20:09:58 -0300 Subject: [PATCH 33/58] Add ForallBend docs --- README.md | 13 +++++++++++-- jsonbender/list_ops.py | 10 +++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a97275e..a7424c7 100644 --- a/README.md +++ b/README.md @@ -207,10 +207,19 @@ assert ret == {'doubles': [0, 2, 4, 6, 8]} ``` For the common case of applying a JSONBender mapping to each element of a list, -the `Forall.bend()` class method is provided. +the `.bend()` *class method* is provided, which returns a `ForallBend` instance +. `.bend()` takes the mapping and the context (optional) which are then passed +to `ForallBend`. + + +##### ForallBend +Bends each element of the list with given mapping and context. + +If no contexxt is passed, it "inherits" at bend-time the context passed to the outer `bend()` call. + ```python -MAPPING = {'list_of_bs': S('list_of_as') >> Forall.bend({'b': S('a')})} +MAPPING = {'list_of_bs': S('list_of_as') >> ForallBend({'b': S('a')})} source = {'list_of_as': [{'a': 23}, {'a': 27}]} ret = bend(MAPPING, source) assert ret == {'list_of_bs': [{'b': 23}, {'b': 27}]} diff --git a/jsonbender/list_ops.py b/jsonbender/list_ops.py index f43aacb..5bc74f7 100644 --- a/jsonbender/list_ops.py +++ b/jsonbender/list_ops.py @@ -76,12 +76,12 @@ def bend(cls, mapping, context=None): class ForallBend(Forall): """ - Bends each element of the list with given mapping and context. + Bends each element of the list with given mapping and context. - mapping: a JSONBender mapping as passed to the `bend()` function. - context: optional. the context that will be passed to `bend()`. - Note that if context is not passed, it defaults at bend-time - to the one passed to the outer mapping. + mapping: a JSONBender mapping as passed to the `bend()` function. + context: optional. the context that will be passed to `bend()`. + Note that if context is not passed, it defaults at bend-time + to the one passed to the outer mapping. """ def __init__(self, mapping, context=None): From 0a28269c3e2adab2f941412f85526260f6027e49 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Thu, 6 Oct 2016 20:12:26 -0300 Subject: [PATCH 34/58] Bump JSONBender to 0.8.0 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index 7561357..c055603 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -4,5 +4,5 @@ from jsonbender.selectors import F, K, S, OptionalS -__version__ = '0.7.1' +__version__ = '0.8.0' From b5d37dad54f3ffa9e7e014cd5741e6392fba74dc Mon Sep 17 00:00:00 2001 From: hdantas Date: Mon, 31 Oct 2016 22:08:42 +0000 Subject: [PATCH 35/58] Added ProtectedFormat. --- jsonbender/string_ops.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/jsonbender/string_ops.py b/jsonbender/string_ops.py index 47ed805..16ddc63 100644 --- a/jsonbender/string_ops.py +++ b/jsonbender/string_ops.py @@ -30,3 +30,27 @@ def raw_execute(self, source): value = self._format_str.format(*args, **kwargs) return Transport(value, transport.context) + +class ProtectedFormat(Format): + """ + Returns a formatted String, like Python's built-in format. + If one of the arguments is None, it evaluates to None + Examples: + fmt = Format('{} {} {last}', S('first'), S('second'), last=S('last')) + source = {'first': 'Edsger', 'second': 'W.', 'last': 'Dijkstra'} + fmt.execute(source) # -> 'Edsger W. Dijkstra' + + fmt = Format('{} {}', S('first'), S('second')) + source = {'first': 'Edsger'} + fmt.execute(source) # -> None + """ + def raw_execute(self, source): + # if any of the args to print are None, return None + if any( + [bender(source) is None for bender in self._positional_benders] + + [bender(source) is None for bender in self._named_benders.values()] + ): + # create an object with property value=None so it can be processed + return type(str('none_obj'), (object,), dict(value=None)) + # else just behave normally + return super(ProtectedFormat, self).raw_execute(source) From ac822ce851068f38b95400394850bf8a2014b9cf Mon Sep 17 00:00:00 2001 From: Craig Blaszczyk Date: Thu, 26 Jan 2017 15:09:22 +0000 Subject: [PATCH 36/58] support python3; add tox to run unit tests --- .gitignore | 1 + jsonbender/core.py | 8 +++++++- jsonbender/list_ops.py | 10 +++++++--- setup.py | 6 ++++++ tests/test_core.py | 8 +++++++- tox.ini | 9 +++++++++ 6 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index b4f6fe8..fbb629b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build dist/ MANIFEST *.egg-info +.tox diff --git a/jsonbender/core.py b/jsonbender/core.py index 3b0e0b1..445a916 100644 --- a/jsonbender/core.py +++ b/jsonbender/core.py @@ -39,6 +39,12 @@ def __mul__(self, other): def __div__(self, other): return Div(self, other) + def __truediv__(self, other): + return Div(self, other) + + def __floordiv__(self, other): + return Div(self, other) + def __rshift__(self, other): return Compose(self, other) @@ -160,7 +166,7 @@ def _bend(mapping, transport): m = 'Error for key {}: {}'.format(k, str(e)) raise BendingException(m) elif isinstance(value, list): - newv = map(lambda v: _bend(v, transport), value) + newv = list(map(lambda v: _bend(v, transport), value)) elif isinstance(value, dict): newv = _bend(value, transport) else: diff --git a/jsonbender/list_ops.py b/jsonbender/list_ops.py index 5bc74f7..542ad3f 100644 --- a/jsonbender/list_ops.py +++ b/jsonbender/list_ops.py @@ -50,7 +50,9 @@ class Forall(ListOp): Forall(lambda i: i * 2)(range(5)) # -> [0, 2, 4, 6, 8] ``` """ - op = map + + def op(self, func, vals): + return list(map(func, vals)) @classmethod def bend(cls, mapping, context=None): @@ -118,7 +120,7 @@ def op(self, func, vals): try: return reduce(func, vals) except TypeError as e: # empty list with no initial value - raise ValueError(e.message) + raise ValueError(e.args[0]) class Filter(ListOp): @@ -132,7 +134,9 @@ class Filter(ListOp): Filter(lambda i: i % 2 == 0)(range(5)) # -> [0, 2, 4] ``` """ - op = filter + + def op(self, func, vals): + return list(filter(func, vals)) class FlatForall(ListOp): diff --git a/setup.py b/setup.py index f6d64fa..f022646 100644 --- a/setup.py +++ b/setup.py @@ -12,5 +12,11 @@ download_url='https://codeload.github.com/Onyo/jsonbender/tar.gz/' + __version__, keywords=['dsl', 'edsl', 'json'], packages=['jsonbender'], + classifiers=[ + 'Intended Audience :: Developers', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.5', + ], ) diff --git a/tests/test_core.py b/tests/test_core.py index e0207e7..38949bf 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,5 +1,7 @@ import unittest +import sys + from jsonbender import S, K from jsonbender.core import bend, BendingException, Context, BinaryOperator from jsonbender.test import BenderTestMixin @@ -105,7 +107,11 @@ def test_op_with_context(self): class TestGetItem(unittest.TestCase, BenderTestMixin): def test_getitem(self): bender = S('val')[2:8:2] - self.assert_bender(bender, {'val': range(10)}, [2, 4, 6]) + if sys.version_info.major == 2: + val = range(10) + else: + val = list(range(10)) + self.assert_bender(bender, {'val': val}, [2, 4, 6]) if __name__ == '__main__': diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..5391968 --- /dev/null +++ b/tox.ini @@ -0,0 +1,9 @@ +[testenv] +deps= + unittest2 + discover +commands=unit2 discover tests -v + +[tox] +envlist = + py{27,35}-app \ No newline at end of file From 3bc31c9e0cd64188ab339e2976dae3660464d4e1 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Thu, 4 May 2017 21:13:34 -0300 Subject: [PATCH 37/58] Add eq op --- jsonbender/core.py | 8 ++++++++ tests/test_core.py | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/jsonbender/core.py b/jsonbender/core.py index 3b0e0b1..4128998 100644 --- a/jsonbender/core.py +++ b/jsonbender/core.py @@ -27,6 +27,9 @@ def raw_execute(self, source): def execute(self, source): raise NotImplementedError() + def __eq__(self, other): + return Eq(self, other) + def __add__(self, other): return Add(self, other) @@ -113,6 +116,11 @@ def op(self, v1, v2): return float(v1) / float(v2) +class Eq(BinaryOperator): + def op(self, v1, v2): + return v1 == v2 + + class Context(Bender): def raw_execute(self, source): transport = Transport.from_source(source) diff --git a/tests/test_core.py b/tests/test_core.py index e0207e7..dd2b0e1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,7 +1,7 @@ import unittest from jsonbender import S, K -from jsonbender.core import bend, BendingException, Context, BinaryOperator +from jsonbender.core import bend, BendingException, Context from jsonbender.test import BenderTestMixin @@ -101,6 +101,10 @@ def test_op_with_context(self): res = bend(mapping, in_, context=context) self.assertEqual(res, {'res': 4}) + def test_eq(self): + self.assert_bender(K(42) == K(42), None, True) + self.assert_bender(K(42) == K(27), None, False) + class TestGetItem(unittest.TestCase, BenderTestMixin): def test_getitem(self): From 4320f7946316a36abecff2565bbb6791592ad166 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Fri, 5 May 2017 07:51:29 -0300 Subject: [PATCH 38/58] Bump version to 0.8.1 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index c055603..66d9737 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -4,5 +4,5 @@ from jsonbender.selectors import F, K, S, OptionalS -__version__ = '0.8.0' +__version__ = '0.8.1' From 5b6890b51b0295f59a2491ffb6c807f3aba3779a Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Thu, 15 Jun 2017 12:24:34 -0300 Subject: [PATCH 39/58] Bump tox tests from 35 to 36 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5391968..ca51193 100644 --- a/tox.ini +++ b/tox.ini @@ -6,4 +6,4 @@ commands=unit2 discover tests -v [tox] envlist = - py{27,35}-app \ No newline at end of file + py{27,36}-app From 5da4a930df766c0dd3a557f9be913cc1bafecd9d Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Thu, 15 Jun 2017 12:17:52 -0300 Subject: [PATCH 40/58] Add control flow benders --- jsonbender/__init__.py | 1 + jsonbender/control_flow.py | 109 +++++++++++++++++++++++++++++++++++++ tests/test_control_flow.py | 82 ++++++++++++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 jsonbender/control_flow.py create mode 100644 tests/test_control_flow.py diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index 66d9737..c5a2c33 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -2,6 +2,7 @@ from jsonbender.list_ops import FlatForall, Forall, Filter, Reduce from jsonbender.string_ops import Format from jsonbender.selectors import F, K, S, OptionalS +from jsonbender.control_flow import Alternation, If, Switch __version__ = '0.8.1' diff --git a/jsonbender/control_flow.py b/jsonbender/control_flow.py new file mode 100644 index 0000000..39f27eb --- /dev/null +++ b/jsonbender/control_flow.py @@ -0,0 +1,109 @@ +from jsonbender.core import Bender +from jsonbender.selectors import K + + +class If(Bender): + """ + Takes a condition bender, and two benders (both default to K(None)). + If the condition bender evaluates to true, return the value of the first + bender. If it evaluates to false, return the value of the second bender. + + Example: + ``` + if_ = If(S('country') == K('China'), S('first_name'), S('last_name')) + if_({'country': 'China', + 'first_name': 'Li', + 'last_name': 'Na'}) # -> 'Li' + + if_({'country': 'Brazil', + 'first_name': 'Gustavo', + 'last_name': 'Kuerten'}) # -> 'Kuerten' + + ``` + """ + + def __init__(self, condition, when_true=K(None), when_false=K(None)): + self.condition = condition + self.when_true = when_true + self.when_false = when_false + + def execute(self, val): + return (self.when_true(val) + if self.condition(val) + else self.when_false(val)) + + +class Alternation(Bender): + """ + Take any number of benders, and return the value of the first one that + doesn't raise a LookupError (KeyError, IndexError etc.). + If all benders raise LookupError, re-raise the last raised exception. + + Example: + ``` + b = Alternation(S(1), S(0), S('key1')) + b(['a', 'b']) # -> 'b' + b(['a']) # -> 'a' + b([]) # -> KeyError + b({}) # -> KeyError + b({'key1': 23}) # -> 23 + ``` + """ + + def __init__(self, *benders): + self.benders = benders + + def execute(self, source): + exc = ValueError() + for bender in self.benders: + try: + result = bender(source) + except LookupError as e: + exc = e + else: + return result + else: + raise exc + + +class Switch(Bender): + """ + Take a key bender, a 'case' container of benders and a default bender + (optional). + The value returned by the key bender is used to get a bender from the + case container, which then returns the result. + If the key is not in the case container, the default is used. + If it's unavailable, raise the original LookupError. + + Example: + ``` + b = Switch(S('service'), + {'twitter': S('handle'), + 'mastodon': S('handle') + K('@') + S('server')}, + default=S('email')) + + b({'service': 'twitter', 'handle': 'etandel'}) # -> 'etandel' + b({'service': 'mastodon', 'handle': 'etandel', + 'server': 'mastodon.social'}) # -> 'etandel@mastodon.social' + b({'service': 'facebook', + 'email': 'email@whatever.com'}) # -> 'email@whatever.com' + ``` + """ + + def __init__(self, key_bender, cases, default=None): + self.key_bender = key_bender + self.cases = cases + self.default = default + + def execute(self, source): + key = self.key_bender(source) + try: + bender = self.cases[key] + except LookupError: + if self.default: + bender = self.default + else: + raise + + return bender(source) + diff --git a/tests/test_control_flow.py b/tests/test_control_flow.py new file mode 100644 index 0000000..c64e619 --- /dev/null +++ b/tests/test_control_flow.py @@ -0,0 +1,82 @@ +from operator import add +import unittest + +from jsonbender import Context, K, S, bend +from jsonbender.control_flow import If, Alternation, Switch +from jsonbender.test import BenderTestMixin + + +class TestIf(BenderTestMixin, unittest.TestCase): + def setUp(self): + self.na_li = {'country': 'China', + 'first_name': 'Li', + 'last_name': 'Na'} + self.guga = {'country': 'Brazil', + 'first_name': 'Gustavo', + 'last_name': 'Kuerten'} + + def test_if_true(self): + if_ = If(S('country') == K('China'), S('first_name'), S('last_name')) + self.assert_bender(if_, self.na_li, 'Li') + + def test_if_false(self): + if_ = If(S('country') == K('China'), S('first_name'), S('last_name')) + self.assert_bender(if_, self.guga, 'Kuerten') + + def test_if_true_default(self): + if_ = If(S('country') == K('China'), when_false=S('last_name')) + self.assert_bender(if_, self.na_li, None) + + def test_if_false_default(self): + if_ = If(S('country') == K('China'), S('first_name')) + self.assert_bender(if_, self.guga, None) + + +class TestAlternation(BenderTestMixin, unittest.TestCase): + def test_empty_benders(self): + self.assertRaises(ValueError, Alternation(), {}) + + def test_matches(self): + bender = Alternation(S(1), S(0), S('key1')) + self.assert_bender(bender, ['a', 'b'], 'b') + self.assert_bender(bender, ['a'], 'a') + self.assert_bender(bender, {'key1': 23}, 23) + + def test_no_match(self): + self.assertRaises(IndexError, Alternation(S(1)), []) + self.assertRaises(KeyError, Alternation(S(1)), {}) + + +class TestSwitch(BenderTestMixin, unittest.TestCase): + def test_match(self): + bender = Switch(S('service'), + {'twitter': S('handle'), + 'mastodon': S('handle') + K('@') + S('server')}, + default=S('email')) + + self.assert_bender(bender, + {'service': 'twitter', 'handle': 'etandel'}, + 'etandel') + self.assert_bender(bender, + {'service': 'mastodon', + 'handle': 'etandel', + 'server': 'mastodon.social'}, + 'etandel@mastodon.social') + + def test__no_match_with_default(self): + bender = Switch(S('service'), + {'twitter': S('handle'), + 'mastodon': S('handle') + K('@') + S('server')}, + default=S('email')) + self.assert_bender(bender, + {'service': 'facebook', + 'email': 'email@whatever.com'}, + 'email@whatever.com') + + def test__no_match_without_default(self): + self.assertRaises(KeyError, Switch(S('key'), {}), {'key': None}) + + +if __name__ == '__main__': + unittest.main() + From f8656af67acc28c3062366a6c98863976596a84b Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Thu, 15 Jun 2017 12:31:33 -0300 Subject: [PATCH 41/58] Add control flow docs to README --- README.md | 65 ++++++++++++++++++++++++++++++++++++++ jsonbender/control_flow.py | 1 - 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a7424c7..cd3e7cd 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,71 @@ ret = bend(MAPPING, source) assert ret == {'doubles_triples': [4, 6, 30, 45, 100, 150]} ``` +#### Control Flow + +Sometimes what bender to use must be decided at bending time, +so JSONBender provides 3 control flow structures: + + +##### Alternation + +Take any number of benders, and return the value of the first one that +doesn't raise a LookupError (KeyError, IndexError etc.). + +If all benders raise LookupError, re-raise the last raised exception. + +```python +b = Alternation(S(1), S(0), S('key1')) + +b(['a', 'b']) # -> 'b' +b(['a']) # -> 'a' +b([]) # -> KeyError +b({}) # -> KeyError +b({'key1': 23}) # -> 23 +``` + +##### If + +Takes a condition bender, and two benders (both default to K(None)). +If the condition bender evaluates to true, return the value of the first +bender. If it evaluates to false, return the value of the second bender. + +```python +if_ = If(S('country') == K('China'), S('first_name'), S('last_name')) +if_({'country': 'China', + 'first_name': 'Li', + 'last_name': 'Na'}) # -> 'Li' + +if_({'country': 'Brazil', + 'first_name': 'Gustavo', + 'last_name': 'Kuerten'}) # -> 'Kuerten' +``` + +##### Switch + +Take a key bender, a 'case' container of benders and a default bender +(optional). + +The value returned by the key bender is used to get a bender from the +case container, which then returns the result. + +If the key is not in the case container, the default is used. + +If it's unavailable, raise the original LookupError. + +```python +b = Switch(S('service'), + {'twitter': S('handle'), + 'mastodon': S('handle') + K('@') + S('server')}, + default=S('email')) + +b({'service': 'twitter', 'handle': 'etandel'}) # -> 'etandel' +b({'service': 'mastodon', 'handle': 'etandel', + 'server': 'mastodon.social'}) # -> 'etandel@mastodon.social' +b({'service': 'facebook', + 'email': 'email@whatever.com'}) # -> 'email@whatever.com' +``` + #### String ops JSONBender currently provides only one string-related bender. diff --git a/jsonbender/control_flow.py b/jsonbender/control_flow.py index 39f27eb..3525006 100644 --- a/jsonbender/control_flow.py +++ b/jsonbender/control_flow.py @@ -18,7 +18,6 @@ class If(Bender): if_({'country': 'Brazil', 'first_name': 'Gustavo', 'last_name': 'Kuerten'}) # -> 'Kuerten' - ``` """ From 6b2bdd9a65f2e3649c2aa42baa11fc958af64dfe Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Fri, 16 Jun 2017 00:49:29 -0300 Subject: [PATCH 42/58] Add &, |, ~ ops --- jsonbender/core.py | 57 +++++++++++++++++++++++++++++++++++++++++++++- tests/test_core.py | 20 ++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/jsonbender/core.py b/jsonbender/core.py index 3f8ca50..0a994ec 100644 --- a/jsonbender/core.py +++ b/jsonbender/core.py @@ -30,6 +30,15 @@ def execute(self, source): def __eq__(self, other): return Eq(self, other) + def __and__(self, other): + return And(self, other) + + def __or__(self, other): + return Or(self, other) + + def __invert__(self): + return Invert(self) + def __add__(self, other): return Add(self, other) @@ -42,6 +51,9 @@ def __mul__(self, other): def __div__(self, other): return Div(self, other) + def __neg__(self): + return Neg(self) + def __truediv__(self, other): return Div(self, other) @@ -75,8 +87,41 @@ def raw_execute(self, source): return self._second.raw_execute(self._first.raw_execute(source)) -class BinaryOperator(Bender): +class UnaryOperator(Bender): + """ + Base class for unary bending operators. Should not be directly + instantiated. + + Whenever a unary op is activated, the op() method is called with the + *value* (that is, the bender is implicitly activated). + + Subclasses must implement the op() method, which takes one value and + should return the desired result. + """ + + def __init__(self, bender): + self.bender = bender + + def op(self, v): + raise NotImplementedError() + + def raw_execute(self, source): + source = Transport.from_source(source) + val = self.op(self.bender(source)) + return Transport(val, source.context) + + +class Neg(UnaryOperator): + def op(self, v): + return -v + + +class Invert(UnaryOperator): + def op(self, v): + return not v + +class BinaryOperator(Bender): """ Base class for binary bending operators. Should not be directly instantiated. @@ -127,6 +172,16 @@ def op(self, v1, v2): return v1 == v2 +class And(BinaryOperator): + def op(self, v1, v2): + return v1 and v2 + + +class Or(BinaryOperator): + def op(self, v1, v2): + return v1 or v2 + + class Context(Bender): def raw_execute(self, source): transport = Transport.from_source(source) diff --git a/tests/test_core.py b/tests/test_core.py index 9e84418..d382636 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -96,6 +96,10 @@ def test_div(self): self.assert_bender(K(4) / K(2), None, 2) self.assertAlmostEqual((K(5) / K(2))(None), 2.5, 2) + def test_neg(self): + self.assert_bender(-K(1), None, -1) + self.assert_bender(-K(-1), None, 1) + def test_op_with_context(self): mapping = {'res': (Context() >> S('b')) - S('a')} in_ = {'a': 23} @@ -107,6 +111,22 @@ def test_eq(self): self.assert_bender(K(42) == K(42), None, True) self.assert_bender(K(42) == K(27), None, False) + def test_and(self): + self.assert_bender(K(True) & K(True), None, True) + self.assert_bender(K(True) & K(False), None, False) + self.assert_bender(K(False) & K(True), None, False) + self.assert_bender(K(False) & K(False), None, False) + + def test_or(self): + self.assert_bender(K(True) | K(True), None, True) + self.assert_bender(K(True) | K(False), None, True) + self.assert_bender(K(False) | K(True), None, True) + self.assert_bender(K(False) | K(False), None, False) + + def test_invert(self): + self.assert_bender(~K(True), None, False) + self.assert_bender(~K(False), None, True) + class TestGetItem(unittest.TestCase, BenderTestMixin): def test_getitem(self): From 302b992f33bdb1129f2742a4e05c12f59c170ec2 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Fri, 16 Jun 2017 00:55:51 -0300 Subject: [PATCH 43/58] Bump version to 0.9.0 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index c5a2c33..b91fc48 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -5,5 +5,5 @@ from jsonbender.control_flow import Alternation, If, Switch -__version__ = '0.8.1' +__version__ = '0.9.0' From 9978e8b22f90a1a58abdece47e74ad544f221a4c Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Fri, 16 Jun 2017 01:18:15 -0300 Subject: [PATCH 44/58] Activate optinal behavior on all LookupErrors --- jsonbender/selectors.py | 2 +- tests/test_selectors.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/jsonbender/selectors.py b/jsonbender/selectors.py index ecffada..fd297cc 100644 --- a/jsonbender/selectors.py +++ b/jsonbender/selectors.py @@ -52,7 +52,7 @@ def __init__(self, *path, **kwargs): def execute(self, source): try: ret = super(OptionalS, self).execute(source) - except KeyError: + except LookupError: return self.default else: return ret diff --git a/tests/test_selectors.py b/tests/test_selectors.py index ef6962a..f32c751 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -49,6 +49,9 @@ def test_opts_with_default(self): self.assert_bender(bender, {'key': {}}, default) self.assert_bender(bender, {}, default) + def test_activate_on_IndexError(self): + self.assert_bender(OptionalS(0), [], None) + class FTestsMixin(BenderTestMixin): def test_f(self): From 69c0527c1163dbead015ca525a47890a34674ff5 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Fri, 16 Jun 2017 01:18:37 -0300 Subject: [PATCH 45/58] Bump version to 0.9.1 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index b91fc48..a72612e 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -5,5 +5,5 @@ from jsonbender.control_flow import Alternation, If, Switch -__version__ = '0.9.0' +__version__ = '0.9.1' From ef6722e5ed77bf3ed6f1b1e46f0164f1109b3660 Mon Sep 17 00:00:00 2001 From: Bob Haddleton Date: Mon, 12 Feb 2018 17:17:45 -0600 Subject: [PATCH 46/58] Update README.md to fix errors in code snippets and show required imports --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cd3e7cd..0f84e34 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ It takes any value as a parameter and always returns that value regardless of th `S()` is a selector for accessing keys and indices: It takes a variable number of keys / indices and returns the corresponding value on the source dict: ```python +from jsonbender import bend, S + MAPPING = {'val': S('a', 'deeply', 'nested', 0, 'value')} ret = bend(MAPPING, {'a': {'deeply': {'nested': [{'value': 42}]}}}) assert ret == {'val': 42} @@ -85,14 +87,16 @@ If any of keys may not exist, `S()` can be "annotated" by calling the `.optional `OptionalS` is like `S()` but does not raise errors when any of the keys is not found. Instead, it returns `None` or the `default` value that is passed on its construction. ```python -source = {'does': {'exist': 23} +from jsonbender import bend, OptionalS + +source = {'does': {'exist': 23}} MAPPING_1 = {'val': OptionalS('does', 'not', 'exist')} -ret = bend(MAPPING_1, }, source) +ret = bend(MAPPING_1, source) assert ret == {'val': None} MAPPING_2 = {'val': OptionalS('does', 'not', 'exist', default=27)} -ret = bend(MAPPING_2, }, source) +ret = bend(MAPPING_2, source) assert ret == {'val': 28} ``` @@ -107,6 +111,8 @@ The extra optional args and kwargs are passed to the function at bending time after the given value. ```python +from jsonbender import bend, F, S + MAPPING = { 'total_number_of_keys': F(len), 'number_of_str_keys': F(lambda source: len([k for k in source.iterkeys() @@ -123,13 +129,14 @@ If the function can't take certain values, you can protect it by calling the `.p ```python import math +from jsonbender import bend, F, S MAPPING_1 = {'sqrt': S('val') >> F(math.sqrt).protect()} -assert bend(MAPPING, {'val': 4}) == {'sqrt': 2} -assert bend(MAPPING, {'val': None}) == {'sqrt': None} +assert bend(MAPPING_1, {'val': 4}) == {'sqrt': 2} +assert bend(MAPPING_1, {'val': None}) == {'sqrt': None} MAPPING_2 = {'sqrt': S('val') >> F(math.sqrt).protect(-1)} -assert bend(MAPPING, {'val': -1}) == {'sqrt': -1} +assert bend(MAPPING_1, {'val': -1}) == {'sqrt': -1} ``` @@ -143,6 +150,8 @@ For the arithmetic `+`, `-`, `*`, `/`, the behavior is to apply the operator to the bended values of each operand. ```python +from jsonbender import bend, K, S + a = S('a') b = S('b') MAPPING = {'add': a + b, 'sub': a - b, 'mul': a * b, 'div': a / b} @@ -176,6 +185,8 @@ iterable. ```python +from jsonbender import bend, Reduce, S + MAPPING = {'sum': S('ints') >> Reduce(lambda acc, i: acc + i)} ret = bend(MAPPING, {'ints': [1, 4, 7, 9]}) assert ret == {'sum': 21} @@ -188,6 +199,8 @@ Builds a new list with the elements of the iterable for which the given function returns True. ```python +from jsonbender import bend, Filter, S + MAPPING = {'even': S('ints') >> Filter(lambda i: i % 2 == 0)} ret = bend(MAPPING, {'ints': range(5)}) assert ret == {'even': [0, 2, 4]} @@ -201,6 +214,8 @@ iterable. ```python +from jsonbender import bend, Forall, S + MAPPING = {'doubles': S('ints') >> Forall(lambda i: i * 2)} ret = bend(MAPPING, {'ints': range(5)}) assert ret == {'doubles': [0, 2, 4, 6, 8]} @@ -215,10 +230,13 @@ to `ForallBend`. ##### ForallBend Bends each element of the list with given mapping and context. -If no contexxt is passed, it "inherits" at bend-time the context passed to the outer `bend()` call. +If no context is passed, it "inherits" at bend-time the context passed to the outer `bend()` call. ```python +from jsonbender import bend, S +from jsonbender.list_ops import ForallBend + MAPPING = {'list_of_bs': S('list_of_as') >> ForallBend({'b': S('a')})} source = {'list_of_as': [{'a': 23}, {'a': 27}]} ret = bend(MAPPING, source) @@ -232,8 +250,11 @@ element of the iterable, which are than "flattened" into a single list. ```python +from jsonbender import bend, S +from jsonbender.list_ops import FlatForall + MAPPING = {'doubles_triples': S('ints') >> FlatForall(lambda x: [x * 1, x * 3])} -source = {'ints': [2, 15, 50]) +source = {'ints': [2, 15, 50]} ret = bend(MAPPING, source) assert ret == {'doubles_triples': [4, 6, 30, 45, 100, 150]} ``` @@ -252,6 +273,9 @@ doesn't raise a LookupError (KeyError, IndexError etc.). If all benders raise LookupError, re-raise the last raised exception. ```python +from jsonbender import S +from jsonbender.control_flow import Alternation + b = Alternation(S(1), S(0), S('key1')) b(['a', 'b']) # -> 'b' @@ -268,6 +292,9 @@ If the condition bender evaluates to true, return the value of the first bender. If it evaluates to false, return the value of the second bender. ```python +from jsonbender import K, S +from jsonbender.control_flow import If + if_ = If(S('country') == K('China'), S('first_name'), S('last_name')) if_({'country': 'China', 'first_name': 'Li', @@ -291,6 +318,8 @@ If the key is not in the case container, the default is used. If it's unavailable, raise the original LookupError. ```python +from jsonbender import K, S +from jsonbender.control_flow import Switch b = Switch(S('service'), {'twitter': S('handle'), 'mastodon': S('handle') + K('@') + S('server')}, @@ -316,6 +345,7 @@ named parameters. It uses the same syntax as `str.format()` ```python +from jsonbender import bend, Format, S MAPPING = {'formatted': Format('{} {} {last}', S('first'), S('second'), @@ -332,6 +362,9 @@ All JSONBenders can be composed with other benders using `<<` and `>>` to make them receive previously bended values. ```python +from jsonbender import bend, F, S +from jsonbender.list_ops import Forall + MAPPING = { 'name': S('name'), 'pythonista': S('prog_langs') >> Forall(str.lower) >> F(lambda ls: 'python' in ls), @@ -354,6 +387,7 @@ Whatever you pass for the argument is can be used at bending time by the ```python +from jsonbender import bend, Context, S MAPPING = { 'name': S('name'), 'age': (Context() >> S('year')) - S('birthyear'), From d53121442da517787a0f63a85b7bb73d95d7b88a Mon Sep 17 00:00:00 2001 From: Bob Haddleton Date: Mon, 12 Feb 2018 17:17:45 -0600 Subject: [PATCH 47/58] Update README.md to fix errors in code snippets and show required imports --- README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index cd3e7cd..4f39cff 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ It takes any value as a parameter and always returns that value regardless of th `S()` is a selector for accessing keys and indices: It takes a variable number of keys / indices and returns the corresponding value on the source dict: ```python +from jsonbender import bend, S + MAPPING = {'val': S('a', 'deeply', 'nested', 0, 'value')} ret = bend(MAPPING, {'a': {'deeply': {'nested': [{'value': 42}]}}}) assert ret == {'val': 42} @@ -85,15 +87,17 @@ If any of keys may not exist, `S()` can be "annotated" by calling the `.optional `OptionalS` is like `S()` but does not raise errors when any of the keys is not found. Instead, it returns `None` or the `default` value that is passed on its construction. ```python -source = {'does': {'exist': 23} +from jsonbender import bend, OptionalS + +source = {'does': {'exist': 23}} MAPPING_1 = {'val': OptionalS('does', 'not', 'exist')} -ret = bend(MAPPING_1, }, source) +ret = bend(MAPPING_1, source) assert ret == {'val': None} MAPPING_2 = {'val': OptionalS('does', 'not', 'exist', default=27)} -ret = bend(MAPPING_2, }, source) -assert ret == {'val': 28} +ret = bend(MAPPING_2, source) +assert ret == {'val': 27} ``` For readability and reusability, prefer using `S().optional()` instead. @@ -107,9 +111,11 @@ The extra optional args and kwargs are passed to the function at bending time after the given value. ```python +from jsonbender import bend, F, S + MAPPING = { 'total_number_of_keys': F(len), - 'number_of_str_keys': F(lambda source: len([k for k in source.iterkeys() + 'number_of_str_keys': F(lambda source: len([k for k in source.keys() if isinstance(k, str)])), 'price_truncated': S('price_as_str') >> F(float) >> F(int), } @@ -123,13 +129,14 @@ If the function can't take certain values, you can protect it by calling the `.p ```python import math +from jsonbender import bend, F, S MAPPING_1 = {'sqrt': S('val') >> F(math.sqrt).protect()} -assert bend(MAPPING, {'val': 4}) == {'sqrt': 2} -assert bend(MAPPING, {'val': None}) == {'sqrt': None} +assert bend(MAPPING_1, {'val': 4}) == {'sqrt': 2} +assert bend(MAPPING_1, {'val': None}) == {'sqrt': None} MAPPING_2 = {'sqrt': S('val') >> F(math.sqrt).protect(-1)} -assert bend(MAPPING, {'val': -1}) == {'sqrt': -1} +assert bend(MAPPING_2, {'val': -1}) == {'sqrt': -1} ``` @@ -143,6 +150,8 @@ For the arithmetic `+`, `-`, `*`, `/`, the behavior is to apply the operator to the bended values of each operand. ```python +from jsonbender import bend, K, S + a = S('a') b = S('b') MAPPING = {'add': a + b, 'sub': a - b, 'mul': a * b, 'div': a / b} @@ -176,6 +185,8 @@ iterable. ```python +from jsonbender import bend, Reduce, S + MAPPING = {'sum': S('ints') >> Reduce(lambda acc, i: acc + i)} ret = bend(MAPPING, {'ints': [1, 4, 7, 9]}) assert ret == {'sum': 21} @@ -188,6 +199,8 @@ Builds a new list with the elements of the iterable for which the given function returns True. ```python +from jsonbender import bend, Filter, S + MAPPING = {'even': S('ints') >> Filter(lambda i: i % 2 == 0)} ret = bend(MAPPING, {'ints': range(5)}) assert ret == {'even': [0, 2, 4]} @@ -201,6 +214,8 @@ iterable. ```python +from jsonbender import bend, Forall, S + MAPPING = {'doubles': S('ints') >> Forall(lambda i: i * 2)} ret = bend(MAPPING, {'ints': range(5)}) assert ret == {'doubles': [0, 2, 4, 6, 8]} @@ -215,10 +230,13 @@ to `ForallBend`. ##### ForallBend Bends each element of the list with given mapping and context. -If no contexxt is passed, it "inherits" at bend-time the context passed to the outer `bend()` call. +If no context is passed, it "inherits" at bend-time the context passed to the outer `bend()` call. ```python +from jsonbender import bend, S +from jsonbender.list_ops import ForallBend + MAPPING = {'list_of_bs': S('list_of_as') >> ForallBend({'b': S('a')})} source = {'list_of_as': [{'a': 23}, {'a': 27}]} ret = bend(MAPPING, source) @@ -232,8 +250,11 @@ element of the iterable, which are than "flattened" into a single list. ```python -MAPPING = {'doubles_triples': S('ints') >> FlatForall(lambda x: [x * 1, x * 3])} -source = {'ints': [2, 15, 50]) +from jsonbender import bend, S +from jsonbender.list_ops import FlatForall + +MAPPING = {'doubles_triples': S('ints') >> FlatForall(lambda x: [x * 2, x * 3])} +source = {'ints': [2, 15, 50]} ret = bend(MAPPING, source) assert ret == {'doubles_triples': [4, 6, 30, 45, 100, 150]} ``` @@ -252,6 +273,9 @@ doesn't raise a LookupError (KeyError, IndexError etc.). If all benders raise LookupError, re-raise the last raised exception. ```python +from jsonbender import S +from jsonbender.control_flow import Alternation + b = Alternation(S(1), S(0), S('key1')) b(['a', 'b']) # -> 'b' @@ -268,6 +292,9 @@ If the condition bender evaluates to true, return the value of the first bender. If it evaluates to false, return the value of the second bender. ```python +from jsonbender import K, S +from jsonbender.control_flow import If + if_ = If(S('country') == K('China'), S('first_name'), S('last_name')) if_({'country': 'China', 'first_name': 'Li', @@ -291,6 +318,9 @@ If the key is not in the case container, the default is used. If it's unavailable, raise the original LookupError. ```python +from jsonbender import K, S +from jsonbender.control_flow import Switch + b = Switch(S('service'), {'twitter': S('handle'), 'mastodon': S('handle') + K('@') + S('server')}, @@ -316,6 +346,8 @@ named parameters. It uses the same syntax as `str.format()` ```python +from jsonbender import bend, Format, S + MAPPING = {'formatted': Format('{} {} {last}', S('first'), S('second'), @@ -332,13 +364,16 @@ All JSONBenders can be composed with other benders using `<<` and `>>` to make them receive previously bended values. ```python +from jsonbender import bend, F, S +from jsonbender.list_ops import Forall + MAPPING = { 'name': S('name'), 'pythonista': S('prog_langs') >> Forall(str.lower) >> F(lambda ls: 'python' in ls), } source = { 'name': 'Mary', - 'prog_lags': ['C', 'Python', 'Lua'], + 'prog_langs': ['C', 'Python', 'Lua'], } ret = bend(MAPPING, source) assert ret == {'name': 'Mary', 'pythonista': True} @@ -354,6 +389,8 @@ Whatever you pass for the argument is can be used at bending time by the ```python +from jsonbender import bend, Context, S + MAPPING = { 'name': S('name'), 'age': (Context() >> S('year')) - S('birthyear'), From 25bdca1feb2e93edde3e030d352e0ec5fcfe2d6e Mon Sep 17 00:00:00 2001 From: Bob Haddleton Date: Mon, 12 Feb 2018 19:06:09 -0600 Subject: [PATCH 48/58] Handle exceptions cleanly --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4f39cff..45251a8 100644 --- a/README.md +++ b/README.md @@ -280,8 +280,16 @@ b = Alternation(S(1), S(0), S('key1')) b(['a', 'b']) # -> 'b' b(['a']) # -> 'a' -b([]) # -> KeyError -b({}) # -> KeyError +try: + b([]) # -> TypeError +except TypeError: + pass + +try: + b({}) # -> KeyError +except KeyError: + pass + b({'key1': 23}) # -> 23 ``` From 2d2a05d7b2865aa3ce0154233378af6bf1960b44 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Mon, 7 May 2018 15:32:07 -0300 Subject: [PATCH 49/58] Fix traversal of lists with non-dict elements. --- jsonbender/core.py | 26 ++++++++++++++------------ tests/test_core.py | 6 ++++++ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/jsonbender/core.py b/jsonbender/core.py index 0a994ec..95daf3c 100644 --- a/jsonbender/core.py +++ b/jsonbender/core.py @@ -220,20 +220,22 @@ def bend(mapping, source, context=None): def _bend(mapping, transport): - res = {} - for k, value in iteritems(mapping): - if isinstance(value, Bender): + if isinstance(mapping, list): + return [_bend(v, transport) for v in mapping] + + elif isinstance(mapping, dict): + res = {} + for k, v in iteritems(mapping): try: - newv = value(transport) + res[k] = _bend(v, transport) except Exception as e: m = 'Error for key {}: {}'.format(k, str(e)) raise BendingException(m) - elif isinstance(value, list): - newv = list(map(lambda v: _bend(v, transport), value)) - elif isinstance(value, dict): - newv = _bend(value, transport) - else: - newv = value - res[k] = newv - return res + return res + + elif isinstance(mapping, Bender): + return mapping(transport) + + else: + return mapping diff --git a/tests/test_core.py b/tests/test_core.py index d382636..9838be5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -61,6 +61,12 @@ def test_nested_mapping_with_lists(self): } self.assertDictEqual(bend(mapping, source), expected) + def test_list_with_non_dict_elements(self): + mapping = {'k': ['foo1', S('bar1')]} + source = {'bar1': 'val 1'} + expected = {'k': ['foo1', 'val 1']} + self.assertDictEqual(bend(mapping, source), expected) + def test_bending_exception_is_raised_when_something_bad_happens(self): mapping = {'a': S('nonexistant')} source = {} From 32f8faaafe3865e22ec7c951edbf74262ba97dda Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Mon, 7 May 2018 15:40:49 -0300 Subject: [PATCH 50/58] Bump version to 0.9.2 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index a72612e..44454b3 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -5,5 +5,5 @@ from jsonbender.control_flow import Alternation, If, Switch -__version__ = '0.9.1' +__version__ = '0.9.2' From 5539c3616e7ce8ce45831e96e3e66055f4096db2 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Thu, 27 Sep 2018 13:04:18 -0300 Subject: [PATCH 51/58] Add contribution section in README --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 45251a8..96f3653 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,13 @@ Installing pip install JSONBender ``` +Contribute +--- +If you have any contributions fork this repository and open a Pull Request. Please, make sure the tests are OK: + +```bash +python -m unittest discover -s tests +``` Usage --- From 9bc818598dbc6e6dbe82366c6f453d2ef8cdb557 Mon Sep 17 00:00:00 2001 From: Ervilis Souza Date: Thu, 27 Sep 2018 15:33:46 -0300 Subject: [PATCH 52/58] Adding LICENSE --- LICENSE | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7b699d7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ + Copyright (c) 2018 JSONBender authors. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. From 3b8bddd5b016c6d3ceed19d8d9c99674b2ef53eb Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Mon, 1 Oct 2018 23:32:21 -0300 Subject: [PATCH 53/58] Mention tox on contributing section on README --- README.md | 12 ++++++++---- tox.ini | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 96f3653..a467d30 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,22 @@ Installing pip install JSONBender ``` -Contribute +Contributing --- -If you have any contributions fork this repository and open a Pull Request. Please, make sure the tests are OK: +If you want to contribute to JSONBender (thanks!), here's how to do it: + +1. Fork the repository. +2. Make sure the tests are all fine. Since we support both python 2 and 3, run the tests with [tox](https://github.com/tox-dev/tox): ```bash -python -m unittest discover -s tests +tox tests ``` +3. Open the pull request! + Usage --- - JSONBender works by calling the `bend()` function with a mapping and the source `dict` as arguments. It raises a `BendingException` if anyting bad happens during the transformation phase. The mapping itself is a dict whose values are benders, i.e. objects that represent the transformations to be done to the source dict. Ex: diff --git a/tox.ini b/tox.ini index ca51193..2df5825 100644 --- a/tox.ini +++ b/tox.ini @@ -6,4 +6,4 @@ commands=unit2 discover tests -v [tox] envlist = - py{27,36}-app + py{27,37}-app From c12cd2a8c260a8ec591adf60956cf12d5b7dc862 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Mon, 1 Oct 2018 23:37:53 -0300 Subject: [PATCH 54/58] Add License section to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index a467d30..ef6852c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,12 @@ It's name is inspired by Nickelodeon's cartoon series [Avatar: The Last Airbende ![aang](http://cdn-static.denofgeek.com/sites/denofgeek/files/9/21//the-last-airbender-aang-the-avatar.jpg) +License +--- + +JSONBender is licensed under the [MIT license](https://choosealicense.com/licenses/mit/). See the LICENSE file for more details. + + Installing --- From 35641853bc8a5f7633891bd214910940dd15d9d4 Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Mon, 1 Oct 2018 23:40:33 -0300 Subject: [PATCH 55/58] Resize aang image --- README.md | 2 +- aang.png | Bin 0 -> 96531 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 aang.png diff --git a/README.md b/README.md index ef6852c..6ce11e8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ About JSONBender is an embedded Python [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) for transforming dicts. It's name is inspired by Nickelodeon's cartoon series [Avatar: The Last Airbender](https://en.wikipedia.org/wiki/Avatar:_The_Last_Airbender). -![aang](http://cdn-static.denofgeek.com/sites/denofgeek/files/9/21//the-last-airbender-aang-the-avatar.jpg) +![aang](aang.png) License diff --git a/aang.png b/aang.png new file mode 100644 index 0000000000000000000000000000000000000000..f957c4fb0177aae8bd9e881379929ecea059b452 GIT binary patch literal 96531 zcmV)}KzqN5P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv|NsC0|NjYC_uK#g010qNS#tmY7ZCse7ZCxC^rW@cta z$IR@es*>Ih$uslp?(EKgc4psaX4k*IuIldU>Z;O_bhrBGNFtnb&N&Jwpnw7j_&Z_j z=QHO3r9WSj9{>P<^Z|GTw*ovH8+i^o@l$*)?akXWMXjoOt6Ru5!KfLD!E`7_@xzK8 z@5s%hE#|7LX>WK7Q6vP%@JXGhju$49{-oa5hhGl78*ypkvDT-mou6_e_{O9Ik!RkV zzNhu3Zki>eK->!`pnwAY;XL9)0fh0j03HDV`d}@KV71g%P)Qz%SG<+&YMfME^p@V> z9^XU8aywQJAJmT+#&@cfFqU1GOu}gTL;Y+TY>dfimf9~>zUp&s%-Lrb%boT-yPglu z?UfngZ!G-TZ{ycOnMhaELeeeW<-Prm#UDFcAnpYeP(T6y2&w=89{hasQ&mYD^aQXJ z01(J)006(m<|*4)D-t2}6-DI$ua0}<3@M6gWDbh3n{1vOVwscXn&^{Q{_3i;OV2EP zz^>a|t!e$9IUsAVYacro!*=~tX_k~}vy-`7;*HBl49dMNlyE1ZmtUS=-M|!+juSdT z{G;trKmi35@Gr%CyvKWlA{3z*ilG>a^;nPfJec=Vdyw(OjZQ}oGM@x-5nZsG1tSnZ zYrH3*#h{SJ^qhEx7M8DCD=LS@O&TgzORSpU^JCk`=$GqXcwa7iV(sw{F)iKP{Yxx@E69JX_|>n|8Ac9xk+ z;rv#{Sp_Zk0tzUgfWHFo!F%u?ztnO!c4IdUrol8AMi^mKwkg|`ZR8CZ&&QIrbiL4B z4pQ@ZKkPv(GDAovCHNyjN4pDG_(3v8bdPI4}=U=pzHQy^}xff7C0R{XOSTGA_!K6KDPukPXbTi$oeo#NCALt&s zhwi}wEWiR%imVmZ$k*6>wwTPM{m=;O)$`Ve#2~j)`oBH)xZUGU`@;8x>qD5N%;6WrA?}E3giQ=?dWjjU*Rw zkqqD+SxKY{J+Vhv$s4hwyp7nA?iZ@FseC;zuEv@^7{9zc{bcWhq;n^ag&Yq5QRdy# zcimi~3_iBSoPwlA(y9v0y^eVf=Uc3Ot+2kx>6=|xJ4wnCdYN~zW^!98Ko_VnTh1yv zp`>x5$-_K0`+WWu)ZF#(bfm^140t)!wMIQjbApk)BuYK77fK32^2>`}m491{0mSY4LVW;{MALI&g zum|9W1SB8<99}TM4n<*)^K2DAt9tMd=0z6^fr6WCU|O}P8Y;IjA5Cr^Up(&e$-6t7 z?uxn|d|~?8c7b=j4!XC~eI#CNu8_;Gs!JVm>?$}m%^IG4GJCeTL|h{7lb z0Qmhs?;zwq9`q~X;xBwsz+Xen0RZ}&1swnYsXsqsj4$JM+*TgPyUQw_Lxu^n1rONK z6+Bk`fjF$7E3jRj#}~0-QgwQP*ixAmY;L-G>L9a6(OnH!E z)01=}NscSDx5&WCdjl?d8%0m+0;L<~vXAIRmuoI)EP3zqR_4qj;rc^1ie}j>e)IC1+u?TvdJ zckL&Xni&c&UqaeS~_fZZ-z+akT(7JAGuS4&%&3jGv~ELkn*gd6H5#oOXx^%VPv z6*SjXC!`AJ@KhY7@we1vHTdVGCMkI-yK|2iADE_FU&>H(90$6#DfHf0U#_n_kQUfX zvn8gL@>+{a`nq&Ox}n|9&VF6|2X`6qF`_;ltWDCaGp;lxS@R7KZ6XbWNjeE9;g|;i zyfA>~I0N7e>H(-%P-qG$;IFX&3$OqR6e#~@&rjxJF6RE)GZ~XH830HwMqmUv`fE=a zltGz)bIva~iGE}X@F!XChkoey=L1MC&*s^P|DzNCPsSBo!Ii(T|MxkvopatBz)rRu z02sr1tmij><_u;)f~4q`m3*gK2*c<^wI@B#W!hJK&Nn4HC**(6*x7TQdyb1SA~4Y$BT=L)B2m+QLoU0G3STAOVeN*T++4Y{?VsL?za-}Du1 z0&?C8UP4;*nw;s`(_UD|qc4zwqf#ai5OqnEtCa z<&i`}5QJcW+(tZdP!8VkMlQfS0pyb0-(C4CKnSUi9Da}NLRV*eZ(NNazyUh;UO@`K%h1SXP zh5RR~g43#j7w3DzmBt?%MGUkZvTdkVKi9@+m?jd9y>!f4&(g~JTr*BHPSc!KV~x~C zc#H&|Erbe!SW=y z&&X>skxV2L|7eT;<6%K8hy}6Ld>H3+85vIq8>zlyMTAyjFkPiCQ-`RR5J8Jew(!J3 zG!{G3oA6Yt%abjJ?3)=#(_P(uZs4i3iwS27pN(XzvQOmgTa^LjknJ_<)%c~W$Bl7o|Zc7n% zP3#}2W7!-`6*h?Tv`g}aniO-AWHAiYPp@^d?uI(Wo3(9WX}LToGuR{4i?)*lkzzcm zkc%*q2+|qh_<(S>kJ<6^+=X5weu988n2L3n&*t)a>?k`*uF)QX8^C*$LpVs*vo*hZ z5+VSy89)^>_@~3KK->%XKgKU__-Hf-$SQss_{}lG&an~f6UwtmB+}B{(%kf1|HSsR z?nJI#Zb*I;!)Se1o2TY8*5mnGbkX|LlIJVDD*kTQsduMuoL=$mOW2N+Y20Odr z=H1rC@@Ko5G~CjpSCe}^Z2PS5d)jY}|I{K+aF3lw1vZLm zg_lB#kRsH;I-J0||2eA{hy*rFiry}zQugPql~xLm zv|a35=B`l7o8L<#X^`kIEM!}(`9Y5z&si&$@O0^FijYj)-{eP&P+9Db(PR($vi^8S zc2GN^qLfRrXasfL?#v`!ifI=D+;r)!&~3 z;8g%TuvWqzldH?=X`l_GT^)-Yb1tugL0PB*{P8?=#<+0aMw z7q8oX5d+1|wxfj?LM7|YyvfGWI*Vq%WGnCFH57l|TBs_-D9a^5Xl~t2o|0qfgLD0J zt7a9;`JNZ4I;$`FBlR)wO;56If>BsQGqoSI4Kw;>f6FOpaBy&Pbgn$Qa@opdn_AoC zx9(rMTiMa&GOwMRfXxa6fH$d{ZAXdK->!`;8)0@ za#P)|TwrO;QE8xgBVLf^pAS6Q`}oT1>SgQsM!nzlYSGoMXsg1_ykpeO%6|SKzNjfq zNPMw2X}G3x-b3C_T%{eP-Jz?@9Pc0V!3D7#K)Ts*REW`T|3owj`d8!G=X8T zkVRS&g+!{d+)F7Ft>zJXDy#AxzLeA?=hP(ChHJoi&Anju%>*yAu)h#u!qR9%FA=GhPqtQ5JpqQ+}0g7q^I8 z#4Y41xk|47J=Wu9ZsumL;To=?C+SIgk{xA7*-=6$rF-}qwwh(Bu1cglPFyAW3$@@u z?}$CwK=qt5P1r(TV*)#_zQsX$oVF5gV<}yZk!m}&o$5z+(dTpqKaHg%o<(2?qSbh% zzEV`%QnC{b){5qtCdxh>88EO$*C*;Ljeba6T7(iPf$=(MSmdzGhEcUC_xozPX+E`c+jPdRIa zXzor3IYCbHoxC-`Tl3av{VQta1UbQ51NZ`v6TB$EkCWc$^`Ca#|3fJd_X7S;@Ndev zg`2tA(#@1>-mi1i9no2^jfaxitI99)pYzghp|8pHXvERJluAx%Z-QYez>Ub6ME!Z9Ih%ZEZTmU(1(9fKcg2-;)mt!LPy$HGeOa2pRGR2 zKTco6tDk0IYCp1@?J=$)Pe?;;JAhm#rvQE*RRGlE?*QcT3;^H)fZOT^JmNWYGQvnz zYy(I#O$PWiazo9)e7eZ`kk=uOSS04?zD_wfYsbRuX>9hx1#=xPIzDrnN>2-CG+Wk-h%OJF-I#ODeqFN_C6--|)Cii+_TkjbYRXmBZwetZ#gp0r)>7(Z4!Ip_k=)o9NscL}O&gRAY@4>M z%?kY*d5G{@cxH(+*R>upyP11iPWs*n2n|f=aHxCrE_u~FY9Fps)~2=HQTy)5Bs0mc zQ3)i4Qkn{P0Pepx%zr)%?H83$3322Ej>88&@cExE^9ABw!2fOrq6&aeejmV3KRd`u ztiTFOP4hd;V)*ixYO`^-hI>WqAGLermtH*JyHBD^F^imYEoYTJL6>ElqKsF6SOqm1 zMYvt=8FVV7-0u3RgtdV&jFxsyLnmRaX)Eo)x%i!}VV8wIydn+ZgYc-G))kX^7z>sUbBY7iwMwFbvPFKO-h^;I@Ub?V-2L+_2weF}Z^_#iITTJ>P1 z)fFsQJS+?n0wvto@oR567s@I!)8{QSiXMI*+pCfheSjUYdaoW!go3!DyM8?tCg<8Sut2h#0qZ5 z4&~O%I+MBcyzcnB#VR<=+UVUSr;OfNvvY1G2W7fs7BxMW@A3CKVsqWLk;bYysWavk z&OVU)-Nws)m7`npsP^C6b*^P@^dh{oXZONi{5HXX*kcMiXFV%75p2aI&2H|2-FzO+ zBQI%X@}9INYyM=A`l$c2SOEyf!YM?Mi;REUmO%TG`0C-B=0aQRdY64^w zZ_FbB-k4kjXaxZEQJ=rVRfLe@z<-Q4;PcsbfH5S(1kh2?3kKF4e-d~3ou#W~G`Euz z%&|S!cOBTd$%>xy-%rW%Sm$!qIgf-0ZOtMp!8@?4bQj-4Qq=ZJl-!GavpMDJ`($wZ zXlfUGDQ&7SSk2Xrw`(D%lXtn*@EnV!rMRM&6IH6nanN_;kqF6aWFFTMb<@Gby(jwk?ac#!$9v4v@f)I)2~ zyyN9`b!~^sL6Z>+Y3viTQp1*>0-g|4ZTU?X;bb@I^B=Id;3axxoqyG%t3BXocvrz zq!msNii;|nRJKac3u^`bLXEhz>~iqaN0l~JnOS$Cui1Zs-;meo4>KOEIO4o#@y_O{ zmlK@g*1E2>?`0^aS*eC7KKfAll%}!_OQN-(wv%*H=#`&u?O@q$%^}734*MxKj=ru5j73Gwy(Kh~|5-qQkf>?cgLkWb@=4u@}ntY&F@rZ;fN3F%IS^1OFmn59X7?ZLyu~2GsO05(f zT}sXLrf#Zks?NcFnPWNEIFIt)@xG2yO`FGtmGq>LBfch6>0+@YSJ_~Ez;Sfr&U`j; zR-@TRHlA)1mka0UXzKwm3XPyr7ha-w5;t{-kaK3KeElj zAwGvVux?fh-7R3_O;{BA!ep zlkuGQUQ~+;4H3?9K`AN2G_6Cn$GOG_|fd63p%WLLy8Cn={yY)8j@gA4L+#) z-^|)lD1}md7N5mu@yXm93)w16;KN>2c(m=d=c@*fXFRq@A853BWY?J;om&hc*DOqa zqJ&BQ#9y;k-$WDoWYLp}ue+E=<_;8^t(mg&#%is8v2M6FOjlb@Q#%zlx~y~xlFkVC zFw4TsH}WbH|oEvk4oxBSDa2`rpNWH|hI02ce zIEj-uNji`Yq{FY2KE`7_mS4%Q73uLF&nmgZ1c3~&2k=PH7!M@&@`!=_{jF0&QYqS%w}`cd+K*ySUqbLO}BVZ zb_?@yYjRdw)sAh`!p+Om!`9AA@f%xaebtq9dX|f>5?<>#-6$;5RiUHRx~dNw%4(4f zku~RUn41^gyv~`L8$-(!DefCf){#uM8{LV9O+p>Mi}Ydhm9uP^n2f8WJa10gu!_d# zY9l_|G*y|0Mx>b#CmMMT9>(9%qwpurIEi)CNK24#5HNV7qQ+Hp5o?eSq=TgdQ`vL& zmAocz$!nD7t=TdFK;(bZ#2@JmdIP;!F3Uw48H~Z&we*yDR9&HNSL2KiGRCDG?ik+o zV6!E6=H9A&wzrQ9E;$BwfN$D~C{mi1a{2n|lJy%S^tnof#s%X2GK$oZL5pY+J=Te)SjH=25xTWMbD;|PyDM@aVvhb%Z z*>2tzeazc(R;ScFIALAZV%xL_4~t$IS^auYHP7lL{R6TL?a7whN?AVoX#PRHs+mT2 z(oFVJ7=cP^VfIWOr)D`7))o@>(LCzIbJ1Ls)h5!>yk=P&^Ws09{d9ECko$GdG+R+= za(%nn{vSg#Tx)u%-hd9yA3d4+s?%X@4SrfD^_A-89X(37~+rlcXs;UKRG z0p}sK87aZ%h%ad#$sOM_Zh zYc-*vS*>X!Yv*6R@N#h4Da|Rn-mYXdug3CBOXYD~=bZKqbM<9|`Iq5{pMH&Au){xQ^?>b=<~n-2P9+eHUk(p%;BjLeUe8 z(35n>Odg64D2mGhNG1*lQ*iP_oRm2eJV0Yhfl--eh9V;U@Hyki5w(0TY!7byx zMk%vXZPL$l+}iD7x4|Q|liN?cYpE~yRd&+3$RtB~6aJA8!a%l@eNx}4z0__Z6=zDb z{(`JWAOaD8band2P$2FF{38fQIhx2Pp*Xss5^0b2>KUb@Qc7$tJSDreHEDo4|82Yb zWiD8bc`O|~Z%yT_LihDoE4(jK+RkG8nq0|0tcI#rKd2q7?jsEKg%K%)7bdfUZwTd^L07{RzQ$48=ON zE?SMJ;t>z9d+gq?_vdjQ=Xojq zoIfW%WGC?f$bUAkdoKVY8YuIzfm-Q7%u|=D9mxl9Woeqm5(a7`&843b{ix3)jv+}RSVt0GMIy5$pJIIE(i z@~o;yTRds8ruA~aWdXJP)6j-LKxeE|By|B=N^OO8*0zd9_SEbY2TFGzH@&<5{?$t7 zs&mg!kbw5Ay}pY$M;_$AR3vw3ws8z4phxm_!N4+Y|0 zz~9Rz02O&fJTot}J~55rHRv|lQRghyp+^CAZ~on7uV(S-mEHTtPkb}AO}76rEQwzD7(t=c?AA~%tQdaW5rZn1`Hjx}?2Xz{A)~W>B7&Zib$RPfVjU*4L zzu+X;SkFk$*&cp`A5({_16cy?N!{3Y%)%Mc7xk4DbQE3*%V>babIb6ZQjh4ooTX_^ zYwYm*;FXM9yp1_rJ4ic%T6s@CiS}WOR8bv9YYEj!3m#2+p)LVE#ccsh6$B1!X2 zyQ~A0YDAz}ct-@RBhQhAED%zPj|X@*$>#U@0ywZsln_D!`7z#z|3Df!gU0+gKSM%r zl=Z|0c++aU4uJUt0OxqmkMjF?&r9)AD23PjAg@I`(=I|AGLZU`fmp|wx*8z0upPi{ z^!;V`Y&Zbip5y>T5xxi}%wmb;B)*VbI)>d=_F1NqYVsZHm)fp1zE!^vcRn^CI?Z!K z;pasr@hAKVOCcqZ!JD#~yaQ6`SNe^9Rmv;nl_G+T5Gzbn-PO0sVey@`L9CZ%PL51Y zIalIj@X_n%TAu8CdXZ3rtf2MZYNaa);ZcFtiVdbgbgMXyhKY9t3lCsR_-J%Rd3862iw#5&U(Nwd10hIq;Pa4xOBEkh zN7V=bA|N;mo5jfh`m$C(J?XR%ErdeEjkuAL=uWyL4Z|=D!~W>1^FK>Lv`hhi8%F^A z=Kt-7{s7sAtpIn!70VP$le|p2U2LsOFm}vrpB}ovVbJ?_nic`Y?zzD=$r72;qtGgc zu{Q0~2W1RO_tU%TUA5gm~!{O(|x!ms8 z_rCI?00Qylx8p4fcyrYw&W6h_4lmDGt(wGYubK{OVhLF2vo_$e>p(lCkLPGrR#hlWU(+3=D?Ntu*dZ=crclK)UbIvE)Pwj+zR_?0MZTy|kpltw zkVyuOVTD{;|u;eJ`7qK2fZKduQe&D4No*lHSja9_^Xs zc#Ylm8tN3qF44y3$7Zioeb%P$UUSZ@A3M9sT-}m)`{O3xoH=022*=|NP3)KFb!E%IcDMNdk2Fe8SzEXkxfQ|BmxZ*2W2av-o zAHZ7nm7e80uoj?s$|@d12J_0iFgB3qB$wnO53?|f%;u3ik}m-Oq5_yq0bs50iH8uP z8K>-Ioy2uYc{PYqKA&`!BY8HBG=ESYqB9#H2lDY`2BJ`(e&IdDr+91GE6CcFrguUm zI?=KbMRM$UfLhXg1q-o|EW}@96E^V$n9LVoJGQg)WFQ%f)3)yny=?EXrk2Mh!>!^s zT3xk@b;&C-!p^WeDfO!9hXi935ntTfm@gk~)?ci%jE4ZN*-_3XP-G6lP|5u{GEmOeX!gc`Q z^8gg(UjY7vz5&QyUWy$NJZMo`QQou0IBrMpvAu3q(t8)y*U6ohuu|hK%&}?97n66m zsvJ@pCBEGI1|wS{!IrQ0ZmV(9;myh%b(ikIT0cm-jw=#+9z30B%whSNEhM4cCZ z!AI#kX%FansLmQk`bLOii!?RKb+s7WctbWxT|_RC=`@sh@g}qklDM6sa32z>6VQrO zBo%}W!X!aZ6h4bm7Ri=iDFBcu0AC=?2DmS!!Y#zp_G$&C3h$#Hg{$tS)hev8-Xpe&VwLTesXnBy<{+<2PRXx~wpK}1bBdcY^`DG>+0jY)Mb3M=NfR9$bR%^k&{*4; zJN_wcUho2V0uugRn){GGWD#tj|GA7DCbLAgRIxWtF?raPu#I-u;4#`QyHLsGnJHIN z#utLqI7k0am!Iu^vT=jm(xioBlHbR-|7@u^Jy$DvvsX~-2HplWNT_+kG6rOn8l+1I$`Uu&l`7nuM7+f zsI{luUi&3;J3Q!bGjMT4kKxf%4k>AJAF)I7`sA*eYsU_p@^X$7Hj#JIle|gBsJz~I zMxF~cJQ}rFEX^g}!g%h=r*V;-CWK67m)T`@nKU2`NP|DR8vJWeAnpbH4e-yX$v-P? z)i|{+8^v0&QRE2M@B~c)@x-F}wZ>c-SfS~*zuCVB&?## zNiq@vBx>*K5_NU}`u;mvcR)6`M=D$a&i`@;B8la`e{#W~Gw~8;(nq+e+R%08@5&oqOs|NewWF-9)zf4Z`Jld}_t0AU zs5wkk(SbRuKKQ7wsktIG=BLyN#$YD$<}`(-5C>vQ4VZxOBn|s9gE*215<%iwPvW53 zSr5{9o%XwZdM;b5EHte$4z^5Z?Rewt{QPF7+wyFDwL~fnNjy)nj>KJBLi?7_LLu+X zA~$^13E~Fzj`@LgxXDppi?pTW=W0;^o{%R10Iz?SGkUJaEb^z7mr zM<+#J$)28|$k`we`48@qgq`l@;YD_9qn-1eqH`KsVk|U7Y7|pu*?u% zNG0IK`{=((W9eC2U(2%e4LMs=+uN^ja&(`3`{cc%&(~JdR&QKCt$U~bhdVa2Y?dEd zE^23K(Qi=Z2;ia=@D{RZ2B`*l>9lCoHn2hXo0N%4HSrb0e=Ie0!EUTpl=hKY{7>ejsHedMLjN}mReO;6(IM?eE?qnUeBJ(NM|^ zogGg;kNLXy?w59^Zr_`>FIgh^ly^lnE$2qUF}E`IKQgl1y_8BS1OVaHKi^2te^m;6?gjiU2QLSdbo{8dio262eyx;T+?t)KV--Na1Px1C_?V7L<3FXtdR_CZR$uPgE8VCG(Z z%gvo0*y}g;qkYODK;am`^h5B24R)3N}MX^%lUF|-D2$`?V8_R(i;(cEr}3M zt2@P1@uT>VM3Xl(fG^>j$Uvbs+rh^ngP&Gvs~^Z}c7{(ExtyjR)D^WHFt_onZ3r|q zwclmyo*PO`@>mipj8}a1-vFfWl)s+dpDRrLmUg5PTiO}mcM;1yxhMCOZVP$R8K-<9 zUNN@r7;@XYPu(KL7P+M*jf?sCIx0U{{*g65u2q(O?6N6cCiU!BvSQ;R@9duF>{W-D z=zt$V#k8Yb+WI`REya&&0x{h*Tk+4M(j?tAVVZ2@X`ZeN-IgxCu&;c_+OF*mUMzK| zZo=VB_xEI6FQ#2+V|2_n8?72{VOrjcxa+mH8f{5FkMH8$$!9?9PM+bZQ1wr;z83h? zd_+HkJ?{ViQMmuNQ6TRBN~8lA1Ss(UkL06pl~n-Xg%7;DDg){;bu*woVRitw#~lFR z)6^5F1@PDS^;e$n1o&!JAK(??g_Z2i&y(}B*a9dM;q}{*Y7YQ_f$szW4%Sj>fxbA9 zTgQ6ad|Y0iw>3kU=|6CLw~N7}TnE|2XZ17YSiL?qPTgue^(g4muq3jrda%i zZ@RwL2$D@Mvajew5>QDEV2;9fUQ(S)o~bRA!F)c#Su8I`AL9bMM!Tzx)xq3L8LVdE zxZ0ARrxUm@;5K%W3;-*Q$HjAhv)euM^NcS*C>uf($d7qzED$l+{PQ79MJ8CQ5aqS^Eq&D-OZTpqw>rS)6=KH)6 zeP?~>%Zf~MSOOD{8lTH0_!c2ll~@zrQPPD2j*~4mE!jW)L5rai zMtxaTY0#O$J(nEOY_+-OcwBDAI>=KrXLPePW}eFR=q6^-KrsagXv2OR1$rE7@#ksO z+5iAa`1AI^JsZydX$r*sUkH90;2QzB^PR+icpigbK;9;ovfjo5Wg^$1zEW7Im>Bat z=v((6ML*Vl5uOv3nI500E>N#p+A9l{cIrliu?b`Zswk6r5xKT?DB4^0TKBOXtTBIp zU33=`0Oh!9vNUD>%4ykEdC#B>We-UxZ=|{?A@UybI=_2vwXv%@N49XNXzOuP|HCPP zB*UAYWSht*91-S;4*aV2ye2)Zn&rIw{%v+_w}eYE%TmAOG{`Pv$zTD9(5=yLw_azN z7|%A6iEtO3XdWVH3ATo<6VCBOx+KKIEpGgH& z7QXX_at&p(Q%74#nG(>!W3S-P(q-@5n*5F6toy14DVZ!sh~jq;tmz~%DUVDdlQ2n5 zR!%D$kwF5X|CKrJ$b&oL3E`Y=AR^n$FJmju<8yc;ahrOUIMBMvO1{Y49L2;+QaW03 zH@coSQWxa(@$c&~SBTe^=U?-Fh#d8Wd(6sQ)pEjVsfHjUfqU_a0I$Sa0X*T?Jm6u! zD;N&}vW><7q!(WU@CAGrZXk(gs5{vX_Kr{Dt$7^ZfF#CDRHf!HU3r^xHb=#6O13&c z?3F$zX?@cXp+?sWb({D(*|*te+_rd^&6Dykd}E{4p*GFz9M}1_Q(TYNJW})(D z)!yPPe12c=qo1i++vc51Ma;0%IlggAltzd(Y|aUTg|BN zOQ}+-l!~PohYd7|CJBXqrC{El9BKgI{U5dk{5L2N_kRrz0swjeXnHXx?gP3|JJ4zb0<#Ny?^wmt{KxlwyjV*d&jQXwj=kZ zhyF0WyF*KpeY~;SN!erCD&3-vJc|&%MLk0F>S#t0qkg6iWCU%?E0Xv01Jlz4Wu|KS z?(i_-Uhk1}Tec2ARd$ZoJcH4tslJ`M%RD|mJ*O%yOl;LV7$qzd_OUl)DxIpdknU(| znJ&_n`gf`A%!{CY`5|8tf;O*JwC zoAR_dr?VgNFLVc%@;Szetemh%4Ai`4x7aixUK1r$)5f!7a&Gp)61LvYq`_{IO$F;? z(^l(QGvN=#!n!n)!lHN#)iKV;qXufA#?SL}6HGAu$IK#xbK*r9#&QP)axQ+sSz?Ei z{D;&@4q}6dRVj_j=Kkhj|Feb<{FN^;M42m49fliE^J#F0JGsVhvKs(8s(k>2s6oGN znTP5QaIUJqyPg}s2B}2=Qj`>Bl=YQ+VFLA{omSkdyx{&I4EYYB%U;%IV4s_)U2RLs8J{zo}uMNxbmou5B)sXF1Pc>xr`P#~?bHJx3c-+}xV&LPzTZ z^`1QK!}EmR8P^;u>F;P0oD|!(He2l6bxz`H={aetzF;kx4f{lWgy-ZW-qKZq%wLcp z%q+|h1_=SEq+X*%lq1Mj8VfJ!PVxj3**892C`{gPBTSgBR;90aoHZR)WFONQl1hEl z+Ps&#lPj#g>T7+)V)-0iT#!hd5Un|Y*GfrMG;hibE${Dh!exQ6;g=eD=dy~h;*vXG zjaOum#zI|j3VlcHI(BJSCuB^T4jfbR-0N4X$ zA_$5Ke-5h#S?>Q13dH?ijW(DAi1*lFzDT(!_frbVr${vaCYb4crSq-& zN2c#ST&=fjF^BUc#Izx~w^B-3r)FtFu|V1Cy+Dn%UiN9@B1)-Bt~ggMn#1YFH0PKj zXPRzwyV7*)#zkB6E>VBOoiEFi=Vp!fURj7cYkkUUTrCT$)p2~9ThGZ(c&=U-qhMp~ zt^{ax?36_>*S2u;X{D;Tg&B#7ms3-Ct~^1tB`diLzlf&*-;L$8C#i}M(i_LH8)@nf zQe7-89HP%CO}$`DQtst!x9IZQ3(jOZ+ozu*=93-5ITA@@$QIg+78BEG62Htw@(xNK zDaVf^n#ajcECKpYnqFe>Laptt7!GKy;#^@Y55;#@ny(>xvRA&YZdYt{sVJ#TH^$_J z7+u&Tt;V4&RPG>m5V}asg;jvMh&`90=w!`4GS_M`2RM}$r{?69{p?XgSM+hG#Q2;C z<_=;N$06)IWAa;C6gQECwOC6l@~8YMQSls8NCyBN_~D;f#4C}<0C|Q|v^@_aedU9^ z8x(DKji-7*xG0Ten~Yh((|xZCO>NteJ2?kQBYsaj#m2H;LIz@`H|lWaPmT-O>>Mj2 z9u+SL$ZI^pazuuU=DAIr6rOf4t5S~6XO%-U#UNhQ?l82wQs(xw@6p}P4DUAKyX>#{ zu=-M4u`M3};3I&J2qqJ8h$sN?hb+`xK!IE4Uy3ODhBd?toJJU%qDIi&nsCiRs~>6* zS95gIw-|Tjc>LFoI;FCCW_}N)0~|DIl+rT73OR)dcOUS!V}5iwjTrQ+sEtsWY~YmCehg`(NBV(dzW_Y z8z8s|uImD9Tvs`rD%#OA%<)}NOyOP5N{P1Kd)!78x$jcOF~Z(en9WSuLwuJxKoiEl zSX&4K`3+?}okl{``(iAeinh9eqAdE+ue=J3;!EkY*jT=cFm;~tNH|V!WG`h7d|Bo} z*+s3EvEFpU_$6JJ|Hafdvr>L_^Jr@g)Fb&$4P2@@eR6VeeB$V*>#zN)4NzOC64M|{ znxma(8BZUGedLScda;&+x71m4PR%cGR4WHS1aNJ|^}qr8W2=T;?1agp20 zRp>#fj<^@hf-UxW_HaBH|J&)%?&m_kbCf!pqL_75l>+w7sv?! zSNMJa_QZo%#{n`9Bj|V%&CYOtq*(`;rNU9tL}4BN&s}et?$vqpQpRbqd7&|3Cho zWr+_0056Twq%weNxKFOJ`lcrq$#TnXzowq{%94O_#fA^4HnqrH*8pu7<#?W+wqqgI z>)IM5#(GCsL5NJ%wRogd#4sMO28lCSAF&44DbK})iksDsbkjw#nY@Ofq@`GzbE*3d zC-ZYV$5&}qR9CW5ksAS(js@IW``qtR*{^rwpJrSVFAB@W(c&6Ov`>an@(^bct8uk` zX5OLQ!_ba#4udH|Hz#A@6_KR$D zw1;U)?N=#TILchrO-eUSaiPADEZ-ts#CdXIoz@{J$z;6CpE~3_J#)R1>Yq|Ldt2T< z>sISGWv}Igb%b_=j_A&lEb*@BNEA(mw2=2Jd|3IJ$x3E_$|_%>|h50a+R0D4?HH7er6VVO3omWe}*TYq(-sc;vv!j z$K;oekM!?yG{K@zfIbp+&5KQ=7{?VdMOiLPWz2k52{N8m!{IMBoe(~y+T4&9A+7B@ zM6{mMep2WB%^urU>^T0l(znvzXDFBCp~^eng`DGE0a6*w09?R@pDqq-Nvp55i11QO zloBT}UO z#wX*9^+EObUgsCS&W*bi9g+8i>>$J@TpM*j+LOeXZoDY=l@4djSYss6wERpU(#oc(>lU2@X<+-3+?J#1E9-~+v)XyQvNcs7h&XktJX(2!8q5acttYJ-zRZ%F zRh&-KRM?mEDCYanmv=22ovS=Km2)=#^Skmt62H}_hp45mgJ@ANB2{X^ zKZuQO7owH!wo`X9R=lSTkXmXJHSKt;az59iC~+Pok1ihIxj|-vIloxWMa?AD7WPtA z=?nK^i^wMRAfG}wb4M5+0qO~ERKwLrWD8H?&($sR5_cxv&AVOcgYPiyOVZucIO&VI zF`Fqr7mkZg(it&8Y^E*${KK0*pNlrnZ)WcJ{+0O3{oSZyw@VBUia53M)c%tpQ|3+< zCuS6HQesE(i!>MWRXrxF&*fT(L`fwTBC}&2IL?pJlhP1ARv3d-D9mK6(KZ#n*};K2 z&PX<%>{-$TlQ_FpDpe10B#W;@Ec#Owr z22eB00?3J9YokZP9rylCx6)7y#n4}`Z{jBJL$(o_H&yO(J^RSs(^e#%RVV#5hTKar z9eyOoRlPprSX^MJO+B@fU{CI{(=?GL(nPGm8ooxEE>D-Ilg6|$Y0N9JN~{vA$SSgm z>O)>!eMsDh8_u$fq|k%prts?HCi2IJPpxav}lCy+E`N<3uc0Rl&@7n)%nUL%R!+ra^=I?32;y^6XA?lGa+JaB+owxq#Kad%QL-%ZeO5qnT!!l)#JO z9Xcqn@}vA+c^7FrTqTRhdaM?03Sku1q3kk(^sn9wE!1-YDEDVs?Z)=v;dD%=}zk37d+G zs*N`e5hv2o`g)q-m~e2`@usg@SIe#WJff46r~6LdM7fq~w$4|2Dm|5+^aMRYPw+~- z60h{PSEK^|rzn{6_rJ?{jK_Gjl3GcwK&K0(#7+R(5W<++lQm^C)G(_bUrGXLOA>{{ zWHq@=K9WZ`OD@v~Y%x2^bA{=28m90SY&YK_)DcUGo+Ozh|JI8CDfg;bH7m8$k?Kfg zvXm$#YQx!0ep-Dd%(f0z%1t@dVPPHLzJ<#q`Y5zUzW4Y2m?+lNXoP3H0i8-_V>V{< zj_8PvJcNhv5L%zqC-t$OtS9S9EV7wBYrrcLub)?>#d!!q`76A_D>0gWBRTv&S*?w? z&Spf>^KvL_{h;5;6H#1rtO!(hYr>hTgD!m2RKypI$BNN&Y+Iy-X$N zk)>&P5fN8W+o5ZaNcBov_t%1C zz^~laPmc>lck4cO$hMJdlG-Lb%S`rk^eSGs*Wp5EPo31XuhC&n^Re=Xyu6%+Vk4VQ zHWB9O=97?~%dcC#EOunV!M(@QX*EqL-8RP~o;_V^EjBN3n&}cS#4E(rs8IQpOf1de zp?{sS*|bHo>3P3clSO9^po(Sx*ks~1o&xJq$z z>HJ0M<;yiG7g+YS_i6VxZnD0x&PS7Iy_P>A_qK)!wS~jniC5)GYE7PN)k-h4hqaef zd%3h)mN%e{c>}SODI9JC7#EUP#y-qS!+xf(p;KL z<9Kf#r95M6)jZ2&>PkA`4ckT-J;opKELIE&EP_8Ky=gDfl@LnFLZK|3CuXVJ)bgw( z&tTCwEA=B2gcoWWkFoS53+QKIRBT5qFf@I9Nv-PKt)<$wNb9gU)0Qk-Jx*_%C;5Dmg+t(ZtRm$C8-u`MTZ#3`uZlNfeSq44zdphB9*#o@}Y(9SNM}ye%$z@4xiD-M8 zvXs(FT3#i~d~0*bRt(jASBuLFtV0|-+jq1zW;{q&QhaSM>gsF0G@jESvTmg@El2nb zou{oM>RAWb!JK5u)m+z=CO6no)scDeY@Ur=^}KpsIZKYx?cy9_6m*)<(OzT71gUrP zzDYfL7i#QOztC*fl2sR8&9yGbowtAWj@7kR>=vribIRkyWuG4i)($tb#LI|)^U6#(f?y8Y3$uzUs%v%?&Jg|7% z>v2!tJeZ`dsx2)ADkI1(>kCa6%{pDOT2nO` zHy=H^Zs~%?H79$mlzcP?a?buZ%2Q2K@=ogFXl3nu@=_hZ6aQZmLNk9p{}gmS6SMg& z#KRll?a2h9Wd`Cy+z^H~m@M4jJ6T0K68m{?HHtQbzv4hMNjR@Uc9U;vAR}Z6_fl2r zsq0UQ3;Q%BQOqi$yZkl#1Sw|yoZF3zlC!b}e#|)y#RUYc2L?TWE383Rygk z4dq;2&lU=+O+mDr&L?L-yDlC~sbHMQR>xM%s%6y270XFAFH88Gf85$FmKmL_W8$ah zmsEUXujjYrtslF^luDT&P`>!dim?aE9=4wLuU52HaNUe{H=5-)DUui!Uo0jy^H9FM zsZ_?`{JF*^h9IXKPLJZ+r)6Xo-&`T8#DQ}=*B@STJWVc1<25xiqqAQa>t-JPJMSF0HpKC^MH zSGzxieU26a^`kWNtz#^2l^^6T%@Z3l$f{aVl%>|1bD}};7OI-=n}SWjju8&S9fo^V zDQxxicI@RCZeP~FZ{gh@kH#CuNTc7L{(NS_`Nq-~+m+76qodvr`C#v0D@nY&sjFqS z@tPg6*V^|L9&0O0k%9-&aR9ify8O{qv4H=Ie{OOAxfGW7?;ihwKkT2!X)SIcy1Z{V7_l^+45$?%(|moa}864Qkw7TshmC2Z%$}3BH>x% zhYufAvxZvBSj(sbcvtq77vhD~CTN2;?6;LQFX0k)2+2aS*iD(IWUK3b1ic&m+&3rY ziOuDCK9l(J>@-f*#->VSf!0k}Y-uJ}v%Dma$v`sj|1}9D9{{k9zXPxi0LWVIMG~;? z=OBIsXhSEnR*0un$W%5@ye+<0F9<`#e(F5pEnH$>R1Z8P>tvZl^24eNT}Zx3)y+Dy zg|%gmc_a%`ms3}ILGB{H)80}S<2aq6JZ6jzpZn&Ej*StJclpRWw!N8{_}wXux( zR6orv`|8Jcafec_f4Z2Moo@5JZAO01`)E0LueoRR1LI1w9No{j(47CZbp9u!G1eh( zk>$eo0XeRgDqo^gN+r8WZ=`tXN}ibVJ}If}^fGrtOf1CcnznIVhmjsbpS^4GY5(U5 zYh8h?;W;_ zZQsB9(2fqN_p;>lQXyAMlrDQbH9KQ&{>fXPA5?qzt#(Mgl??`O4&COk)8wAzwa~Yk zWuK+GyoIcT9}N(FsFPGkZEq=}yq4Z-hU&sddF2y7!-{au?d6N+ndU-l5o^bWJ4M*J z*#+1oIJ~qSnft+N?H!Z2m1{sO3G4L>+C@yPq#^J5a3T9XErg!5CwWSX zsRk)U(evKSQ&=Ia6#MaGe3aPu52$wq{6F*0B<^qV7H{DLANW{ZtS(j;bC7YJ<+asQ zo^5HS7-1)0H(jzW%`b%3sLw_yvL!_RE-$hyurB=5>~VjNMFH^dW|sR$-Y2jb3NNMl zV!Ap-9ZACREjK!QbC%2P9+!Hb@(#`R4|ZEeXUZ&pIj>}?VofkCHViWy5bJ46>thr> zE5$mBVYDjA zB@tLo#vqM2kX#bM=U@mMM_XV$ey)MER-H?y@mRKv?!;g|jI85T@d0Zopud=uVtmD#z1y2yFURxa zj+A|Q>td%EXR4EON|6X@opp*-NxNGaEJkXz>^U7Ib|8c4OX-VX2dyO0>pVhq(F|0I zX=<`gB$BQ~matcNVN-+j*L9^X%Iho#pDrP@19~c7@}`@QB|c56lTy?$&32P+ac*F? zlIjq*G^td2?meaR_hyIG&c-t4IYOMpNg1o@%skV&yPdVYN3w+$ z=IuE~w?zB?wn@XwjM&*vt?N)Dzmhg+aFOc)kDdJr?XYXH({)eEzWtV4Y&BY2iYP^u zWn>MCpt|NM?ME6LS-zT8GN5yjJin~_@9)mPSCL#1YSJq?ck_#F1B zefLM-+x&O`bUC=z?*J6>*~GqRs;#Q-7X>{+pkxpAjhkU~w;jKM&8S z!Yiw*NkyuNCte4V#96`rfArJV5zyQONBk-7)kjamcdVyfUpCWsKf$>Y@_ z$`7j2Tsm4E&pG<=Tmp3Y87@QX0kj?tlFm55)0H*c@#^&BLk=d@iY?aEBTD|5wJ>S1 zuCOLbs>~O%RF+L=@COjoS0FSNz-h8r5Ev(g@C`)(JV8Zz6diGh{I;4;4(ySHwTJ*A zTQMG>gSg|rOQVR@WRV(4rgLXLkt{|w0PvvS9r4>Fv>ou4CelDQ4M3X@Zdt zyK8};i@gakyfQy2`M6O9yYfRTG?YE8@6<-t2xF#oQZZhvWWWpEMd2MU>D$0-r^n~7 zyT4zKS(h|6(KgXBAu#E6%G_&xZ)~~MG2hm_Kt5Z1Xt}Fp-EXZs>vSsJl4>k!D(bP% zvzU7azJnGMOOp)qIQ1)!phHxd3@1zJB7CJ~L`m}mn~95MefGf2Z_toO(;-D_PtZAZr$P9q9^>!|ON+kJURi(*khS@JM>5+71_d|8)rZkZ&zRc^6R z9(+Hzh62kL>k6mQw#@{OUW+=PYVE|5tbxXrf+&0?eOR*W!Hsklb%6^>5eH~`aH?KW z?LGQH`R9N^i!lUSbBXZ>-C`j8NEnYrBV4TN{t6a6y-1<`Pf!03p1a}!?gOZbssL$Dnv>?_3+aST_aEPWdDFhZ z#R^hLDyeAsmUoytlN+=Fo*@SxrQy0`x?3z)eW2uEEio}q%;1yxXiUTu8UO&Q2>^tA zauW%-!8vN6kJwQts&;xe;o6`}9!=vsPAc~_(SolOuijHOGDfdcA7wtx1IPk?g;$YA z^T*@?3B_=fMitB>(@>fpL?_Y_90b5q06+!g0$7Ae0H$y!fYaYijv_&V#N)99;6=z_ zlEX6L@Y`}Y?X?~dtvc&<>Loy<^ z*aaBk9lq46T5oLwzc1y!8>2Uj=rVfyh-aRqrK{FzGF2qDNgcvsEqdKJI!CZ4OUM#@ z0$`6E9Od^=TYaavDQCrCVT5pm2tpTfwcNLIS!G<6ooaLWCLXcj*7#oQd+hEu_r~#C zrEjPD6))O1s9NZtlIAiu^7rH^`2&6okAC#cZ%V}p?Z)KkM@t`gFb3tjWlpZuFzj#{ zANE3)P5T%XcZ&hkUa+MPxy&Z3lX;$!ujEsv`=Mu+29}=brF^RvZs9-t^ycH*Q=Vn2 zR#;V~+$YV4i65IkIB|FU!zO*c_WaWKTU4C>nle`D!u1;T>kWr|J(-ZBaVJOG}H9jKW(At5Zf zgDEv}_P1}AQ<}T$z!)LSz%q#=GcX4-j_KG$N}1u;b_`XSmM^0dnSm25WClus zKW#4p%s{39%s>dA%nYtbAA?j=Qd=f(A~G5zP-&O_US!p91^&c^7D6TlkO2T) z#7_f^aSm`#?kUv~Ld5%YJ!wEo;2{6PN{cn5TI9CPL+KUytX?-)&2=q0#_zgUv+dWm zI&Abz&PyUm3u6!bNdM+3c(MS?$t_dX)>!fFYsg!Vd+k5gdEMc`MW#^`%(7D3tT8#0 zizq_e_yy$$3xf*+QHo3%?R}p4$;3 z*-7K2xpX?6j;R=nv1~N(@0~XPO}64K0C!9Tm@k{a_M6|D8yf8w)R}a4RLwSb>%Od1 zovz1f^^@G2E#=F^Pr?b|M9Gh3vdT>5D|BmY9QP%h>~kTucj19sMhEedQ`|j(h@89L+ zjyFl4>e{x`PUI6pPla^$UmKECrbg8-thmn3$s%^KyYIBrJj3#c?|#tzL6L{K#a@QK z3)=3r%)PeDD0aadmN}3w&1A_dwH{J2*vTi@4eS>=>n0y0Ze*k~3^|yh>Bs{4aCs#y zgJ97MM+u?D0CJrBFoEtOkF4`~JvuhEM}|#q|LeK;Ry^KTFQQ>g%k#P&ws#D!Op%kU zC*ljoo`?xNGwHh#1}MP_cZOLx5KamebC7n9yJ z^5m#lJ-_srRCZtRDVI4sFmIBvvw4~9{%L%EN0)I@dNu!6-|7fGV*8&|r}Jy9k7fXE zLzmGflnUkkXvrwxzv7?6E%TF~K6i;rT$0LY#%tQyjIk|Y(_85x_hZMj4YZ4NPNXu* zV3XX|*xzUoE{M}u!#bO)@2xU7H6>|!Lbm4wAH82CJ{4JFf1b&E@E*uTF3ANTLIj}T zAJKmA|I6oohHvEG_&45AZYWoi6Rm^H^-Zm$Hkzg4efC7&gBHSbp$u;%O0TLFwMLRClQ~yNddGq?Gc>)0UA&y#DO;v*r*#(BNMVwr z(3Zq&MQNojPP1By6iZ5NH5JiZy~u8|OyMXziHXfqGZ;_}PvK0alS706WDpPM$5?e7 zU|rd6wFJLNCedrei(JPPwFc9mFnNVhqyc$K2q7e$uERQnu%VnQC5boD;e+O+jb3$w zJ-Dfmb%oj$t?-Gw$~&3U-0WxaR#GiF=BMg*S;^XNDsB!HKI+^Y6k{YWK^Bn@sHksh z{vm82hPXZPo#RKDdRo0rZ?vVg6~rF83!0_UB=L?=hsusOMS+g9@zEw}7GO6qih+ju zIwws(DN25sQ_T0O%>y?5(#_+0kI!oyQU7C;MF*Q7xp6epkmFd-xw!LLyBxdZlI4U^ zHUiIN8J5;L;c4Syd+a^3`ONCVw(f?T4jKA5DO>k0cK6rbPpa3xQME~Ohhw^}!a*UFHhGV=rT1u{jfBKVR8m`~P2rlWX1#&Sjc3wF{ z64mGIxs6G8Q1g2AiB*OrebvHZU(IE0rf!ZQV&2<`3VlkpdsXg!sEekNRmn>>HYBoa zGTqk6g$~}?^46N}zd%@XtJ@B%JDCilW*weLedu}A;NB>TY5>0C>mMx}1^j3HGl_dF zViAkkn2p(NG_TK&8DYL=E+uTG3xuiCAu>$(Y#nWFWeU=k)LfIoV_d#<{Sm2Or?vRmxK7byvB2Q5jO z=N)0y)kc|td9t;uFoO9S-_RIb;ZMb>G?_jnar`Z!v{uPobd@%X1I3p*U!fm$(N+=r ziG%5BvXLg!7#c%kXd+!sFOvjv9|}!JSu%w5K?OvRs<_MzY!3o)oju|E*f;ipy;d)> zcf1BGq+BOw_=h}0ej7`$b(pe}_{gV}Ps|0isI8Q5Dr^q3e335ec1y8pWo4Ms6`kRR z5NVv$&H1`zEu+$9a$M}IsrM~1rd^!BX`3}&cJw{x^U`~+^LP7xww=Y-{DV?o{ba14 zS5&+YS=lc?6P?uYd;>p0jOsAHTQkB`-;yua(R_tg?i||ReVyyko2%aizm6*rT6|;K z$(>tuo!@!O$*C7Y&IhKA&McW(#CXu$Sm`P`30gGN1kq8}=Oc0k8GG!tI$A5KZlb6e zr@4U)HcWo1o-%E-Fgi@#WGauYm_jNmg5o8=qU(f6alSHM8Lcc5<_NB|n%G`iqUpf( zw4oY=Y06aI6Z2>yy$j$F2)?T|StIRHok`av_jLA+tmWElO*8Ru?bcOn%Z3myGNl~X zbd^2`2Gd$o`}`C#o%>qHsb2DH5&G%k39&a%H9HxnO2xz(WEUo4C@;ask(qP>S;*HS z2yxg+9s%5ye@9<5CegHk=94s1tz?!>0X8i)8pAr}KE7+(9&EMVcCEJ5M4Kn1gj4DK zG1(7o#%kB=uCXZTtUmeqii}8QO{W&)q9!fQ%O!28H!aEfuzo@SEi4pQ=Abp(Ap&28 ziGQ?Y6!2g1&m`_0&;cFj1Ns2>41?_s*^SIwVft#iAJb*%mz z#FWN2k~882I!+i*Y7qz0gKnb1;&J$p8Cb?#SR19K*qho(^`yO;lj2tzChQZZDb1By z=3v{>HcobH)enk>G>5aeUiM+fgz-W{-jeMgmBr#bi)?{OoKAxc2DcY37B=$pt?#X9 z9oAm^O&Z99@~Y^rO1<@3b1$WYWxutv%E&|XrA;*XSR(9K-Pr&#S5q8LNXH6GRbG`h zBBQBDn#r5hsbmGaC*NanWHyVn1d?5(tvpD0PvV6}2u5)P^O|hBe2GTUhNL~4$>-A5 z^dk-AZDEID>I>GFC6Y*bnO4#EVnw)zszot?^<^<6kW?iL`5-!#_rxPIj8!L8FcK%& zlUiu3>!n$1Y{avPK+HCR)8@R^*12euHYdBj@mkKltX`?7b@jxyLb#XNy}Hv^!$e&( z;R7wq7Aq^&oV-^l9)c*{kfupHB|om^!`U?UiMg@k!Zz}ezrtFbRk+4n*gBz{Fb?NP zQO)s;Ey-c2t*cZGPV_H-?bcDvo>GIVPpvz5Zma3davC(<-En*SecAk!bctImUgq7# z68bj!USgP9)$}DVLtF)C+6*gw`c;>dk+R~ceyXzhR|{DHVgTSur**3iwuYBEi=wJukWaiA$XE~q+JZA0~l zrKPop&_h^2_L5UrPSSZJ8qK}QMXMq*9w4T(AJ)h;)Wnq9nWx8c_Tj2 z*AznJLTrp$2G1?kX$5hx@D8qjv|JSM*ZAk?Q2mV0_>2d5fCuVMb*H-1`;hk`?;%-@ zv+Ruf*)wjTy@U!v1?^(XOY2MZK4##HI8N!RTt@4xZE5+bH=Bht@n}$Ff!DmZbH<_* zi)KMEaX%Ub{954aJ>LI=@A*9$j3vAhNr1f}-+rE5jCfU=E6r6;@IDxbhBQ#Tryk`w zEQWfDkHkylv(S#*=RI%&M@TdB7?0=?dJ2b$FBwWDQiXIR4asgA#dp$Fno3h4K)}6T zhX)QE_C5M|)&Yy3Wh#%Q{b)Wl(tK4=_bHVqC@2x8iPwc{)RAV9<#Z8WNEcI;Tp(R} z4swt~hvEmhNWbz=_>2jB0-r#tkSe4K{UjQ-X}l$|7e-h+@Ymd3Jw*yU8E?NoaO;vn0@xkK3*gYEO$-%!S1$bRGyKo>LuAkBrI2>q+8-8VWpa;rYTL8{b~%;suK}Khmxr{Nfy(-w4iQu1*HG12gv8?d?hz(UN)>UKoDX0Jc}% z!=mMhj40m1BU({>MT-z3l%+rLiL?>Qkh63diNY21#4)sGyZLIK%2U-~7Q^#^d z$aO*DspLH=t7)RxShIqcS7o^~Z24Ednv5cp+!wD>h0aV0jIZ=VD4tbeo|o!Y+If^= zX5NaN6`402@3@q5>;JQ>m~EC1_Z$i!6dkc2?NvYZ87rC-k>4k8gy1Oj z5mMEfiiiB2r;;=_l%K~vCSn1mup^`yt-`lr9l3)Qs-iAXGf6ATgyys^xy2>&2_Ntt z4)hb5j+&ffjq_R0ufC~zZ=ajN*GLR|pYUD{wz#IBBr#m%K4cL+O2*SY+#7M~QsR#X zWFlEetI&^J0Pqp-$opS^S0VubL-|lXl=u)I;=>o=pnG8H6hwJQi)(dbW@~=4Rej=pvTal;6;C zb*0sn!Y_n-hkFZ!globz_KY{dYEm34X-w9K)SKy-Sqe5`O#1a4WR%{3vXKnbt>Jf*m(OU zHh1OWshjeUxL+uqcaFOVI^81o8Nxwvqv6(r^2v>inVlC-F0ybO?O=Ptsl2+I@1#b- ziM+vfEFo=JXSCr~v;hFp0k1&-l=`Ekqk#XMf2L>|006Jc>+-rN4KtgzTffO>RZ6L| zzU4jhl(UvL*%zcD(rh^JyRuO&Ee+GSY7M4zxxH~)$+&X0Dr6|FrLlJJj;uW0s�C3rc0{ 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%0AW06O*41NWC7ZZiP! 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 literal 0 HcmV?d00001 From 429f058f7a7662e7824ab0266bb8a86349db506d Mon Sep 17 00:00:00 2001 From: csestelo Date: Tue, 2 Oct 2018 00:54:18 -0300 Subject: [PATCH 56/58] implement ne operator --- jsonbender/core.py | 8 ++++++++ tests/test_core.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/jsonbender/core.py b/jsonbender/core.py index 95daf3c..50030f0 100644 --- a/jsonbender/core.py +++ b/jsonbender/core.py @@ -30,6 +30,9 @@ def execute(self, source): def __eq__(self, other): return Eq(self, other) + def __ne__(self, other): + return Ne(self, other) + def __and__(self, other): return And(self, other) @@ -172,6 +175,11 @@ def op(self, v1, v2): return v1 == v2 +class Ne(BinaryOperator): + def op(self, v1, v2): + return v1 != v2 + + class And(BinaryOperator): def op(self, v1, v2): return v1 and v2 diff --git a/tests/test_core.py b/tests/test_core.py index 9838be5..a421504 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -117,6 +117,10 @@ def test_eq(self): self.assert_bender(K(42) == K(42), None, True) self.assert_bender(K(42) == K(27), None, False) + def test_ne(self): + self.assert_bender(K(42) != K(42), None, False) + self.assert_bender(K(42) != K(27), None, True) + def test_and(self): self.assert_bender(K(True) & K(True), None, True) self.assert_bender(K(True) & K(False), None, False) From f127d021117628876fdfae92a73477bc8d21342e Mon Sep 17 00:00:00 2001 From: Elias Tandel Barrionovo Date: Sun, 7 Oct 2018 12:42:24 -0300 Subject: [PATCH 57/58] Bump version to 0.9.3 --- jsonbender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonbender/__init__.py b/jsonbender/__init__.py index 44454b3..1589dfb 100644 --- a/jsonbender/__init__.py +++ b/jsonbender/__init__.py @@ -5,5 +5,5 @@ from jsonbender.control_flow import Alternation, If, Switch -__version__ = '0.9.2' +__version__ = '0.9.3' From 66203c1f0be5489bb3023ffaef1c227740c60496 Mon Sep 17 00:00:00 2001 From: s-ferri-fortop <139751443+s-ferri-fortop@users.noreply.github.com> Date: Tue, 5 Mar 2024 12:39:09 +0100 Subject: [PATCH 58/58] Forall and ForallBend accept None as the source and return None --- jsonbender/list_ops.py | 2 ++ tests/test_list_ops.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/jsonbender/list_ops.py b/jsonbender/list_ops.py index 542ad3f..6647582 100644 --- a/jsonbender/list_ops.py +++ b/jsonbender/list_ops.py @@ -52,6 +52,8 @@ class Forall(ListOp): """ def op(self, func, vals): + if vals is None: + return None return list(map(func, vals)) @classmethod diff --git a/tests/test_list_ops.py b/tests/test_list_ops.py index 587c5a3..2c667c4 100644 --- a/tests/test_list_ops.py +++ b/tests/test_list_ops.py @@ -19,6 +19,9 @@ class TestForall(ListOpTestCase): def test_empty_list(self): self.assert_list_op([], lambda i: i*2, []) + def test_none_list(self): + self.assert_list_op(None, lambda i: i*2, None) + def test_nonempty_list(self): self.assert_list_op(range(1, 5), lambda i: i*2, [2, 4, 6, 8]) @@ -32,6 +35,11 @@ def test_bend(self): [{'a': 23}, {'a': 27}], [{'b': 23}, {'b': 27}]) + def test_bend_none(self): + self.assert_bender(self.cls.bend({'b': S('a')}), + None, + None) + def test_bend_with_context(self): mapping = {'b': Context() >> S('c')} context = {'c': 42}