Context Managers

If possible, one should avoid relying on Blender operators, but sometimes they are just too useful to pass over. In those cases, it is necessary to manage the context and modes explicitly.

Blender provides bpy.types.Context.temp_override to manage the context.

As for modes, Blender provides bpy.ops.object.mode_set and bpy.ops.object.mode_set_with_submode. These operators are wrapped by the mode() context manager.

>>> import bpy
>>> from ddg.blender.context import mode
>>>
>>> bpy.ops.mesh.primitive_cube_add()
{'FINISHED'}
>>> cube = bpy.context.object
>>> with mode(cube, mode="EDIT", exit_mode="OBJECT"):
...     print(f"Mode within the context: {cube.mode}")
...
Mode within the context: EDIT
>>> print(f"Mode after exiting the context: {cube.mode}")
Mode after exiting the context: OBJECT

Note that Blender doesn’t write changes to edit meshes to the mesh until the object exits edit mode.

>>> import bmesh
>>> import numpy as np
>>>
>>> co_original = np.vstack([v.co for v in cube.data.vertices])
>>> with mode(cube, mode="EDIT", exit_mode="OBJECT"):
...     bm = bmesh.from_edit_mesh(cube.data)
...     for v in bm.verts:
...         v.co *= 3
...     bmesh.update_edit_mesh(cube.data)
...     co_in_context = np.vstack([v.co for v in cube.data.vertices])
...     # Note that even after calling bmesh.update_edit_mesh,
...     # cube.data.vertices coordinates haven't changed.
...     # The changes are visible in the UI however!
...     assert np.allclose(co_original, co_in_context)
...

Upon exiting the context, the mode is set to OBJECT mode. Since this is a different mode from EDIT mode, the changes made in EDIT mode are written to cube.data.

>>> co_after_exit = np.vstack([v.co for v in cube.data.vertices])
>>> assert np.allclose(3 * co_original, co_after_exit)

The above example motivates bmesh_from_mesh(), yet another context manager. It creates a BMesh from a mesh and automatically calls bmesh.update_edit_mesh or bmesh.to_mesh depending on whether the mesh is in edit mode or not. In the latter case, it also ensures that the BMesh is freed. The above example (without the assertions) can be refactored as such :

>>> from ddg.blender.bmesh import bmesh_from_mesh
>>> with mode(cube, mode="EDIT", exit_mode="OBJECT"):
...     with bmesh_from_mesh(cube.data) as bm:
...         for v in bm.verts:
...             v.co *= 3
...