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
This commit is contained in:
Nick Alberelli
2025-10-11 03:04:57 +00:00
committed by Campbell Barton
parent 78029fa777
commit 877283a09a
5 changed files with 663 additions and 132 deletions

View File

@@ -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 (
"<function bpy.ops.{:s}.{:s} at 0x{:x}'>".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):

View File

@@ -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

View File

@@ -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;

View File

@@ -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 <Python.h>
#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("<function bpy.ops.%s at %p>", 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(
"<function bpy.ops.%s.%s at %p>", 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("<bpy.ops.%s function>", 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;
}
/** \} */

View File

@@ -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 <Python.h>
#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();