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 | 375 | return tender |
376 | 376 | |
377 | 377 | |
378 | -def get_from_object(obj, attribute): | |
378 | +def get_from_object(obj, path): | |
379 | 379 | """Gets data from a dictionary using a dotted accessor-string""" |
380 | - jsonpath_expr = parse_path(attribute) | |
380 | + jsonpath_expr = parse_path(path) | |
381 | 381 | return_list = [i.value for i in jsonpath_expr.find(obj)] |
382 | 382 | if return_list: |
383 | 383 | return return_list[0] |
384 | 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 | 453 | def wait_to_date(date_stamp): |
... | ... | @@ -439,18 +479,23 @@ def merge_dicts(a, b): |
439 | 479 | |
440 | 480 | |
441 | 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 | 499 | return data_dict |
455 | 500 | |
456 | 501 | |
... | ... | @@ -463,10 +508,26 @@ def munch_dict(arg=None, data=False): |
463 | 508 | |
464 | 509 | |
465 | 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 | 533 | def get_id_from_string(string): |
... | ... | @@ -601,7 +662,7 @@ def compare_rationale_types(type1, type2): |
601 | 662 | def delete_from_dictionary(variable, path): |
602 | 663 | if not type(path) in STR_TYPES: |
603 | 664 | raise TypeError('path must be one of: ' + |
604 | - str([x.__name__ for x in STR_TYPES])) | |
665 | + str(STR_TYPES)) | |
605 | 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