diff options
author | Campbell Barton <campbell@blender.org> | 2022-04-13 09:40:07 +0300 |
---|---|---|
committer | Campbell Barton <campbell@blender.org> | 2022-04-20 05:19:35 +0300 |
commit | f438344cf243632e497772cf1f855b9c8856fd37 (patch) | |
tree | d7121f1e456c2f499ba0e92dd2f38869c39b844c /source/blender/python | |
parent | 6d9268c2c7362ec772a3ff956ee888e877682a01 (diff) |
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
Diffstat (limited to 'source/blender/python')
-rw-r--r-- | source/blender/python/intern/CMakeLists.txt | 2 | ||||
-rw-r--r-- | source/blender/python/intern/bpy_operator.c | 22 | ||||
-rw-r--r-- | source/blender/python/intern/bpy_rna_context.c | 299 | ||||
-rw-r--r-- | source/blender/python/intern/bpy_rna_context.h | 17 | ||||
-rw-r--r-- | source/blender/python/intern/bpy_rna_types_capi.c | 16 |
5 files changed, 354 insertions, 2 deletions
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_override_enter, METH_NOARGS}, + {"__exit__", (PyCFunction)bpy_rna_context_temp_override_exit, METH_VARARGS}, + {NULL}, +}; + +static PyTypeObject BPyContextTempOverride_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ContextTempOverride", + .tp_basicsize = sizeof(BPyContextTempOverride), + .tp_dealloc = (destructor)bpy_rna_context_temp_override__tp_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = bpy_rna_context_temp_override__tp_methods, +}; + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Context Temporary Override Method + * \{ */ + +static PyObject *bpy_context_temp_override_extract_known_args(const char *const *kwds_static, + PyObject *kwds) +{ + PyObject *sentinel = Py_Ellipsis; + PyObject *kwds_parse = PyDict_New(); + for (int i = 0; kwds_static[i]; i++) { + PyObject *key = PyUnicode_FromString(kwds_static[i]); + PyObject *val = _PyDict_Pop(kwds, key, sentinel); + if (val != sentinel) { + if (PyDict_SetItem(kwds_parse, key, val) == -1) { + BLI_assert_unreachable(); + } + } + Py_DECREF(key); + Py_DECREF(val); + } + return kwds_parse; +} + +PyDoc_STRVAR(bpy_context_temp_override_doc, + ".. method:: temp_override(window, area, region, **keywords)\n" + "\n" + " Context manager to temporarily override members in the context.\n" + "\n" + " :arg window: Window override or None.\n" + " :type window: :class:`bpy.types.Window`\n" + " :arg area: Area override or None.\n" + " :type area: :class:`bpy.types.Area`\n" + " :arg region: Region override or None.\n" + " :type region: :class:`bpy.types.Region`\n" + " :arg keywords: Additional keywords override context members.\n" + " :return: The context manager .\n" + " :rtype: context manager\n"); +static PyObject *bpy_context_temp_override(PyObject *self, PyObject *args, PyObject *kwds) +{ + const PointerRNA *context_ptr = pyrna_struct_as_ptr(self, &RNA_Context); + if (context_ptr == NULL) { + return NULL; + } + /* Needed because the keywords copied into `kwds_parse` could contain anything. + * As the types of keys aren't checked. */ + if (!PyArg_ValidateKeywordArguments(kwds)) { + return NULL; + } + + struct { + struct BPy_StructRNA_Parse window; + struct BPy_StructRNA_Parse area; + struct BPy_StructRNA_Parse region; + } params = { + .window = {.type = &RNA_Window}, + .area = {.type = &RNA_Area}, + .region = {.type = &RNA_Region}, + }; + + static const char *const _keywords[] = {"window", "area", "region", NULL}; + static _PyArg_Parser _parser = { + "|$" /* Optional, keyword only arguments. */ + "O&" /* `window` */ + "O&" /* `area` */ + "O&" /* `region` */ + ":temp_override", + _keywords, + 0, + }; + /* Parse known keywords, the remaining keywords are set using #CTX_py_state_push. */ + kwds = PyDict_Copy(kwds); + { + PyObject *kwds_parse = bpy_context_temp_override_extract_known_args(_keywords, kwds); + const int parse_result = _PyArg_ParseTupleAndKeywordsFast(args, + kwds_parse, + &_parser, + pyrna_struct_as_ptr_or_null_parse, + ¶ms.window, + pyrna_struct_as_ptr_or_null_parse, + ¶ms.area, + pyrna_struct_as_ptr_or_null_parse, + ¶ms.region); + Py_DECREF(kwds_parse); + if (parse_result == -1) { + Py_DECREF(kwds); + return NULL; + } + } + + bContext *C = context_ptr->data; + { + /* Merge existing keys that don't exist in the keywords passed in. + * This makes it possible to nest context overrides. */ + PyObject *context_dict_current = CTX_py_dict_get(C); + if (context_dict_current != NULL) { + PyDict_Merge(kwds, context_dict_current, 0); + } + } + + ContextStore ctx_temp = {NULL}; + if (params.window.ptr != NULL) { + ctx_temp.win = params.window.ptr->data; + ctx_temp.win_is_set = true; + } + if (params.area.ptr != NULL) { + ctx_temp.area = params.area.ptr->data; + ctx_temp.area_is_set = true; + } + + if (params.region.ptr != NULL) { + ctx_temp.region = params.region.ptr->data; + ctx_temp.region_is_set = true; + } + + BPyContextTempOverride *ret = PyObject_New(BPyContextTempOverride, &BPyContextTempOverride_Type); + ret->context = C; + ret->ctx_temp = ctx_temp; + memset(&ret->ctx_init, 0, sizeof(ret->ctx_init)); + + ret->py_state_context_dict = kwds; + + return (PyObject *)ret; +} + +/** \} */ + +PyMethodDef BPY_rna_context_temp_override_method_def = { + "temp_override", + (PyCFunction)bpy_context_temp_override, + METH_VARARGS | METH_KEYWORDS, + bpy_context_temp_override_doc, +}; diff --git a/source/blender/python/intern/bpy_rna_context.h b/source/blender/python/intern/bpy_rna_context.h new file mode 100644 index 00000000000..ddd328131e6 --- /dev/null +++ b/source/blender/python/intern/bpy_rna_context.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup pythonintern + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +extern PyMethodDef BPY_rna_context_temp_override_method_def; + +#ifdef __cplusplus +} +#endif diff --git a/source/blender/python/intern/bpy_rna_types_capi.c b/source/blender/python/intern/bpy_rna_types_capi.c index a5299bc1616..376195ab845 100644 --- a/source/blender/python/intern/bpy_rna_types_capi.c +++ b/source/blender/python/intern/bpy_rna_types_capi.c @@ -22,6 +22,7 @@ #include "bpy_library.h" #include "bpy_rna.h" #include "bpy_rna_callback.h" +#include "bpy_rna_context.h" #include "bpy_rna_data.h" #include "bpy_rna_id_collection.h" #include "bpy_rna_text.h" @@ -159,6 +160,17 @@ static struct PyGetSetDef pyrna_windowmanager_getset[] = { /** \} */ /* -------------------------------------------------------------------- */ +/** \name Context Type + * \{ */ + +static struct PyMethodDef pyrna_context_methods[] = { + {NULL, NULL, 0, NULL}, /* #BPY_rna_context_temp_override_method_def */ + {NULL, NULL, 0, NULL}, +}; + +/** \} */ + +/* -------------------------------------------------------------------- */ /** \name Space Type * \{ */ @@ -254,6 +266,10 @@ void BPY_rna_types_extend_capi(void) /* WindowManager */ pyrna_struct_type_extend_capi( &RNA_WindowManager, pyrna_windowmanager_methods, pyrna_windowmanager_getset); + + /* Context */ + ARRAY_SET_ITEMS(pyrna_context_methods, BPY_rna_context_temp_override_method_def); + pyrna_struct_type_extend_capi(&RNA_Context, pyrna_context_methods, NULL); } /** \} */ |