diff --git a/README.md b/README.md index ce3bb9c..fd92086 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,10 @@ R.add(date(1,2,3), date(1,2,3)) # float('nan) - [ ] applyTo - [ ] ascend - [ ] assoc -- [ ] assocPath +- [x] assocPath + +Currently, we only support list and dict type. + - [x] 0.2.0 binary - [ ] bind - [ ] both diff --git a/ramda/__init__.py b/ramda/__init__.py index d462584..52c164c 100644 --- a/ramda/__init__.py +++ b/ramda/__init__.py @@ -8,6 +8,7 @@ from .ap import ap from .append import append from .apply import apply +from .assocPath import assocPath from .binary import binary from .chain import chain from .clone import clone diff --git a/ramda/assocPath.py b/ramda/assocPath.py new file mode 100644 index 0000000..946e359 --- /dev/null +++ b/ramda/assocPath.py @@ -0,0 +1,22 @@ +from .isNil import isNil +from .private._assoc import _assoc +from .private._curry3 import _curry3 +from .private._has import _has +from .private._isInteger import _isInteger + + +def inner_assocPath(path, val, obj): + if len(path) == 0: + return val + idx = path[0] + if len(path) > 1: + if not isNil(obj) and _has(obj, idx) and isinstance(obj[idx], (dict, list)): + nextObj = obj[idx] + elif _isInteger(path[1]): + nextObj = [] + else: + nextObj = {} + val = inner_assocPath(path[1:], val, nextObj) + return _assoc(idx, val, obj) + +assocPath = _curry3(inner_assocPath) diff --git a/ramda/filter.py b/ramda/filter.py index 2145b49..345293a 100644 --- a/ramda/filter.py +++ b/ramda/filter.py @@ -1,6 +1,5 @@ -import copy - from .keys import keys +from .private._clone import _clone from .private._curry2 import _curry2 from .private._dispatchable import _dispatchable from .private._filter import _filter @@ -28,7 +27,7 @@ def inner_reduce(acc, key): # so we delete attr from original object if not match delattr(acc, key) return acc - return _reduce(inner_reduce, {} if isinstance(filterable, dict) or _has(filterable, 'get') else copy.deepcopy(filterable), keys(filterable)) + return _reduce(inner_reduce, {} if isinstance(filterable, dict) or _has(filterable, 'get') else _clone(filterable), keys(filterable)) # pylint: disable=redefined-builtin diff --git a/ramda/map.py b/ramda/map.py index 4a14cd0..4ed4215 100644 --- a/ramda/map.py +++ b/ramda/map.py @@ -1,7 +1,6 @@ -import copy - from .curryN import curryN from .keys import keys +from .private._clone import _clone from .private._curry2 import _curry2 from .private._dispatchable import _dispatchable from .private._has import _has @@ -32,7 +31,7 @@ def inner_reduce(acc, key): return curryN(funcArgsLength(functor), lambda *arguments: fn(functor(*arguments))) if _isArrayLike(functor): return _map(fn, functor) - return _reduce(inner_reduce, {} if isinstance(functor, dict) or _has(functor, 'get') else copy.deepcopy(functor), keys(functor)) + return _reduce(inner_reduce, {} if isinstance(functor, dict) or _has(functor, 'get') else _clone(functor), keys(functor)) # pylint: disable=redefined-builtin diff --git a/ramda/private/_assoc.py b/ramda/private/_assoc.py new file mode 100644 index 0000000..8b8ac68 --- /dev/null +++ b/ramda/private/_assoc.py @@ -0,0 +1,15 @@ +from ._isArray import _isArray +from ._isInteger import _isInteger + + +def _assoc(prop, val, obj): + if _isInteger(prop) and _isArray(obj): + arr = obj[:] + while len(arr) <= prop: + arr.append(None) + arr[prop] = val + return arr + # We have 2 cases, dict or object + if isinstance(obj, dict): + return {**obj, prop: val} + raise Exception('We only support dict or array for assoc') diff --git a/ramda/private/_has.py b/ramda/private/_has.py index ee49cde..94a0296 100644 --- a/ramda/private/_has.py +++ b/ramda/private/_has.py @@ -8,8 +8,14 @@ def _has(obj, key): if key is None: return isinstance(obj, dict) and key in obj if isinstance(obj, dict): - return key in obj or hasattr(obj, key) + try: + return key in obj or hasattr(obj, key) + except(TypeError): + return False if _isArrayLike(obj): if isinstance(key, int): return key < len(obj) - return hasattr(obj, key) + try: + return hasattr(obj, key) + except(TypeError): + return False diff --git a/test/test_assocPath.py b/test/test_assocPath.py new file mode 100644 index 0000000..693133f --- /dev/null +++ b/test/test_assocPath.py @@ -0,0 +1,55 @@ + +import unittest + +import ramda as R + +""" +https://github.com/ramda/ramda/blob/master/test/assocPath.js +""" + + +class TestAssocPath(unittest.TestCase): + def test_makes_a_shallow_clone_of_an_object_overriding_only_what_is_necessary_for_the_path(self): + obj1 = {'a': {'b': 1, 'c': 2, 'd': {'e': 3}}, 'f': {'g': {'h': 4, 'i': [5, 6, 7], 'j': {'k': 6, 'l': 7}}}, 'm': 8} + obj2 = R.assocPath(['f', 'g', 'i', 1], 42, obj1) + self.assertEqual([5, 42, 7], obj2['f']['g']['i']) + self.assertEqual(obj1['a'], obj2['a']) + self.assertEqual(obj1['m'], obj2['m']) + self.assertEqual(obj1['f']['g']['h'], obj2['f']['g']['h']) + self.assertEqual(obj1['f']['g']['j'], obj2['f']['g']['j']) + + def test_is_the_equivalent_of_clone_and_setPath_if_the_property_is_not_on_the_original(self): + obj1 = {'a': 1, 'b': {'c': 2, 'd': 3}, 'e': 4, 'f': 5} + obj2 = R.assocPath(['x', 0, 'y'], 42, obj1) + self.assertEqual({'a': 1, 'b': {'c': 2, 'd': 3}, 'e': 4, 'f': 5, 'x': [{'y': 42}]}, obj2) + self.assertEqual(obj1['a'], obj2['a']) + self.assertEqual(obj1['b'], obj2['b']) + self.assertEqual(obj1['e'], obj2['e']) + self.assertEqual(obj1['f'], obj2['f']) + + def test_overwrites_primitive_values_with_keys_in_the_path(self): + obj1 = {'a': 'str'} + obj2 = R.assocPath(['a', 'b'], 42, obj1) + self.assertEqual({'a': {'b': 42}}, obj2) + + def test_empty_path_replaces_the_whole_object(self): + self.assertEqual(3, R.assocPath([], 3, {'a': 1, 'b': 2})) + + def test_replaces_None_with_a_new_object(self): + self.assertEqual({'foo': {'bar': {'baz': 42}}}, R.assocPath(['foo', 'bar', 'baz'], 42, {'foo': None})) + + def test_assoc_with_numeric_index(self): + self.assertEqual(['a'], R.assocPath([0], 'a', [])) + self.assertEqual([['a']], R.assocPath([0, 0], 'a', [])) + self.assertEqual([[None, 'a']], R.assocPath([0, 1], 'a', [])) + + self.assertEqual({0: 'a'}, R.assocPath([0], 'a', {})) + self.assertEqual({0: ['a']}, R.assocPath([0, 0], 'a', {})) + + def test_throws_exception_if_3rd_argument_is_not_array_nor_object(self): + with self.assertRaises(Exception, msg='We only support dict or array for assoc'): + R.assocPath([1, 2, 3], 42, 'str') + + +if __name__ == '__main__': + unittest.main()