From ebe04bd3cafaa1f88bd51eee5b3e7bef38ae69bc Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Tue, 20 Apr 2021 11:57:28 +1000 Subject: PyAPI: support Operator.poll functions 'disabled' hint Python scripts can now define the reason it's poll function fails using: `Operator.poll_message_set(message, ...)` This supports both regular text as well as delaying message creation using a callback which should be used in situations where constructing detailed messages is too much overhead for a poll function. Ref D11001 --- source/blender/python/intern/CMakeLists.txt | 2 + source/blender/python/intern/bpy_interface.c | 11 ++ source/blender/python/intern/bpy_operator.c | 6 +- source/blender/python/intern/bpy_rna_operator.c | 150 ++++++++++++++++++++++ source/blender/python/intern/bpy_rna_operator.h | 31 +++++ source/blender/python/intern/bpy_rna_types_capi.c | 18 +++ 6 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 source/blender/python/intern/bpy_rna_operator.c create mode 100644 source/blender/python/intern/bpy_rna_operator.h (limited to 'source/blender/python') diff --git a/source/blender/python/intern/CMakeLists.txt b/source/blender/python/intern/CMakeLists.txt index 9ac8d4d9f47..2be2105d327 100644 --- a/source/blender/python/intern/CMakeLists.txt +++ b/source/blender/python/intern/CMakeLists.txt @@ -79,6 +79,7 @@ set(SRC bpy_rna_driver.c bpy_rna_gizmo.c bpy_rna_id_collection.c + bpy_rna_operator.c bpy_rna_types_capi.c bpy_rna_ui.c bpy_traceback.c @@ -118,6 +119,7 @@ set(SRC bpy_rna_driver.h bpy_rna_gizmo.h bpy_rna_id_collection.h + bpy_rna_operator.h bpy_rna_types_capi.h bpy_rna_ui.h bpy_traceback.h diff --git a/source/blender/python/intern/bpy_interface.c b/source/blender/python/intern/bpy_interface.c index 5f31e0bb74d..4144063cf5c 100644 --- a/source/blender/python/intern/bpy_interface.c +++ b/source/blender/python/intern/bpy_interface.c @@ -167,6 +167,14 @@ void bpy_context_clear(bContext *UNUSED(C), const PyGILState_STATE *gilstate) } } +static void bpy_context_end(bContext *C) +{ + if (UNLIKELY(C == NULL)) { + return; + } + CTX_wm_operator_poll_msg_clear(C); +} + /** * Use for `CTX_*_set(..)` functions need to set values which are later read back as expected. * In this case we don't want the Python context to override the values as it causes problems @@ -524,6 +532,9 @@ void BPY_python_end(void) /* finalizing, no need to grab the state, except when we are a module */ gilstate = PyGILState_Ensure(); + /* Clear Python values in the context so freeing the context after Python exits doesn't crash. */ + bpy_context_end(BPY_context_get()); + /* Decrement user counts of all callback functions. */ BPY_rna_props_clear_all(); diff --git a/source/blender/python/intern/bpy_operator.c b/source/blender/python/intern/bpy_operator.c index a4622915b73..4a5e2552598 100644 --- a/source/blender/python/intern/bpy_operator.c +++ b/source/blender/python/intern/bpy_operator.c @@ -244,12 +244,16 @@ static PyObject *pyop_call(PyObject *UNUSED(self), PyObject *args) } if (WM_operator_poll_context((bContext *)C, ot, context) == false) { - const char *msg = CTX_wm_operator_poll_msg_get(C); + bool msg_free = false; + const char *msg = CTX_wm_operator_poll_msg_get(C, &msg_free); PyErr_Format(PyExc_RuntimeError, "Operator bpy.ops.%.200s.poll() %.200s", opname, msg ? msg : "failed, context is incorrect"); CTX_wm_operator_poll_msg_clear(C); + if (msg_free) { + MEM_freeN((void *)msg); + } error_val = -1; } else { diff --git a/source/blender/python/intern/bpy_rna_operator.c b/source/blender/python/intern/bpy_rna_operator.c new file mode 100644 index 00000000000..6e0db3eca49 --- /dev/null +++ b/source/blender/python/intern/bpy_rna_operator.c @@ -0,0 +1,150 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** \file + * \ingroup pythonintern + * + * This file extends `bpy.types.Operator` with C/Python API methods and attributes. + */ + +#include + +#include "BLI_string.h" + +#include "BKE_context.h" + +#include "../generic/python_utildefines.h" + +#include "BPY_extern.h" +#include "bpy_capi_utils.h" + +/* -------------------------------------------------------------------- */ +/** \name Operator `poll_message_set` Method + * \{ */ + +static char *pyop_poll_message_get_fn(bContext *UNUSED(C), void *user_data) +{ + PyGILState_STATE gilstate = PyGILState_Ensure(); + + PyObject *py_args = user_data; + PyObject *py_func_or_msg = PyTuple_GET_ITEM(py_args, 0); + + if (PyUnicode_Check(py_func_or_msg)) { + return BLI_strdup(PyUnicode_AsUTF8(py_func_or_msg)); + } + + PyObject *py_args_after_first = PyTuple_GetSlice(py_args, 1, PY_SSIZE_T_MAX); + PyObject *py_msg = PyObject_CallObject(py_func_or_msg, py_args_after_first); + Py_DECREF(py_args_after_first); + + char *msg = NULL; + bool error = false; + + /* NULL for no string. */ + if (py_msg == NULL) { + error = true; + } + else { + if (py_msg == Py_None) { + /* pass */ + } + else if (PyUnicode_Check(py_msg)) { + msg = BLI_strdup(PyUnicode_AsUTF8(py_msg)); + } + else { + PyErr_Format(PyExc_TypeError, + "poll_message_set(function, ...): expected string or None, got %.200s", + Py_TYPE(py_msg)->tp_name); + error = true; + } + Py_DECREF(py_msg); + } + + if (error) { + PyErr_Print(); + PyErr_Clear(); + } + + PyGILState_Release(gilstate); + return msg; +} + +static void pyop_poll_message_free_fn(bContext *UNUSED(C), void *user_data) +{ + /* Handles the GIL. */ + BPY_DECREF(user_data); +} + +PyDoc_STRVAR(BPY_rna_operator_poll_message_set_doc, + ".. method:: poll_message_set(message, ...)\n" + "\n" + " Set the message to show in the tool-tip when poll fails.\n" + "\n" + " When message is callable, " + "additional user defined positional arguments are passed to the message function.\n" + "\n" + " :param message: The message or a function that returns the message.\n" + " :type message: string or a callable that returns a string or None.\n"); + +static PyObject *BPY_rna_operator_poll_message_set(PyObject *UNUSED(self), PyObject *args) +{ + const ssize_t args_len = PyTuple_GET_SIZE(args); + if (args_len == 0) { + PyErr_SetString(PyExc_ValueError, + "poll_message_set(message, ...): requires a message argument"); + return NULL; + } + + PyObject *py_func_or_msg = PyTuple_GET_ITEM(args, 0); + + if (PyUnicode_Check(py_func_or_msg)) { + if (args_len > 1) { + PyErr_SetString(PyExc_ValueError, + "poll_message_set(message): does not support additional arguments"); + return NULL; + } + } + else if (PyCallable_Check(py_func_or_msg)) { + /* pass */ + } + else { + PyErr_Format(PyExc_TypeError, + "poll_message_set(message, ...): " + "expected at least 1 string or callable argument, got %.200s", + Py_TYPE(py_func_or_msg)->tp_name); + return NULL; + } + + bContext *C = BPY_context_get(); + struct bContextPollMsgDyn_Params params = { + .get_fn = pyop_poll_message_get_fn, + .free_fn = pyop_poll_message_free_fn, + .user_data = Py_INCREF_RET(args), + }; + + CTX_wm_operator_poll_msg_set_dynamic(C, ¶ms); + + Py_RETURN_NONE; +} + +PyMethodDef BPY_rna_operator_poll_message_set_method_def = { + "poll_message_set", + (PyCFunction)BPY_rna_operator_poll_message_set, + METH_VARARGS | METH_STATIC, + BPY_rna_operator_poll_message_set_doc, +}; + +/** \} */ diff --git a/source/blender/python/intern/bpy_rna_operator.h b/source/blender/python/intern/bpy_rna_operator.h new file mode 100644 index 00000000000..8040d8a492a --- /dev/null +++ b/source/blender/python/intern/bpy_rna_operator.h @@ -0,0 +1,31 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** \file + * \ingroup pythonintern + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +extern PyMethodDef BPY_rna_operator_poll_message_set_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 9b15e84663d..2f6e197d1e2 100644 --- a/source/blender/python/intern/bpy_rna_types_capi.c +++ b/source/blender/python/intern/bpy_rna_types_capi.c @@ -41,6 +41,8 @@ #include "bpy_rna_types_capi.h" #include "bpy_rna_ui.h" +#include "bpy_rna_operator.h" + #include "../generic/py_capi_utils.h" #include "RNA_access.h" @@ -86,6 +88,17 @@ static struct PyMethodDef pyrna_uilayout_methods[] = { /** \} */ +/* -------------------------------------------------------------------- */ +/** \name Operator + * \{ */ + +static struct PyMethodDef pyrna_operator_methods[] = { + {NULL, NULL, 0, NULL}, /* #BPY_rna_operator_poll_message_set */ + {NULL, NULL, 0, NULL}, +}; + +/** \} */ + /* -------------------------------------------------------------------- */ /** \name Window Manager Clipboard Property * @@ -228,6 +241,11 @@ void BPY_rna_types_extend_capi(void) /* Space */ pyrna_struct_type_extend_capi(&RNA_Space, pyrna_space_methods, NULL); + /* wmOperator */ + ARRAY_SET_ITEMS(pyrna_operator_methods, BPY_rna_operator_poll_message_set_method_def); + BLI_assert(ARRAY_SIZE(pyrna_operator_methods) == 2); + pyrna_struct_type_extend_capi(&RNA_Operator, pyrna_operator_methods, NULL); + /* WindowManager */ pyrna_struct_type_extend_capi( &RNA_WindowManager, pyrna_windowmanager_methods, pyrna_windowmanager_getset); -- cgit v1.2.3