Files
test/source/blender/python/intern/bpy_library_write.cc

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

208 lines
6.1 KiB
C++
Raw Permalink Normal View History

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup pythonintern
*
* Python API for writing a set of data-blocks into a file.
* Useful for writing out asset-libraries, defines: `bpy.data.libraries.write(...)`.
*/
#include <Python.h>
2023-07-21 16:05:33 +10:00
#include <cstddef>
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BKE_blendfile.hh"
#include "BKE_global.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_report.hh"
#include "BLO_writefile.hh"
#include "RNA_types.hh"
#include "bpy_capi_utils.hh"
#include "bpy_library.hh" /* Declaration for #BPY_library_load_method_def */
#include "bpy_rna.hh"
#include "../generic/py_capi_utils.hh"
#include "../generic/python_compat.hh" /* IWYU pragma: keep. */
using namespace blender::bke::blendfile;
PyDoc_STRVAR(
/* Wrap. */
bpy_lib_write_doc,
".. method:: write(filepath, datablocks, *, "
"path_remap=False, fake_user=False, compress=False)\n"
"\n"
" Write data-blocks into a blend file.\n"
"\n"
" .. note::\n"
"\n"
" Indirectly referenced data-blocks will be expanded and written too.\n"
"\n"
" :arg filepath: The path to write the blend-file.\n"
" :type filepath: str | bytes\n"
" :arg datablocks: set of data-blocks.\n"
" :type datablocks: set[:class:`bpy.types.ID`]\n"
" :arg path_remap: Optionally remap paths when writing the file:\n"
"\n"
" - ``NONE`` No path manipulation (default).\n"
" - ``RELATIVE`` Remap paths that are already relative to the new location.\n"
" - ``RELATIVE_ALL`` Remap all paths to be relative to the new location.\n"
" - ``ABSOLUTE`` Make all paths absolute on writing.\n"
"\n"
" :type path_remap: str\n"
" :arg fake_user: When True, data-blocks will be written with fake-user flag enabled.\n"
" :type fake_user: bool\n"
" :arg compress: When True, write a compressed blend file.\n"
" :type compress: bool\n");
static PyObject *bpy_lib_write(BPy_PropertyRNA *self, PyObject *args, PyObject *kw)
{
/* args */
PyC_UnicodeAsBytesAndSize_Data filepath_data = {nullptr};
char filepath_abs[FILE_MAX];
PyObject *datablocks = nullptr;
const PyC_StringEnumItems path_remap_items[] = {
{BLO_WRITE_PATH_REMAP_NONE, "NONE"},
{BLO_WRITE_PATH_REMAP_RELATIVE, "RELATIVE"},
{BLO_WRITE_PATH_REMAP_RELATIVE_ALL, "RELATIVE_ALL"},
{BLO_WRITE_PATH_REMAP_ABSOLUTE, "ABSOLUTE"},
{0, nullptr},
};
PyC_StringEnum path_remap = {path_remap_items, BLO_WRITE_PATH_REMAP_NONE};
bool use_fake_user = false, use_compress = false;
static const char *_keywords[] = {
"filepath",
"datablocks",
"path_remap",
"fake_user",
"compress",
nullptr,
};
static _PyArg_Parser _parser = {
PY_ARG_PARSER_HEAD_COMPAT()
"O&" /* `filepath` */
"O!" /* `datablocks` */
"|$" /* Optional keyword only arguments. */
"O&" /* `path_remap` */
"O&" /* `fake_user` */
"O&" /* `compress` */
":write",
_keywords,
2023-08-03 19:14:53 +10:00
nullptr,
};
if (!_PyArg_ParseTupleAndKeywordsFast(args,
kw,
&_parser,
PyC_ParseUnicodeAsBytesAndSize,
&filepath_data,
&PySet_Type,
&datablocks,
PyC_ParseStringEnum,
&path_remap,
PyC_ParseBool,
&use_fake_user,
PyC_ParseBool,
&use_compress))
{
return nullptr;
}
RNA: Make the `PointerRNA` struct non-trivial. For now, PointerRNA is made non-trivial by giving explicit default values to its members. Besides of BPY python binding code, the change is relatively trivial. The main change (besides the creation/deletion part) is the replacement of `memset` by zero-initialized assignment (using `{}`). makesrna required changes are quite small too. The big piece of this PR is the refactor of the BPY RNA code. It essentially brings back allocation and deletion of the BPy_StructRNA, BPy_Pointer etc. python objects into 'cannonical process', using `__new__`, and `__init__` callbacks (and there matching CAPI functions). Existing code was doing very low-level manipulations to create these data, which is not really easy to understand, and AFAICT incompatible with handling C++ data that needs to be constructed and destructed. Unfortunately, similar change in destruction code (using `__del__` and matching `tp_finalize` CAPI callback) is not possible, because of technical low-level implementation details in CPython (see [1] for details). `std::optional` pointer management is used to encapsulate PointerRNA data. This allows to keep control on _when_ actual RNA creation is done, and to have a safe destruction in `tp_dealloc` callbacks. Note that a critical change in Blender's Python API will be that classes inherinting from `bpy_struct` etc. will now have to properly call the base class `__new__` and/or `__init__`if they define them. Implements #122431. [1] https://discuss.python.org/t/cpython-usage-of-tp-finalize-in-c-defined-static-types-with-no-custom-tp-dealloc/64100
2024-10-30 15:08:37 +01:00
Main *bmain_src = static_cast<Main *>(self->ptr->data); /* Typically #G_MAIN */
int write_flags = 0;
if (use_compress) {
write_flags |= G_FILE_COMPRESS;
}
STRNCPY(filepath_abs, filepath_data.value);
Py_XDECREF(filepath_data.value_coerce);
BLI_path_abs(filepath_abs, BKE_main_blendfile_path_from_global());
PartialWriteContext partial_write_ctx{*bmain_src};
const PartialWriteContext::IDAddOptions add_options{
(PartialWriteContext::IDAddOperations::ADD_DEPENDENCIES |
PartialWriteContext::IDAddOperations(
use_fake_user ? PartialWriteContext::IDAddOperations::SET_FAKE_USER : 0))};
if (PySet_GET_SIZE(datablocks) > 0) {
PyObject *it = PyObject_GetIter(datablocks);
PyObject *key;
while ((key = PyIter_Next(it))) {
/* Borrow from the set. */
Py_DECREF(key);
ID *id;
if (!pyrna_id_FromPyObject(key, &id)) {
PyErr_Format(PyExc_TypeError, "Expected an ID type, not %.200s", Py_TYPE(key)->tp_name);
break;
}
partial_write_ctx.id_add(id, add_options, nullptr);
}
Py_DECREF(it);
if (key) {
return nullptr;
}
}
BLI_assert(partial_write_ctx.is_valid());
/* write blend */
ReportList reports;
BKE_reports_init(&reports, RPT_STORE | RPT_PRINT_HANDLED_BY_OWNER);
bool success = partial_write_ctx.write(
filepath_abs, write_flags, path_remap.value_found, reports);
PyObject *py_return_value;
if (success) {
BKE_reports_print(&reports, RPT_ERROR_ALL);
py_return_value = Py_None;
Py_INCREF(py_return_value);
}
else {
if (BPy_reports_to_error(&reports, PyExc_IOError, false) == 0) {
PyErr_SetString(PyExc_IOError, "Unknown error writing library data");
}
py_return_value = nullptr;
}
BKE_reports_free(&reports);
return py_return_value;
}
#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_library_write_method_def = {
"write",
(PyCFunction)bpy_lib_write,
METH_VARARGS | METH_KEYWORDS,
bpy_lib_write_doc,
};
#ifdef __GNUC__
# ifdef __clang__
# pragma clang diagnostic pop
# else
# pragma GCC diagnostic pop
# endif
#endif