7
$\begingroup$

I feel like I'm always spending far too much time playing scripting whack-a-mole whenever I want to override an operator in one of my scripts. I'd like to build a comprehensive one-stop-shop list for the most used operator overrides.

$\endgroup$
1
  • 1
    $\begingroup$ Since Blender 3.2 passing the context to operators as a dictionary has been deprecated as part of the inclusion of Context.temp_override which should be used instead. $\endgroup$ Jun 8 at 19:10

2 Answers 2

3
$\begingroup$

Similarly to the method I used in this answer, one can derive operator overrides from the way they are coded internally (wether in C or in Python). It's time consuming and sometimes headache-inducing, but I believe it's worth it, at least for the most used ones.

Following this Answer it seems there are currently around 2200 operators in total in Blender. It's a hard task but I at least intend to update the list every time I come across a situation where I need an override.

Most of the solutions I randomly find suggest overriding the area, screen, space or window which is very cumbersome and prevents using operators headlessly, when most of the time these override parameters are not required.

One can use the search function of their internet navigator to find the relevant operator and see how the override fares :

Since Blender V3.2 the supported way to use operator override is with the Context.temp_override method.

Example :

    bpy.ops.object.delete({"selected_objects": objects_to_delete})

Should now be written as

with context.temp_override(selected_objects=objects_to_delete):
    bpy.ops.object.delete()

Here's the pre 3.2 syntax. You just have to pass the dictionary keys as keyword arguments and the values as the argument values to make it work in 3.2+.

bpy.ops.ed.lib_id_generate_preview({"id": obj})  # Note : need to wait a little bit for preview to generate
bpy.ops.ed.lib_id_load_custom_preview({"id": obj}, filepath=file_path)

bpy.ops.info.select_all({"area": area})  # Where area.type == "INFO"
bpy.ops.info.report_copy({"area": area})  # Where area.type == "INFO"

bpy.ops.object.delete({"selected_objects": objects})
bpy.ops.object.duplicates_make_real({"selected_objects": selected_objects})
bpy.ops.object.geometry_nodes_input_attribute_toggle({"object": obj}, prop_path="[\"Input_i_use_attribute\"]", modifier_name="my_mod_name")
bpy.ops.object.join({"active_object": obj, "selected_editable_objects": objects})
bpy.ops.object.make_single_user({"selected_objects": selected_objects}, object=True, obdata=True, material=False, animation=False)
bpy.ops.object.material_slot_remove({'object': obj})
bpy.ops.object.modifier_apply({"object": obj, "view_layer": view_layer, "scene": scene}, modifier="weld")  # scene and view_layer parameters are not mandatory, modifier parameter is the modifier name
bpy.ops.object.modifier_move_down({'object': obj}, modifier=mod_name)
bpy.ops.object.modifier_move_to_index({'object': obj}, modifier=mod.name, index=i)
bpy.ops.object.modifier_move_up({'object': obj}, modifier=mod_name)
bpy.ops.object.origin_set({"selected_editable_objects": editable_objects}, type='GEOMETRY_ORIGIN', center='MEDIAN')  # Where selected_editable_objects is an iterable of editable objects
bpy.ops.object.parent_clear({"selected_editable_objects": editable_objects}, type="CLEAR_KEEP_TRANSFORM")  # Where selected_editable_objects is an iterable of editable objects and type is in ['CLEAR', 'CLEAR_KEEP_TRANSFORM', 'CLEAR_INVERSE']
bpy.ops.object.posemode_toggle({"active_object": arm})  # Where arm.type = "ARMATURE"
bpy.ops.object.select_all({"area": area}, action='SELECT') # action in ('TOGGLE', 'SELECT', 'DESELECT', 'INVERT')
bpy.ops.object.transform_apply({"selected_editable_objects": mesh_objects}, location=True, rotation=True, scale=True)  # Where selected_editable_objects is an iterable of editable objects

bpy.ops.mesh.customdata_custom_splitnormals_clear({"mesh": mesh}) # Mesh can be obtained eg with obj.data

bpy.ops.palette.extract_from_image({"area": area}, threshold=1) # Where area.image is set to an instance of bpy.types.Image and area.type == "IMAGE_EDITOR"

bpy.ops.screen.area_close({"area": area})

bpy.ops.workspace.delete({"workspace": workspace})

Note : bpy.ops.object.convert can't be overriden. See https://developer.blender.org/T93188

$\endgroup$
4
  • $\begingroup$ have you asked about this in devtalk? perhaps someone has a script that they can run over the source to generate such a table. $\endgroup$ Dec 27, 2021 at 22:28
  • $\begingroup$ That's a great idea ! I'll make sure to ask there, although I think since there are quite intricate dependencies and edge cases it might be a bit hard to get a reliable table. $\endgroup$
    – Gorgious
    Dec 27, 2021 at 22:39
  • 2
    $\begingroup$ @Gorgious Good initiative! Too bad that the only two upvotes are mine from a month ago... I'd propose linking the Q&A's as well. What do you think? I would take care of it, if there is some time next week. $\endgroup$
    – p2or
    Jan 23 at 21:52
  • $\begingroup$ @p2or Yeah I think all the efforts we make in this direction will bear fruits in the long run even if it is not payed in upvotes !! Thanks for the interest :) $\endgroup$
    – Gorgious
    Jan 23 at 22:49
2
$\begingroup$

I'm still working on my script that helps me to detect what elements a context override requires, but I guess I might as well share it now and update it later...

Making context copy

First, prepare a situation, in which an operator works. If you can simply run it from a console, then you can type:

c = {k:v for k, v in C.copy().items() if v is not None}

This makes a copy of a current context, as well as removes empty (None) values for readability. Theoretically a context could require some value to be in the context (override) and be None, but I just assume this is not the case.

If you can't make he operator work from console, perhaps you can do this little trick, that changes area type, and then makes context copy, and changes it back:

C.area.type = 'NODE_EDITOR'; c = {k:v for k, v in C.copy().items() if v is not None};

(you can add ; C.area.type = 'CONSOLE' after, but you can also just press ShiftF4 to return to the console)

(typing C.area.type = 0 will error with a list of available area types)

If your valid context appears in more exotic circumstances: a timer, msgbus/draw/property callback, an operator or whatever else - you can just save the context copy assigned to some type (I recommend any except bpy.types.Context), for example:

bpy.types.WindowManager.context_copy = C.copy()

Context Override Checker:

import bpy
from bpy import data as D, context as C

log_path = "E:/log.txt"


'''
Usage:
    
c = {k:v for k, v in C.copy().items() if v is not None}
D.texts['Text'].as_module().test(c)
'''


def log(text):
    with open(file=log_path, mode="a") as f:
        f.write(text)


def test(context_override):
    cpy = context_override.copy()
    
    # overriding those 3 keys with `None` tends to crash Blender, so I just skip checking those
    cpy.pop('scene')
    cpy.pop('view_layer')
    cpy.pop('blend_data')
    
    # you can skip removing those keys if you know they're needed
    skip = [
        'object',
        'selected_objects'
    ]
    to_remove = {k:v for k,v in cpy.items() if k not in skip}
    
    for k, v in to_remove.items():
        #prepare the environment here...
        log(k)
        cpy[k] = None  # override context value with None
        try:
            '''
                IMPORTANT!
                REMEMBER TO PASS CONTEXT OVERRIDE TO THE OPERATOR
                                 |
                                 |
            '''  #               |
                 #               v
            result = bpy.ops...(cpy, arguments...)            
            C.view_layer.update()
            if ... : # find a way to check if the operator failed
                log(f"  {result=}")
                raise ValueError
            else:
                log(" - success\n")
                # now undo all changes a successful operator has caused
                C.view_layer.update()
        except ValueError:
            log(" - ERROR\n") 
            cpy[k] = v  # restore required value
            
        # perhaps some cleanup regardless of the operator's success or failure
    
    what_is_left = {k:v for k,v in cpy.items() if v is not None}
    log(f"\n\n\n{what_is_left}") # show what's left in the context override

Then in console:

D.texts['Text'].as_module().test(c)

Example

import bpy
from bpy import data as D, context as C

log_path = "E:/log.txt"


def log(text):
    with open(file=log_path, mode="a") as f:
        f.write(text)


def test(context_override):
    cpy = context_override.copy()
    cpy.pop('scene')
    cpy.pop('view_layer')
    cpy.pop('blend_data')
    
    skip = [
#        'object',
#        'selected_objects'
    ]
    to_remove = {k:v for k,v in cpy.items() if k not in skip}
    
    for k, v in to_remove.items():
        #prepare the environment here...
        cube = D.objects['Cube']
        log(k)
        cpy[k] = None  # override context value with None
        try:
            cube.location = 0,0,0
            result = bpy.ops.transform.translate(cpy, value=(0, 3, 0))     
            C.view_layer.update()
            if cube.location.y < 1:  # should be 3
                log(f"  {result=}")
                raise ValueError
            else:
                log(" - success\n")
                # no need to undo anything here, as the location is set to 0 after `try`
        except ValueError:
            log(" - ERROR\n") 
            cpy[k] = v  # restore required value
    
    what_is_left = {k:v for k,v in cpy.items() if v is not None}
    log(f"\n\n\n{what_is_left}") # show what's left in the context override

Then in console:

>>> c = {k:v for k, v in C.copy().items() if v is not None}
>>> D.texts['Text'].as_module().test(c)

Resulting log.txt file

active_object - success
active_operator - success
area - success
asset_library_ref - success
collection - success
editable_objects - success
engine - success
evaluated_depsgraph_get - success
layer_collection - success
mode - success
object - success
objects_in_mode - success
objects_in_mode_unique_data - success
preferences - success
region - success
screen - success
selectable_objects - success
selected_editable_objects - success
selected_editable_sequences - success
selected_objects  result={'CANCELLED'} - ERROR
selected_sequences - success
sequences - success
space_data - success
tool_settings - success
visible_objects - success
window - success
window_manager - success
workspace - success



{'selected_objects': [bpy.data.objects['Cube']]}
$\endgroup$
2
  • $\begingroup$ Ooh nice I do like the bulldozer approach, test every possible combination and selectively remove keys. Smart ! Would need another recursion of tests I guess if there are more than one mandatory key, like bpy.ops.object.join which might send the computation through the roof but I like the approach. Thankfully there aren't that many. Cheers $\endgroup$
    – Gorgious
    Feb 9 at 6:25
  • $\begingroup$ @Gorgious the assumption is that breaking any link breaks the chain, i.e. if an operator uses multiple values, not providing any will fail the operator. But indeed I find it quite likely an operator might use one value and fall back to another if it's not provided, in which case perhaps the script could try again in reversed order? 🤔 $\endgroup$ Feb 9 at 9:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.