diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2b2e000 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include LICENSE +include transmogrifydict.png +include README.md +include requirements.txt +global-exclude *.pyc diff --git a/README.md b/README.md index 858569d..8ec8b9b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,147 @@ # transmogrifydict -The "turn a dict from one API into a dict for another" python module. +The "map a dict from one API into a dict for another" python module. ![That dict is so cool...](/transmogrifydict.png) -Py2: [![Python 2 Build Status](https://semaphoreci.com/api/v1/emergence/transmogrifydict-py2/branches/master/shields_badge.svg)](https://semaphoreci.com/emergence/transmogrifydict-py2) -Py3: [![Python 3 Build Status](https://semaphoreci.com/api/v1/emergence/transmogrifydict-py3/branches/master/shields_badge.svg)](https://semaphoreci.com/emergence/transmogrifydict-py3) - -Py2: [![Python 2 Coverage](https://docs.emergence.com/transmogrifydict/py2-coverage/coverage.svg)](https://docs.emergence.com/transmogrifydict/py2-coverage/) -Py3: [![Python 3 Coverage](https://docs.emergence.com/transmogrifydict/py3-coverage/coverage.svg)](https://docs.emergence.com/transmogrifydict/py3-coverage/) +| Python | Branch | Build Status | Coverage Status | +| ------ | ------ | ------------ | --------------- | +| 2.7 | master | [![Python 2 Build Status](https://semaphoreci.com/api/v1/emergence/transmogrifydict-py2/branches/master/shields_badge.svg)](https://semaphoreci.com/emergence/transmogrifydict-py2/branches/master) | [![Python 2 Coverage](https://docs.emergence.com/transmogrifydict/htmlcov_py2_master/coverage.svg)](https://docs.emergence.com/transmogrifydict/htmlcov_py2_master/) | +| 2.7 | develop | [![Python 2 Build Status](https://semaphoreci.com/api/v1/emergence/transmogrifydict-py2/branches/develop/shields_badge.svg)](https://semaphoreci.com/emergence/transmogrifydict-py2/branches/develop) | [![Python 2 Coverage](https://docs.emergence.com/transmogrifydict/htmlcov_py2_develop/coverage.svg)](https://docs.emergence.com/transmogrifydict/htmlcov_py2_develop/) | +| 3.5 | master | [![Python 3 Build Status](https://semaphoreci.com/api/v1/emergence/transmogrifydict-py3/branches/master/shields_badge.svg)](https://semaphoreci.com/emergence/transmogrifydict-py3/branches/master) | [![Python 3 Coverage](https://docs.emergence.com/transmogrifydict/htmlcov_py3_master/coverage.svg)](https://docs.emergence.com/transmogrifydict/htmlcov_py3_master/) | +| 3.5 | develop | [![Python 3 Build Status](https://semaphoreci.com/api/v1/emergence/transmogrifydict-py3/branches/develop/shields_badge.svg)](https://semaphoreci.com/emergence/transmogrifydict-py3/branches/develop) | [![Python 3 Coverage](https://docs.emergence.com/transmogrifydict/htmlcov_py3_develop/coverage.svg)](https://docs.emergence.com/transmogrifydict/htmlcov_py3_develop/) | ## methods -* `resolve_path_to_value(source, path)` - fetch a value out of `source` using `path` as the pointer to the desired value. see docstring for path string formats. -* `resolve_mapping_to_dict(mapping, source)` - move values from `source` into a returned dict, using `mapping` for paths and returned keys. see `resolve_path_to_value`'s docstring for path string formats. +* `resolve_mapping_to_dict(mapping, source)` - move values from `source` into a returned dict, using `mapping` for paths and returned keys. + + ```python + from transmogrifydict import resolve_mapping_to_dict + + mapping = { + 'a': 'd', + 'b': 'e', + 'c': 'f' + } + + source = { + 'd': 1, + 'e': 2, + 'f': 3 + } + + resolve_mapping_to_dict(mapping, source) + # { + # 'a': 1, + # 'b': 2, + # 'c': 3, + # } + ``` + +* `resolve_path_to_value(source, path)` - fetch a value out of `source` using `path` as the pointer to the desired value. see docstring for path string formats. + + ```python + from transmogrifydict import resolve_path_to_value + + source = { + 'd': 1, + 'e': 2, + 'f': 3 + } + + found, value = resolve_path_to_value(source, 'e') + + print((found, value)) + # (True, 2) + ``` + +## `path` or `mapping` value format +```python +from transmogrifydict import resolve_path_to_value + +source = { + 'some-key': { + 'another-key': '123' + } +} + +# dot notation can be used to descend into dictionaries. +resolve_path_to_value(source, 'some-key.another-key') +# (True, '123') + +source = { + 'some-key': '{"another-key":"123"}' +} + +# dot notation can also be used to descend into json strings that are dictionary like +resolve_path_to_value(source, 'some-key.another-key') +# (True, '123') + +source = { + 'some-key': { + 'another-key': ['1', '2', '3'] + } +} + +# square brackets can be used to get specific indexes from a list +resolve_path_to_value(source, 'some-key.another-key[1]') +# (True, '2') + +source = { + 'some-key': { + 'another-key': [ + { + 'filter-key': 'yeah', + 'each-key': 'a', + }, + { + 'filter-key': 'yeah', + 'each-key': 'b', + }, + { + 'filter-key': 'nah', + 'each-key': 'c', + } + ] + } +} + +# dot notation can be used after square brackets if the list contains dict-like values +resolve_path_to_value(source, 'some-key.another-key[1].each-key') +# (True, ['b']) + +# square brackets can be used to iterate over arrays to descend into the items +resolve_path_to_value(source, 'some-key.another-key[].each-key') +# (True, ['a', 'b', 'c']) + +# when iterating over a list, a filter can be applied using [key=value] +resolve_path_to_value(source, 'some-key.another-key[filter-key=yeah].each-key') +# (True, ['a', 'b']) + +source = { + 'a-key': [ + { + 'b-key': { + 'c-key': 1, + 'd-key': 2, + } + }, + { + 'b-key': { + 'c-key': 1, + 'd-key': 3, + } + }, + { + 'b-key': { + 'c-key': 0, + 'd-key': 4, + } + } + ] +} +# tidle notation can be used to filter on sub keys of dict list items. +resolve_path_to_value(source, 'a-key[b-key~c-key=1].b-key.d-key') +# (True, [2, 3, 4]) +# +``` diff --git a/setup.py b/setup.py index 72c7c8e..a45d092 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='transmogrifydict', url='https://github.com/emergence/transmogrifydict', - version='1.1.0', + version='1.1.1', description='The "turn a dict from one API into a dict for another" python module.', author='Emergence by Design', author_email='support@emergence.com', diff --git a/tests/test_cases.py b/tests/test_cases.py new file mode 100644 index 0000000..522b6ac --- /dev/null +++ b/tests/test_cases.py @@ -0,0 +1,68 @@ +import doctest + +from collections import OrderedDict + +from tests.helpers import BaseUnitTest, KwargsToOutputDynamicTestsMetaClass +import transmogrifydict +from six import with_metaclass + + +def load_tests(loader, tests, ignore): + tests.addTests(doctest.DocTestSuite(transmogrifydict)) + return tests + + +class ResolvePathToValueTestCase(with_metaclass(KwargsToOutputDynamicTestsMetaClass, BaseUnitTest)): + func = transmogrifydict.resolve_path_to_value + tests = OrderedDict(( + ( + 'simple', + { + 'kwargs': { + 'source': { + 'one': 1, + 'two': 2, + 'three': 3 + }, + 'path': 'two' + }, + 'output': (True, 2) + } + ), + ( + 'with_sub_keys', + { + 'kwargs': { + 'source': { + 'one': 1, + 'two': { + 'sleeping': 'bereft of life', + 'pining for the fjords': 'has ceased to be', + 'stunned': 'an ex-parrot', + }, + 'three': 3 + }, + 'path': 'two.sleeping' + }, + 'output': (True, 'bereft of life') + } + ), + ( + 'with_sub_keys_with_spaces', + { + 'kwargs': { + 'source': { + 'one': 1, + 't w o': { + 'sleeping': 'bereft of life', + 'pining for the fjords': 'has ceased to be', + 'stunned': 'an ex-parrot', + }, + 'three': 3 + }, + 'path': 't w o.pining for the fjords' + }, + 'output': (True, 'has ceased to be') + } + ), + )) diff --git a/tests/test_resolve_mapping_to_dict.py b/tests/test_resolve_mapping_to_dict.py deleted file mode 100644 index 78cff01..0000000 --- a/tests/test_resolve_mapping_to_dict.py +++ /dev/null @@ -1,40 +0,0 @@ -from collections import OrderedDict - -from tests.helpers import BaseUnitTest, KwargsToOutputDynamicTestsMetaClass -from transmogrifydict import resolve_mapping_to_dict -from six import with_metaclass - - -class ResolvePathToValueTestCase(with_metaclass(KwargsToOutputDynamicTestsMetaClass, BaseUnitTest)): - func = resolve_mapping_to_dict - tests = OrderedDict(( - ( - 'doctest_test_1', - { - 'kwargs': { - 'mapping': { - 'a': 'x[type=other_type].aa', - 'b': 'x[type=some_type].bb', - 'c': 'x[type=other_type].cc', - }, - 'source': { - 'x': [ - { - 'type': 'some_type', - 'aa': '4', - 'bb': '5', - 'cc': '6' - }, - { - 'type': 'other_type', - 'aa': '1', - 'bb': '2', - 'cc': '3' - } - ] - } - }, - 'output': {'a': '1', 'c': '3', 'b': '5'} - } - ), - )) diff --git a/tests/test_resolve_path_to_value.py b/tests/test_resolve_path_to_value.py deleted file mode 100644 index 656d182..0000000 --- a/tests/test_resolve_path_to_value.py +++ /dev/null @@ -1,367 +0,0 @@ -from collections import OrderedDict - -from tests.helpers import BaseUnitTest, KwargsToOutputDynamicTestsMetaClass -from transmogrifydict import resolve_path_to_value -from six import with_metaclass - - -class ResolvePathToValueTestCase(with_metaclass(KwargsToOutputDynamicTestsMetaClass, BaseUnitTest)): - func = resolve_path_to_value - tests = OrderedDict(( - ( - 'simple', - { - 'kwargs': { - 'source': { - 'one': 1, - 'two': 2, - 'three': 3 - }, - 'path': 'two' - }, - 'output': (True, 2) - } - ), - ( - 'with_sub_keys', - { - 'kwargs': { - 'source': { - 'one': 1, - 'two': { - 'sleeping': 'bereft of life', - 'pining for the fjords': 'has ceased to be', - 'stunned': 'an ex-parrot', - }, - 'three': 3 - }, - 'path': 'two.sleeping' - }, - 'output': (True, 'bereft of life') - } - ), - ( - 'with_sub_keys_with_spaces', - { - 'kwargs': { - 'source': { - 'one': 1, - 't w o': { - 'sleeping': 'bereft of life', - 'pining for the fjords': 'has ceased to be', - 'stunned': 'an ex-parrot', - }, - 'three': 3 - }, - 'path': 't w o.pining for the fjords' - }, - 'output': (True, 'has ceased to be') - } - ), - ( - 'docstring_test_1', - { - 'kwargs': { - 'source': { - 'first_key': 'a', - 'second_key': [ - 'x', - 'y', - 'z', - ], - 'third_key': [ - { - 'b': 1, - 'c': 2, - 'h': 'asdf' - }, - { - 'b': 3, - 'c': 4, - 'h': 'qw"er' - } - ], - 'fourth_key': [ - { - 'd': { - 'f': 5, - 'g': 6 - }, - 'e': { - 'f': 7, - 'g': 8 - } - }, - { - 'd': { - 'f': 9, - 'g': 10 - }, - 'e': { - 'f': 11, - 'g': 12 - } - } - ] - }, - 'path': 'first_key' - }, - 'output': (True, 'a') - } - ), - ( - 'docstring_test_2', - { - 'kwargs': { - 'source': { - 'first_key': 'a', - 'second_key': [ - 'x', - 'y', - 'z', - ], - 'third_key': [ - { - 'b': 1, - 'c': 2, - 'h': 'asdf' - }, - { - 'b': 3, - 'c': 4, - 'h': 'qw"er' - } - ], - 'fourth_key': [ - { - 'd': { - 'f': 5, - 'g': 6 - }, - 'e': { - 'f': 7, - 'g': 8 - } - }, - { - 'd': { - 'f': 9, - 'g': 10 - }, - 'e': { - 'f': 11, - 'g': 12 - } - } - ] - }, - 'path': 'second_key[1]' - }, - 'output': (True, 'y') - } - ), - ( - 'docstring_test_3', - { - 'kwargs': { - 'source': { - 'first_key': 'a', - 'second_key': [ - 'x', - 'y', - 'z', - ], - 'third_key': [ - { - 'b': 1, - 'c': 2, - 'h': 'asdf' - }, - { - 'b': 3, - 'c': 4, - 'h': 'qw"er' - } - ], - 'fourth_key': [ - { - 'd': { - 'f': 5, - 'g': 6 - }, - 'e': { - 'f': 7, - 'g': 8 - } - }, - { - 'd': { - 'f': 9, - 'g': 10 - }, - 'e': { - 'f': 11, - 'g': 12 - } - } - ] - }, - 'path': 'third_key[b=3]' - }, - 'output': (True, {'h': 'qw"er', 'c': 4, 'b': 3}) - } - ), - ( - 'docstring_test_4', - { - 'kwargs': { - 'source': { - 'first_key': 'a', - 'second_key': [ - 'x', - 'y', - 'z', - ], - 'third_key': [ - { - 'b': 1, - 'c': 2, - 'h': 'asdf' - }, - { - 'b': 3, - 'c': 4, - 'h': 'qw"er' - } - ], - 'fourth_key': [ - { - 'd': { - 'f': 5, - 'g': 6 - }, - 'e': { - 'f': 7, - 'g': 8 - } - }, - { - 'd': { - 'f': 9, - 'g': 10 - }, - 'e': { - 'f': 11, - 'g': 12 - } - } - ] - }, - 'path': 'third_key[h="qw"er"]' - }, - 'output': (True, {'h': 'qw"er', 'c': 4, 'b': 3}) - } - ), - ( - 'docstring_test_5', - { - 'kwargs': { - 'source': { - 'first_key': 'a', - 'second_key': [ - 'x', - 'y', - 'z', - ], - 'third_key': [ - { - 'b': 1, - 'c': 2, - 'h': 'asdf' - }, - { - 'b': 3, - 'c': 4, - 'h': 'qw"er' - } - ], - 'fourth_key': [ - { - 'd': { - 'f': 5, - 'g': 6 - }, - 'e': { - 'f': 7, - 'g': 8 - } - }, - { - 'd': { - 'f': 9, - 'g': 10 - }, - 'e': { - 'f': 11, - 'g': 12 - } - } - ] - }, - 'path': 'third_key[h=asdf].c' - }, - 'output': (True, 2) - } - ), - ( - 'docstring_test_6', - { - 'kwargs': { - 'source': { - 'first_key': 'a', - 'second_key': [ - 'x', - 'y', - 'z', - ], - 'third_key': [ - { - 'b': 1, - 'c': 2, - 'h': 'asdf' - }, - { - 'b': 3, - 'c': 4, - 'h': 'qw"er' - } - ], - 'fourth_key': [ - { - 'd': { - 'f': 5, - 'g': 6 - }, - 'e': { - 'f': 7, - 'g': 8 - } - }, - { - 'd': { - 'f': 9, - 'g': 10 - }, - 'e': { - 'f': 11, - 'g': 12 - } - } - ] - }, - 'path': 'fourth_key[d~g=6].e.f' - }, - 'output': (True, 7) - } - ), - )) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2ae054f --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[flake8] +max-line-length = 120 +exclude=venv,venvpy3,dist,.eggs +max-complexity=20 + diff --git a/transmogrifydict.py b/transmogrifydict.py index 8ad39f0..c4d095c 100644 --- a/transmogrifydict.py +++ b/transmogrifydict.py @@ -1,9 +1,32 @@ import json import six +import re + +# (?>> source_dict = { ... 'first_key': 'a', - ... 'second_key' : [ - ... 'x', - ... 'y', - ... 'z', - ... ], + ... 'second_key' : ['x', 'y', 'z'], ... 'third_key' : [ - ... { - ... 'b': 1, - ... 'c': 2, - ... 'h': 'asdf' - ... }, - ... { - ... 'b': 3, - ... 'c': 4, - ... 'h': 'qw"er' - ... } + ... {'c': 'asdf'}, + ... {'b': 3}, + ... {'b': '5'}, + ... {'h': 'qw"er'} ... ], ... 'fourth_key': [ ... { - ... 'd': { - ... 'f': 5, - ... 'g': 6 - ... }, - ... 'e': { - ... 'f': 7, - ... 'g': 8 - ... } + ... 'd': {'f': 5, 'g': 6}, + ... 'e': {'f': 7, 'g': 8} ... }, ... { - ... 'd': { - ... 'f': 9, - ... 'g': 10 - ... }, - ... 'e': { - ... 'f': 11, - ... 'g': 12 - ... } + ... 'd': {'f': 9, 'g': 10}, + ... 'e': {'f': 11, 'g': 12} ... } - ... ] + ... ], + ... 'fifth_key': [ + ... {'b.c': '9.a'}, + ... {'b[c': '9[a'}, + ... {'b]c': '9]a'}, + ... {'b\c': '9\\a'}, + ... ], + ... 'sixth_key': { + ... 'a': [ + ... {'b':6}, + ... {'b':5}, + ... {'b':4}, + ... ], + ... 'c': [ + ... {'d':100}, + ... {'d':{'e': 3}}, + ... {'d':{'e': 2}}, + ... ], + ... 'f': [] + ... }, + ... 'seventh_key': { + ... 'bad_api': '{"z":1,"y":2,"x":3}', + ... 'bad_json': '{"z":1!"y":2,"x":3}', + ... } ... } + >>> resolve_path_to_value(source_dict, 'zero_key')[0] + False >>> resolve_path_to_value(source_dict, 'first_key') (True, 'a') >>> resolve_path_to_value(source_dict, 'second_key[1]') (True, 'y') + >>> resolve_path_to_value(source_dict, 'second_key[4]') + Traceback (most recent call last): + ... + IndexError: index 4 out of range on array at 'second_key'. >>> resolve_path_to_value(source_dict, 'third_key[b=3]') - (True, {'h': 'qw"er', 'c': 4, 'b': 3}) - >>> resolve_path_to_value(source_dict, 'third_key[h="qw"er"]') - (True, {'h': 'qw"er', 'c': 4, 'b': 3}) - >>> resolve_path_to_value(source_dict, 'third_key[h=asdf].c') - (True, 2) + (True, {'b': 3}) + >>> resolve_path_to_value(source_dict, 'third_key[b=4]')[0] + False + >>> resolve_path_to_value(source_dict, 'third_key[b="5"]') + (True, {'b': '5'}) + >>> resolve_path_to_value(source_dict, 'third_key[h=qw"er]') + (True, {'h': 'qw"er'}) + >>> resolve_path_to_value(source_dict, 'third_key[c=asdf].c') + (True, 'asdf') + >>> resolve_path_to_value(source_dict, 'third_key[c=asdf].b') + (False, {'c': 'asdf'}) >>> resolve_path_to_value(source_dict, 'fourth_key[d~g=6].e.f') (True, 7) + >>> resolve_path_to_value(source_dict, r'fifth_key[b\.c=9\.a].b\.c') + (True, '9.a') + >>> resolve_path_to_value(source_dict, r'fifth_key[b\[c=9\[a].b\[c') + (True, '9[a') + >>> resolve_path_to_value(source_dict, r'fifth_key[b\]c=9\]a].b\]c') + (True, '9]a') + >>> resolve_path_to_value(source_dict, r'fifth_key[b\\c=9\\a].b\\c') + (True, '9\\a') + >>> resolve_path_to_value(source_dict, 'sixth_key.a[].b') + (True, [6, 5, 4]) + >>> resolve_path_to_value(source_dict, 'sixth_key.c[].d.e') + (True, [3, 2]) + >>> resolve_path_to_value(source_dict, 'sixth_key.c[].x') + (False, []) + >>> resolve_path_to_value(source_dict, 'sixth_key.f') + (True, []) + >>> resolve_path_to_value(source_dict, 'sixth_key.f[]') + (True, []) + >>> resolve_path_to_value(source_dict, 'sixth_key.f[].g') + (False, []) + >>> resolve_path_to_value(source_dict, 'seventh_key.bad_api.x') + (True, 3) + >>> results = resolve_path_to_value(source_dict, 'seventh_key.bad_api.a') + >>> results[0] + False + >>> results[1] == {'x': 3, 'y': 2, 'z': 1} + True + >>> resolve_path_to_value(source_dict, 'seventh_key.bad_api[bad-squares]') + Traceback (most recent call last): + ... + ValueError: Bad square brackets syntax on 'bad-squares' + >>> resolve_path_to_value(source_dict, 'seventh_key.bad_api[a=b=c=]') + Traceback (most recent call last): + ... + ValueError: too many unquoted equals signs in square brackets for 'a=b=c=' + >>> resolve_path_to_value(source_dict, 'seventh_key[0]') + Traceback (most recent call last): + ... + ValueError: array expected at 'seventh_key', found dict-like object. + >>> resolve_path_to_value(source_dict, 'seventh_key[]') + Traceback (most recent call last): + ... + ValueError: array expected at 'seventh_key', found dict-like object. + >>> resolve_path_to_value(source_dict, 'seventh_key.bad_json.z') + Traceback (most recent call last): + ... + ValueError: string found when looking for dict-like object at 'seventh_key.bad_json'. failed to convert to json. :param source: potentially holds the desired value :type source: dict @@ -85,41 +168,77 @@ def resolve_path_to_value(source, path): """ mapped_value = source found_value = True - # noinspection PyUnresolvedReferences - for path_part in path.split('.'): - parts = path_part.split('[') + path_parts_break = False + + path_parts = _non_quoted_split(PERIOD_SPLIT, path) + + for path_parts_index, path_part_raw in enumerate(path_parts): + # split on non quoted open bracket + + parts = _non_quoted_split(OPEN_SQUARE_BRACKET_SPLIT, path_part_raw) + key = parts[0] + array = parts[1:] + # future: when dropping python 2 support do this instead. + # key, *array = _non_quoted_split(OPEN_SQUARE_BRACKET_SPLIT, path_part_raw) + + key = _un_slash_escape(key) try: if isinstance(mapped_value, six.string_types): # ugh, maybe it is json? try: mapped_value = json.loads(mapped_value) except ValueError: - found_value = False - break - mapped_value = mapped_value[parts[0]] + raise ValueError( + 'string found when looking for dict-like object at {!r}. failed to convert to json.'.format( + '.'.join(path_parts[:path_parts_index]) + ) + ) + if not hasattr(mapped_value, 'keys'): + found_value = False + break + mapped_value = mapped_value[key] except KeyError: found_value = False break - for array_part_raw in parts[1:]: - array_part = array_part_raw[:-1] + for array_part_raw in array: + array_part = array_part_raw.strip(']') if array_part.isdigit(): # [0] - if hasattr(mapped_value, 'keys'): - break - mapped_value = mapped_value[int(array_part)] + try: + mapped_value = mapped_value[int(array_part)] + except KeyError: + raise ValueError('array expected at {!r}, found dict-like object.'.format( + '.'.join(path_parts[:path_parts_index] + [key]) + )) + except IndexError: + raise IndexError('index {!r} out of range on array at {!r}.'.format( + int(array_part), + '.'.join(path_parts[:path_parts_index] + [key]) + )) elif '=' in array_part: # [Key=Value] or [Key~SubKey=Value] - find_key, find_value = array_part.split('=') + # split on non quoted equals signs + equal_parts = _non_quoted_split(EQUAL_SPLIT, array_part) + find_key = equal_parts[0] + find_value = equal_parts[1:] + # future: when dropping python 2 support do this instead. + # find_key, *find_value = _non_quoted_split(EQUAL_SPLIT, array_part) + if len(find_value) >= 2: + raise ValueError('too many unquoted equals signs in square brackets for {!r}'.format(array_part)) + find_value = find_value[0] if find_value.isdigit(): find_value = int(find_value) elif find_value.startswith('"') and find_value.endswith('"'): find_value = find_value[1:-1] - for item in hasattr(mapped_value, 'keys') and [mapped_value] or mapped_value: + if isinstance(find_value, six.string_types): + find_value = _un_slash_escape(find_value) + for item in [mapped_value] if hasattr(mapped_value, 'keys') else mapped_value: sub_item = item - sub_keys = find_key.split('~') + sub_keys = _non_quoted_split(TIDLE_SPLIT, find_key) try: while sub_keys: - sub_item = sub_item[sub_keys.pop(0)] + sub_key = _un_slash_escape(sub_keys.pop(0)) + sub_item = sub_item[sub_key] except (KeyError, IndexError): pass else: @@ -129,11 +248,29 @@ def resolve_path_to_value(source, path): else: # raise KeyError('no item with %r == %r' % (find_key, find_value)) found_value = False + path_parts_break = True # break the outer loop, we are done here. break + elif array_part == '': + # empty [] + if hasattr(mapped_value, 'keys'): + raise ValueError('array expected at {!r}, found dict-like object.'.format( + '.'.join(path_parts[:path_parts_index] + [key]) + )) + if not mapped_value: + if path_parts[path_parts_index+1:]: + found_value = False + path_parts_break = True # break the outer loop, we are done here. + break + remainder = '.'.join(path_parts[path_parts_index+1:]) + mapped_value = [resolve_path_to_value(x, remainder) for x in mapped_value] + mapped_value = [value for found, value in mapped_value if found] + if not mapped_value: + found_value = False + path_parts_break = True # break the outer loop, we are done here. + break else: - raise ValueError('Expected square brackets to have be either "[number]", or "[key=value]" or ' - '"[key~subkey=value]". got: %r' % array_part) - if not found_value: + raise ValueError('Bad square brackets syntax on {!r}'.format(array_part)) + if path_parts_break: break return found_value, mapped_value @@ -164,8 +301,8 @@ def resolve_mapping_to_dict(mapping, source): ... } ... ] ... } - >>> resolve_mapping_to_dict(mapping, source) - {'a': '1', 'c': '3', 'b': '5'} + >>> resolve_mapping_to_dict(mapping, source) == {'a': '1', 'b': '5', 'c': '3'} + True :param mapping: values are paths to find the corresponding value in `source`, keys are were to store said values :type mapping: dict