Blender V4.5
bpy_cli_command.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2024 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
10#include <Python.h>
11
12#include "BLI_utildefines.h"
13
14#include "bpy_capi_utils.hh"
15
17
19#include "../generic/python_compat.hh" /* IWYU pragma: keep. */
20
21#include "bpy_cli_command.hh" /* Own include. */
22
23static const char *bpy_cli_command_capsule_name = "bpy_cli_command";
24static const char *bpy_cli_command_capsule_name_invalid = "bpy_cli_command<invalid>";
25
26/* -------------------------------------------------------------------- */
29
33static PyObject *py_argv_from_bytes(const int argc, const char **argv)
34{
35 /* Copy functionality from Python's internal `sys.argv` initialization. */
36 PyConfig config;
37 PyConfig_InitPythonConfig(&config);
38 PyStatus status = PyConfig_SetBytesArgv(&config, argc, (char *const *)argv);
39 PyObject *py_argv = nullptr;
40 if (UNLIKELY(PyStatus_Exception(status))) {
41 PyErr_Format(PyExc_ValueError, "%s", status.err_msg);
42 }
43 else {
44 BLI_assert(argc == config.argv.length);
45 py_argv = PyList_New(config.argv.length);
46 for (Py_ssize_t i = 0; i < config.argv.length; i++) {
47 PyList_SET_ITEM(py_argv, i, PyUnicode_FromWideChar(config.argv.items[i], -1));
48 }
49 }
50 PyConfig_Clear(&config);
51 return py_argv;
52}
53
55
56/* -------------------------------------------------------------------- */
59
61 PyObject *py_exec_fn,
62 const int argc,
63 const char **argv)
64{
65 PyGILState_STATE gilstate;
66 bpy_context_set(C, &gilstate);
67
68 int exit_code = EXIT_FAILURE;
69
70 /* For the most part `sys.argv[-argc:]` is sufficient & less trouble than re-creating this
71 * list. Don't do this because:
72 * - Python scripts *could* have manipulated `sys.argv` (although it's bad practice).
73 * - We may want to support invoking commands directly,
74 * where the arguments aren't necessarily from `sys.argv`.
75 */
76 bool has_error = false;
77 PyObject *py_argv = py_argv_from_bytes(argc, argv);
78
79 if (py_argv == nullptr) {
80 has_error = true;
81 }
82 else {
83 PyObject *exec_args = PyTuple_New(1);
84 PyTuple_SET_ITEM(exec_args, 0, py_argv);
85
86 PyObject *result = PyObject_Call(py_exec_fn, exec_args, nullptr);
87
88 Py_DECREF(exec_args); /* Frees `py_argv` too. */
89
90 /* Convert `sys.exit` into a return-value.
91 * NOTE: typically `sys.exit` *doesn't* need any special handling,
92 * however it's neater if we use the same code paths for exiting either way. */
93 if ((result == nullptr) && PyErr_ExceptionMatches(PyExc_SystemExit)) {
94 PyObject *error_type, *error_value, *error_traceback;
95 PyErr_Fetch(&error_type, &error_value, &error_traceback);
96 if (PyObject_TypeCheck(error_value, (PyTypeObject *)PyExc_SystemExit) &&
97 (((PySystemExitObject *)error_value)->code != nullptr))
98 {
99 /* When `SystemExit(..)` is raised. */
100 result = ((PySystemExitObject *)error_value)->code;
101 }
102 else {
103 /* When `sys.exit()` is called. */
104 result = error_value;
105 }
106 Py_INCREF(result);
107 PyErr_Restore(error_type, error_value, error_traceback);
108 PyErr_Clear();
109 }
110
111 if (result == nullptr) {
112 has_error = true;
113 }
114 else {
115 if (!PyLong_Check(result)) {
116 PyErr_Format(PyExc_TypeError,
117 "Expected an int return value, not a %.200s",
118 Py_TYPE(result)->tp_name);
119 has_error = true;
120 }
121 else {
122 const int exit_code_test = PyC_Long_AsI32(result);
123 if ((exit_code_test == -1) && PyErr_Occurred()) {
124 exit_code = EXIT_SUCCESS;
125 has_error = true;
126 }
127 else {
128 exit_code = exit_code_test;
129 }
130 }
131 Py_DECREF(result);
132 }
133 }
134
135 if (has_error) {
136 PyErr_Print();
137 PyErr_Clear();
138 }
139
140 bpy_context_clear(C, &gilstate);
141
142 return exit_code;
143}
144
145static void bpy_cli_command_free(PyObject *py_exec_fn)
146{
147 /* An explicit unregister clears to avoid acquiring a lock. */
148 if (py_exec_fn) {
149 PyGILState_STATE gilstate = PyGILState_Ensure();
150 Py_DECREF(py_exec_fn);
151 PyGILState_Release(gilstate);
152 }
153}
154
156
157/* -------------------------------------------------------------------- */
160
162 public:
163 BPyCommandHandler(const std::string &id, PyObject *py_exec_fn)
165 {
166 }
168 {
170 }
171
172 int exec(bContext *C, int argc, const char **argv) override
173 {
174 return bpy_cli_command_exec(C, this->py_exec_fn, argc, argv);
175 }
176
177 PyObject *py_exec_fn = nullptr;
178};
179
181
182/* -------------------------------------------------------------------- */
185
187 /* Wrap. */
188 bpy_cli_command_register_doc,
189 ".. method:: register_cli_command(id, execute)\n"
190 "\n"
191 " Register a command, accessible via the (``-c`` / ``--command``) command-line argument.\n"
192 "\n"
193 " :arg id: The command identifier (must pass an ``str.isidentifier`` check).\n"
194 "\n"
195 " If the ``id`` is already registered, a warning is printed and "
196 "the command is inaccessible to prevent accidents invoking the wrong command.\n"
197 " :type id: str\n"
198 " :arg execute: Callback, taking a single list of strings and returns an int.\n"
199 " The arguments are built from all command-line arguments following the command id.\n"
200 " The return value should be 0 for success, 1 on failure "
201 "(specific error codes from the ``os`` module can also be used).\n"
202 " :type execute: callable\n"
203 " :return: The command handle which can be passed to :func:`unregister_cli_command`.\n"
204 "\n"
205 " This uses Python's capsule type "
206 "however the result should be considered an opaque handle only used for unregistering.\n"
207 " :rtype: capsule\n");
208static PyObject *bpy_cli_command_register(PyObject * /*self*/, PyObject *args, PyObject *kw)
209{
210 PyObject *py_id;
211 PyObject *py_exec_fn;
212
213 static const char *_keywords[] = {
214 "id",
215 "execute",
216 nullptr,
217 };
218 static _PyArg_Parser _parser = {
220 "O!" /* `id` */
221 "O" /* `execute` */
222 ":register_cli_command",
223 _keywords,
224 nullptr,
225 };
226 if (!_PyArg_ParseTupleAndKeywordsFast(args, kw, &_parser, &PyUnicode_Type, &py_id, &py_exec_fn))
227 {
228 return nullptr;
229 }
230 if (!PyUnicode_IsIdentifier(py_id)) {
231 PyErr_SetString(PyExc_ValueError, "The command id is not a valid identifier");
232 return nullptr;
233 }
234 if (!PyCallable_Check(py_exec_fn)) {
235 PyErr_SetString(PyExc_ValueError, "The execute argument must be callable");
236 return nullptr;
237 }
238
239 const char *id = PyUnicode_AsUTF8(py_id);
240
241 std::unique_ptr<CommandHandler> cmd_ptr = std::make_unique<BPyCommandHandler>(
242 std::string(id), Py_NewRef(py_exec_fn));
243 void *cmd_p = cmd_ptr.get();
244
245 BKE_blender_cli_command_register(std::move(cmd_ptr));
246
247 return PyCapsule_New(cmd_p, bpy_cli_command_capsule_name, nullptr);
248}
249
251 /* Wrap. */
252 bpy_cli_command_unregister_doc,
253 ".. method:: unregister_cli_command(handle)\n"
254 "\n"
255 " Unregister a CLI command.\n"
256 "\n"
257 " :arg handle: The return value of :func:`register_cli_command`.\n"
258 " :type handle: capsule\n");
259static PyObject *bpy_cli_command_unregister(PyObject * /*self*/, PyObject *value)
260{
261 if (!PyCapsule_CheckExact(value)) {
262 PyErr_Format(PyExc_TypeError,
263 "Expected a capsule returned from register_cli_command(...), found a: %.200s",
264 Py_TYPE(value)->tp_name);
265 return nullptr;
266 }
267
268 BPyCommandHandler *cmd = static_cast<BPyCommandHandler *>(
269 PyCapsule_GetPointer(value, bpy_cli_command_capsule_name));
270 if (cmd == nullptr) {
271 const char *capsule_name = PyCapsule_GetName(value);
272 if (capsule_name == bpy_cli_command_capsule_name_invalid) {
273 PyErr_SetString(PyExc_ValueError, "The command has already been removed");
274 }
275 else {
276 PyErr_Format(PyExc_ValueError,
277 "Unrecognized capsule ID \"%.200s\"",
278 capsule_name ? capsule_name : "<null>");
279 }
280 return nullptr;
281 }
282
283 /* Don't acquire the GIL when un-registering. */
284 Py_CLEAR(cmd->py_exec_fn);
285
286 /* Don't allow removing again. */
287 PyCapsule_SetName(value, bpy_cli_command_capsule_name_invalid);
288
290
291 Py_RETURN_NONE;
292}
293
294#ifdef __GNUC__
295# ifdef __clang__
296# pragma clang diagnostic push
297# pragma clang diagnostic ignored "-Wcast-function-type"
298# else
299# pragma GCC diagnostic push
300# pragma GCC diagnostic ignored "-Wcast-function-type"
301# endif
302#endif
303
305 "register_cli_command",
306 (PyCFunction)bpy_cli_command_register,
307 METH_STATIC | METH_VARARGS | METH_KEYWORDS,
308 bpy_cli_command_register_doc,
309};
311 "unregister_cli_command",
312 (PyCFunction)bpy_cli_command_unregister,
313 METH_STATIC | METH_O,
314 bpy_cli_command_unregister_doc,
315};
316
317#ifdef __GNUC__
318# ifdef __clang__
319# pragma clang diagnostic pop
320# else
321# pragma GCC diagnostic pop
322# endif
323#endif
324
Blender CLI Generic --command Support.
bool BKE_blender_cli_command_unregister(CommandHandler *cmd)
void BKE_blender_cli_command_register(std::unique_ptr< CommandHandler > cmd)
#define BLI_assert(a)
Definition BLI_assert.h:46
#define UNLIKELY(x)
#define C
Definition RandGen.cpp:29
void bpy_context_clear(struct bContext *C, const PyGILState_STATE *gilstate)
void bpy_context_set(struct bContext *C, PyGILState_STATE *gilstate)
PyMethodDef BPY_cli_command_register_def
static PyObject * py_argv_from_bytes(const int argc, const char **argv)
static void bpy_cli_command_free(PyObject *py_exec_fn)
static const char * bpy_cli_command_capsule_name
static int bpy_cli_command_exec(bContext *C, PyObject *py_exec_fn, const int argc, const char **argv)
static PyObject * bpy_cli_command_register(PyObject *, PyObject *args, PyObject *kw)
static PyObject * bpy_cli_command_unregister(PyObject *, PyObject *value)
PyDoc_STRVAR(bpy_cli_command_register_doc, ".. method:: register_cli_command(id, execute)\n" "\n" " Register a command, accessible via the (``-c`` / ``--command``) command-line argument.\n" "\n" " :arg id: The command identifier (must pass an ``str.isidentifier`` check).\n" "\n" " If the ``id`` is already registered, a warning is printed and " "the command is inaccessible to prevent accidents invoking the wrong command.\n" " :type id: str\n" " :arg execute: Callback, taking a single list of strings and returns an int.\n" " The arguments are built from all command-line arguments following the command id.\n" " The return value should be 0 for success, 1 on failure " "(specific error codes from the ``os`` module can also be used).\n" " :type execute: callable\n" " :return: The command handle which can be passed to :func:`unregister_cli_command`.\n" "\n" " This uses Python's capsule type " "however the result should be considered an opaque handle only used for unregistering.\n" " :rtype: capsule\n")
static const char * bpy_cli_command_capsule_name_invalid
PyMethodDef BPY_cli_command_unregister_def
int exec(bContext *C, int argc, const char **argv) override
BPyCommandHandler(const std::string &id, PyObject *py_exec_fn)
~BPyCommandHandler() override
CommandHandler(const std::string &id)
header-only compatibility defines.
#define PY_ARG_PARSER_HEAD_COMPAT()
i
Definition text_draw.cc:230