Files
test2/source/blender/python/intern/bpy_rna_id_collection.c
Sergey Sharybin c1bc70b711 Cleanup: Add a copyright notice to files and use SPDX format
A lot of files were missing copyright field in the header and
the Blender Foundation contributed to them in a sense of bug
fixing and general maintenance.

This change makes it explicit that those files are at least
partially copyrighted by the Blender Foundation.

Note that this does not make it so the Blender Foundation is
the only holder of the copyright in those files, and developers
who do not have a signed contract with the foundation still
hold the copyright as well.

Another aspect of this change is using SPDX format for the
header. We already used it for the license specification,
and now we state it for the copyright as well, following the
FAQ:

    https://reuse.software/faq/
2023-05-31 16:19:06 +02:00

439 lines
13 KiB
C

/* SPDX-FileCopyrightText: 2023 Blender Foundation
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup pythonintern
*
* This file adds some helpers related to ID/Main handling, that cannot fit well in RNA itself.
*/
#include <Python.h>
#include <stddef.h>
#include "MEM_guardedalloc.h"
#include "BLI_bitmap.h"
#include "BLI_utildefines.h"
#include "BKE_global.h"
#include "BKE_lib_id.h"
#include "BKE_lib_query.h"
#include "BKE_main.h"
#include "DNA_ID.h"
/* Those following are only to support hack of not listing some internal
* 'backward' pointers in generated user_map. */
#include "DNA_key_types.h"
#include "DNA_object_types.h"
#include "WM_api.h"
#include "WM_types.h"
#include "bpy_capi_utils.h"
#include "bpy_rna_id_collection.h"
#include "../generic/py_capi_rna.h"
#include "../generic/py_capi_utils.h"
#include "../generic/python_utildefines.h"
#include "RNA_access.h"
#include "RNA_enum_types.h"
#include "RNA_types.h"
#include "bpy_rna.h"
typedef struct IDUserMapData {
/** We loop over data-blocks that this ID points to (do build a reverse lookup table) */
PyObject *py_id_curr;
ID *id_curr;
/** Filter the values we add into the set. */
BLI_bitmap *types_bitmap;
/** Set to fill in as we iterate. */
PyObject *user_map;
/** true when we're only mapping a subset of all the ID's (subset arg is passed). */
bool is_subset;
} IDUserMapData;
static int id_code_as_index(const short idcode)
{
return (int)*((ushort *)&idcode);
}
static bool id_check_type(const ID *id, const BLI_bitmap *types_bitmap)
{
return BLI_BITMAP_TEST_BOOL(types_bitmap, id_code_as_index(GS(id->name)));
}
static int foreach_libblock_id_user_map_callback(LibraryIDLinkCallbackData *cb_data)
{
ID **id_p = cb_data->id_pointer;
if (*id_p) {
IDUserMapData *data = cb_data->user_data;
const int cb_flag = cb_data->cb_flag;
if (data->types_bitmap) {
if (!id_check_type(*id_p, data->types_bitmap)) {
return IDWALK_RET_NOP;
}
}
if (cb_flag & IDWALK_CB_LOOPBACK) {
/* We skip loop-back pointers like Key.from here,
* since it's some internal pointer which is not relevant info for py/API level. */
return IDWALK_RET_NOP;
}
if (cb_flag & (IDWALK_CB_EMBEDDED | IDWALK_CB_EMBEDDED_NOT_OWNING)) {
/* We skip private pointers themselves, like root node trees, we'll 'link' their own ID
* pointers to their 'ID owner' instead. */
return IDWALK_RET_NOP;
}
PyObject *key = pyrna_id_CreatePyObject(*id_p);
PyObject *set;
if ((set = PyDict_GetItem(data->user_map, key)) == NULL) {
/* limit to key's added already */
if (data->is_subset) {
return IDWALK_RET_NOP;
}
set = PySet_New(NULL);
PyDict_SetItem(data->user_map, key, set);
Py_DECREF(set);
}
Py_DECREF(key);
if (data->py_id_curr == NULL) {
data->py_id_curr = pyrna_id_CreatePyObject(data->id_curr);
}
PySet_Add(set, data->py_id_curr);
}
return IDWALK_RET_NOP;
}
PyDoc_STRVAR(bpy_user_map_doc,
".. method:: user_map(subset, key_types, value_types)\n"
"\n"
" Returns a mapping of all ID data-blocks in current ``bpy.data`` to a set of all "
"datablocks using them.\n"
"\n"
" For list of valid set members for key_types & value_types, see: "
":class:`bpy.types.KeyingSetPath.id_type`.\n"
"\n"
" :arg subset: When passed, only these data-blocks and their users will be "
"included as keys/values in the map.\n"
" :type subset: sequence\n"
" :arg key_types: Filter the keys mapped by ID types.\n"
" :type key_types: set of strings\n"
" :arg value_types: Filter the values in the set by ID types.\n"
" :type value_types: set of strings\n"
" :return: dictionary of :class:`bpy.types.ID` instances, with sets of ID's as "
"their values.\n"
" :rtype: dict\n");
static PyObject *bpy_user_map(PyObject *UNUSED(self), PyObject *args, PyObject *kwds)
{
#if 0 /* If someone knows how to get a proper 'self' in that case... */
BPy_StructRNA *pyrna = (BPy_StructRNA *)self;
Main *bmain = pyrna->ptr.data;
#else
Main *bmain = G_MAIN; /* XXX Ugly, but should work! */
#endif
ListBase *lb;
ID *id;
PyObject *subset = NULL;
PyObject *key_types = NULL;
PyObject *val_types = NULL;
BLI_bitmap *key_types_bitmap = NULL;
BLI_bitmap *val_types_bitmap = NULL;
PyObject *ret = NULL;
IDUserMapData data_cb = {NULL};
static const char *_keywords[] = {"subset", "key_types", "value_types", NULL};
static _PyArg_Parser _parser = {
"|$" /* Optional keyword only arguments. */
"O" /* `subset` */
"O!" /* `key_types` */
"O!" /* `value_types` */
":user_map",
_keywords,
0,
};
if (!_PyArg_ParseTupleAndKeywordsFast(
args, kwds, &_parser, &subset, &PySet_Type, &key_types, &PySet_Type, &val_types))
{
return NULL;
}
if (key_types) {
key_types_bitmap = pyrna_enum_bitmap_from_set(
rna_enum_id_type_items, key_types, sizeof(short), true, USHRT_MAX, "key types");
if (key_types_bitmap == NULL) {
goto error;
}
}
if (val_types) {
val_types_bitmap = pyrna_enum_bitmap_from_set(
rna_enum_id_type_items, val_types, sizeof(short), true, USHRT_MAX, "value types");
if (val_types_bitmap == NULL) {
goto error;
}
}
if (subset) {
PyObject *subset_fast = PySequence_Fast(subset, "user_map");
if (subset_fast == NULL) {
goto error;
}
PyObject **subset_array = PySequence_Fast_ITEMS(subset_fast);
Py_ssize_t subset_len = PySequence_Fast_GET_SIZE(subset_fast);
data_cb.user_map = _PyDict_NewPresized(subset_len);
data_cb.is_subset = true;
for (; subset_len; subset_array++, subset_len--) {
PyObject *set = PySet_New(NULL);
PyDict_SetItem(data_cb.user_map, *subset_array, set);
Py_DECREF(set);
}
Py_DECREF(subset_fast);
}
else {
data_cb.user_map = PyDict_New();
}
data_cb.types_bitmap = key_types_bitmap;
FOREACH_MAIN_LISTBASE_BEGIN (bmain, lb) {
FOREACH_MAIN_LISTBASE_ID_BEGIN (lb, id) {
/* We cannot skip here in case we have some filter on key types... */
if (key_types_bitmap == NULL && val_types_bitmap != NULL) {
if (!id_check_type(id, val_types_bitmap)) {
break;
}
}
if (!data_cb.is_subset &&
/* We do not want to pre-add keys of filtered out types. */
(key_types_bitmap == NULL || id_check_type(id, key_types_bitmap)) &&
/* We do not want to pre-add keys when we have filter on value types,
* but not on key types. */
(val_types_bitmap == NULL || key_types_bitmap != NULL))
{
PyObject *key = pyrna_id_CreatePyObject(id);
PyObject *set;
/* We have to insert the key now,
* otherwise ID unused would be missing from final dict... */
if ((set = PyDict_GetItem(data_cb.user_map, key)) == NULL) {
set = PySet_New(NULL);
PyDict_SetItem(data_cb.user_map, key, set);
Py_DECREF(set);
}
Py_DECREF(key);
}
if (val_types_bitmap != NULL && !id_check_type(id, val_types_bitmap)) {
continue;
}
data_cb.id_curr = id;
BKE_library_foreach_ID_link(
NULL, id, foreach_libblock_id_user_map_callback, &data_cb, IDWALK_CB_NOP);
if (data_cb.py_id_curr) {
Py_DECREF(data_cb.py_id_curr);
data_cb.py_id_curr = NULL;
}
}
FOREACH_MAIN_LISTBASE_ID_END;
}
FOREACH_MAIN_LISTBASE_ID_END;
ret = data_cb.user_map;
error:
if (key_types_bitmap != NULL) {
MEM_freeN(key_types_bitmap);
}
if (val_types_bitmap != NULL) {
MEM_freeN(val_types_bitmap);
}
return ret;
}
PyDoc_STRVAR(bpy_batch_remove_doc,
".. method:: batch_remove(ids)\n"
"\n"
" Remove (delete) several IDs at once.\n"
"\n"
" WARNING: Considered experimental feature currently.\n"
"\n"
" Note that this function is quicker than individual calls to :func:`remove()` "
"(from :class:`bpy.types.BlendData`\n"
" ID collections), but less safe/versatile (it can break Blender, e.g. by removing "
"all scenes...).\n"
"\n"
" :arg ids: Iterables of IDs (types can be mixed).\n"
" :type subset: sequence\n");
static PyObject *bpy_batch_remove(PyObject *UNUSED(self), PyObject *args, PyObject *kwds)
{
#if 0 /* If someone knows how to get a proper 'self' in that case... */
BPy_StructRNA *pyrna = (BPy_StructRNA *)self;
Main *bmain = pyrna->ptr.data;
#else
Main *bmain = G_MAIN; /* XXX Ugly, but should work! */
#endif
PyObject *ids = NULL;
PyObject *ret = NULL;
static const char *_keywords[] = {"ids", NULL};
static _PyArg_Parser _parser = {
"O" /* `ids` */
":batch_remove",
_keywords,
0,
};
if (!_PyArg_ParseTupleAndKeywordsFast(args, kwds, &_parser, &ids)) {
return ret;
}
if (ids) {
BKE_main_id_tag_all(bmain, LIB_TAG_DOIT, false);
PyObject *ids_fast = PySequence_Fast(ids, "batch_remove");
if (ids_fast == NULL) {
goto error;
}
PyObject **ids_array = PySequence_Fast_ITEMS(ids_fast);
Py_ssize_t ids_len = PySequence_Fast_GET_SIZE(ids_fast);
for (; ids_len; ids_array++, ids_len--) {
ID *id;
if (!pyrna_id_FromPyObject(*ids_array, &id)) {
PyErr_Format(
PyExc_TypeError, "Expected an ID type, not %.200s", Py_TYPE(*ids_array)->tp_name);
Py_DECREF(ids_fast);
goto error;
}
id->tag |= LIB_TAG_DOIT;
}
Py_DECREF(ids_fast);
BKE_id_multi_tagged_delete(bmain);
/* Force full redraw, mandatory to avoid crashes when running this from UI... */
WM_main_add_notifier(NC_WINDOW, NULL);
}
else {
goto error;
}
Py_INCREF(Py_None);
ret = Py_None;
error:
return ret;
}
PyDoc_STRVAR(bpy_orphans_purge_doc,
".. method:: orphans_purge()\n"
"\n"
" Remove (delete) all IDs with no user.\n"
"\n"
" :arg do_local_ids: Include unused local IDs in the deletion, defaults to True\n"
" :type do_local_ids: bool, optional\n"
" :arg do_linked_ids: Include unused linked IDs in the deletion, defaults to True\n"
" :type do_linked_ids: bool, optional\n"
" :arg do_recursive: Recursively check for unused IDs, ensuring no orphaned one "
"remain after a single run of that function, defaults to False\n"
" :type do_recursive: bool, optional\n"
" :return: The number of deleted IDs.\n");
static PyObject *bpy_orphans_purge(PyObject *UNUSED(self), PyObject *args, PyObject *kwds)
{
#if 0 /* If someone knows how to get a proper 'self' in that case... */
BPy_StructRNA *pyrna = (BPy_StructRNA *)self;
Main *bmain = pyrna->ptr.data;
#else
Main *bmain = G_MAIN; /* XXX Ugly, but should work! */
#endif
int num_tagged[INDEX_ID_MAX] = {0};
bool do_local_ids = true;
bool do_linked_ids = true;
bool do_recursive_cleanup = false;
static const char *_keywords[] = {"do_local_ids", "do_linked_ids", "do_recursive", NULL};
static _PyArg_Parser _parser = {
"|" /* Optional arguments. */
"O&" /* `do_local_ids` */
"O&" /* `do_linked_ids` */
"O&" /* `do_recursive` */
":orphans_purge",
_keywords,
0,
};
if (!_PyArg_ParseTupleAndKeywordsFast(args,
kwds,
&_parser,
PyC_ParseBool,
&do_local_ids,
PyC_ParseBool,
&do_linked_ids,
PyC_ParseBool,
&do_recursive_cleanup))
{
return NULL;
}
/* Tag all IDs to delete. */
BKE_lib_query_unused_ids_tag(
bmain, LIB_TAG_DOIT, do_local_ids, do_linked_ids, do_recursive_cleanup, num_tagged);
if (num_tagged[INDEX_ID_NULL] == 0) {
return PyLong_FromSize_t(0);
}
const size_t num_datablocks_deleted = BKE_id_multi_tagged_delete(bmain);
/* Force full redraw, mandatory to avoid crashes when running this from UI... */
WM_main_add_notifier(NC_WINDOW, NULL);
return PyLong_FromSize_t(num_datablocks_deleted);
}
PyMethodDef BPY_rna_id_collection_user_map_method_def = {
"user_map",
(PyCFunction)bpy_user_map,
METH_STATIC | METH_VARARGS | METH_KEYWORDS,
bpy_user_map_doc,
};
PyMethodDef BPY_rna_id_collection_batch_remove_method_def = {
"batch_remove",
(PyCFunction)bpy_batch_remove,
METH_STATIC | METH_VARARGS | METH_KEYWORDS,
bpy_batch_remove_doc,
};
PyMethodDef BPY_rna_id_collection_orphans_purge_method_def = {
"orphans_purge",
(PyCFunction)bpy_orphans_purge,
METH_STATIC | METH_VARARGS | METH_KEYWORDS,
bpy_orphans_purge_doc,
};