Struct.field_set - support setting subproperties (#92899)

Extend `Struct.field_set()` so that the `path` type can be a
`dna.FieldPath`, to mirror `Struct.field_get`'s `path` type, and allow
users to set file-block's subproperties. For example this allows
setting `object_block[(b'id', 'name')]`.

Also, along the way, added a test for getting subproperty value.

Reviewed-on: https://projects.blender.org/blender/blender-asset-tracer/pulls/92899
This commit is contained in:
Andrej730 2025-11-24 15:07:46 +01:00 committed by Sybren A. Stüvel
parent c69612b264
commit 47c3e8d77f
3 changed files with 29 additions and 4 deletions

View File

@ -726,7 +726,7 @@ class BlendFileBlock:
hsh = zlib.adler32(str(value).encode(), hsh) hsh = zlib.adler32(str(value).encode(), hsh)
return hsh return hsh
def set(self, path: bytes, value): def set(self, path: dna.FieldPath, value):
dna_struct = self.bfile.structs[self.sdna_index] dna_struct = self.bfile.structs[self.sdna_index]
self.bfile.mark_modified() self.bfile.mark_modified()
self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET) self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET)
@ -835,7 +835,7 @@ class BlendFileBlock:
def __getitem__(self, path: dna.FieldPath): def __getitem__(self, path: dna.FieldPath):
return self.get(path) return self.get(path)
def __setitem__(self, item: bytes, value) -> None: def __setitem__(self, item: dna.FieldPath, value) -> None:
self.set(item, value) self.set(item, value)
def has_field(self, name: bytes) -> bool: def has_field(self, name: bytes) -> bool:

View File

@ -310,7 +310,7 @@ class Struct:
self, self,
file_header: header.BlendFileHeader, file_header: header.BlendFileHeader,
fileobj: typing.IO[bytes], fileobj: typing.IO[bytes],
path: bytes, path: FieldPath,
value: typing.Any, value: typing.Any,
): ):
"""Write a value to the blend file. """Write a value to the blend file.
@ -319,7 +319,6 @@ class Struct:
struct on disk (e.g. the start of the BlendFileBlock containing the struct on disk (e.g. the start of the BlendFileBlock containing the
data). data).
""" """
assert isinstance(path, bytes), "path should be bytes, but is %s" % type(path)
field, offset = self.field_from_path(file_header.pointer_size, path) field, offset = self.field_from_path(file_header.pointer_size, path)

View File

@ -93,6 +93,10 @@ class StructTest(unittest.TestCase):
self.s_ulong = dna.Struct(b"ulong", 8) self.s_ulong = dna.Struct(b"ulong", 8)
self.s_uint64 = dna.Struct(b"uint64_t", 8) self.s_uint64 = dna.Struct(b"uint64_t", 8)
self.s_uint128 = dna.Struct(b"uint128_t", 16) # non-supported type self.s_uint128 = dna.Struct(b"uint128_t", 16) # non-supported type
self.s_substruct = dna.Struct(b"substruct")
self.f_substruct_name = dna.Field(self.s_char, dna.Name(b"name[10]"), 10, 0)
self.s_substruct.append_field(self.f_substruct_name)
self.f_next = dna.Field(self.s, dna.Name(b"*next"), 8, 0) self.f_next = dna.Field(self.s, dna.Name(b"*next"), 8, 0)
self.f_prev = dna.Field(self.s, dna.Name(b"*prev"), 8, 8) self.f_prev = dna.Field(self.s, dna.Name(b"*prev"), 8, 8)
@ -109,6 +113,7 @@ class StructTest(unittest.TestCase):
self.f_testint = dna.Field(self.s_int, dna.Name(b"testint"), 4, 4178) self.f_testint = dna.Field(self.s_int, dna.Name(b"testint"), 4, 4178)
self.f_testfloat = dna.Field(self.s_float, dna.Name(b"testfloat"), 4, 4182) self.f_testfloat = dna.Field(self.s_float, dna.Name(b"testfloat"), 4, 4182)
self.f_testulong = dna.Field(self.s_ulong, dna.Name(b"testulong"), 8, 4186) self.f_testulong = dna.Field(self.s_ulong, dna.Name(b"testulong"), 8, 4186)
self.f_substruct = dna.Field(self.s_substruct, dna.Name(b"testsubstruct"), 10, 4194)
self.s.append_field(self.f_next) self.s.append_field(self.f_next)
self.s.append_field(self.f_prev) self.s.append_field(self.f_prev)
@ -125,6 +130,7 @@ class StructTest(unittest.TestCase):
self.s.append_field(self.f_testint) self.s.append_field(self.f_testint)
self.s.append_field(self.f_testfloat) self.s.append_field(self.f_testfloat)
self.s.append_field(self.f_testulong) self.s.append_field(self.f_testulong)
self.s.append_field(self.f_substruct)
def test_autosize(self): def test_autosize(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
@ -263,6 +269,19 @@ class StructTest(unittest.TestCase):
self.assertAlmostEqual(2.79, val[1]) self.assertAlmostEqual(2.79, val[1])
fileobj.seek.assert_called_with(4144, os.SEEK_CUR) fileobj.seek.assert_called_with(4144, os.SEEK_CUR)
def test_struct_field_get_subproperty(self):
fileobj = mock.MagicMock(io.BufferedReader)
fileobj.read.return_value = b"my_name"
_, val = self.s.field_get(
self.FakeHeader(),
fileobj,
(b"testsubstruct", b"name"),
as_str=True,
)
self.assertEqual("my_name", val)
fileobj.seek.assert_called_with(4194, os.SEEK_CUR)
def test_char_field_set(self): def test_char_field_set(self):
fileobj = mock.MagicMock(io.BufferedReader) fileobj = mock.MagicMock(io.BufferedReader)
value = 255 value = 255
@ -344,3 +363,10 @@ class StructTest(unittest.TestCase):
expected = struct.pack(b">f", value) expected = struct.pack(b">f", value)
self.s.field_set(self.FakeHeader(), fileobj, b"testfloat", value) self.s.field_set(self.FakeHeader(), fileobj, b"testfloat", value)
fileobj.write.assert_called_with(expected) fileobj.write.assert_called_with(expected)
def test_struct_field_set_subproperty(self):
fileobj = mock.MagicMock(io.BufferedReader)
expected = b"new_name\x00"
self.s.field_set(self.FakeHeader(), fileobj, (b"testsubstruct", b"name"), "new_name")
fileobj.write.assert_called_with(expected)