Add a basic support to BKE library linking code to generate liboverrides. #104746

Merged
Bastien Montagne merged 1 commits from mont29/blender:F-link-override-basics into main 2023-03-02 17:22:54 +01:00
4 changed files with 380 additions and 9 deletions

View File

@ -121,6 +121,8 @@ void *BKE_blendfile_link_append_context_item_userdata_get(
struct BlendfileLinkAppendContext *lapp_context, struct BlendfileLinkAppendContextItem *item);
struct ID *BKE_blendfile_link_append_context_item_newid_get(
struct BlendfileLinkAppendContext *lapp_context, struct BlendfileLinkAppendContextItem *item);
struct ID *BKE_blendfile_link_append_context_item_liboverrideid_get(
struct BlendfileLinkAppendContext *lapp_context, struct BlendfileLinkAppendContextItem *item);
short BKE_blendfile_link_append_context_item_idcode_get(
struct BlendfileLinkAppendContext *lapp_context, struct BlendfileLinkAppendContextItem *item);
@ -175,6 +177,45 @@ void BKE_blendfile_append(struct BlendfileLinkAppendContext *lapp_context,
void BKE_blendfile_link(struct BlendfileLinkAppendContext *lapp_context,
struct ReportList *reports);
/**
* Options controlling the behavior of liboverrides creation.
*/
typedef enum eBKELibLinkOverride {
BKE_LIBLINK_OVERRIDE_INIT = 0,
/**
* Try to find a matching existing liboverride first, instead of always creating a new one.
*
* \note Takes into account the #BKE_LIBLINK_CREATE_RUNTIME flag too (i.e. only checks for
* runtime liboverrides if that flag is set, and vice-versa).
*/
BKE_LIBLINK_OVERRIDE_USE_EXISTING_LIBOVERRIDES = 1 << 0,
/**
* Create (or return an existing) runtime liboverride, instead of a regular saved-in-blendfiles
* one. See also the #LIB_TAG_RUNTIME tag of IDs in DNA_ID.h.
*
* \note Typically, usage of this flag implies that no linked IDs are instantiated, such that
* their usages remain indirect.
*/
BKE_LIBLINK_OVERRIDE_CREATE_RUNTIME = 1 << 1,
} eBKELibLinkOverride;
/**
* Create (or find existing) liboverrides from linked data.
*
* The IDs processed by this functions are the one that have been linked by a previous call to
* #BKE_blendfile_link on the same `lapp_context`.
*
* Control over how liboverrides are created is done through the extra #eBKELibLinkOverride flags.
*
* \warning Currently this function only performs very (very!) basic liboverrides, with no handling
* of dependencies or hierarchies. It is not expected to be directly exposed to users in its
* current state, but rather as a helper for specific use-cases like 'presets assets' handling.
*/
void BKE_blendfile_override(struct BlendfileLinkAppendContext *lapp_context,
const eBKELibLinkOverride flags,
struct ReportList *reports);
/**
* Try to relocate all linked IDs added to `lapp_context`, belonging to the given `library`.
*

View File

@ -41,6 +41,7 @@
#include "BKE_lib_query.h"
#include "BKE_lib_remap.h"
#include "BKE_main.h"
#include "BKE_main_namemap.h"
#include "BKE_material.h"
#include "BKE_object.h"
#include "BKE_report.h"
@ -76,6 +77,10 @@ typedef struct BlendfileLinkAppendContextItem {
/** Library ID from which the #new_id has been linked (NULL until it has been successfully
* linked). */
Library *source_library;
/** Liboverride of the linked ID (NULL until it has been successfully created or an existing one
* has been found). */
ID *liboverride_id;
/** Opaque user data pointer. */
void *userdata;
} BlendfileLinkAppendContextItem;
@ -351,6 +356,12 @@ ID *BKE_blendfile_link_append_context_item_newid_get(
return item->new_id;
}
ID *BKE_blendfile_link_append_context_item_liboverrideid_get(
BlendfileLinkAppendContext *UNUSED(lapp_context), BlendfileLinkAppendContextItem *item)
{
return item->liboverride_id;
}
short BKE_blendfile_link_append_context_item_idcode_get(
struct BlendfileLinkAppendContext *UNUSED(lapp_context),
struct BlendfileLinkAppendContextItem *item)
@ -1316,6 +1327,92 @@ void BKE_blendfile_link(BlendfileLinkAppendContext *lapp_context, ReportList *re
if ((lapp_context->params->flag & FILE_LINK) != 0) {
blendfile_link_append_proxies_convert(lapp_context->params->bmain, reports);
}
BKE_main_namemap_clear(lapp_context->params->bmain);
}
void BKE_blendfile_override(BlendfileLinkAppendContext *lapp_context,
const eBKELibLinkOverride flags,
ReportList *UNUSED(reports))
{
if (lapp_context->num_items == 0) {
/* Nothing to override. */
return;
}
Main *bmain = lapp_context->params->bmain;
/* Liboverride only makes sense if data was linked, not appended. */
BLI_assert((lapp_context->params->flag & FILE_LINK) != 0);
const bool set_runtime = (flags & BKE_LIBLINK_OVERRIDE_CREATE_RUNTIME) != 0;
const bool do_use_exisiting_liboverrides = (flags &
BKE_LIBLINK_OVERRIDE_USE_EXISTING_LIBOVERRIDES) != 0;
GHash *linked_ids_to_local_liboverrides = NULL;
mont29 marked this conversation as resolved
Review

picky can be declared in the for loop.

*picky* can be declared in the for loop.
if (do_use_exisiting_liboverrides) {
linked_ids_to_local_liboverrides = BLI_ghash_ptr_new(__func__);
ID *id_iter;
FOREACH_MAIN_ID_BEGIN (bmain, id_iter) {
if (ID_IS_LINKED(id_iter)) {
continue;
}
if (!ID_IS_OVERRIDE_LIBRARY_REAL(id_iter)) {
continue;
}
/* Do not consider regular liboverrides if runtime ones are requested, and vice-versa. */
if ((set_runtime && (id_iter->tag & LIB_TAG_RUNTIME) == 0) ||
(!set_runtime && (id_iter->tag & LIB_TAG_RUNTIME) != 0)) {
continue;
}
/* In case several liboverrides exist of the same data, only consider the first found one. */
ID **id_ptr;
if (BLI_ghash_ensure_p(linked_ids_to_local_liboverrides,
id_iter->override_library->reference,
(void ***)&id_ptr)) {
continue;
}
*id_ptr = id_iter;
}
FOREACH_MAIN_ID_END;
}
for (LinkNode *itemlink = lapp_context->items.list; itemlink; itemlink = itemlink->next) {
BlendfileLinkAppendContextItem *item = itemlink->link;
ID *id = item->new_id;
if (id == NULL) {
continue;
}
BLI_assert(item->userdata == NULL);
if (do_use_exisiting_liboverrides) {
item->liboverride_id = BLI_ghash_lookup(linked_ids_to_local_liboverrides, id);
}
if (item->liboverride_id == NULL) {
item->liboverride_id = BKE_lib_override_library_create_from_id(bmain, id, false);
if (set_runtime) {
item->liboverride_id->tag |= LIB_TAG_RUNTIME;
if ((id->tag & LIB_TAG_PRE_EXISTING) == 0) {
/* If the linked ID is newly linked, in case its override is runtime-only, assume its
* reference to be indirectly linked.
*
* This is more of an heuristic for 'as best as possible' user feedback in the UI
* (Outliner), which is expected to be valid in almost all practical usecases. Direct or
* indirect linked status is properly checked before saving .blend file. */
id->tag &= ~LIB_TAG_EXTERN;
id->tag |= LIB_TAG_INDIRECT;
}
}
}
}
if (do_use_exisiting_liboverrides) {
BLI_ghash_free(linked_ids_to_local_liboverrides, NULL, NULL);
}
BKE_main_namemap_clear(bmain);
}
/** \} */

View File

@ -58,6 +58,10 @@ typedef struct {
BlendFileReadReport bf_reports;
int flag;
bool create_liboverrides;
eBKELibLinkOverride liboverride_flags;
PyObject *dict;
/* Borrowed reference to the `bmain`, taken from the RNA instance of #RNA_BlendDataLibraries.
* Defaults to #G.main, Otherwise use a temporary #Main when `bmain_is_temp` is true. */
@ -137,7 +141,9 @@ static PyTypeObject bpy_lib_Type = {
PyDoc_STRVAR(
bpy_lib_load_doc,
".. method:: load(filepath, link=False, relative=False, assets_only=False)\n"
".. method:: load(filepath, link=False, relative=False, assets_only=False,\n"
" create_liboverrides=False, reuse_liboverrides=False,\n"
" create_liboverrides_runtime=False)\n"
"\n"
" Returns a context manager which exposes 2 library objects on entering.\n"
" Each object has attributes matching bpy.data which are lists of strings to be linked.\n"
@ -149,7 +155,16 @@ PyDoc_STRVAR(
" :arg relative: When True the path is stored relative to the open blend file.\n"
" :type relative: bool\n"
mont29 marked this conversation as resolved
Review

The new arguments are missing from the declaration which currently reads.

.. method:: load(filepath, link=False, relative=False, assets_only=False)

Useful as it provides defaults.

The new arguments are missing from the declaration which currently reads. ``.. method:: load(filepath, link=False, relative=False, assets_only=False)`` Useful as it provides defaults.
" :arg assets_only: If True, only list data-blocks marked as assets.\n"
mont29 marked this conversation as resolved
Review

picky no space before newline needed.

*picky* no space before newline needed.
" :type assets_only: bool\n");
" :type assets_only: bool\n"
mont29 marked this conversation as resolved
Review

picky only 3 additional spaces of indentation is required.

*picky* only 3 additional spaces of indentation is required.
" :arg create_liboverrides: If True and ``link`` is True, liboverrides will\n"
" be created for linked data.\n"
mont29 marked this conversation as resolved
Review

literal quoted values should need to be double-back-tick quoted.

literal quoted values should need to be double-back-tick quoted.
" :type create_liboverrides: bool\n"
" :arg reuse_liboverrides: If True and ``create_liboverride`` is True,\n"
" search for existing liboverride first.\n"
mont29 marked this conversation as resolved
Review

create_runtime reads ambiguously and doesn't obviously relate to library-overrides.

While it's a bit of a mouthful, Prefer create_liboverrides_runtime.

`create_runtime` reads ambiguously and doesn't obviously relate to library-overrides. While it's a bit of a mouthful, Prefer `create_liboverrides_runtime`.
" :type reuse_liboverrides: bool\n"
" :arg create_liboverrides_runtime: If True and ``create_liboverride`` is True,\n"
" create (or search for existing) runtime liboverride.\n"
" :type create_liboverrides_runtime: bool\n");
static PyObject *bpy_lib_load(BPy_PropertyRNA *self, PyObject *args, PyObject *kw)
{
Main *bmain_base = CTX_data_main(BPY_context_get());
@ -157,8 +172,19 @@ static PyObject *bpy_lib_load(BPy_PropertyRNA *self, PyObject *args, PyObject *k
BPy_Library *ret;
mont29 marked this conversation as resolved
Review

Code in the body of this function accounts for link being false & create_liboverrides being true. e.g.

const bool create_liboverrides = !do_append && self->create_liboverrides;

This adds some uncertainty in how arguments should be used, implying that checking self->create_liboverrides without do_append might cause bugs or unexpected behavior.


Suggest to raise an exception when invalid arguments combination are passed in. This way there is no need to check combinations of options are valid in the functions body.

Alternatively, set create_liboverrides false when link is false, and warn that invalid combination of arguments were passed in.

Code in the body of this function accounts for `link` being false & `create_liboverrides` being true. e.g. `const bool create_liboverrides = !do_append && self->create_liboverrides;` This adds some uncertainty in how arguments should be used, implying that checking `self->create_liboverrides` without `do_append` might cause bugs or unexpected behavior. ---- Suggest to raise an exception when invalid arguments combination are passed in. This way there is no need to check combinations of options are valid in the functions body. Alternatively, set `create_liboverrides` false when `link` is false, and warn that invalid combination of arguments were passed in.
const char *filepath = NULL;
bool is_rel = false, is_link = false, use_assets_only = false;
bool create_liboverrides = false, reuse_liboverrides = false,
create_liboverrides_runtime = false;
static const char *_keywords[] = {"filepath", "link", "relative", "assets_only", NULL};
static const char *_keywords[] = {
"filepath",
"link",
"relative",
"assets_only",
mont29 marked this conversation as resolved
Review

picky NULL, }; to reduce right shift.

*picky* ```NULL, };``` to reduce right shift.
"create_liboverrides",
"reuse_liboverrides",
"create_liboverrides_runtime",
NULL,
};
static _PyArg_Parser _parser = {
"s" /* `filepath` */
/* Optional keyword only arguments. */
@ -166,6 +192,9 @@ static PyObject *bpy_lib_load(BPy_PropertyRNA *self, PyObject *args, PyObject *k
"O&" /* `link` */
"O&" /* `relative` */
"O&" /* `assets_only` */
"O&" /* `create_liboverrides` */
"O&" /* `reuse_liboverrides` */
"O&" /* `create_liboverrides_runtime` */
":load",
_keywords,
0,
@ -179,7 +208,28 @@ static PyObject *bpy_lib_load(BPy_PropertyRNA *self, PyObject *args, PyObject *k
PyC_ParseBool,
&is_rel,
PyC_ParseBool,
&use_assets_only)) {
&use_assets_only,
PyC_ParseBool,
&create_liboverrides,
PyC_ParseBool,
&reuse_liboverrides,
PyC_ParseBool,
&create_liboverrides_runtime)) {
return NULL;
}
if (!is_link && create_liboverrides) {
PyErr_SetString(PyExc_ValueError, "`link` is False but `create_liboverrides` is True");
return NULL;
}
if (!create_liboverrides && reuse_liboverrides) {
PyErr_SetString(PyExc_ValueError,
"`create_liboverrides` is False but `reuse_liboverrides` is True");
return NULL;
}
if (!create_liboverrides && create_liboverrides_runtime) {
PyErr_SetString(PyExc_ValueError,
"`create_liboverrides` is False but `create_liboverrides_runtime` is True");
return NULL;
}
@ -195,6 +245,12 @@ static PyObject *bpy_lib_load(BPy_PropertyRNA *self, PyObject *args, PyObject *k
ret->blo_handle = NULL;
ret->flag = ((is_link ? FILE_LINK : 0) | (is_rel ? FILE_RELPATH : 0) |
(use_assets_only ? FILE_ASSETS_ONLY : 0));
ret->create_liboverrides = create_liboverrides;
ret->liboverride_flags =
create_liboverrides ?
((reuse_liboverrides ? BKE_LIBLINK_OVERRIDE_USE_EXISTING_LIBOVERRIDES : 0) |
(create_liboverrides_runtime ? BKE_LIBLINK_OVERRIDE_CREATE_RUNTIME : 0)) :
0;
ret->dict = _PyDict_NewPresized(INDEX_ID_MAX);
@ -267,6 +323,8 @@ static PyObject *bpy_lib_enter(BPy_Library *self)
self_from->blo_handle = NULL;
self_from->flag = 0;
self_from->create_liboverrides = false;
self_from->liboverride_flags = BKE_LIBLINK_OVERRIDE_INIT;
self_from->dict = from_dict; /* owns the dict */
/* return pair */
@ -339,6 +397,10 @@ static bool bpy_lib_exit_lapp_context_items_cb(BlendfileLinkAppendContext *lapp_
const int py_list_index = POINTER_AS_INT(
BKE_blendfile_link_append_context_item_userdata_get(lapp_context, item));
ID *new_id = BKE_blendfile_link_append_context_item_newid_get(lapp_context, item);
ID *liboverride_id = data->py_library->create_liboverrides ?
BKE_blendfile_link_append_context_item_liboverrideid_get(lapp_context,
item) :
NULL;
BLI_assert(py_list_index < data->py_list_size);
@ -349,7 +411,12 @@ static bool bpy_lib_exit_lapp_context_items_cb(BlendfileLinkAppendContext *lapp_
BLI_assert(item_src != Py_None);
PyObject *py_item;
if (new_id != NULL) {
if (liboverride_id != NULL) {
PointerRNA newid_ptr;
RNA_id_pointer_create(liboverride_id, &newid_ptr);
py_item = pyrna_struct_CreatePyObject(&newid_ptr);
}
else if (new_id != NULL) {
PointerRNA newid_ptr;
RNA_id_pointer_create(new_id, &newid_ptr);
py_item = pyrna_struct_CreatePyObject(&newid_ptr);
@ -374,6 +441,10 @@ static PyObject *bpy_lib_exit(BPy_Library *self, PyObject *UNUSED(args))
{
Main *bmain = self->bmain;
const bool do_append = ((self->flag & FILE_LINK) == 0);
const bool create_liboverrides = self->create_liboverrides;
/* Code in #bpy_lib_load should have raised exception in case of incompatible parameter values.
*/
BLI_assert(!do_append || !create_liboverrides);
BKE_main_id_tag_all(bmain, LIB_TAG_PRE_EXISTING, true);
@ -438,6 +509,9 @@ static PyObject *bpy_lib_exit(BPy_Library *self, PyObject *UNUSED(args))
if (do_append) {
BKE_blendfile_append(lapp_context, NULL);
}
else if (create_liboverrides) {
BKE_blendfile_override(lapp_context, self->liboverride_flags, NULL);
}
/* If enabled, replace named items in given lists by the final matching new ID pointer. */
#ifdef USE_RNA_DATABLOCKS

View File

@ -518,19 +518,23 @@ class TestBlendLibLibraryRelocate(TestBlendLibLinkHelper):
assert orig_data == relocate_data
# Python library loader context manager.
class TestBlendLibDataLibrariesLoad(TestBlendLibLinkHelper):
def __init__(self, args):
self.args = args
def test_link_relocate(self):
def do_libload_init(self):
output_dir = self.args.output_dir
output_lib_path = self.init_lib_data_basic()
# Simple link of a single Object, and reload.
self.reset_blender()
return output_lib_path
with bpy.data.libraries.load(filepath=output_lib_path) as lib_ctx:
def do_libload(self, **load_kwargs):
with bpy.data.libraries.load(**load_kwargs) as lib_ctx:
lib_src, lib_link = lib_ctx
assert len(lib_src.meshes) == 1
@ -543,12 +547,164 @@ class TestBlendLibDataLibrariesLoad(TestBlendLibLinkHelper):
lib_link.collections.append(lib_src.collections[0])
# Linking happens when living the context manager.
# Linking/append/liboverride happens when living the context manager.
class TestBlendLibDataLibrariesLoadAppend(TestBlendLibDataLibrariesLoad):
def test_libload_append(self):
output_lib_path = self.do_libload_init()
self.do_libload(filepath=output_lib_path, link=False, create_liboverrides=False)
assert len(bpy.data.meshes) == 1
assert len(bpy.data.objects) == 1 # This code does no instantiation.
assert len(bpy.data.collections) == 1
# Append, so all data should have been made local.
assert bpy.data.meshes[0].library is None
assert bpy.data.objects[0].library is None
assert bpy.data.collections[0].library is None
class TestBlendLibDataLibrariesLoadLink(TestBlendLibDataLibrariesLoad):
def test_libload_link(self):
output_lib_path = self.do_libload_init()
self.do_libload(filepath=output_lib_path, link=True, create_liboverrides=False)
assert len(bpy.data.meshes) == 1
assert len(bpy.data.objects) == 1 # This code does no instantiation.
assert len(bpy.data.collections) == 1
# Link, so all data should have remained linked.
assert bpy.data.meshes[0].library is not None
assert bpy.data.objects[0].library is not None
assert bpy.data.collections[0].library is not None
class TestBlendLibDataLibrariesLoadLibOverride(TestBlendLibDataLibrariesLoad):
def test_libload_liboverride(self):
output_lib_path = self.do_libload_init()
self.do_libload(filepath=output_lib_path, link=True, create_liboverrides=True)
assert len(bpy.data.meshes) == 1
assert len(bpy.data.objects) == 1 # This code does no instantiation.
assert len(bpy.data.collections) == 2 # The linked one and its local liboverride.
# Link + LibOverride, so linked data should have remained linked.
assert bpy.data.meshes[-1].library is not None
Review

picky is None / is not None (autopep8 even makes this change automatically).

*picky* `is None` / `is not None` (autopep8 even makes this change automatically).
assert bpy.data.objects[-1].library is not None
assert bpy.data.collections[-1].library is not None
# Only explicitely linked data gets a liboverride, without any handling of hierarchy/dependencies.
assert bpy.data.collections[0].library is None
assert bpy.data.collections[0].is_runtime_data == False
assert bpy.data.collections[0].override_library is not None
assert bpy.data.collections[0].override_library.reference == bpy.data.collections[-1]
# Should create another liboverride for the linked collection.
self.do_libload(filepath=output_lib_path, link=True, create_liboverrides=True, reuse_liboverrides=False)
assert len(bpy.data.meshes) == 1
assert len(bpy.data.objects) == 1 # This code does no instantiation.
assert len(bpy.data.collections) == 3 # The linked one and its two local liboverrides.
# Link + LibOverride, so linked data should have remained linked.
assert bpy.data.meshes[-1].library is not None
assert bpy.data.objects[-1].library is not None
assert bpy.data.collections[-1].library is not None
# Only explicitely linked data gets a liboverride, without any handling of hierarchy/dependencies.
assert bpy.data.collections[1].library is None
assert bpy.data.collections[1].is_runtime_data == False
assert bpy.data.collections[1].override_library is not None
assert bpy.data.collections[1].override_library.reference == bpy.data.collections[-1]
# This call should not change anything, first liboverrides should be found and 'reused'.
self.do_libload(filepath=output_lib_path, link=True, create_liboverrides=True, reuse_liboverrides=True)
assert len(bpy.data.meshes) == 1
assert len(bpy.data.objects) == 1 # This code does no instantiation.
assert len(bpy.data.collections) == 3 # The linked one and its two local liboverrides.
# Link + LibOverride, so linked data should have remained linked.
assert bpy.data.meshes[-1].library is not None
assert bpy.data.objects[-1].library is not None
assert bpy.data.collections[-1].library is not None
# Only explicitely linked data gets a liboverride, without any handling of hierarchy/dependencies.
assert bpy.data.collections[1].library is None
assert bpy.data.collections[1].is_runtime_data == False
assert bpy.data.collections[1].override_library is not None
assert bpy.data.collections[1].override_library.reference == bpy.data.collections[-1]
def test_libload_liboverride_runtime(self):
output_lib_path = self.do_libload_init()
self.do_libload(filepath=output_lib_path, link=True,
create_liboverrides=True,
create_liboverrides_runtime=True)
assert len(bpy.data.meshes) == 1
assert len(bpy.data.objects) == 1 # This code does no instantiation.
assert len(bpy.data.collections) == 2 # The linked one and its local liboverride.
# Link + LibOverride, so linked data should have remained linked.
assert bpy.data.meshes[-1].library is not None
assert bpy.data.objects[-1].library is not None
assert bpy.data.collections[-1].library is not None
# Only explicitely linked data gets a liboverride, without any handling of hierarchy/dependencies.
assert bpy.data.collections[0].library is None
assert bpy.data.collections[0].is_runtime_data == True
assert bpy.data.collections[0].override_library is not None
assert bpy.data.collections[0].override_library.reference == bpy.data.collections[-1]
# This call should not change anything, first liboverrides should be found and 'reused'.
self.do_libload(filepath=output_lib_path,
link=True,
create_liboverrides=True,
create_liboverrides_runtime=True,
reuse_liboverrides=True)
assert len(bpy.data.meshes) == 1
assert len(bpy.data.objects) == 1 # This code does no instantiation.
assert len(bpy.data.collections) == 2 # The linked one and its local liboverride.
# Link + LibOverride, so linked data should have remained linked.
assert bpy.data.meshes[-1].library is not None
assert bpy.data.objects[-1].library is not None
assert bpy.data.collections[-1].library is not None
# Only explicitely linked data gets a liboverride, without any handling of hierarchy/dependencies.
assert bpy.data.collections[0].library is None
assert bpy.data.collections[0].is_runtime_data == True
assert bpy.data.collections[0].override_library is not None
assert bpy.data.collections[0].override_library.reference == bpy.data.collections[-1]
# Should create another liboverride for the linked collection, since this time we request a non-runtime one.
self.do_libload(filepath=output_lib_path,
link=True,
create_liboverrides=True,
create_liboverrides_runtime=False,
reuse_liboverrides=True)
assert len(bpy.data.meshes) == 1
assert len(bpy.data.objects) == 1 # This code does no instantiation.
assert len(bpy.data.collections) == 3 # The linked one and its two local liboverrides.
# Link + LibOverride, so linked data should have remained linked.
assert bpy.data.meshes[-1].library is not None
assert bpy.data.objects[-1].library is not None
assert bpy.data.collections[-1].library is not None
# Only explicitely linked data gets a liboverride, without any handling of hierarchy/dependencies.
assert bpy.data.collections[1].library is None
assert bpy.data.collections[1].is_runtime_data == False
assert bpy.data.collections[1].override_library is not None
assert bpy.data.collections[1].override_library.reference == bpy.data.collections[-1]
TESTS = (
TestBlendLibLinkSaveLoadBasic,
@ -559,7 +715,10 @@ TESTS = (
TestBlendLibLibraryReload,
TestBlendLibLibraryRelocate,
TestBlendLibDataLibrariesLoad,
TestBlendLibDataLibrariesLoadAppend,
TestBlendLibDataLibrariesLoadLink,
TestBlendLibDataLibrariesLoadLibOverride,
)