Files
test2/source/blender/python/intern/bpy_cli_command.cc
Campbell Barton e3d6051181 Cleanup: suppress cast-function-type warnings for CLANG
Extend the existing GCC pragma's and add the warning suppression
for Cycles & Freestyle.
2025-04-01 12:06:03 +11:00

323 lines
9.3 KiB
C++

/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup pythonintern
*
* Wrap `BKE_command_cli_*` to support custom CLI commands.
*/
#include <Python.h>
#include "BLI_utildefines.h"
#include "bpy_capi_utils.hh"
#include "BKE_blender_cli_command.hh"
#include "../generic/py_capi_utils.hh"
#include "../generic/python_compat.hh"
#include "bpy_cli_command.hh" /* Own include. */
static const char *bpy_cli_command_capsule_name = "bpy_cli_command";
static const char *bpy_cli_command_capsule_name_invalid = "bpy_cli_command<invalid>";
/* -------------------------------------------------------------------- */
/** \name Internal Utilities
* \{ */
/**
* Return a list of strings, compatible with the construction of Python's `sys.argv`.
*/
static PyObject *py_argv_from_bytes(const int argc, const char **argv)
{
/* Copy functionality from Python's internal `sys.argv` initialization. */
PyConfig config;
PyConfig_InitPythonConfig(&config);
PyStatus status = PyConfig_SetBytesArgv(&config, argc, (char *const *)argv);
PyObject *py_argv = nullptr;
if (UNLIKELY(PyStatus_Exception(status))) {
PyErr_Format(PyExc_ValueError, "%s", status.err_msg);
}
else {
BLI_assert(argc == config.argv.length);
py_argv = PyList_New(config.argv.length);
for (Py_ssize_t i = 0; i < config.argv.length; i++) {
PyList_SET_ITEM(py_argv, i, PyUnicode_FromWideChar(config.argv.items[i], -1));
}
}
PyConfig_Clear(&config);
return py_argv;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal Implementation
* \{ */
static int bpy_cli_command_exec(bContext *C,
PyObject *py_exec_fn,
const int argc,
const char **argv)
{
PyGILState_STATE gilstate;
bpy_context_set(C, &gilstate);
int exit_code = EXIT_FAILURE;
/* For the most part `sys.argv[-argc:]` is sufficient & less trouble than re-creating this
* list. Don't do this because:
* - Python scripts *could* have manipulated `sys.argv` (although it's bad practice).
* - We may want to support invoking commands directly,
* where the arguments aren't necessarily from `sys.argv`.
*/
bool has_error = false;
PyObject *py_argv = py_argv_from_bytes(argc, argv);
if (py_argv == nullptr) {
has_error = true;
}
else {
PyObject *exec_args = PyTuple_New(1);
PyTuple_SET_ITEM(exec_args, 0, py_argv);
PyObject *result = PyObject_Call(py_exec_fn, exec_args, nullptr);
Py_DECREF(exec_args); /* Frees `py_argv` too. */
/* Convert `sys.exit` into a return-value.
* NOTE: typically `sys.exit` *doesn't* need any special handling,
* however it's neater if we use the same code paths for exiting either way. */
if ((result == nullptr) && PyErr_ExceptionMatches(PyExc_SystemExit)) {
PyObject *error_type, *error_value, *error_traceback;
PyErr_Fetch(&error_type, &error_value, &error_traceback);
if (PyObject_TypeCheck(error_value, (PyTypeObject *)PyExc_SystemExit) &&
(((PySystemExitObject *)error_value)->code != nullptr))
{
/* When `SystemExit(..)` is raised. */
result = ((PySystemExitObject *)error_value)->code;
}
else {
/* When `sys.exit()` is called. */
result = error_value;
}
Py_INCREF(result);
PyErr_Restore(error_type, error_value, error_traceback);
PyErr_Clear();
}
if (result == nullptr) {
has_error = true;
}
else {
if (!PyLong_Check(result)) {
PyErr_Format(PyExc_TypeError,
"Expected an int return value, not a %.200s",
Py_TYPE(result)->tp_name);
has_error = true;
}
else {
const int exit_code_test = PyC_Long_AsI32(result);
if ((exit_code_test == -1) && PyErr_Occurred()) {
exit_code = EXIT_SUCCESS;
has_error = true;
}
else {
exit_code = exit_code_test;
}
}
Py_DECREF(result);
}
}
if (has_error) {
PyErr_Print();
PyErr_Clear();
}
bpy_context_clear(C, &gilstate);
return exit_code;
}
static void bpy_cli_command_free(PyObject *py_exec_fn)
{
/* An explicit unregister clears to avoid acquiring a lock. */
if (py_exec_fn) {
PyGILState_STATE gilstate = PyGILState_Ensure();
Py_DECREF(py_exec_fn);
PyGILState_Release(gilstate);
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal Class
* \{ */
class BPyCommandHandler : public CommandHandler {
public:
BPyCommandHandler(const std::string &id, PyObject *py_exec_fn)
: CommandHandler(id), py_exec_fn(py_exec_fn)
{
}
~BPyCommandHandler() override
{
bpy_cli_command_free(this->py_exec_fn);
}
int exec(bContext *C, int argc, const char **argv) override
{
return bpy_cli_command_exec(C, this->py_exec_fn, argc, argv);
}
PyObject *py_exec_fn = nullptr;
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public Methods
* \{ */
PyDoc_STRVAR(
/* Wrap. */
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"
" :rtype: capsule\n");
static PyObject *bpy_cli_command_register(PyObject * /*self*/, PyObject *args, PyObject *kw)
{
PyObject *py_id;
PyObject *py_exec_fn;
static const char *_keywords[] = {
"id",
"execute",
nullptr,
};
static _PyArg_Parser _parser = {
PY_ARG_PARSER_HEAD_COMPAT()
"O!" /* `id` */
"O" /* `execute` */
":register_cli_command",
_keywords,
nullptr,
};
if (!_PyArg_ParseTupleAndKeywordsFast(args, kw, &_parser, &PyUnicode_Type, &py_id, &py_exec_fn))
{
return nullptr;
}
if (!PyUnicode_IsIdentifier(py_id)) {
PyErr_SetString(PyExc_ValueError, "The command id is not a valid identifier");
return nullptr;
}
if (!PyCallable_Check(py_exec_fn)) {
PyErr_SetString(PyExc_ValueError, "The execute argument must be callable");
return nullptr;
}
const char *id = PyUnicode_AsUTF8(py_id);
std::unique_ptr<CommandHandler> cmd_ptr = std::make_unique<BPyCommandHandler>(
std::string(id), Py_NewRef(py_exec_fn));
void *cmd_p = cmd_ptr.get();
BKE_blender_cli_command_register(std::move(cmd_ptr));
return PyCapsule_New(cmd_p, bpy_cli_command_capsule_name, nullptr);
}
PyDoc_STRVAR(
/* Wrap. */
bpy_cli_command_unregister_doc,
".. method:: unregister_cli_command(handle)\n"
"\n"
" Unregister a CLI command.\n"
"\n"
" :arg handle: The return value of :func:`register_cli_command`.\n"
" :type handle: capsule\n");
static PyObject *bpy_cli_command_unregister(PyObject * /*self*/, PyObject *value)
{
if (!PyCapsule_CheckExact(value)) {
PyErr_Format(PyExc_TypeError,
"Expected a capsule returned from register_cli_command(...), found a: %.200s",
Py_TYPE(value)->tp_name);
return nullptr;
}
BPyCommandHandler *cmd = static_cast<BPyCommandHandler *>(
PyCapsule_GetPointer(value, bpy_cli_command_capsule_name));
if (cmd == nullptr) {
const char *capsule_name = PyCapsule_GetName(value);
if (capsule_name == bpy_cli_command_capsule_name_invalid) {
PyErr_SetString(PyExc_ValueError, "The command has already been removed");
}
else {
PyErr_Format(PyExc_ValueError,
"Unrecognized capsule ID \"%.200s\"",
capsule_name ? capsule_name : "<null>");
}
return nullptr;
}
/* Don't acquire the GIL when un-registering. */
Py_CLEAR(cmd->py_exec_fn);
/* Don't allow removing again. */
PyCapsule_SetName(value, bpy_cli_command_capsule_name_invalid);
BKE_blender_cli_command_unregister((CommandHandler *)cmd);
Py_RETURN_NONE;
}
#ifdef __GNUC__
# ifdef __clang__
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wcast-function-type"
# else
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wcast-function-type"
# endif
#endif
PyMethodDef BPY_cli_command_register_def = {
"register_cli_command",
(PyCFunction)bpy_cli_command_register,
METH_STATIC | METH_VARARGS | METH_KEYWORDS,
bpy_cli_command_register_doc,
};
PyMethodDef BPY_cli_command_unregister_def = {
"unregister_cli_command",
(PyCFunction)bpy_cli_command_unregister,
METH_STATIC | METH_O,
bpy_cli_command_unregister_doc,
};
#ifdef __GNUC__
# ifdef __clang__
# pragma clang diagnostic pop
# else
# pragma GCC diagnostic pop
# endif
#endif
/** \} */