Python: add function for file iteration/remapping bpy.data.file_path_foreach()
Add a function `bpy.data.file_path_foreach()` with a callback function
`visit_path_fn`:
```py
def visit_path_fn(
owner_id: bpy.types.ID,
file_path: str,
_meta: typing.Any,
) -> str | None:
return file_path.replace('xxx', 'yyy') or None
bpy.data.file_path_foreach(
visit_path_fn,
*,
subset=None,
visit_types=None,
flags={},
)
```
When the function returns `None`, nothing happens. When it returns a
string, the reported file path is replaced with the returned string.
`subset` and `visit_types` have the same meaning as `subset` resp.
`key_types` in `bpy.data.file_path_map()`. Since the latter parameter
doesn't affect keys in a map, but rather the visited ID types, I named
it `visit_types` instead.
`flags` wraps most of `eBPathForeachFlag` as `set[str]`, with their
prefixes removed. `BKE_BPATH_FOREACH_PATH_RESOLVE_TOKEN` becomes
`RESOLVE_TOKEN`, etc. `BKE_BPATH_FOREACH_PATH_ABSOLUTE` is excluded,
as it only affects a field in the C++ callback structure that is not
passed to Python at all. If it turns out to be useful at some point,
we can always add it.
`_meta` is for future use, and is intended to give metadata of the
path (like whether it's an input or an output path, a single file or
the first file in a sequence, supports variables, etc.). By adding
this to the API now, we avoid a breaking change when this feature is
actually implemented.
Design task: #145734
Pull Request: https://projects.blender.org/blender/blender/pulls/146261
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
./blender.bin --background --python tests/python/bl_blendfile_liblink.py
|
||||
blender -b -P tests/python/bl_blendfile_relationships.py --src-test-dir tests/files --output-dir /tmp/blendfile_io/
|
||||
"""
|
||||
__all__ = (
|
||||
"main",
|
||||
@@ -12,11 +12,14 @@ __all__ = (
|
||||
import bpy
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from bpy.path import native_pathsep
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
||||
from bl_blendfile_utils import TestBlendLibLinkHelper
|
||||
_my_dir = Path(__file__).resolve().parent
|
||||
sys.path.append(str(_my_dir))
|
||||
|
||||
from bl_blendfile_utils import TestBlendLibLinkHelper, TestHelper
|
||||
|
||||
|
||||
class TestBlendUserMap(TestBlendLibLinkHelper):
|
||||
@@ -166,9 +169,131 @@ class TestBlendFilePathMap(TestBlendLibLinkHelper):
|
||||
self.assertRaises(TypeError, bpy.data.file_path_map, subset=[bpy.data.objects[0], bpy.data.images[0], "FooBar"])
|
||||
|
||||
|
||||
class TestBlendFilePathForeach(TestHelper):
|
||||
testdir: Path
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
||||
# File paths can get long, and thus also so can diffs when things go wrong.
|
||||
self.maxDiff = 10240
|
||||
self.testdir = Path(self.args.src_test_dir) / "libraries_and_linking"
|
||||
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "library_test_scene.blend"))
|
||||
|
||||
# Name the library data-blocks after their filename, so that they can be reliably identified later.
|
||||
# Otherwise we're stuck with 'Lib', 'Lib.001', etc.
|
||||
for lib_id in bpy.data.libraries[:]:
|
||||
abspath = Path(str(bpy.path.abspath(lib_id.filepath)))
|
||||
lib_id.name = abspath.stem
|
||||
|
||||
def test_without_args(self) -> None:
|
||||
"""Test file_path_foreach() without any arguments, it should report everything but packed files."""
|
||||
|
||||
visited_paths = self._file_path_foreach()
|
||||
self.assertEqual({
|
||||
(bpy.data.libraries['direct_linked_A'], self.testdir / "libraries/direct_linked_A.blend"),
|
||||
(bpy.data.libraries['direct_linked_B'], self.testdir / "libraries/direct_linked_B.blend"),
|
||||
(bpy.data.libraries['indirect_datablocks'], self.testdir / "libraries/indirect_datablocks.blend"),
|
||||
}, visited_paths)
|
||||
|
||||
def test_with_nondefault_flag(self) -> None:
|
||||
"""Test file_path_foreach() with a non-default flag.
|
||||
|
||||
If any non-default flag works, it's enough as a test to assume the flags
|
||||
are parsed and passed to the C++ code correctly. There is no need to
|
||||
exhaustively test all the flags here.
|
||||
"""
|
||||
|
||||
# The default value for `flags` is {'SKIP_PACKED',
|
||||
# 'SKIP_WEAK_REFERENCES'}, so to also see the packed files, only pass
|
||||
# `SKIP_WEAK_REFERENCES`.
|
||||
visited_paths = self._file_path_foreach(flags={'SKIP_WEAK_REFERENCES'})
|
||||
self.assertEqual({
|
||||
(bpy.data.libraries['direct_linked_A'], self.testdir / "libraries/direct_linked_A.blend"),
|
||||
(bpy.data.libraries['direct_linked_B'], self.testdir / "libraries/direct_linked_B.blend"),
|
||||
(bpy.data.libraries['indirect_datablocks'], self.testdir / "libraries/indirect_datablocks.blend"),
|
||||
(bpy.data.images['pack.png'], self.testdir / "libraries/pack.png"),
|
||||
}, visited_paths, "testing without SKIP_PACKED")
|
||||
|
||||
def test_filepath_rewriting(self) -> None:
|
||||
# Store the pre-modification value, as its use of (back)slashes is platform-dependent.
|
||||
image_filepath_before = str(bpy.data.images['pack.png'].filepath)
|
||||
|
||||
def visit_path_fn(owner_id: bpy.types.ID, path: str, _meta: None) -> str | None:
|
||||
return "//{}-rewritten.blend".format(owner_id.name)
|
||||
bpy.data.file_path_foreach(visit_path_fn)
|
||||
|
||||
libs = bpy.data.libraries
|
||||
self.assertEqual(libs['direct_linked_A'].filepath, "//direct_linked_A-rewritten.blend")
|
||||
self.assertEqual(libs['direct_linked_B'].filepath, "//direct_linked_B-rewritten.blend")
|
||||
self.assertEqual(libs['indirect_datablocks'].filepath, "//indirect_datablocks-rewritten.blend")
|
||||
self.assertEqual(
|
||||
bpy.data.images['pack.png'].filepath,
|
||||
image_filepath_before,
|
||||
"Packed file should not have changed")
|
||||
|
||||
def test_exception_passing(self) -> None:
|
||||
"""Python exceptions in the callback function should be raised by file_path_foreach()."""
|
||||
# Any Python exception should work, not just built-in ones.
|
||||
class CustomException(Exception):
|
||||
pass
|
||||
|
||||
def visit_path_fn(_owner_id: bpy.types.ID, _path: str, meta: None) -> str | None:
|
||||
raise CustomException("arg0", 1, "arg2")
|
||||
|
||||
try:
|
||||
bpy.data.file_path_foreach(visit_path_fn)
|
||||
except CustomException as ex:
|
||||
self.assertEqual(("arg0", 1, "arg2"), ex.args, "Parameters passed to the exception should be retained")
|
||||
else:
|
||||
self.fail("Expected exception not thrown")
|
||||
|
||||
def test_meta_parameter(self) -> None:
|
||||
def visit_path_fn(_owner_id: bpy.types.ID, _path: str, meta: None) -> str | None:
|
||||
# This is proven to work by the `test_exception_passing()` test above.
|
||||
self.assertIsNone(
|
||||
meta,
|
||||
"The meta parameter is expected to be None; this test is expected to fail "
|
||||
"once the metadata feature is actually getting implemented, and should then "
|
||||
"be replaced with a proper test.")
|
||||
bpy.data.file_path_foreach(visit_path_fn)
|
||||
|
||||
@staticmethod
|
||||
def _file_path_foreach(
|
||||
subtypes: set[str] | None = None,
|
||||
visit_keys: set[str] | None = None,
|
||||
flags: set[str] | None = None,
|
||||
) -> set[tuple[bpy.types.ID, Path]]:
|
||||
"""Call bpy.data.file_path_foreach(), returning the visited paths as set of (datablock, path) tuples.
|
||||
|
||||
A set is used because the order of visiting is not relevant.
|
||||
"""
|
||||
visisted_paths: set[tuple[bpy.types.ID, Path]] = set()
|
||||
|
||||
def visit_path_fn(owner_id: bpy.types.ID, path: str, _meta: None) -> str | None:
|
||||
abspath = Path(str(bpy.path.abspath(path, library=owner_id.library)))
|
||||
visisted_paths.add((owner_id, abspath))
|
||||
|
||||
# Dynamically build the keyword arguments, because the None values are not
|
||||
# valid, and should be encoded as not having the kwarg.
|
||||
kwargs = {}
|
||||
if subtypes is not None:
|
||||
kwargs['subtypes'] = subtypes
|
||||
if visit_keys is not None:
|
||||
kwargs['visit_keys'] = visit_keys
|
||||
if flags is not None:
|
||||
kwargs['flags'] = flags
|
||||
|
||||
bpy.data.file_path_foreach(visit_path_fn, **kwargs)
|
||||
|
||||
return visisted_paths
|
||||
|
||||
|
||||
TESTS = (
|
||||
TestBlendUserMap,
|
||||
TestBlendFilePathMap,
|
||||
TestBlendFilePathForeach,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -39,8 +39,14 @@ class TestHelper(unittest.TestCase):
|
||||
if not inst_attr_id.startswith("test_"):
|
||||
continue
|
||||
inst_attr = getattr(self, inst_attr_id)
|
||||
if callable(inst_attr):
|
||||
if not callable(inst_attr):
|
||||
continue
|
||||
|
||||
self.setUp()
|
||||
try:
|
||||
inst_attr()
|
||||
finally:
|
||||
self.tearDown()
|
||||
|
||||
|
||||
class TestBlendLibLinkHelper(TestHelper):
|
||||
|
||||
Reference in New Issue
Block a user