Commit be4854acf87b5604032c07c563e5d8917d1b9d98

Authored by ivanka12
Committed by GitHub
2 parents ce099487 b62c01ae

Merge pull request #823 from selurvedu/prozorro/set_to_object

Rewrite set_to_object() and related utility functions
... ... @@ -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