Fix #155928: Undo argument can no longer be passed to the operator

Resolve regression in [0] which didn't account for a single positional
boolean argument.

[0]: 877283a09a

Ref !155997

Pull Request: https://projects.blender.org/blender/blender/pulls/157122
This commit is contained in:
Campbell Barton 2026-03-20 18:27:11 +11:00 • committed by Gitea
parent 2edae0c5e0
commit e622d316e2
2 changed files with 61 additions and 21 deletions

View file

@ -19,6 +19,7 @@
#include "../generic/py_capi_utils.hh"
#include "../generic/python_compat.hh" /* IWYU pragma: keep. */
#include "../generic/python_utildefines.hh"
#include "bpy_capi_utils.hh"
#include "bpy_operator_function.hh"
@ -139,35 +140,30 @@ static PyObject *bpy_op_fn_call(BPyOpFunction *self, PyObject *args, PyObject *k
char idname_py[OP_MAX_TYPENAME];
WM_operator_py_idname(idname_py, self->idname);
PyObject *opname = PyUnicode_FromString(idname_py);
if (!opname) {
/* Parse the user's positional args (context string, undo boolean)
* before forwarding to `pyop_call` which expects strict types. */
const char *context_str;
bool is_undo;
if (!bpy_op_fn_parse_args(args, &context_str, &is_undo)) {
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);
/* Build args tuple for `pyop_call`: (opname, kw, context_str, is_undo).
* Items are set immediately so `Py_DECREF(new_args)` handles cleanup on error. */
PyObject *new_args = PyTuple_New(4);
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);
PyTuple_SET_ITEMS(new_args,
PyUnicode_FromString(idname_py),
kwargs ? Py_NewRef(kwargs) : PyDict_New(),
PyUnicode_FromString(context_str),
PyLong_FromLong(is_undo));
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);
if (PyErr_Occurred()) {
Py_DECREF(new_args);
return nullptr;
}
/* Pre-call view-layer update.

View file

@ -37,12 +37,56 @@ class TestOperatorCallPositionalArgs(unittest.TestCase):
def test_invoke_default(self):
self.assertEqual(bpy.ops.wm.clear_recent_files('INVOKE_DEFAULT'), {'FINISHED'})
def test_undo_flag(self):
self.assertEqual(bpy.ops.wm.clear_recent_files(True), {'FINISHED'})
def test_undo_flag_false(self):
self.assertEqual(bpy.ops.wm.clear_recent_files(False), {'FINISHED'})
def test_undo_flag_int(self):
self.assertEqual(bpy.ops.wm.clear_recent_files(1), {'FINISHED'})
def test_undo_flag_int_zero(self):
self.assertEqual(bpy.ops.wm.clear_recent_files(0), {'FINISHED'})
def test_context_and_undo(self):
self.assertEqual(bpy.ops.wm.clear_recent_files('EXEC_DEFAULT', True), {'FINISHED'})
def test_context_and_undo_int(self):
self.assertEqual(bpy.ops.wm.clear_recent_files('EXEC_DEFAULT', 1), {'FINISHED'})
def test_negative_int(self):
self.assertEqual(bpy.ops.wm.clear_recent_files(-1), {'FINISHED'})
class TestOperatorCallPositionalArgsInvalid(unittest.TestCase):
def test_args_invalid_type(self):
for arg in (1.0, None, [], {}, (), set(), b'EXEC_DEFAULT'):
with self.assertRaises(ValueError):
bpy.ops.wm.clear_recent_files(arg)
def test_args_invalid_order(self):
for args in (
(True, 'EXEC_DEFAULT'),
(1, 'EXEC_DEFAULT'),
):
with self.assertRaises(ValueError):
bpy.ops.wm.clear_recent_files(*args)
def test_args_invalid_duplicates(self):
for args in (
(True, True),
(False, False),
(1, 1),
(1, True),
('EXEC_DEFAULT', 'EXEC_DEFAULT'),
('EXEC_DEFAULT', True, 'extra'),
(True, True, 'EXEC_DEFAULT', 'EXEC_DEFAULT'),
):
with self.assertRaises(ValueError):
bpy.ops.wm.clear_recent_files(*args)
class TestOperatorCallKeywordArgs(unittest.TestCase):