[Bf-blender-cvs] [f438344cf24] master: PyAPI: temporary context override support

Campbell Barton noreply at git.blender.org
Wed Apr 20 04:22:13 CEST 2022


Commit: f438344cf243632e497772cf1f855b9c8856fd37
Author: Campbell Barton
Date:   Wed Apr 13 16:40:07 2022 +1000
Branches: master
https://developer.blender.org/rBf438344cf243632e497772cf1f855b9c8856fd37

PyAPI: temporary context override support

Support a way to temporarily override the context from Python.

- Added method `Context.temp_override` context manager.
- Special support for windowing variables "window", "area" and "region",
  other context members such as "active_object".
- Nesting context overrides is supported.
- Previous windowing members are restored when the context exists unless
  they have been removed.
- Overriding context members by passing a dictionary into operators in
  `bpy.ops` has been deprecated and warns when used.

This allows the window in a newly loaded file to be used, see: T92464

Reviewed by: mont29

Ref D13126

===================================================================

A	doc/python_api/examples/bpy.types.Context.temp_override.1.py
A	doc/python_api/examples/bpy.types.Context.temp_override.2.py
A	doc/python_api/examples/bpy.types.Context.temp_override.3.py
M	source/blender/python/intern/CMakeLists.txt
M	source/blender/python/intern/bpy_operator.c
A	source/blender/python/intern/bpy_rna_context.c
A	source/blender/python/intern/bpy_rna_context.h
M	source/blender/python/intern/bpy_rna_types_capi.c

===================================================================

diff --git a/doc/python_api/examples/bpy.types.Context.temp_override.1.py b/doc/python_api/examples/bpy.types.Context.temp_override.1.py
new file mode 100644
index 00000000000..68f0eef93c3
--- /dev/null
+++ b/doc/python_api/examples/bpy.types.Context.temp_override.1.py
@@ -0,0 +1,19 @@
+"""
+Overriding the context can be used to temporarily activate another ``window`` / ``area`` & ``region``,
+as well as other members such as the ``active_object`` or ``bone``.
+
+Notes:
+
+- When overriding window, area and regions: the arguments must be consistent,
+  so any region argument that's passed in must be contained by the current area or the area passed in.
+  The same goes for the area needing to be contained in the current window.
+
+- Temporary context overrides may be nested, when this is done, members will be added to the existing overrides.
+
+- Context members are restored outside the scope of the context.
+  The only exception to this is when the data is no longer available.
+
+  In the event windowing data was removed (for example), the state of the context is left as-is.
+  While this isn't likely to happen, explicit window operation such as closing windows or loading a new file
+  remove the windowing data that was set before the temporary context was created.
+"""
diff --git a/doc/python_api/examples/bpy.types.Context.temp_override.2.py b/doc/python_api/examples/bpy.types.Context.temp_override.2.py
new file mode 100644
index 00000000000..ce3e1594baa
--- /dev/null
+++ b/doc/python_api/examples/bpy.types.Context.temp_override.2.py
@@ -0,0 +1,15 @@
+"""
+Overriding the context can be useful to set the context after loading files
+(which would otherwise by None). For example:
+"""
+
+import bpy
+from bpy import context
+
+# Reload the current file and select all.
+bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath)
+window = context.window_manager.windows[0]
+with context.temp_override(window=window):
+    bpy.ops.mesh.primitive_uv_sphere_add()
+    # The context override is needed so it's possible to set edit-mode.
+    bpy.ops.object.mode_set(mode='EDIT')
diff --git a/doc/python_api/examples/bpy.types.Context.temp_override.3.py b/doc/python_api/examples/bpy.types.Context.temp_override.3.py
new file mode 100644
index 00000000000..e670bb7bafa
--- /dev/null
+++ b/doc/python_api/examples/bpy.types.Context.temp_override.3.py
@@ -0,0 +1,16 @@
+"""
+This example shows how it's possible to add an object to the scene in another window.
+"""
+import bpy
+from bpy import context
+
+win_active = context.window
+win_other = None
+for win_iter in context.window_manager.windows:
+    if win_iter != win_active:
+        win_other = win_iter
+        break
+
+# Add cube in the other window.
+with context.temp_override(window=win_other):
+    bpy.ops.mesh.primitive_cube_add()
diff --git a/source/blender/python/intern/CMakeLists.txt b/source/blender/python/intern/CMakeLists.txt
index 86dc5800b67..e4e198ab812 100644
--- a/source/blender/python/intern/CMakeLists.txt
+++ b/source/blender/python/intern/CMakeLists.txt
@@ -60,6 +60,7 @@ set(SRC
   bpy_rna_anim.c
   bpy_rna_array.c
   bpy_rna_callback.c
+  bpy_rna_context.c
   bpy_rna_data.c
   bpy_rna_driver.c
   bpy_rna_gizmo.c
@@ -101,6 +102,7 @@ set(SRC
   bpy_rna.h
   bpy_rna_anim.h
   bpy_rna_callback.h
+  bpy_rna_context.h
   bpy_rna_data.h
   bpy_rna_driver.h
   bpy_rna_gizmo.h
diff --git a/source/blender/python/intern/bpy_operator.c b/source/blender/python/intern/bpy_operator.c
index db0067fc18e..0cfe6dab2f5 100644
--- a/source/blender/python/intern/bpy_operator.c
+++ b/source/blender/python/intern/bpy_operator.c
@@ -60,6 +60,18 @@ static wmOperatorType *ot_lookup_from_py_string(PyObject *value, const char *py_
   return ot;
 }
 
+static void op_context_override_deprecated_warning(void)
+{
+  if (PyErr_WarnEx(PyExc_DeprecationWarning,
+                   "Passing in context overrides is deprecated in favor of "
+                   "Context.temp_override(..)",
+                   1) < 0) {
+    /* The function has no return value, the exception cannot
+     * be reported to the caller, so just log it. */
+    PyErr_WriteUnraisable(NULL);
+  }
+}
+
 static PyObject *pyop_poll(PyObject *UNUSED(self), PyObject *args)
 {
   wmOperatorType *ot;
@@ -113,7 +125,10 @@ static PyObject *pyop_poll(PyObject *UNUSED(self), PyObject *args)
   if (ELEM(context_dict, NULL, Py_None)) {
     context_dict = NULL;
   }
-  else if (!PyDict_Check(context_dict)) {
+  else if (PyDict_Check(context_dict)) {
+    op_context_override_deprecated_warning();
+  }
+  else {
     PyErr_Format(PyExc_TypeError,
                  "Calling operator \"bpy.ops.%s.poll\" error, "
                  "custom context expected a dict or None, got a %.200s",
@@ -218,7 +233,10 @@ static PyObject *pyop_call(PyObject *UNUSED(self), PyObject *args)
   if (ELEM(context_dict, NULL, Py_None)) {
     context_dict = NULL;
   }
-  else if (!PyDict_Check(context_dict)) {
+  else if (PyDict_Check(context_dict)) {
+    op_context_override_deprecated_warning();
+  }
+  else {
     PyErr_Format(PyExc_TypeError,
                  "Calling operator \"bpy.ops.%s\" error, "
                  "custom context expected a dict or None, got a %.200s",
diff --git a/source/blender/python/intern/bpy_rna_context.c b/source/blender/python/intern/bpy_rna_context.c
new file mode 100644
index 00000000000..085a8323cc1
--- /dev/null
+++ b/source/blender/python/intern/bpy_rna_context.c
@@ -0,0 +1,299 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup pythonintern
+ *
+ * This file adds some helper methods to the context, that cannot fit well in RNA itself.
+ */
+
+#include <Python.h>
+
+#include "BLI_listbase.h"
+#include "BLI_utildefines.h"
+
+#include "BKE_context.h"
+
+#include "WM_api.h"
+#include "WM_types.h"
+
+#include "bpy_rna_context.h"
+
+#include "RNA_access.h"
+#include "RNA_prototypes.h"
+
+#include "bpy_rna.h"
+
+/* -------------------------------------------------------------------- */
+/** \name Temporary Context Override (Python Context Manager)
+ * \{ */
+
+typedef struct ContextStore {
+  wmWindow *win;
+  bool win_is_set;
+  ScrArea *area;
+  bool area_is_set;
+  ARegion *region;
+  bool region_is_set;
+} ContextStore;
+
+typedef struct BPyContextTempOverride {
+  PyObject_HEAD /* Required Python macro. */
+  bContext *context;
+
+  ContextStore ctx_init;
+  ContextStore ctx_temp;
+  /** Bypass Python overrides set when calling an operator from Python. */
+  struct bContext_PyState py_state;
+  /**
+   * This dictionary is used to store members that don't have special handling,
+   * see: #bpy_context_temp_override_extract_known_args,
+   * these will then be accessed via #BPY_context_member_get.
+   *
+   * This also supports nested *stacking*, so a nested temp-context-overrides
+   * will overlay the new members on the old members (instead of ignoring them).
+   */
+  PyObject *py_state_context_dict;
+} BPyContextTempOverride;
+
+static void bpy_rna_context_temp_override__tp_dealloc(BPyContextTempOverride *self)
+{
+  PyObject_DEL(self);
+}
+
+static PyObject *bpy_rna_context_temp_override_enter(BPyContextTempOverride *self)
+{
+  bContext *C = self->context;
+
+  CTX_py_state_push(C, &self->py_state, self->py_state_context_dict);
+
+  self->ctx_init.win = CTX_wm_window(C);
+  self->ctx_init.win_is_set = (self->ctx_init.win != self->ctx_temp.win);
+  self->ctx_init.area = CTX_wm_area(C);
+  self->ctx_init.area_is_set = (self->ctx_init.area != self->ctx_temp.area);
+  self->ctx_init.region = CTX_wm_region(C);
+  self->ctx_init.region_is_set = (self->ctx_init.region != self->ctx_temp.region);
+
+  wmWindow *win = self->ctx_temp.win_is_set ? self->ctx_temp.win : self->ctx_init.win;
+  bScreen *screen = win ? WM_window_get_active_screen(win) : NULL;
+  ScrArea *area = self->ctx_temp.area_is_set ? self->ctx_temp.area : self->ctx_init.area;
+  ARegion *region = self->ctx_temp.region_is_set ? self->ctx_temp.region : self->ctx_init.region;
+
+  /* Sanity check, the region is in the screen/area. */
+  if (self->ctx_temp.region_is_set && (region != NULL)) {
+    if (area == NULL) {
+      PyErr_SetString(PyExc_TypeError, "Region set with NULL area");
+      return NULL;
+    }
+    if ((screen && BLI_findindex(&screen->regionbase, region) == -1) &&
+        (BLI_findindex(&area->regionbase, region) == -1)) {
+      PyErr_SetString(PyExc_TypeError, "Region not found in area");
+      return NULL;
+    }
+  }
+
+  if (self->ctx_temp.area_is_set && (area != NULL)) {
+    if (screen == NULL) {
+      PyErr_SetString(PyExc_TypeError, "Area set with NULL screen");
+      return NULL;
+    }
+    if (BLI_findindex(&screen->areabase, area) == -1) {
+      PyErr_SetString(PyExc_TypeError, "Area not found in screen");
+      return NULL;
+    }
+  }
+
+  if (self->ctx_temp.win_is_set) {
+    CTX_wm_window_set(C, self->ctx_temp.win);
+  }
+  if (self->ctx_temp.area_is_set) {
+    CTX_wm_area_set(C, self->ctx_temp.area);
+  }
+  if (self->ctx_temp.region_is_set) {
+    CTX_wm_region_set(C, self->ctx_temp.region);
+  }
+
+  Py_RETURN_NONE;
+}
+
+static PyObject *bpy_rna_context_temp_override_exit(BPyContextTempOverride *self,
+                                                    PyObject *UNUSED(args))
+{
+  bContext *C = self->context;
+
+  /* Special case where the window is expected to be freed on file-read,
+   * in this case the window should not be restored, see: T92818. */
+  bool do_restore = true;
+  if (self->ctx_init.win) {
+    wmWindowManager *wm = CTX_wm_manager(C);
+    if (BLI_findindex(&wm->windows, self->ctx_init.win) == -1) {
+      CTX_wm_window_set(C, NULL);
+      do_restore = false;
+    }
+  }
+
+  if (do_restore) {
+    if (self->ctx_init.win_is_set) {
+      CTX_wm_window_set(C, self->ctx_init.win);
+    }
+    if (self->ctx_init.area_is_set) {
+      CTX_wm_area_set(C, self->ctx_init.area);
+    }
+    if (self->ctx_init.region_is_set) {
+      CTX_wm_region_set(C, self->ctx_init.region);
+    }
+  }
+
+  CTX_py_state_pop(C, &self->py_state);
+  Py_CLEAR(self->py_state_context_dict);
+
+  Py_RETURN_NONE;
+}
+
+static PyMethodDef bpy_rna_context_temp_override__tp_methods[] = {
+    {"__enter__", (PyCFunction)bpy_rna_context_temp_overri

@@ Diff output truncated at 10240 characters. @@



More information about the Bf-blender-cvs mailing list