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