Commit be4854acf87b5604032c07c563e5d8917d1b9d98
Committed by
GitHub
Merge pull request #823 from selurvedu/prozorro/set_to_object
Rewrite set_to_object() and related utility functions
Showing
2 changed files
with
177 additions
and
43 deletions
| @@ -375,39 +375,79 @@ def set_access_key(tender, access_token): | @@ -375,39 +375,79 @@ def set_access_key(tender, access_token): | ||
| 375 | return tender | 375 | return tender |
| 376 | 376 | ||
| 377 | 377 | ||
| 378 | -def get_from_object(obj, attribute): | 378 | +def get_from_object(obj, path): |
| 379 | """Gets data from a dictionary using a dotted accessor-string""" | 379 | """Gets data from a dictionary using a dotted accessor-string""" |
| 380 | - jsonpath_expr = parse_path(attribute) | 380 | + jsonpath_expr = parse_path(path) |
| 381 | return_list = [i.value for i in jsonpath_expr.find(obj)] | 381 | return_list = [i.value for i in jsonpath_expr.find(obj)] |
| 382 | if return_list: | 382 | if return_list: |
| 383 | return return_list[0] | 383 | return return_list[0] |
| 384 | else: | 384 | else: |
| 385 | - raise AttributeError('Attribute not found: {0}'.format(attribute)) | 385 | + raise AttributeError('Attribute not found: {0}'.format(path)) |
| 386 | + | ||
| 387 | + | ||
| 388 | +def set_to_object(obj, path, value): | ||
| 389 | + def recur(obj, path, value): | ||
| 390 | + if not isinstance(obj, dict): | ||
| 391 | + raise TypeError('expected %s, got %s' % | ||
| 392 | + (dict.__name__, type(obj))) | ||
| 393 | + | ||
| 394 | + # Search the list index in path to value | ||
| 395 | + groups = re.search(r'^(?P<key>[0-9a-zA-Z_]+)(?:\[(?P<index>-?\d+)\])?' | ||
| 396 | + '(?:\.(?P<suffix>.+))?$', path) | ||
| 397 | + | ||
| 398 | + err = RuntimeError('could not parse the path: ' + path) | ||
| 399 | + if not groups: | ||
| 400 | + raise err | ||
| 401 | + | ||
| 402 | + gd = {k: v for k, v in groups.groupdict().items() if v is not None} | ||
| 403 | + is_list = False | ||
| 404 | + suffix = None | ||
| 405 | + | ||
| 406 | + if 'key' not in gd: | ||
| 407 | + raise err | ||
| 408 | + key = gd['key'] | ||
| 409 | + | ||
| 410 | + if 'index' in gd: | ||
| 411 | + is_list = True | ||
| 412 | + index = int(gd['index']) | ||
| 413 | + | ||
| 414 | + if 'suffix' in gd: | ||
| 415 | + suffix = gd['suffix'] | ||
| 416 | + | ||
| 417 | + if is_list: | ||
| 418 | + if key not in obj: | ||
| 419 | + obj[key] = [] | ||
| 420 | + elif not isinstance(obj[key], list): | ||
| 421 | + raise TypeError('expected %s, got %s' % | ||
| 422 | + (list.__name__, type(obj[key]))) | ||
| 423 | + | ||
| 424 | + plusone = 1 if index >= 0 else 0 | ||
| 425 | + if len(obj[key]) < abs(index) + plusone: | ||
| 426 | + while not len(obj[key]) == abs(index) + plusone: | ||
| 427 | + extension = [None] * (abs(index) + plusone - len(obj[key])) | ||
| 428 | + if index < 0: | ||
| 429 | + obj[key] = extension + obj[key] | ||
| 430 | + else: | ||
| 431 | + obj[key].extend(extension) | ||
| 432 | + if suffix: | ||
| 433 | + obj[key][index] = {} | ||
| 434 | + if suffix: | ||
| 435 | + obj[key][index] = recur(obj[key][index], suffix, value) | ||
| 436 | + else: | ||
| 437 | + obj[key][index] = value | ||
| 438 | + else: | ||
| 439 | + if key not in obj: | ||
| 440 | + obj[key] = {} | ||
| 441 | + if suffix: | ||
| 442 | + obj[key] = recur(obj[key], suffix, value) | ||
| 443 | + else: | ||
| 444 | + obj[key] = value | ||
| 386 | 445 | ||
| 446 | + return obj | ||
| 387 | 447 | ||
| 388 | -def set_to_object(obj, attribute, value): | ||
| 389 | - # Search the list index in path to value | ||
| 390 | - list_index = re.search('\d+', attribute) | ||
| 391 | - if list_index and attribute != 'stage2TenderID': | ||
| 392 | - list_index = list_index.group(0) | ||
| 393 | - parent, child = attribute.split('[' + list_index + '].')[:2] | ||
| 394 | - # Split attribute to path to lits (parent) and path to value in list element (child) | ||
| 395 | - try: | ||
| 396 | - # Get list from parent | ||
| 397 | - listing = get_from_object(obj, parent) | ||
| 398 | - # Create object with list_index if he don`t exist | ||
| 399 | - if len(listing) < int(list_index) + 1: | ||
| 400 | - listing.append({}) | ||
| 401 | - except AttributeError: | ||
| 402 | - # Create list if he don`t exist | ||
| 403 | - listing = [{}] | ||
| 404 | - # Update list in parent | ||
| 405 | - xpathnew(obj, parent, listing, separator='.') | ||
| 406 | - # Set value in obj | ||
| 407 | - xpathnew(obj, '.'.join([parent, list_index, child]), value, separator='.') | ||
| 408 | - else: | ||
| 409 | - xpathnew(obj, attribute, value, separator='.') | ||
| 410 | - return munchify(obj) | 448 | + if not isinstance(path, STR_TYPES): |
| 449 | + raise TypeError('Path must be one of ' + str(STR_TYPES)) | ||
| 450 | + return munchify(recur(obj, path, value)) | ||
| 411 | 451 | ||
| 412 | 452 | ||
| 413 | def wait_to_date(date_stamp): | 453 | def wait_to_date(date_stamp): |
| @@ -439,18 +479,23 @@ def merge_dicts(a, b): | @@ -439,18 +479,23 @@ def merge_dicts(a, b): | ||
| 439 | 479 | ||
| 440 | 480 | ||
| 441 | def create_data_dict(path_to_value=None, value=None): | 481 | def create_data_dict(path_to_value=None, value=None): |
| 442 | - data_dict = munchify({'data': {}}) | ||
| 443 | - if isinstance(path_to_value, basestring) and value: | ||
| 444 | - list_items = re.search('\d+', path_to_value) | ||
| 445 | - if list_items: | ||
| 446 | - list_items = list_items.group(0) | ||
| 447 | - path_to_value = path_to_value.split('[' + list_items + ']') | ||
| 448 | - path_to_value.insert(1, '.' + list_items) | ||
| 449 | - set_to_object(data_dict, path_to_value[0], []) | ||
| 450 | - set_to_object(data_dict, ''.join(path_to_value[:2]), {}) | ||
| 451 | - set_to_object(data_dict, ''.join(path_to_value), value) | ||
| 452 | - else: | ||
| 453 | - data_dict = set_to_object(data_dict, path_to_value, value) | 482 | + """Create a dictionary with one key, 'data'. |
| 483 | + | ||
| 484 | + If `path_to_value` is not given, set the key's value | ||
| 485 | + to an empty dictionary. | ||
| 486 | + If `path_to_value` is given, set the key's value to `value`. | ||
| 487 | + In case it's the latter and if `value` is not set, | ||
| 488 | + the key's value is set to `None`. | ||
| 489 | + | ||
| 490 | + Please note that `path_to_value` is relative to the parent dictionary, | ||
| 491 | + thus, you may need to prepend `data.` to your path string. | ||
| 492 | + | ||
| 493 | + To better understand how `path_to_value` is handled, | ||
| 494 | + please refer to the `set_to_object()` function. | ||
| 495 | + """ | ||
| 496 | + data_dict = {'data': {}} | ||
| 497 | + if path_to_value: | ||
| 498 | + data_dict = set_to_object(data_dict, path_to_value, value) | ||
| 454 | return data_dict | 499 | return data_dict |
| 455 | 500 | ||
| 456 | 501 | ||
| @@ -463,10 +508,26 @@ def munch_dict(arg=None, data=False): | @@ -463,10 +508,26 @@ def munch_dict(arg=None, data=False): | ||
| 463 | 508 | ||
| 464 | 509 | ||
| 465 | def get_id_from_object(obj): | 510 | def get_id_from_object(obj): |
| 466 | - obj_id = re.match(r'(^[filq]-[0-9a-fA-F]{8}): ', obj.get('title', '')) | ||
| 467 | - if not obj_id: | ||
| 468 | - obj_id = re.match(r'(^[filq]-[0-9a-fA-F]{8}): ', obj.get('description', '')) | ||
| 469 | - return obj_id.group(1) | 511 | + regex = r'(^[filq]-[0-9a-fA-F]{8}): ' |
| 512 | + | ||
| 513 | + title = obj.get('title', '') | ||
| 514 | + if title: | ||
| 515 | + if not isinstance(title, STR_TYPES): | ||
| 516 | + raise TypeError('title must be one of %s' % str(STR_TYPES)) | ||
| 517 | + obj_id = re.match(regex, title) | ||
| 518 | + if obj_id and len(obj_id.groups()) >= 1: | ||
| 519 | + return obj_id.group(1) | ||
| 520 | + | ||
| 521 | + description = obj.get('description', '') | ||
| 522 | + if description: | ||
| 523 | + if not isinstance(description, STR_TYPES): | ||
| 524 | + raise TypeError('description must be one of %s' % str(STR_TYPES)) | ||
| 525 | + obj_id = re.match(regex, description) | ||
| 526 | + if obj_id and len(obj_id.groups()) >= 1: | ||
| 527 | + return obj_id.group(1) | ||
| 528 | + | ||
| 529 | + raise VaueError('could not find object ID in "title": "%s", ' | ||
| 530 | + '"description": "%s"' % (title, description)) | ||
| 470 | 531 | ||
| 471 | 532 | ||
| 472 | def get_id_from_string(string): | 533 | def get_id_from_string(string): |
| @@ -601,7 +662,7 @@ def compare_rationale_types(type1, type2): | @@ -601,7 +662,7 @@ def compare_rationale_types(type1, type2): | ||
| 601 | def delete_from_dictionary(variable, path): | 662 | def delete_from_dictionary(variable, path): |
| 602 | if not type(path) in STR_TYPES: | 663 | if not type(path) in STR_TYPES: |
| 603 | raise TypeError('path must be one of: ' + | 664 | raise TypeError('path must be one of: ' + |
| 604 | - str([x.__name__ for x in STR_TYPES])) | 665 | + str(STR_TYPES)) |
| 605 | return xpathdelete(variable, path, separator='.') | 666 | return xpathdelete(variable, path, separator='.') |
| 606 | 667 | ||
| 607 | 668 |
| 1 | +from unittest import TestCase, main | ||
| 2 | + | ||
| 3 | +from op_robot_tests.tests_files.service_keywords import set_to_object | ||
| 4 | + | ||
| 5 | + | ||
| 6 | +class TestSetToObject(TestCase): | ||
| 7 | + def test_raises_1(self): | ||
| 8 | + given = None | ||
| 9 | + with self.assertRaises(TypeError): | ||
| 10 | + given = set_to_object(given, 'foo[0]', 1) | ||
| 11 | + | ||
| 12 | + def test_raises_2(self): | ||
| 13 | + given = {'foo': None} | ||
| 14 | + with self.assertRaises(TypeError): | ||
| 15 | + given = set_to_object(given, 'foo[0]', 1) | ||
| 16 | + | ||
| 17 | + def test_1(self): | ||
| 18 | + given = {} | ||
| 19 | + expected = {'foo': 1} | ||
| 20 | + given = set_to_object(given, 'foo', 1) | ||
| 21 | + self.assertEqual(given, expected) | ||
| 22 | + | ||
| 23 | + def test_2(self): | ||
| 24 | + given = {'foo': []} | ||
| 25 | + expected = {'foo': [1]} | ||
| 26 | + given = set_to_object(given, 'foo[0]', 1) | ||
| 27 | + self.assertEqual(given, expected) | ||
| 28 | + | ||
| 29 | + def test_3(self): | ||
| 30 | + given = {} | ||
| 31 | + expected = {'foo': [1]} | ||
| 32 | + given = set_to_object(given, 'foo[0]', 1) | ||
| 33 | + self.assertEqual(given, expected) | ||
| 34 | + | ||
| 35 | + def test_4(self): | ||
| 36 | + given = {} | ||
| 37 | + expected = {'foo': {'bar': 1}} | ||
| 38 | + given = set_to_object(given, 'foo.bar', 1) | ||
| 39 | + self.assertEqual(given, expected) | ||
| 40 | + | ||
| 41 | + def test_5(self): | ||
| 42 | + given = {} | ||
| 43 | + expected = {'foo': [None, {'bar': 1}]} | ||
| 44 | + given = set_to_object(given, 'foo[1].bar', 1) | ||
| 45 | + self.assertEqual(given, expected) | ||
| 46 | + | ||
| 47 | + def test_6(self): | ||
| 48 | + given = {} | ||
| 49 | + expected = {'foo': [{'bar': [1]}]} | ||
| 50 | + given = set_to_object(given, 'foo[0].bar[0]', 1) | ||
| 51 | + self.assertEqual(given, expected) | ||
| 52 | + | ||
| 53 | + def test_7(self): | ||
| 54 | + given = {} | ||
| 55 | + expected = {'foo': [{'bar': [1]}, None, None]} | ||
| 56 | + given = set_to_object(given, 'foo[-3].bar[-1]', 1) | ||
| 57 | + self.assertEqual(given, expected) | ||
| 58 | + | ||
| 59 | + def test_8(self): | ||
| 60 | + given = {'foo': [{'bar': [1]}]} | ||
| 61 | + expected = {'foo': [{'bar': [1]}, None, {'baz': [2]}]} | ||
| 62 | + given = set_to_object(given, 'foo[2].baz[0]', 2) | ||
| 63 | + self.assertEqual(given, expected) | ||
| 64 | + | ||
| 65 | + def test_9(self): | ||
| 66 | + given = {'foo': [{'bar': [1]}]} | ||
| 67 | + expected = {'foo': [{'baz': [2, None]}, None, {'bar': [1]}]} | ||
| 68 | + given = set_to_object(given, 'foo[-3].baz[-2]', 2) | ||
| 69 | + self.assertEqual(given, expected) | ||
| 70 | + | ||
| 71 | + | ||
| 72 | +if __name__ == '__main__': | ||
| 73 | + main() |
Please
register
or
login
to post a comment