Files
test/source/blender/python/intern/bpy_msgbus.cc
Campbell Barton 0777c1861e Cleanup: ignore GCC cast-function-type warning for PyMethodDef's
While these warnings point to real errors in the code,
PyMethodDef are an exception where functions with different numbers
of arguments are all cast to the same function type.
2023-07-21 14:08:59 +10:00

429 lines
12 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Foundation
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup pythonintern
* This file defines '_bpy_msgbus' module, exposed as 'bpy.msgbus'.
*/
#include <Python.h>
#include "../generic/py_capi_rna.h"
#include "../generic/py_capi_utils.h"
#include "../generic/python_utildefines.h"
#include "../mathutils/mathutils.h"
#include "BLI_utildefines.h"
#include "BKE_context.h"
#include "WM_api.h"
#include "WM_message.h"
#include "WM_types.h"
#include "RNA_access.h"
#include "RNA_define.h"
#include "RNA_enum_types.h"
#include "bpy_capi_utils.h"
#include "bpy_gizmo_wrap.h" /* own include */
#include "bpy_intern_string.h"
#include "bpy_rna.h"
#include "bpy_msgbus.h" /* own include */
/* -------------------------------------------------------------------- */
/** \name Internal Utils
* \{ */
#define BPY_MSGBUS_RNA_MSGKEY_DOC \
" :arg key: Represents the type of data being subscribed to\n" \
"\n" \
" Arguments include\n" \
" - :class:`bpy.types.Property` instance.\n" \
" - :class:`bpy.types.Struct` type.\n" \
" - (:class:`bpy.types.Struct`, str) type and property name.\n" \
" :type key: Multiple\n"
/**
* There are multiple ways we can get RNA from Python,
* it's also possible to register a type instead of an instance.
*
* This function handles converting Python to RNA subscription information.
*
* \param py_sub: See #BPY_MSGBUS_RNA_MSGKEY_DOC for description.
* \param msg_key_params: Message key with all members zeroed out.
* \return -1 on failure, 0 on success.
*/
static int py_msgbus_rna_key_from_py(PyObject *py_sub,
wmMsgParams_RNA *msg_key_params,
const char *error_prefix)
{
/* Allow common case, object rotation, location - etc. */
if (BaseMathObject_CheckExact(py_sub)) {
BaseMathObject *py_sub_math = (BaseMathObject *)py_sub;
if (py_sub_math->cb_user == nullptr) {
PyErr_Format(PyExc_TypeError, "%s: math argument has no owner", error_prefix);
return -1;
}
py_sub = py_sub_math->cb_user;
/* Common case will use BPy_PropertyRNA_Check below. */
}
if (BPy_PropertyRNA_Check(py_sub)) {
BPy_PropertyRNA *data_prop = (BPy_PropertyRNA *)py_sub;
PYRNA_PROP_CHECK_INT(data_prop);
msg_key_params->ptr = data_prop->ptr;
msg_key_params->prop = data_prop->prop;
}
else if (BPy_StructRNA_Check(py_sub)) {
/* NOTE: this isn't typically used since we don't edit structs directly. */
BPy_StructRNA *data_srna = (BPy_StructRNA *)py_sub;
PYRNA_STRUCT_CHECK_INT(data_srna);
msg_key_params->ptr = data_srna->ptr;
}
/* TODO: property / type, not instance. */
else if (PyType_Check(py_sub)) {
StructRNA *data_type = pyrna_struct_as_srna(py_sub, false, error_prefix);
if (data_type == nullptr) {
return -1;
}
msg_key_params->ptr.type = data_type;
}
else if (PyTuple_CheckExact(py_sub)) {
if (PyTuple_GET_SIZE(py_sub) == 2) {
PyObject *data_type_py = PyTuple_GET_ITEM(py_sub, 0);
PyObject *data_prop_py = PyTuple_GET_ITEM(py_sub, 1);
StructRNA *data_type = pyrna_struct_as_srna(data_type_py, false, error_prefix);
if (data_type == nullptr) {
return -1;
}
if (!PyUnicode_CheckExact(data_prop_py)) {
PyErr_Format(PyExc_TypeError, "%s: expected property to be a string", error_prefix);
return -1;
}
PointerRNA data_type_ptr{};
data_type_ptr.type = data_type;
const char *data_prop_str = PyUnicode_AsUTF8(data_prop_py);
PropertyRNA *data_prop = RNA_struct_find_property(&data_type_ptr, data_prop_str);
if (data_prop == nullptr) {
PyErr_Format(PyExc_TypeError,
"%s: struct %.200s does not contain property %.200s",
error_prefix,
RNA_struct_identifier(data_type),
data_prop_str);
return -1;
}
msg_key_params->ptr.type = data_type;
msg_key_params->prop = data_prop;
}
else {
PyErr_Format(PyExc_ValueError, "%s: Expected a pair (type, property_id)", error_prefix);
return -1;
}
}
return 0;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal Callbacks
* \{ */
#define BPY_MSGBUS_USER_DATA_LEN 2
/* Follow wmMsgNotifyFn spec */
static void bpy_msgbus_notify(bContext *C,
wmMsgSubscribeKey * /*msg_key*/,
wmMsgSubscribeValue *msg_val)
{
PyGILState_STATE gilstate;
bpy_context_set(C, &gilstate);
PyObject *user_data = static_cast<PyObject *>(msg_val->user_data);
BLI_assert(PyTuple_GET_SIZE(user_data) == BPY_MSGBUS_USER_DATA_LEN);
PyObject *callback_args = PyTuple_GET_ITEM(user_data, 0);
PyObject *callback_notify = PyTuple_GET_ITEM(user_data, 1);
const bool is_write_ok = pyrna_write_check();
if (!is_write_ok) {
pyrna_write_set(true);
}
PyObject *ret = PyObject_CallObject(callback_notify, callback_args);
if (ret == nullptr) {
PyC_Err_PrintWithFunc(callback_notify);
}
else {
if (ret != Py_None) {
PyErr_SetString(PyExc_ValueError, "the return value must be None");
PyC_Err_PrintWithFunc(callback_notify);
}
Py_DECREF(ret);
}
bpy_context_clear(C, &gilstate);
if (!is_write_ok) {
pyrna_write_set(false);
}
}
/* Follow wmMsgSubscribeValueFreeDataFn spec */
static void bpy_msgbus_subscribe_value_free_data(wmMsgSubscribeKey * /*msg_key*/,
wmMsgSubscribeValue *msg_val)
{
const PyGILState_STATE gilstate = PyGILState_Ensure();
Py_DECREF(msg_val->owner);
Py_DECREF(msg_val->user_data);
PyGILState_Release(gilstate);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public Message Bus API
* \{ */
PyDoc_STRVAR(
bpy_msgbus_subscribe_rna_doc,
".. function:: subscribe_rna(key, owner, args, notify, options=set())\n"
"\n"
" Register a message bus subscription. It will be cleared when another blend file is\n"
" loaded, or can be cleared explicitly via :func:`bpy.msgbus.clear_by_owner`.\n"
"\n" BPY_MSGBUS_RNA_MSGKEY_DOC
" :arg owner: Handle for this subscription (compared by identity).\n"
" :type owner: Any type.\n"
" :arg options: Change the behavior of the subscriber.\n"
"\n"
" - ``PERSISTENT`` when set, the subscriber will be kept when remapping ID data.\n"
"\n"
" :type options: set of str.\n"
"\n"
".. note::\n"
"\n"
" All subscribers will be cleared on file-load. Subscribers can be re-registered on load,\n"
" see :mod:`bpy.app.handlers.load_post`.\n");
static PyObject *bpy_msgbus_subscribe_rna(PyObject * /*self*/, PyObject *args, PyObject *kw)
{
const char *error_prefix = "subscribe_rna";
PyObject *py_sub = nullptr;
PyObject *py_owner = nullptr;
PyObject *callback_args = nullptr;
PyObject *callback_notify = nullptr;
enum {
IS_PERSISTENT = (1 << 0),
};
PyObject *py_options = nullptr;
const EnumPropertyItem py_options_enum[] = {
{IS_PERSISTENT, "PERSISTENT", 0, ""},
{0, nullptr, 0, nullptr, nullptr},
};
int options = 0;
if (PyTuple_GET_SIZE(args) != 0) {
PyErr_Format(PyExc_TypeError, "%s: only keyword arguments are supported", error_prefix);
return nullptr;
}
static const char *_keywords[] = {
"key",
"owner",
"args",
"notify",
"options",
nullptr,
};
static _PyArg_Parser _parser = {
"O" /* `key` */
"O" /* `owner` */
"O!" /* `args` */
"O" /* `notify` */
"|$" /* Optional keyword only arguments. */
"O!" /* `options` */
":subscribe_rna",
_keywords,
0,
};
if (!_PyArg_ParseTupleAndKeywordsFast(args,
kw,
&_parser,
&py_sub,
&py_owner,
&PyTuple_Type,
&callback_args,
&callback_notify,
&PySet_Type,
&py_options))
{
return nullptr;
}
if (py_options &&
(pyrna_enum_bitfield_from_set(py_options_enum, py_options, &options, error_prefix) == -1))
{
return nullptr;
}
/* NOTE: we may want to have a way to pass this in. */
bContext *C = BPY_context_get();
wmMsgBus *mbus = CTX_wm_message_bus(C);
wmMsgParams_RNA msg_key_params = {{0}};
wmMsgSubscribeValue msg_val_params = {0};
if (py_msgbus_rna_key_from_py(py_sub, &msg_key_params, error_prefix) == -1) {
return nullptr;
}
if (!PyFunction_Check(callback_notify)) {
PyErr_Format(PyExc_TypeError,
"notify expects a function, found %.200s",
Py_TYPE(callback_notify)->tp_name);
return nullptr;
}
if (options != 0) {
if (options & IS_PERSISTENT) {
msg_val_params.is_persistent = true;
}
}
/* owner can be anything. */
{
msg_val_params.owner = py_owner;
Py_INCREF(py_owner);
}
{
PyObject *user_data = PyTuple_New(2);
PyTuple_SET_ITEMS(user_data, Py_INCREF_RET(callback_args), Py_INCREF_RET(callback_notify));
msg_val_params.user_data = user_data;
}
msg_val_params.notify = bpy_msgbus_notify;
msg_val_params.free_data = bpy_msgbus_subscribe_value_free_data;
WM_msg_subscribe_rna_params(mbus, &msg_key_params, &msg_val_params, __func__);
if (0) { /* For debugging. */
WM_msg_dump(mbus, __func__);
}
Py_RETURN_NONE;
}
PyDoc_STRVAR(
bpy_msgbus_publish_rna_doc,
".. function:: publish_rna(key)\n"
"\n" BPY_MSGBUS_RNA_MSGKEY_DOC
"\n"
" Notify subscribers of changes to this property\n"
" (this typically doesn't need to be called explicitly since changes will automatically "
"publish updates).\n"
" In some cases it may be useful to publish changes explicitly using more general keys.\n");
static PyObject *bpy_msgbus_publish_rna(PyObject * /*self*/, PyObject *args, PyObject *kw)
{
const char *error_prefix = "publish_rna";
PyObject *py_sub = nullptr;
if (PyTuple_GET_SIZE(args) != 0) {
PyErr_Format(PyExc_TypeError, "%s: only keyword arguments are supported", error_prefix);
return nullptr;
}
static const char *_keywords[] = {
"key",
nullptr,
};
static _PyArg_Parser _parser = {
"O" /* `key` */
":publish_rna",
_keywords,
0,
};
if (!_PyArg_ParseTupleAndKeywordsFast(args, kw, &_parser, &py_sub)) {
return nullptr;
}
/* NOTE: we may want to have a way to pass this in. */
bContext *C = BPY_context_get();
wmMsgBus *mbus = CTX_wm_message_bus(C);
wmMsgParams_RNA msg_key_params = {{0}};
if (py_msgbus_rna_key_from_py(py_sub, &msg_key_params, error_prefix) == -1) {
return nullptr;
}
WM_msg_publish_rna_params(mbus, &msg_key_params);
Py_RETURN_NONE;
}
PyDoc_STRVAR(bpy_msgbus_clear_by_owner_doc,
".. function:: clear_by_owner(owner)\n"
"\n"
" Clear all subscribers using this owner.\n");
static PyObject *bpy_msgbus_clear_by_owner(PyObject * /*self*/, PyObject *py_owner)
{
bContext *C = BPY_context_get();
wmMsgBus *mbus = CTX_wm_message_bus(C);
WM_msgbus_clear_by_owner(mbus, py_owner);
Py_RETURN_NONE;
}
#if (defined(__GNUC__) && !defined(__clang__))
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wcast-function-type"
#endif
static PyMethodDef BPy_msgbus_methods[] = {
{"subscribe_rna",
(PyCFunction)bpy_msgbus_subscribe_rna,
METH_VARARGS | METH_KEYWORDS,
bpy_msgbus_subscribe_rna_doc},
{"publish_rna",
(PyCFunction)bpy_msgbus_publish_rna,
METH_VARARGS | METH_KEYWORDS,
bpy_msgbus_publish_rna_doc},
{"clear_by_owner",
(PyCFunction)bpy_msgbus_clear_by_owner,
METH_O,
bpy_msgbus_clear_by_owner_doc},
{nullptr, nullptr, 0, nullptr},
};
#if (defined(__GNUC__) && !defined(__clang__))
# pragma GCC diagnostic pop
#endif
static PyModuleDef _bpy_msgbus_def = {
/*m_base*/ PyModuleDef_HEAD_INIT,
/*m_name*/ "msgbus",
/*m_doc*/ nullptr,
/*m_size*/ 0,
/*m_methods*/ BPy_msgbus_methods,
/*m_slots*/ nullptr,
/*m_traverse*/ nullptr,
/*m_clear*/ nullptr,
/*m_free*/ nullptr,
};
PyObject *BPY_msgbus_module()
{
PyObject *submodule;
submodule = PyModule_Create(&_bpy_msgbus_def);
return submodule;
}
/** \} */