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