diff --git a/blender_asset_tracer/blendfile/__init__.py b/blender_asset_tracer/blendfile/__init__.py index 3c1c354..eb07fc2 100644 --- a/blender_asset_tracer/blendfile/__init__.py +++ b/blender_asset_tracer/blendfile/__init__.py @@ -340,7 +340,7 @@ class BlendFileBlock: path: dna.FieldPath, default=..., sdna_index_refine=None, - null_terminated: typing.Optional[bool] = None, + null_terminated=True, as_str=True, base_index=0, ) -> typing.Any: @@ -353,8 +353,8 @@ class BlendFileBlock: :param default: The value to return when the field does not exist. Use Ellipsis (the default value) to raise a KeyError instead. :param null_terminated: Only used when reading bytes or strings. When - True, stops reading at the first zero byte. Defaults to the same - value as `as_str`, as this is mostly used for string data. + True, stops reading at the first zero byte; be careful with this + when reading binary data. :param as_str: When True, automatically decode bytes to string (assumes UTF-8 encoding). """ @@ -383,7 +383,7 @@ class BlendFileBlock: path_root: dna.FieldPath = b'', default=..., sdna_index_refine=None, - null_terminated: typing.Optional[bool] = None, + null_terminated=True, as_str=True, base_index=0, ) -> typing.Iterator[typing.Tuple[bytes, typing.Any]]: @@ -417,10 +417,6 @@ class BlendFileBlock: yield from self.get_recursive_iter(f.name.name_only, path_full, default=default, null_terminated=null_terminated, as_str=as_str) - def items_recursive_iter(self): - for k in self.keys(): - yield from self.get_recursive_iter(k, as_str=False) - def get_data_hash(self): """ Generates a 'hash' that can be used instead of addr_old as block id, and that should be 'stable' across .blend @@ -434,7 +430,7 @@ class BlendFileBlock: self.file.header, self.file.handle, k).dna_name.is_pointer hsh = 1 - for k, v in self.items_recursive_iter(): + for k, v in self.items_recursive(): if not _is_pointer(self, k): hsh = zlib.adler32(str(v).encode(), hsh) return hsh @@ -486,27 +482,31 @@ class BlendFileBlock: # Python convenience API # dict like access - def __getitem__(self, item): - return self.get(item, as_str=False) + def __getitem__(self, path: dna.FieldPath): + return self.get(path, as_str=False) def __setitem__(self, item, value): self.set(item, value) - def keys(self): - return (f.dna_name.name_only for f in self.dna_type.fields) + def keys(self) -> typing.Iterator[bytes]: + """Generator, yields all field names of this block.""" + return (f.name.name_only for f in self.dna_type.fields) def values(self): for k in self.keys(): try: yield self[k] - except NotImplementedError as ex: - msg, dna_name, dna_type = ex.args - yield "<%s>" % dna_type.dna_type_id.decode('ascii') + except exceptions.NoReaderImplemented as ex: + yield '<%s>' % ex.dna_type.dna_type_id.decode('ascii') def items(self): for k in self.keys(): try: yield (k, self[k]) - except NotImplementedError as ex: - msg, dna_name, dna_type = ex.args - yield (k, "<%s>" % dna_type.dna_type_id.decode('ascii')) + except exceptions.NoReaderImplemented as ex: + yield (k, '<%s>' % ex.dna_type.dna_type_id.decode('ascii')) + + def items_recursive(self): + """Generator, yields (property path, property value) recursively for all properties.""" + for k in self.keys(): + yield from self.get_recursive_iter(k, as_str=False) diff --git a/blender_asset_tracer/blendfile/dna.py b/blender_asset_tracer/blendfile/dna.py index efcd489..84fb600 100644 --- a/blender_asset_tracer/blendfile/dna.py +++ b/blender_asset_tracer/blendfile/dna.py @@ -181,7 +181,7 @@ class Struct: fileobj: typing.BinaryIO, path: FieldPath, default=..., - null_terminated: typing.Optional[bool]=None, + null_terminated=True, as_str=True, ): """Read the value of the field from the blend file. @@ -196,8 +196,8 @@ class Struct: :param default: The value to return when the field does not exist. Use Ellipsis (the default value) to raise a KeyError instead. :param null_terminated: Only used when reading bytes or strings. When - True, stops reading at the first zero byte. Defaults to the same - value as `as_str`, as this is mostly used for string data. + True, stops reading at the first zero byte. Be careful with this + default when reading binary data. :param as_str: When True, automatically decode bytes to string (assumes UTF-8 encoding). """ diff --git a/tests/blendfiles/basic_file.blend b/tests/blendfiles/basic_file.blend index b573b7b..a4e4d51 100644 Binary files a/tests/blendfiles/basic_file.blend and b/tests/blendfiles/basic_file.blend differ diff --git a/tests/test_blendfile_dna.py b/tests/test_blendfile_dna.py index 126bff4..fdcb280 100644 --- a/tests/test_blendfile_dna.py +++ b/tests/test_blendfile_dna.py @@ -208,8 +208,7 @@ class StructTest(unittest.TestCase): fileobj = mock.MagicMock(io.BufferedReader) fileobj.read.return_value = b'\x01\x02\x03\x04\xff\xfe\xfd\xfa\x00dummydata' - val = self.s.field_get(self.FakeHeader(), fileobj, b'path', - as_str=False, null_terminated=True) + val = self.s.field_get(self.FakeHeader(), fileobj, b'path', as_str=False) self.assertEqual(b'\x01\x02\x03\x04\xff\xfe\xfd\xfa', val) fileobj.seek.assert_called_with(16, os.SEEK_CUR) @@ -217,7 +216,8 @@ class StructTest(unittest.TestCase): fileobj = mock.MagicMock(io.BufferedReader) fileobj.read.return_value = b'\x01\x02\x03\x04\xff\xfe\xfd\xfa\x00dummydata' - val = self.s.field_get(self.FakeHeader(), fileobj, b'path', as_str=False) + val = self.s.field_get(self.FakeHeader(), fileobj, b'path', + as_str=False, null_terminated=False) self.assertEqual(b'\x01\x02\x03\x04\xff\xfe\xfd\xfa\x00dummydata', val) fileobj.seek.assert_called_with(16, os.SEEK_CUR) diff --git a/tests/test_blendfile_loading.py b/tests/test_blendfile_loading.py index e658acc..9415cbe 100644 --- a/tests/test_blendfile_loading.py +++ b/tests/test_blendfile_loading.py @@ -12,18 +12,16 @@ class BlendFileBlockTest(unittest.TestCase): cls.blendfiles = pathlib.Path(__file__).with_name('blendfiles') def setUp(self): - self.bf = None + self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend') def tearDown(self): if self.bf: self.bf.close() def test_loading(self): - self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend') self.assertFalse(self.bf.is_compressed) def test_some_properties(self): - self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend') ob = self.bf.code_index[b'OB'][0] self.assertEqual('Object', ob.dna_type_name) @@ -55,7 +53,6 @@ class BlendFileBlockTest(unittest.TestCase): self.assertEqual('MECube³', mname) def test_get_recursive_iter(self): - self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend') ob = self.bf.code_index[b'OB'][0] assert isinstance(ob, blendfile.BlendFileBlock) @@ -72,10 +69,82 @@ class BlendFileBlockTest(unittest.TestCase): ((b'id', b'lib'), 0), ((b'id', b'name'), 'OBümlaut'), ((b'id', b'flag'), 0), - ((b'id', b'tag'), 1024), - ((b'id', b'us'), 1), - ((b'id', b'icon_id'), 0), - ((b'id', b'recalc'), 0), - ((b'id', b'pad'), 0), ], - list(gen)[:-2]) # the last 2 properties are pointers and change when saving. + list(gen)[:6]) + + def test_iter_recursive(self): + ob = self.bf.code_index[b'OB'][0] + assert isinstance(ob, blendfile.BlendFileBlock) + + # We can't test all of them in a reliable way, but it shouldn't crash. + all_items = list(ob.items_recursive()) + + # And we can check the first few items. + self.assertEqual( + [((b'id', b'next'), 0), + ((b'id', b'prev'), 0), + ((b'id', b'newid'), 0), + ((b'id', b'lib'), 0), + ((b'id', b'name'), + b'OB\xc3\xbcmlaut'), + ((b'id', b'flag'), 0), + ], all_items[:6]) + + def test_items(self): + ma = self.bf.code_index[b'MA'][0] + assert isinstance(ma, blendfile.BlendFileBlock) + + # We can't test all of them in a reliable way, but it shouldn't crash. + all_items = list(ma.items()) + + # And we can check the first few items. + self.assertEqual( + [(b'id', ''), # not recursed into. + (b'adt', 0), + (b'material_type', 0), + (b'flag', 0), + (b'r', 0.8000000715255737), + (b'g', 0.03218378871679306), + (b'b', 0.36836329102516174), + (b'specr', 1.0)], + all_items[:8]) + + def test_keys(self): + ma = self.bf.code_index[b'MA'][0] + assert isinstance(ma, blendfile.BlendFileBlock) + + # We can't test all of them in a reliable way, but it shouldn't crash. + all_keys = list(ma.keys()) + + # And we can check the first few items. + self.assertEqual( + [b'id', b'adt', b'material_type', b'flag', b'r', b'g', b'b', b'specr'], + all_keys[:8]) + + def test_values(self): + ma = self.bf.code_index[b'MA'][0] + assert isinstance(ma, blendfile.BlendFileBlock) + + # We can't test all of them in a reliable way, but it shouldn't crash. + all_values = list(ma.values()) + + # And we can check the first few items. + self.assertEqual( + ['', + 0, + 0, + 0, + 0.8000000715255737, + 0.03218378871679306, + 0.36836329102516174, + 1.0], + all_values[:8]) + + def test_get_via_dict_interface(self): + ma = self.bf.code_index[b'MA'][0] + assert isinstance(ma, blendfile.BlendFileBlock) + self.assertEqual(0.8000000715255737, ma[b'r']) + + ob = self.bf.code_index[b'OB'][0] + assert isinstance(ob, blendfile.BlendFileBlock) + self.assertEqual('OBümlaut', ob[b'id', b'name'].decode())