1import unittest 2 3__all__ = ( 4 "Clay", 5 "MoveLayerCollectionTesting", 6 "MoveSceneCollectionSyncTesting", 7 "MoveSceneCollectionTesting", 8 "ViewLayerTesting", 9 "compare_files", 10 "dump", 11 "get_layers", 12 "get_scene_collections", 13 "query_scene", 14 "setup_extra_arguments", 15) 16 17# ############################################################ 18# Layer Collection Crawler 19# ############################################################ 20 21 22def listbase_iter(data, struct, listbase): 23 element = data.get_pointer((struct, listbase, b'first')) 24 while element is not None: 25 yield element 26 element = element.get_pointer(b'next') 27 28 29def linkdata_iter(collection, data): 30 element = collection.get_pointer((data, b'first')) 31 while element is not None: 32 yield element 33 element = element.get_pointer(b'next') 34 35 36def get_layer_collection(layer_collection): 37 data = {} 38 flag = layer_collection.get(b'flag') 39 40 data['is_visible'] = (flag & (1 << 0)) != 0 41 data['is_selectable'] = (flag & (1 << 1)) != 0 42 data['is_disabled'] = (flag & (1 << 2)) != 0 43 44 scene_collection = layer_collection.get_pointer(b'scene_collection') 45 if scene_collection is None: 46 name = 'Fail!' 47 else: 48 name = scene_collection.get(b'name') 49 data['name'] = name 50 51 objects = [] 52 for link in linkdata_iter(layer_collection, b'object_bases'): 53 ob_base = link.get_pointer(b'data') 54 ob = ob_base.get_pointer(b'object') 55 objects.append(ob.get((b'id', b'name'))[2:]) 56 data['objects'] = objects 57 58 collections = {} 59 for nested_layer_collection in linkdata_iter(layer_collection, b'layer_collections'): 60 subname, subdata = get_layer_collection(nested_layer_collection) 61 collections[subname] = subdata 62 data['collections'] = collections 63 64 return name, data 65 66 67def get_layer(scene, layer): 68 data = {} 69 name = layer.get(b'name') 70 71 data['name'] = name 72 data['engine'] = scene.get((b'r', b'engine')) 73 74 active_base = layer.get_pointer(b'basact') 75 if active_base: 76 ob = active_base.get_pointer(b'object') 77 data['active_object'] = ob.get((b'id', b'name'))[2:] 78 else: 79 data['active_object'] = "" 80 81 objects = [] 82 for link in linkdata_iter(layer, b'object_bases'): 83 ob = link.get_pointer(b'object') 84 objects.append(ob.get((b'id', b'name'))[2:]) 85 data['objects'] = objects 86 87 collections = {} 88 for layer_collection in linkdata_iter(layer, b'layer_collections'): 89 subname, subdata = get_layer_collection(layer_collection) 90 collections[subname] = subdata 91 data['collections'] = collections 92 93 return name, data 94 95 96def get_layers(scene): 97 """Return all the render layers and their data""" 98 layers = {} 99 for layer in linkdata_iter(scene, b'view_layers'): 100 name, data = get_layer(scene, layer) 101 layers[name] = data 102 return layers 103 104 105def get_scene_collection_objects(collection, listbase): 106 objects = [] 107 for link in linkdata_iter(collection, listbase): 108 ob = link.get_pointer(b'data') 109 if ob is None: 110 name = 'Fail!' 111 else: 112 name = ob.get((b'id', b'name'))[2:] 113 objects.append(name) 114 return objects 115 116 117def get_scene_collection(collection): 118 """""" 119 data = {} 120 name = collection.get(b'name') 121 122 data['name'] = name 123 data['objects'] = get_scene_collection_objects(collection, b'objects') 124 125 collections = {} 126 for nested_collection in linkdata_iter(collection, b'scene_collections'): 127 subname, subdata = get_scene_collection(nested_collection) 128 collections[subname] = subdata 129 data['collections'] = collections 130 131 return name, data 132 133 134def get_scene_collections(scene): 135 """Return all the scene collections ahd their data""" 136 master_collection = scene.get_pointer(b'collection') 137 return get_scene_collection(master_collection) 138 139 140def query_scene(filepath, name, callbacks): 141 """Return the equivalent to bpy.context.scene""" 142 from io_blend_utils.blend import blendfile 143 144 with blendfile.open_blend(filepath) as blend: 145 scenes = [block for block in blend.blocks if block.code == b'SC'] 146 for scene in scenes: 147 if scene.get((b'id', b'name'))[2:] != name: 148 continue 149 150 return [callback(scene) for callback in callbacks] 151 152 153# ############################################################ 154# Utils 155# ############################################################ 156 157def dump(data): 158 import json 159 return json.dumps( 160 data, 161 sort_keys=True, 162 indent=4, 163 separators=(',', ': '), 164 ) 165 166 167# ############################################################ 168# Tests 169# ############################################################ 170 171PDB = False 172DUMP_DIFF = True 173UPDATE_DIFF = False # HACK used to update tests when something change 174 175 176def compare_files(file_a, file_b): 177 import filecmp 178 179 if not filecmp.cmp( 180 file_a, 181 file_b): 182 183 if DUMP_DIFF: 184 import subprocess 185 subprocess.call(["diff", "-u", file_b, file_a]) 186 187 if UPDATE_DIFF: 188 import subprocess 189 subprocess.call(["cp", "-u", file_a, file_b]) 190 191 if PDB: 192 import pdb 193 print("Files differ:", file_b, file_a) 194 pdb.set_trace() 195 196 return False 197 198 return True 199 200 201class ViewLayerTesting(unittest.TestCase): 202 _test_simple = False 203 _extra_arguments = [] 204 205 @classmethod 206 def setUpClass(cls): 207 """Runs once""" 208 cls.pretest_parsing() 209 210 @classmethod 211 def get_root(cls): 212 """ 213 return the folder with the test files 214 """ 215 arguments = {} 216 for argument in cls._extra_arguments: 217 name, value = argument.split('=') 218 cls.assertTrue(name and name.startswith("--"), "Invalid argument \"{0}\"".format(argument)) 219 cls.assertTrue(value, "Invalid argument \"{0}\"".format(argument)) 220 arguments[name[2:]] = value.strip('"') 221 222 return arguments.get('testdir') 223 224 @classmethod 225 def pretest_parsing(cls): 226 """ 227 Test if the arguments are properly set, and store ROOT 228 name has extra _ because we need this test to run first 229 """ 230 root = cls.get_root() 231 cls.assertTrue(root, "Testdir not set") 232 233 def setUp(self): 234 """Runs once per test""" 235 import bpy 236 bpy.ops.wm.read_factory_settings() 237 238 def path_exists(self, filepath): 239 import os 240 self.assertTrue( 241 os.path.exists(filepath), 242 "Test file \"{0}\" not found".format(filepath)) 243 244 def do_object_add(self, filepath_json, add_mode): 245 """ 246 Testing for adding objects and see if they 247 go to the right collection 248 """ 249 import bpy 250 import os 251 import tempfile 252 import filecmp 253 254 ROOT = self.get_root() 255 with tempfile.TemporaryDirectory() as dirpath: 256 filepath_layers = os.path.join(ROOT, 'layers.blend') 257 258 # open file 259 bpy.ops.wm.open_mainfile('EXEC_DEFAULT', filepath=filepath_layers) 260 self.rename_collections() 261 262 # create sub-collections 263 three_b = bpy.data.objects.get('T.3b') 264 three_c = bpy.data.objects.get('T.3c') 265 266 scene = bpy.context.scene 267 subzero = scene.master_collection.collections['1'].collections.new('sub-zero') 268 scorpion = subzero.collections.new('scorpion') 269 subzero.objects.link(three_b) 270 scorpion.objects.link(three_c) 271 layer = scene.view_layers.new('Fresh new Layer') 272 layer.collections.link(subzero) 273 274 # change active collection 275 layer.collections.active_index = 3 276 self.assertEqual(layer.collections.active.name, 'scorpion', "Run: test_syncing_object_add") 277 278 # change active layer 279 override = bpy.context.copy() 280 override["view_layer"] = layer 281 override["scene_collection"] = layer.collections.active.collection 282 283 # add new objects 284 if add_mode == 'EMPTY': 285 bpy.ops.object.add(override) # 'Empty' 286 287 elif add_mode == 'CYLINDER': 288 bpy.ops.mesh.primitive_cylinder_add(override) # 'Cylinder' 289 290 elif add_mode == 'TORUS': 291 bpy.ops.mesh.primitive_torus_add(override) # 'Torus' 292 293 # save file 294 filepath_objects = os.path.join(dirpath, 'objects.blend') 295 bpy.ops.wm.save_mainfile('EXEC_DEFAULT', filepath=filepath_objects) 296 297 # get the generated json 298 datas = query_scene(filepath_objects, 'Main', (get_scene_collections, get_layers)) 299 self.assertTrue(datas, "Data is not valid") 300 301 filepath_objects_json = os.path.join(dirpath, "objects.json") 302 with open(filepath_objects_json, "w") as f: 303 for data in datas: 304 f.write(dump(data)) 305 306 self.assertTrue(compare_files( 307 filepath_objects_json, 308 filepath_json, 309 ), 310 "Scene dump files differ") 311 312 def do_object_add_no_collection(self, add_mode): 313 """ 314 Test for adding objects when no collection 315 exists in render layer 316 """ 317 import bpy 318 319 # empty layer of collections 320 321 layer = bpy.context.view_layer 322 while layer.collections: 323 layer.collections.unlink(layer.collections[0]) 324 325 # add new objects 326 if add_mode == 'EMPTY': 327 bpy.ops.object.add() # 'Empty' 328 329 elif add_mode == 'CYLINDER': 330 bpy.ops.mesh.primitive_cylinder_add() # 'Cylinder' 331 332 elif add_mode == 'TORUS': 333 bpy.ops.mesh.primitive_torus_add() # 'Torus' 334 335 self.assertEqual(len(layer.collections), 1, "New collection not created") 336 collection = layer.collections[0] 337 self.assertEqual(len(collection.objects), 1, "New collection is empty") 338 339 def do_object_link(self, master_collection): 340 import bpy 341 self.assertEqual(master_collection.name, "Master Collection") 342 self.assertEqual(master_collection, bpy.context.scene.master_collection) 343 master_collection.objects.link(bpy.data.objects.new('object', None)) 344 345 def do_scene_copy(self, filepath_json_reference, copy_mode, data_callbacks): 346 import bpy 347 import os 348 import tempfile 349 import filecmp 350 351 ROOT = self.get_root() 352 with tempfile.TemporaryDirectory() as dirpath: 353 filepath_layers = os.path.join(ROOT, 'layers.blend') 354 355 (self.path_exists(f) for f in ( 356 filepath_layers, 357 filepath_json_reference, 358 )) 359 360 filepath_saved = os.path.join(dirpath, '{0}.blend'.format(copy_mode)) 361 filepath_json = os.path.join(dirpath, "{0}.json".format(copy_mode)) 362 363 bpy.ops.wm.open_mainfile('EXEC_DEFAULT', filepath=filepath_layers) 364 self.rename_collections() 365 bpy.ops.scene.new(type=copy_mode) 366 bpy.ops.wm.save_mainfile('EXEC_DEFAULT', filepath=filepath_saved) 367 368 datas = query_scene(filepath_saved, 'Main.001', data_callbacks) 369 self.assertTrue(datas, "Data is not valid") 370 371 with open(filepath_json, "w") as f: 372 for data in datas: 373 f.write(dump(data)) 374 375 self.assertTrue(compare_files( 376 filepath_json, 377 filepath_json_reference, 378 ), 379 "Scene copy \"{0}\" test failed".format(copy_mode.title())) 380 381 def do_object_delete(self, del_mode): 382 import bpy 383 import os 384 import tempfile 385 import filecmp 386 387 ROOT = self.get_root() 388 with tempfile.TemporaryDirectory() as dirpath: 389 filepath_layers = os.path.join(ROOT, 'layers.blend') 390 filepath_reference_json = os.path.join(ROOT, 'layers_object_delete.json') 391 392 # open file 393 bpy.ops.wm.open_mainfile('EXEC_DEFAULT', filepath=filepath_layers) 394 self.rename_collections() 395 396 # create sub-collections 397 three_b = bpy.data.objects.get('T.3b') 398 three_d = bpy.data.objects.get('T.3d') 399 400 scene = bpy.context.scene 401 402 # mangle the file a bit with some objects linked across collections 403 subzero = scene.master_collection.collections['1'].collections.new('sub-zero') 404 scorpion = subzero.collections.new('scorpion') 405 subzero.objects.link(three_d) 406 scorpion.objects.link(three_b) 407 scorpion.objects.link(three_d) 408 409 # object to delete 410 ob = three_d 411 412 # delete object 413 if del_mode == 'DATA': 414 bpy.data.objects.remove(ob, do_unlink=True) 415 416 elif del_mode == 'OPERATOR': 417 bpy.context.view_layer.update() # update depsgraph 418 bpy.ops.object.select_all(action='DESELECT') 419 ob.select_set(True) 420 self.assertTrue(ob.select_get()) 421 bpy.ops.object.delete() 422 423 # save file 424 filepath_generated = os.path.join(dirpath, 'generated.blend') 425 bpy.ops.wm.save_mainfile('EXEC_DEFAULT', filepath=filepath_generated) 426 427 # get the generated json 428 datas = query_scene(filepath_generated, 'Main', (get_scene_collections, get_layers)) 429 self.assertTrue(datas, "Data is not valid") 430 431 filepath_generated_json = os.path.join(dirpath, "generated.json") 432 with open(filepath_generated_json, "w") as f: 433 for data in datas: 434 f.write(dump(data)) 435 436 self.assertTrue(compare_files( 437 filepath_generated_json, 438 filepath_reference_json, 439 ), 440 "Scene dump files differ") 441 442 def do_visibility_object_add(self, add_mode): 443 import bpy 444 445 scene = bpy.context.scene 446 447 # delete all objects of the file 448 for ob in bpy.data.objects: 449 bpy.data.objects.remove(ob, do_unlink=True) 450 451 # real test 452 layer = scene.view_layers.new('Visibility Test') 453 layer.collections.unlink(layer.collections[0]) 454 455 scene_collection = scene.master_collection.collections.new("Collection") 456 layer.collections.link(scene_collection) 457 458 bpy.context.view_layer.update() # update depsgraph 459 460 self.assertEqual(len(bpy.data.objects), 0) 461 462 # add new objects 463 if add_mode == 'EMPTY': 464 bpy.ops.object.add() # 'Empty' 465 466 elif add_mode == 'CYLINDER': 467 bpy.ops.mesh.primitive_cylinder_add() # 'Cylinder' 468 469 elif add_mode == 'TORUS': 470 bpy.ops.mesh.primitive_torus_add() # 'Torus' 471 472 self.assertEqual(len(bpy.data.objects), 1) 473 474 new_ob = bpy.data.objects[0] 475 self.assertTrue(new_ob.visible_get(), "Object should be visible") 476 477 def cleanup_tree(self): 478 """ 479 Remove any existent layer and collections, 480 leaving only the one view_layer we can't remove 481 """ 482 import bpy 483 scene = bpy.context.scene 484 while len(scene.view_layers) > 1: 485 scene.view_layers.remove(scene.view_layers[1]) 486 487 layer = scene.view_layers[0] 488 while layer.collections: 489 layer.collections.unlink(layer.collections[0]) 490 491 master_collection = scene.master_collection 492 while master_collection.collections: 493 master_collection.collections.remove(master_collection.collections[0]) 494 495 def rename_collections(self, collection=None): 496 """ 497 Rename 'Collection 1' to '1' 498 """ 499 def strip_name(collection): 500 import re 501 if collection.name.startswith("Default Collection"): 502 collection.name = '1' 503 else: 504 collection.name = re.findall(r'\d+', collection.name)[0] 505 506 if collection is None: 507 import bpy 508 collection = bpy.context.scene.master_collection 509 510 for nested_collection in collection.collections: 511 strip_name(nested_collection) 512 self.rename_collections(nested_collection) 513 514 515class MoveSceneCollectionTesting(ViewLayerTesting): 516 """ 517 To be used by tests of view_layer_move_into_scene_collection 518 """ 519 520 def get_initial_scene_tree_map(self): 521 collections_map = [ 522 ['A', [ 523 ['i', None], 524 ['ii', None], 525 ['iii', None], 526 ]], 527 ['B', None], 528 ['C', [ 529 ['1', None], 530 ['2', None], 531 ['3', [ 532 ['dog', None], 533 ['cat', None], 534 ]], 535 ]], 536 ] 537 return collections_map 538 539 def build_scene_tree(self, tree_map, collection=None, ret_dict=None): 540 """ 541 Returns a flat dictionary with new scene collections 542 created from a nested tuple of nested tuples (name, tuple) 543 """ 544 import bpy 545 546 if collection is None: 547 collection = bpy.context.scene.master_collection 548 549 if ret_dict is None: 550 ret_dict = {collection.name: collection} 551 self.assertEqual(collection.name, "Master Collection") 552 553 for name, nested_collections in tree_map: 554 new_collection = collection.collections.new(name) 555 ret_dict[name] = new_collection 556 557 if nested_collections: 558 self.build_scene_tree(nested_collections, new_collection, ret_dict) 559 560 return ret_dict 561 562 def setup_tree(self): 563 """ 564 Cleanup file, and populate it with class scene tree map 565 """ 566 self.cleanup_tree() 567 self.assertTrue( 568 hasattr(self, "get_initial_scene_tree_map"), 569 "Test class has no get_initial_scene_tree_map method implemented") 570 571 return self.build_scene_tree(self.get_initial_scene_tree_map()) 572 573 def get_scene_tree_map(self, collection=None, ret_list=None): 574 """ 575 Extract the scene collection tree from scene 576 Return as a nested list of nested lists (name, list) 577 """ 578 import bpy 579 580 if collection is None: 581 scene = bpy.context.scene 582 collection = scene.master_collection 583 584 if ret_list is None: 585 ret_list = [] 586 587 for nested_collection in collection.collections: 588 new_collection = [nested_collection.name, None] 589 ret_list.append(new_collection) 590 591 if nested_collection.collections: 592 new_collection[1] = list() 593 self.get_scene_tree_map(nested_collection, new_collection[1]) 594 595 return ret_list 596 597 def compare_tree_maps(self): 598 """ 599 Compare scene with expected (class defined) data 600 """ 601 self.assertEqual(self.get_scene_tree_map(), self.get_reference_scene_tree_map()) 602 603 604class MoveSceneCollectionSyncTesting(MoveSceneCollectionTesting): 605 """ 606 To be used by tests of view_layer_move_into_scene_collection_sync 607 """ 608 609 def get_initial_layers_tree_map(self): 610 layers_map = [ 611 ['Layer 1', [ 612 'Master Collection', 613 'C', 614 '3', 615 ]], 616 ['Layer 2', [ 617 'C', 618 '3', 619 'dog', 620 'cat', 621 ]], 622 ] 623 return layers_map 624 625 def get_reference_layers_tree_map(self): 626 """ 627 For those classes we don't expect any changes in the layer tree 628 """ 629 return self.get_initial_layers_tree_map() 630 631 def setup_tree(self): 632 tree = super(MoveSceneCollectionSyncTesting, self).setup_tree() 633 634 import bpy 635 scene = bpy.context.scene 636 637 self.assertTrue( 638 hasattr(self, "get_initial_layers_tree_map"), 639 "Test class has no get_initial_layers_tree_map method implemented") 640 641 layers_map = self.get_initial_layers_tree_map() 642 643 for layer_name, collections_names in layers_map: 644 layer = scene.view_layers.new(layer_name) 645 layer.collections.unlink(layer.collections[0]) 646 647 for collection_name in collections_names: 648 layer.collections.link(tree[collection_name]) 649 650 return tree 651 652 def compare_tree_maps(self): 653 """ 654 Compare scene with expected (class defined) data 655 """ 656 super(MoveSceneCollectionSyncTesting, self).compare_tree_maps() 657 658 import bpy 659 scene = bpy.context.scene 660 layers_map = self.get_reference_layers_tree_map() 661 662 for layer_name, collections_names in layers_map: 663 layer = scene.view_layers.get(layer_name) 664 self.assertTrue(layer) 665 self.assertEqual(len(collections_names), len(layer.collections)) 666 667 for i, collection_name in enumerate(collections_names): 668 self.assertEqual(collection_name, layer.collections[i].name) 669 self.verify_collection_tree(layer.collections[i]) 670 671 def verify_collection_tree(self, layer_collection): 672 """ 673 Check if the LayerCollection mimics the SceneLayer tree 674 """ 675 scene_collection = layer_collection.collection 676 self.assertEqual(len(layer_collection.collections), len(scene_collection.collections)) 677 678 for i, nested_collection in enumerate(layer_collection.collections): 679 self.assertEqual(nested_collection.collection.name, scene_collection.collections[i].name) 680 self.assertEqual(nested_collection.collection, scene_collection.collections[i]) 681 self.verify_collection_tree(nested_collection) 682 683 684class MoveLayerCollectionTesting(MoveSceneCollectionSyncTesting): 685 """ 686 To be used by tests of view_layer_move_into_layer_collection 687 """ 688 689 def parse_move(self, path, sep='.'): 690 """ 691 convert 'Layer 1.C.2' into: 692 bpy.context.scene.view_layers['Layer 1'].collections['C'].collections['2'] 693 """ 694 import bpy 695 696 paths = path.split(sep) 697 layer = bpy.context.scene.view_layers[paths[0]] 698 collections = layer.collections 699 700 for subpath in paths[1:]: 701 collection = collections[subpath] 702 collections = collection.collections 703 704 return collection 705 706 def move_into(self, src, dst): 707 layer_collection_src = self.parse_move(src) 708 layer_collection_dst = self.parse_move(dst) 709 return layer_collection_src.move_into(layer_collection_dst) 710 711 def move_above(self, src, dst): 712 layer_collection_src = self.parse_move(src) 713 layer_collection_dst = self.parse_move(dst) 714 return layer_collection_src.move_above(layer_collection_dst) 715 716 def move_below(self, src, dst): 717 layer_collection_src = self.parse_move(src) 718 layer_collection_dst = self.parse_move(dst) 719 return layer_collection_src.move_below(layer_collection_dst) 720 721 722class Clay: 723 def __init__(self, extra_kid_layer=False): 724 import bpy 725 726 self._scene = bpy.context.scene 727 self._layer = self._fresh_layer() 728 self._object = bpy.data.objects.new('guinea pig', bpy.data.meshes.new('mesh')) 729 730 # update depsgraph 731 self._layer.update() 732 733 scene_collection_grandma = self._scene.master_collection.collections.new("Grandma") 734 scene_collection_mom = scene_collection_grandma.collections.new("Mom") 735 scene_collection_kid = scene_collection_mom.collections.new("Kid") 736 scene_collection_kid.objects.link(self._object) 737 738 layer_collection_grandma = self._layer.collections.link(scene_collection_grandma) 739 layer_collection_mom = layer_collection_grandma.collections[0] 740 layer_collection_kid = layer_collection_mom.collections[0] 741 742 # store the variables 743 self._scene_collections = { 744 'grandma': scene_collection_grandma, 745 'mom': scene_collection_mom, 746 'kid': scene_collection_kid, 747 } 748 self._layer_collections = { 749 'grandma': layer_collection_grandma, 750 'mom': layer_collection_mom, 751 'kid': layer_collection_kid, 752 } 753 754 if extra_kid_layer: 755 layer_collection_extra = self._layer.collections.link(scene_collection_kid) 756 self._layer_collections['extra'] = layer_collection_extra 757 758 self._update() 759 760 def _fresh_layer(self): 761 import bpy 762 763 # remove all other objects 764 while bpy.data.objects: 765 bpy.data.objects.remove(bpy.data.objects[0]) 766 767 # remove all the other collections 768 while self._scene.master_collection.collections: 769 self._scene.master_collection.collections.remove( 770 self._scene.master_collection.collections[0]) 771 772 layer = self._scene.view_layers.new('Evaluation Test') 773 layer.collections.unlink(layer.collections[0]) 774 bpy.context.window.view_layer = layer 775 776 # remove all other layers 777 for layer_iter in self._scene.view_layers: 778 if layer_iter != layer: 779 self._scene.view_layers.remove(layer_iter) 780 781 return layer 782 783 def _update(self): 784 """ 785 Force depsgrpah evaluation 786 and update pointers to IDProperty collections 787 """ 788 ENGINE = 'BLENDER_CLAY' 789 790 self._layer.update() # flush depsgraph evaluation 791 792 # change scene settings 793 self._properties = { 794 'scene': self._scene.collection_properties[ENGINE], 795 'object': self._object.collection_properties[ENGINE], 796 } 797 798 for key, value in self._layer_collections.items(): 799 self._properties[key] = self._layer_collections[key].engine_overrides[ENGINE] 800 801 def get(self, name, data_path): 802 self._update() 803 return getattr(self._properties[name], data_path) 804 805 def set(self, name, data_path, value): 806 self._update() 807 self._properties[name].use(data_path) 808 setattr(self._properties[name], data_path, value) 809 810 811def setup_extra_arguments(filepath): 812 """ 813 Create a value which is assigned to: ``UnitTesting._extra_arguments`` 814 """ 815 import sys 816 817 extra_arguments = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] 818 sys.argv = [filepath] + extra_arguments[1:] 819 820 return extra_arguments 821