Ported iteration over properties

This commit is contained in:
Sybren A. Stüvel 2018-02-22 17:11:41 +01:00
parent 6175f6c592
commit 63c7e4fcef
5 changed files with 104 additions and 35 deletions

View File

@ -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)

View File

@ -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).
"""

Binary file not shown.

View File

@ -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)

View File

@ -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', '<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(
['<ID>',
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())