Files
test2/source/blender/python/intern/bpy_cli_command.cc
Campbell Barton 6197335658 Build: resolve errors & deprecation warnings with Python 3.14 beta
Python 3.14 has moved some functionality into the public API,
use the updated names even with older Python versions.

Also resolve an error caused by variable reuse with delayed annotation
evaluation for TextureProperties_MixIn on startup.

Resolve #140695.
2025-06-20 04:19:35 +00:00

326 lines
9.5 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" /* IWYU pragma: keep. */
#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"
"\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 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
/** \} */