[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