From 877283a09ac9ce6348debf13ca05d7b903f1ed17 Mon Sep 17 00:00:00 2001 From: Nick Alberelli Date: Sat, 11 Oct 2025 03:04:57 +0000 Subject: [PATCH] PyAPI: bpy.ops callable type in the C-API Moving `__call__` logic into Python's C-API means error messages caused by operators reference the code of the caller instead of the call from `.scripts/modules/bpy/ops.py`. When a full call stack is available this isn't so important, however when logging callers it's not useful to log the call from `ops.py`. There minor performance advantages for moving from Python to C++ however this wasn't a significant consideration for the change. Note that this is a fairly direct conversion from Python to C++, there is room for refactoring to simplify the calling logic. See #144746 for the motivation & #147448 for further improvements. Ref !145344 --- scripts/modules/bpy/ops.py | 127 +--- source/blender/python/intern/CMakeLists.txt | 1 + source/blender/python/intern/bpy_operator.cc | 21 +- .../python/intern/bpy_operator_function.cc | 589 ++++++++++++++++++ .../python/intern/bpy_operator_function.hh | 57 ++ 5 files changed, 663 insertions(+), 132 deletions(-) create mode 100644 source/blender/python/intern/bpy_operator_function.cc create mode 100644 source/blender/python/intern/bpy_operator_function.hh diff --git a/scripts/modules/bpy/ops.py b/scripts/modules/bpy/ops.py index 42a59d5cbc3..105ec6d796a 100644 --- a/scripts/modules/bpy/ops.py +++ b/scripts/modules/bpy/ops.py @@ -7,139 +7,20 @@ from _bpy import ops as _ops_module # op_add = _ops_module.add _op_dir = _ops_module.dir -_op_poll = _ops_module.poll -_op_call = _ops_module.call -_op_as_string = _ops_module.as_string -_op_get_rna_type = _ops_module.get_rna_type -_op_get_bl_options = _ops_module.get_bl_options +_op_create_function = _ops_module.create_function _ModuleType = type(_ops_module) -# ----------------------------------------------------------------------------- -# Callable Operator Wrapper - -class _BPyOpsSubModOp: - """ - Utility class to fake submodule operators. - - eg. bpy.ops.object.somefunc - """ - - __slots__ = ("_module", "_func") - - def _get_doc(self): - idname = self.idname() - sig = _op_as_string(self.idname()) - # XXX You never quite know what you get from bpy.types, - # with operators... Operator and OperatorProperties - # are shadowing each other, and not in the same way for - # native ops and py ones! See #39158. - # op_class = getattr(bpy.types, idname) - op_class = _op_get_rna_type(idname) - descr = op_class.description - return "{:s}\n{:s}".format(sig, descr) - - @staticmethod - def _parse_args(args): - C_exec = 'EXEC_DEFAULT' - C_undo = False - - is_exec = is_undo = False - - for arg in args: - if is_exec is False and isinstance(arg, str): - if is_undo is True: - raise ValueError("string arg must come before the boolean") - C_exec = arg - is_exec = True - elif is_undo is False and isinstance(arg, int): - C_undo = arg - is_undo = True - else: - raise ValueError("1-2 args execution context is supported") - - return C_exec, C_undo - - @staticmethod - def _view_layer_update(context): - view_layer = context.view_layer - if view_layer: # None in background mode - view_layer.update() - else: - import bpy - for scene in bpy.data.scenes: - for view_layer in scene.view_layers: - view_layer.update() - - __doc__ = property(_get_doc) - - def __init__(self, module, func): - self._module = module - self._func = func - - def poll(self, *args): - C_exec, _C_undo = _BPyOpsSubModOp._parse_args(args) - return _op_poll(self.idname_py(), C_exec) - - def idname(self): - # `submod.foo` -> `SUBMOD_OT_foo`. - return self._module.upper() + "_OT_" + self._func - - def idname_py(self): - return self._module + "." + self._func - - def __call__(self, *args, **kw): - import bpy - context = bpy.context - - # Get the operator from blender - wm = context.window_manager - - # Run to account for any RNA values the user changes. - # NOTE: We only update active view-layer, since that's what - # operators are supposed to operate on. There might be some - # corner cases when operator need a full scene update though. - _BPyOpsSubModOp._view_layer_update(context) - - if args: - C_exec, C_undo = _BPyOpsSubModOp._parse_args(args) - ret = _op_call(self.idname_py(), kw, C_exec, C_undo) - else: - ret = _op_call(self.idname_py(), kw) - - if 'FINISHED' in ret and context.window_manager == wm: - _BPyOpsSubModOp._view_layer_update(context) - - return ret - - def get_rna_type(self): - """Internal function for introspection""" - return _op_get_rna_type(self.idname()) - - @property - def bl_options(self): - return _op_get_bl_options(self.idname()) - - def __repr__(self): # useful display, repr(op) - return _op_as_string(self.idname()) - - def __str__(self): # used for print(...) - return ( - "".format( - self._module, self._func, id(self), - ) - ) - - # ----------------------------------------------------------------------------- # Sub-Module Access def _bpy_ops_submodule__getattr__(module, func): - # Return a value from `bpy.ops.{module}.{func}` + # Return a `BPyOpsCallable` object that bypasses Python `__call__` overhead + # for improved operator execution performance. if func.startswith("__"): raise AttributeError(func) - return _BPyOpsSubModOp(module, func) + return _op_create_function(module, func) def _bpy_ops_submodule__dir__(module): diff --git a/source/blender/python/intern/CMakeLists.txt b/source/blender/python/intern/CMakeLists.txt index 7584aa33fef..b98dc143902 100644 --- a/source/blender/python/intern/CMakeLists.txt +++ b/source/blender/python/intern/CMakeLists.txt @@ -46,6 +46,7 @@ set(SRC bpy_library_write.cc bpy_msgbus.cc bpy_operator.cc + bpy_operator_function.cc bpy_operator_wrap.cc bpy_path.cc bpy_props.cc diff --git a/source/blender/python/intern/bpy_operator.cc b/source/blender/python/intern/bpy_operator.cc index 7d5144c390d..16308556d10 100644 --- a/source/blender/python/intern/bpy_operator.cc +++ b/source/blender/python/intern/bpy_operator.cc @@ -18,6 +18,7 @@ #include "RNA_types.hh" #include "BLI_listbase.h" +#include "BLI_string.h" #include "../generic/py_capi_rna.hh" #include "../generic/py_capi_utils.hh" @@ -26,6 +27,7 @@ #include "BPY_extern.hh" #include "bpy_capi_utils.hh" #include "bpy_operator.hh" +#include "bpy_operator_function.hh" #include "bpy_operator_wrap.hh" #include "bpy_rna.hh" /* for setting argument properties & type method `get_rna_type`. */ @@ -62,7 +64,7 @@ static wmOperatorType *ot_lookup_from_py_string(PyObject *value, const char *py_ return ot; } -static PyObject *pyop_poll(PyObject * /*self*/, PyObject *args) +PyObject *pyop_poll(PyObject * /*self*/, PyObject *args) { wmOperatorType *ot; const char *opname; @@ -128,7 +130,7 @@ static PyObject *pyop_poll(PyObject * /*self*/, PyObject *args) return Py_NewRef(ret); } -static PyObject *pyop_call(PyObject * /*self*/, PyObject *args) +PyObject *pyop_call(PyObject * /*self*/, PyObject *args) { wmOperatorType *ot; int error_val = 0; @@ -305,7 +307,7 @@ static PyObject *pyop_call(PyObject * /*self*/, PyObject *args) return pyrna_enum_bitfield_as_set(rna_enum_operator_return_items, int(retval)); } -static PyObject *pyop_as_string(PyObject * /*self*/, PyObject *args) +PyObject *pyop_as_string(PyObject * /*self*/, PyObject *args) { wmOperatorType *ot; @@ -397,7 +399,7 @@ static PyObject *pyop_dir(PyObject * /*self*/) return list; } -static PyObject *pyop_getrna_type(PyObject * /*self*/, PyObject *value) +PyObject *pyop_getrna_type(PyObject * /*self*/, PyObject *value) { wmOperatorType *ot; if ((ot = ot_lookup_from_py_string(value, "get_rna_type")) == nullptr) { @@ -409,7 +411,7 @@ static PyObject *pyop_getrna_type(PyObject * /*self*/, PyObject *value) return (PyObject *)pyrna; } -static PyObject *pyop_get_bl_options(PyObject * /*self*/, PyObject *value) +PyObject *pyop_get_bl_options(PyObject * /*self*/, PyObject *value) { wmOperatorType *ot; if ((ot = ot_lookup_from_py_string(value, "get_bl_options")) == nullptr) { @@ -429,12 +431,9 @@ static PyObject *pyop_get_bl_options(PyObject * /*self*/, PyObject *value) #endif static PyMethodDef bpy_ops_methods[] = { - {"poll", (PyCFunction)pyop_poll, METH_VARARGS, nullptr}, - {"call", (PyCFunction)pyop_call, METH_VARARGS, nullptr}, - {"as_string", (PyCFunction)pyop_as_string, METH_VARARGS, nullptr}, {"dir", (PyCFunction)pyop_dir, METH_NOARGS, nullptr}, {"get_rna_type", (PyCFunction)pyop_getrna_type, METH_O, nullptr}, - {"get_bl_options", (PyCFunction)pyop_get_bl_options, METH_O, nullptr}, + {"create_function", (PyCFunction)pyop_create_function, METH_VARARGS, nullptr}, {"macro_define", (PyCFunction)PYOP_wrap_macro_define, METH_VARARGS, nullptr}, {nullptr, nullptr, 0, nullptr}, }; @@ -463,6 +462,10 @@ PyObject *BPY_operator_module() { PyObject *submodule; + if (BPyOpFunction_InitTypes() < 0) { + return nullptr; + } + submodule = PyModule_Create(&bpy_ops_module); return submodule; diff --git a/source/blender/python/intern/bpy_operator_function.cc b/source/blender/python/intern/bpy_operator_function.cc new file mode 100644 index 00000000000..48701f01193 --- /dev/null +++ b/source/blender/python/intern/bpy_operator_function.cc @@ -0,0 +1,589 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup pythonintern + * + * This file implements the #BPyOpFunction type, + * a Python C-API implementation of a callable Blender operator. + */ + +#include + +#include "BLI_listbase.h" +#include "BLI_string.h" + +#include "BKE_main.hh" +#include "DNA_scene_types.h" + +#include "../generic/py_capi_utils.hh" +#include "../generic/python_compat.hh" /* IWYU pragma: keep. */ + +#include "bpy_capi_utils.hh" +#include "bpy_operator_function.hh" + +#include "BKE_context.hh" +#include "BKE_scene.hh" + +#include "WM_api.hh" + +#include "DEG_depsgraph.hh" + +/* -------------------------------------------------------------------- */ +/** \name Private Utility Functions + * \{ */ + +/** + * Update view layer dependencies. + * If there is no active view layer update all view layers. + */ +static void bpy_op_fn_view_layer_update(bContext *C) +{ + Main *bmain = CTX_data_main(C); + ViewLayer *view_layer = CTX_data_view_layer(C); + + /* None in background mode. */ + if (view_layer) { + /* Update the active view layer. */ + Scene *scene = CTX_data_scene(C); + Depsgraph *depsgraph = BKE_scene_ensure_depsgraph(bmain, scene, view_layer); + if (depsgraph && !DEG_is_evaluating(depsgraph)) { + DEG_make_active(depsgraph); + BKE_scene_graph_update_tagged(depsgraph, bmain); + } + } + else { + /* No active view layer: update all view layers in all scenes. */ + LISTBASE_FOREACH (Scene *, scene_iter, &bmain->scenes) { + LISTBASE_FOREACH (ViewLayer *, vl, &scene_iter->view_layers) { + Depsgraph *depsgraph = BKE_scene_ensure_depsgraph(bmain, scene_iter, vl); + if (depsgraph && !DEG_is_evaluating(depsgraph)) { + DEG_make_active(depsgraph); + BKE_scene_graph_update_tagged(depsgraph, bmain); + } + } + } + } +} + +static bool bpy_op_fn_parse_args(PyObject *args, const char **r_context_str, bool *r_is_undo) +{ + const char *C_exec = "EXEC_DEFAULT"; + bool C_undo = false; + bool is_exec = false; + bool is_undo_set = false; + + Py_ssize_t args_len = PyTuple_GET_SIZE(args); + for (Py_ssize_t i = 0; i < args_len; i++) { + PyObject *arg = PyTuple_GET_ITEM(args, i); + + if (!is_exec && PyUnicode_Check(arg)) { + if (is_undo_set) { + PyErr_SetString(PyExc_ValueError, "string arg must come before the boolean"); + return false; + } + C_exec = PyUnicode_AsUTF8(arg); + is_exec = true; + } + else if ((r_is_undo != nullptr) && (!is_undo_set && (PyBool_Check(arg) || PyLong_Check(arg)))) + { + C_undo = PyObject_IsTrue(arg); + is_undo_set = true; + } + else { + PyErr_SetString(PyExc_ValueError, "1-2 args execution context is supported"); + return false; + } + } + + *r_context_str = C_exec; + if (r_is_undo) { + *r_is_undo = C_undo; + } + return true; +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Type Functions for #BPyOpFunction + * \{ */ + +static void bpy_op_fn_dealloc(BPyOpFunction *self) +{ + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static PyObject *bpy_op_fn_call(BPyOpFunction *self, PyObject *args, PyObject *kwargs) +{ + bContext *C = BPY_context_get(); + if (UNLIKELY(C == nullptr)) { + PyErr_SetString(PyExc_RuntimeError, "Context is None, cannot call an operator"); + return nullptr; + } + + /* Store the window manager before operator execution to check if it changes. */ + wmWindowManager *wm = CTX_wm_manager(C); + + /* Convert Blender format to Python format for the call. */ + char idname_py[OP_MAX_TYPENAME]; + WM_operator_py_idname(idname_py, self->idname); + + PyObject *opname = PyUnicode_FromString(idname_py); + if (!opname) { + return nullptr; + } + + PyObject *kwobj = kwargs ? Py_NewRef(kwargs) : PyDict_New(); + if (!kwobj) { + Py_DECREF(opname); + return nullptr; + } + + /* Build args tuple for `pyop_call: (opname, kw, ...extra args...)`. + * Create the child objects first so we can handle allocation failures cleanly. */ + Py_ssize_t args_len = PyTuple_GET_SIZE(args); + PyObject *new_args = PyTuple_New(2 + args_len); + if (!new_args) { + Py_DECREF(opname); + Py_DECREF(kwobj); + return nullptr; + } + + /* Steal references into the tuple. */ + PyTuple_SET_ITEM(new_args, 0, opname); + PyTuple_SET_ITEM(new_args, 1, kwobj); + + for (Py_ssize_t i = 0; i < args_len; i++) { + PyObject *item = Py_NewRef(PyTuple_GET_ITEM(args, i)); + BLI_assert(item); + PyTuple_SET_ITEM(new_args, i + 2, item); + } + + /* Pre-call view-layer update. + * + * Run to account for any RNA values the user changes. + * NOTE: We only update active view-layer, since that's what + * operators are supposed to operate on. There might be some + * corner cases when operator need a full scene update though. */ + bpy_op_fn_view_layer_update(C); + + PyObject *result = pyop_call(nullptr, new_args); + Py_DECREF(new_args); + + /* Post-call: if operator finished and window manager unchanged, update view-layer again. */ + if (result) { + /* Check membership 'FINISHED' in result using a single temporary PyObject. */ + PyObject *finished_str = PyUnicode_FromString("FINISHED"); + if (finished_str) { + int has_finished = PySequence_Contains(result, finished_str); + if (has_finished == 1) { + if (CTX_wm_manager(C) == wm) { + bpy_op_fn_view_layer_update(C); + } + } + else if (has_finished == -1) { + PyErr_Clear(); + } + Py_DECREF(finished_str); + } + else { + PyErr_Clear(); + } + } + return result; +} + +/** + * Return a user-friendly string representation of the operator. + */ +static PyObject *bpy_op_fn_str(BPyOpFunction *self) +{ + char idname_py[OP_MAX_TYPENAME]; + WM_operator_py_idname(idname_py, self->idname); + + /* Extract module and function from idname_py. */ + const char *dot_pos = strchr(idname_py, '.'); + if (!dot_pos) { + return PyUnicode_FromFormat("", idname_py, (void *)self); + } + + size_t op_mod_str_len = dot_pos - idname_py; + char op_mod_str[OP_MAX_TYPENAME]; + char op_fn_str[OP_MAX_TYPENAME]; + + /* Copy with bounds checking. */ + if (op_mod_str_len >= sizeof(op_mod_str)) { + /* Truncate if necessary. */ + op_mod_str_len = sizeof(op_mod_str) - 1; + } + memcpy(op_mod_str, idname_py, op_mod_str_len); + op_mod_str[op_mod_str_len] = '\0'; + BLI_strncpy(op_fn_str, dot_pos + 1, sizeof(op_fn_str)); + + return PyUnicode_FromFormat( + "", op_mod_str, op_fn_str, (void *)self); +} + +/** + * Return a string representation of the operator for debugging. + */ +static PyObject *bpy_op_fn_repr(BPyOpFunction *self) +{ + /* Use the same format as the original Python implementation */ + PyObject *args = PyTuple_New(1); + if (!args) { + return nullptr; + } + PyObject *name_obj = PyUnicode_FromString(self->idname); + if (!name_obj) { + Py_DECREF(args); + return nullptr; + } + PyTuple_SET_ITEM(args, 0, name_obj); + + PyObject *result = pyop_as_string(nullptr, args); + Py_DECREF(args); + + if (!result) { + /* Fallback to simple string if pyop_as_string fails. */ + PyErr_Clear(); + char idname_py[OP_MAX_TYPENAME]; + WM_operator_py_idname(idname_py, self->idname); + return PyUnicode_FromFormat("", idname_py); + } + + return result; +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Methods for #BPyOpFunctionType + * \{ */ + +PyDoc_STRVAR( + /* Wrap. */ + bpy_op_fn_poll_doc, + ".. method:: poll(context='EXEC_DEFAULT')\n" + "\n" + "Test if the operator can be executed in the current context.\n" + "\n" + ":arg context: Execution context (optional)\n" + ":type context: str\n" + ":return: True if the operator can be executed\n" + ":rtype: bool\n"); +static PyObject *bpy_op_fn_poll(BPyOpFunction *self, PyObject *args) +{ + const char *context_str; + if (!bpy_op_fn_parse_args(args, &context_str, nullptr)) { + return nullptr; + } + + /* Convert Blender format to Python format for the poll call. */ + char idname_py[OP_MAX_TYPENAME]; + WM_operator_py_idname(idname_py, self->idname); + + PyObject *idname_obj = PyUnicode_FromString(idname_py); + if (!idname_obj) { + return nullptr; + } + + PyObject *context_obj = PyUnicode_FromString(context_str); + if (!context_obj) { + Py_DECREF(idname_obj); + return nullptr; + } + + PyObject *poll_args = PyTuple_New(2); + if (!poll_args) { + Py_DECREF(idname_obj); + Py_DECREF(context_obj); + return nullptr; + } + + PyTuple_SET_ITEM(poll_args, 0, idname_obj); + PyTuple_SET_ITEM(poll_args, 1, context_obj); + + PyObject *result = pyop_poll(nullptr, poll_args); + Py_DECREF(poll_args); + return result; +} + +PyDoc_STRVAR( + /* Wrap. */ + bpy_op_fn_idname_doc, + ".. method:: idname()\n" + "\n" + ":return: Return the Blender-format operator idname (e.g., 'OBJECT_OT_select_all').\n" + ":rtype: str\n"); +static PyObject *bpy_op_fn_idname(BPyOpFunction *self, PyObject * /*args*/) +{ + return PyUnicode_FromString(self->idname); +} + +PyDoc_STRVAR( + /* Wrap. */ + bpy_op_fn_idname_py_doc, + ".. method:: idname_py()\n" + "\n" + ":return: Return the Python-format operator idname (e.g., 'object.select_all').\n" + ":rtype: str\n"); +static PyObject *bpy_op_fn_idname_py(BPyOpFunction *self, PyObject * /*args*/) +{ + char idname_py[OP_MAX_TYPENAME]; + WM_operator_py_idname(idname_py, self->idname); + return PyUnicode_FromString(idname_py); +} + +PyDoc_STRVAR( + /* Wrap. */ + bpy_op_fn_get_rna_type_doc, + ".. method:: get_rna_type()\n" + "\n" + "Get the RNA type definition for this operator.\n" + "\n" + ":return: RNA type object for introspection\n" + ":rtype: :class:`bpy.types.Struct`\n"); +static PyObject *bpy_op_fn_get_rna_type(BPyOpFunction *self, PyObject * /*args*/) +{ + PyObject *idname_obj = PyUnicode_FromString(self->idname); + if (!idname_obj) { + return nullptr; + } + + PyObject *result = pyop_getrna_type(nullptr, idname_obj); + Py_DECREF(idname_obj); + return result; +} + +static PyObject *bpy_op_fn_get_doc_impl(BPyOpFunction *self) +{ + /* Get operator signature using Blender format idname: + * `_op_as_string(self.idname())` where `idname()` returns Blender format). */ + PyObject *args = PyTuple_New(1); + if (!args) { + return nullptr; + } + PyObject *name_obj = PyUnicode_FromString(self->idname); + if (!name_obj) { + Py_DECREF(args); + return nullptr; + } + PyTuple_SET_ITEM(args, 0, name_obj); + + PyObject *sig_result = pyop_as_string(nullptr, args); + Py_DECREF(args); + + if (!sig_result) { + /* Fallback to simple string if pyop_as_string fails. */ + PyErr_Clear(); + char idname_py[OP_MAX_TYPENAME]; + WM_operator_py_idname(idname_py, self->idname); + return PyUnicode_FromFormat("bpy.ops.%s(...)", idname_py); + } + + /* Get RNA type and description using Blender format idname. */ + PyObject *idname_bl_obj = PyUnicode_FromString(self->idname); + if (!idname_bl_obj) { + Py_DECREF(sig_result); + return sig_result; /* Return just signature on failure. */ + } + + PyObject *rna_type = pyop_getrna_type(nullptr, idname_bl_obj); + Py_DECREF(idname_bl_obj); + + if (!rna_type) { + PyErr_Clear(); + return sig_result; /* Return just signature on failure. */ + } + + /* Get description attribute from RNA type. */ + PyObject *description = PyObject_GetAttrString(rna_type, "description"); + Py_DECREF(rna_type); + + if (!description) { + PyErr_Clear(); + return sig_result; /* Return just signature on failure. */ + } + + /* Combine signature and description with newline. */ + PyObject *combined = PyUnicode_FromFormat("%U\n%U", sig_result, description); + Py_DECREF(sig_result); + Py_DECREF(description); + + if (!combined) { + char idname_py[OP_MAX_TYPENAME]; + WM_operator_py_idname(idname_py, self->idname); + return PyUnicode_FromFormat("bpy.ops.%s(...)", idname_py); + } + + return combined; +} + +/** Method definitions for BPyOpFunction. */ +static PyMethodDef bpy_op_fn_methods[] = { + {"poll", (PyCFunction)bpy_op_fn_poll, METH_VARARGS, bpy_op_fn_poll_doc}, + {"get_rna_type", (PyCFunction)bpy_op_fn_get_rna_type, METH_NOARGS, bpy_op_fn_get_rna_type_doc}, + {"idname", (PyCFunction)bpy_op_fn_idname, METH_NOARGS, bpy_op_fn_idname_doc}, + {"idname_py", (PyCFunction)bpy_op_fn_idname_py, METH_NOARGS, bpy_op_fn_idname_py_doc}, + {nullptr, nullptr, 0, nullptr}}; + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Get/Set for #BPyOpFunctionType + * \{ */ + +PyDoc_STRVAR( + /* Wrap. */ + bpy_op_fn_get_bl_options_doc, + "Set of option flags for this operator (e.g. 'REGISTER', 'UNDO')"); +static PyObject *bpy_op_fn_get_bl_options(BPyOpFunction *self, void * /*closure*/) +{ + PyObject *idname_obj = PyUnicode_FromString(self->idname); + if (!idname_obj) { + return nullptr; + } + PyObject *result = pyop_get_bl_options(nullptr, idname_obj); + Py_DECREF(idname_obj); + return result; +} + +static PyObject *bpy_op_fn_get_doc(BPyOpFunction *self, void * /*closure*/) +{ + return bpy_op_fn_get_doc_impl(self); +} + +static PyGetSetDef bpy_op_fn_getsetters[] = { + {"bl_options", + (getter)bpy_op_fn_get_bl_options, + nullptr, + bpy_op_fn_get_bl_options_doc, + nullptr}, + /* No doc-string, as this is standard part of the Python spec. */ + {"__doc__", (getter)bpy_op_fn_get_doc, nullptr, nullptr, nullptr}, + {nullptr, nullptr, nullptr, nullptr, nullptr}}; + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Type Declaration #BPyOpFunctionType + * \{ */ + +PyDoc_STRVAR( + /* Wrap. */ + bpy_op_fn_doc, + "BPyOpFunction(context='EXEC_DEFAULT', undo=False, **kwargs)\n" + "\n" + " Execute the operator with the given parameters.\n" + "\n" + " :arg context: Execution context (optional)\n" + " :type context: str\n" + " :arg undo: Force undo behavior (optional)\n" + " :type undo: bool\n" + " :arg kwargs: Operator properties\n" + " :return: Set of completion status flags\n" + " :rtype: set[str]\n"); +PyTypeObject BPyOpFunctionType = { + /*ob_base*/ PyVarObject_HEAD_INIT(nullptr, 0) + /*tp_name*/ "BPyOpFunction", + /*tp_basicsize*/ sizeof(BPyOpFunction), + /*tp_itemsize*/ 0, + /*tp_dealloc*/ (destructor)bpy_op_fn_dealloc, + /*tp_print*/ 0, + /*tp_getattr*/ nullptr, + /*tp_setattr*/ nullptr, + /*tp_as_async*/ nullptr, + /*tp_repr*/ (reprfunc)bpy_op_fn_repr, + /*tp_as_number*/ nullptr, + /*tp_as_sequence*/ nullptr, + /*tp_as_mapping*/ nullptr, + /*tp_hash*/ nullptr, + /*tp_call*/ (ternaryfunc)bpy_op_fn_call, + /*tp_str*/ (reprfunc)bpy_op_fn_str, + /*tp_getattro*/ PyObject_GenericGetAttr, + /*tp_setattro*/ nullptr, + /*tp_as_buffer*/ nullptr, + /*tp_flags*/ Py_TPFLAGS_DEFAULT, + /*tp_doc*/ bpy_op_fn_doc, + /*tp_traverse*/ nullptr, + /*tp_clear*/ nullptr, + /*tp_richcompare*/ nullptr, + /*tp_weaklistoffset*/ 0, + /*tp_iter*/ nullptr, + /*tp_iternext*/ nullptr, + /*tp_methods*/ bpy_op_fn_methods, + /*tp_members*/ nullptr, + /*tp_getset*/ bpy_op_fn_getsetters, + /*tp_base*/ nullptr, + /*tp_dict*/ nullptr, + /*tp_descr_get*/ nullptr, + /*tp_descr_set*/ nullptr, + /*tp_dictoffset*/ 0, + /*tp_init*/ nullptr, + /*tp_alloc*/ nullptr, + /*tp_new*/ nullptr, + /*tp_free*/ nullptr, + /*tp_is_gc*/ nullptr, + /*tp_bases*/ nullptr, + /*tp_mro*/ nullptr, + /*tp_cache*/ nullptr, + /*tp_subclasses*/ nullptr, + /*tp_weaklist*/ nullptr, + /*tp_del*/ nullptr, + /*tp_version_tag*/ 0, + /*tp_finalize*/ nullptr, + /*tp_vectorcall*/ nullptr, +}; + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Public API + * \{ */ + +int BPyOpFunction_InitTypes() +{ + if (PyType_Ready(&BPyOpFunctionType) < 0) { + return -1; + } + return 0; +} + +PyObject *pyop_create_function(PyObject * /*self*/, PyObject *args) +{ + const char *op_mod_str, *op_fn_str; + + if (!PyArg_ParseTuple(args, "ss", &op_mod_str, &op_fn_str)) { + return nullptr; + } + + /* Validate operator name lengths before constructing strings. */ + size_t bl_len = strlen(op_mod_str) + 4 + strlen(op_fn_str) + 1; /* "_OT_" + null terminator */ + if (bl_len > OP_MAX_TYPENAME) { + PyErr_Format(PyExc_ValueError, "Operator name too long: %s.%s", op_mod_str, op_fn_str); + return nullptr; + } + + /* Create a new #BPyOpFunction instance for direct operator execution. */ + BPyOpFunction *op_fn = (BPyOpFunction *)PyObject_New(BPyOpFunction, &BPyOpFunctionType); + if (!op_fn) { + return nullptr; + } + + /* Construct the Blender `idname` (e.g., `OBJECT_OT_select_all`). */ + char op_mod_str_upper[OP_MAX_TYPENAME]; + BLI_strncpy(op_mod_str_upper, op_mod_str, sizeof(op_mod_str_upper)); + BLI_str_toupper_ascii(op_mod_str_upper, sizeof(op_mod_str_upper)); + + const size_t idname_len = BLI_snprintf( + op_fn->idname, sizeof(op_fn->idname), "%s_OT_%s", op_mod_str_upper, op_fn_str); + /* Prevented by the #OP_MAX_TYPENAME check. */ + BLI_assert(idname_len < sizeof(op_fn->idname)); + UNUSED_VARS_NDEBUG(idname_len); + return (PyObject *)op_fn; +} + +/** \} */ diff --git a/source/blender/python/intern/bpy_operator_function.hh b/source/blender/python/intern/bpy_operator_function.hh new file mode 100644 index 00000000000..6a9b21ef0e4 --- /dev/null +++ b/source/blender/python/intern/bpy_operator_function.hh @@ -0,0 +1,57 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup pythonintern + * + * This file implements the #BPyOpFunction type, + * a Python C-API implementation of a callable Blender operator. + */ + +#pragma once + +#include + +#include "DNA_windowmanager_types.h" + +/** + * A callable operator. + * + * Exposed by `bpy.ops.{module}.{operator}()` to allow Blender operators to be called from Python. + */ +typedef struct { + PyObject_HEAD + /** Operator ID name (e.g., `OBJECT_OT_select_all`). */ + char idname[OP_MAX_TYPENAME]; +} BPyOpFunction; + +extern PyTypeObject BPyOpFunctionType; + +#define BPyOpFunction_Check(v) (PyObject_TypeCheck(v, &BPyOpFunctionType)) +#define BPyOpFunction_CheckExact(v) (Py_TYPE(v) == &BPyOpFunctionType) + +/* Forward declarations for external functions from `bpy_operator.cc`. */ + +PyObject *pyop_poll(PyObject *self, PyObject *args); +PyObject *pyop_call(PyObject *self, PyObject *args); +PyObject *pyop_as_string(PyObject *self, PyObject *args); +PyObject *pyop_getrna_type(PyObject *self, PyObject *value); +PyObject *pyop_get_bl_options(PyObject *self, PyObject *value); + +/** + * Create a new BPyOpFunction object for the given operator module and function. + * + * \param self: Unused (required by Python C API). + * \param args: Python tuple containing module and function name strings. + * \return A new #BPyOpFunction object or NULL on error. + */ +PyObject *pyop_create_function(PyObject *self, PyObject *args); + +/** + * Initialize the BPyOpFunction type. + * This must be called before using any BPyOpFunction functions. + * + * \return 0 on success, -1 on failure + */ +int BPyOpFunction_InitTypes();