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.
2 Answers
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.file.execute({"window": window, "area":area})
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_copy_to_selected({"object": obj, "selected_objects": selected_objects}, modifier=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.parent_set({"object": obj, "selected_editable_objects": editable_objects})
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.shade_smooth({"selected_editable_objects": mesh_objects})
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.script.execute_preset({"window": window, "area":area}, filepath=preset_path, menu_idname=MENU_IDNAME)
bpy.ops.sequencer.sound_strip_add({"area": area}, filepath=filepath)
bpy.ops.text.reload({"area": area})
bpy.ops.workspace.delete({"workspace": workspace})
bpy.ops.ptcache.bake({'scene'=scene, 'active_object'=obj, 'point_cache'=point_cache}) # where point_cache is the point_cache property on the modifier you want to bake (e.g. a cloth modifier)
bpy.ops.fluid.bake_data({'scene'=scene, 'active_object'=obj})
Note :
bpy.ops.object.convert
can't be overriden. See https://developer.blender.org/T93188
-
$\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$– GorgiousDec 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$– p2orJan 23, 2022 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$– GorgiousJan 23, 2022 at 22:49
-
$\begingroup$ @Gorgious how about
bpy.ops.uv.smart_project
? it doesn't work withselected_objects
orselected_editable_objects
. i can't tell it which objects are selected using:with bpy.context.temp_override(selected_objects=selected_objects): bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.smart_project()
. i have to manually select the objects with a for loop before executingsmart_project
$\endgroup$ Jul 28 at 6:53
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']]}
-
$\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$– GorgiousFeb 9, 2022 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, 2022 at 9:58
Context.temp_override
which should be used instead. $\endgroup$