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, path: dna.FieldPath,
default=..., default=...,
sdna_index_refine=None, sdna_index_refine=None,
null_terminated: typing.Optional[bool] = None, null_terminated=True,
as_str=True, as_str=True,
base_index=0, base_index=0,
) -> typing.Any: ) -> typing.Any:
@ -353,8 +353,8 @@ class BlendFileBlock:
:param default: The value to return when the field does not exist. :param default: The value to return when the field does not exist.
Use Ellipsis (the default value) to raise a KeyError instead. Use Ellipsis (the default value) to raise a KeyError instead.
:param null_terminated: Only used when reading bytes or strings. When :param null_terminated: Only used when reading bytes or strings. When
True, stops reading at the first zero byte. Defaults to the same True, stops reading at the first zero byte; be careful with this
value as `as_str`, as this is mostly used for string data. when reading binary data.
:param as_str: When True, automatically decode bytes to string :param as_str: When True, automatically decode bytes to string
(assumes UTF-8 encoding). (assumes UTF-8 encoding).
""" """
@ -383,7 +383,7 @@ class BlendFileBlock:
path_root: dna.FieldPath = b'', path_root: dna.FieldPath = b'',
default=..., default=...,
sdna_index_refine=None, sdna_index_refine=None,
null_terminated: typing.Optional[bool] = None, null_terminated=True,
as_str=True, as_str=True,
base_index=0, base_index=0,
) -> typing.Iterator[typing.Tuple[bytes, typing.Any]]: ) -> 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, yield from self.get_recursive_iter(f.name.name_only, path_full, default=default,
null_terminated=null_terminated, as_str=as_str) 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): 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 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 self.file.header, self.file.handle, k).dna_name.is_pointer
hsh = 1 hsh = 1
for k, v in self.items_recursive_iter(): for k, v in self.items_recursive():
if not _is_pointer(self, k): if not _is_pointer(self, k):
hsh = zlib.adler32(str(v).encode(), hsh) hsh = zlib.adler32(str(v).encode(), hsh)
return hsh return hsh
@ -486,27 +482,31 @@ class BlendFileBlock:
# Python convenience API # Python convenience API
# dict like access # dict like access
def __getitem__(self, item): def __getitem__(self, path: dna.FieldPath):
return self.get(item, as_str=False) return self.get(path, as_str=False)
def __setitem__(self, item, value): def __setitem__(self, item, value):
self.set(item, value) self.set(item, value)
def keys(self): def keys(self) -> typing.Iterator[bytes]:
return (f.dna_name.name_only for f in self.dna_type.fields) """Generator, yields all field names of this block."""
return (f.name.name_only for f in self.dna_type.fields)
def values(self): def values(self):
for k in self.keys(): for k in self.keys():
try: try:
yield self[k] yield self[k]
except NotImplementedError as ex: except exceptions.NoReaderImplemented as ex:
msg, dna_name, dna_type = ex.args yield '<%s>' % ex.dna_type.dna_type_id.decode('ascii')
yield "<%s>" % dna_type.dna_type_id.decode('ascii')
def items(self): def items(self):
for k in self.keys(): for k in self.keys():
try: try:
yield (k, self[k]) yield (k, self[k])
except NotImplementedError as ex: except exceptions.NoReaderImplemented as ex:
msg, dna_name, dna_type = ex.args yield (k, '<%s>' % ex.dna_type.dna_type_id.decode('ascii'))
yield (k, "<%s>" % 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, fileobj: typing.BinaryIO,
path: FieldPath, path: FieldPath,
default=..., default=...,
null_terminated: typing.Optional[bool]=None, null_terminated=True,
as_str=True, as_str=True,
): ):
"""Read the value of the field from the blend file. """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. :param default: The value to return when the field does not exist.
Use Ellipsis (the default value) to raise a KeyError instead. Use Ellipsis (the default value) to raise a KeyError instead.
:param null_terminated: Only used when reading bytes or strings. When :param null_terminated: Only used when reading bytes or strings. When
True, stops reading at the first zero byte. Defaults to the same True, stops reading at the first zero byte. Be careful with this
value as `as_str`, as this is mostly used for string data. default when reading binary data.
:param as_str: When True, automatically decode bytes to string :param as_str: When True, automatically decode bytes to string
(assumes UTF-8 encoding). (assumes UTF-8 encoding).
""" """

Binary file not shown.

View File

@ -208,8 +208,7 @@ class StructTest(unittest.TestCase):
fileobj = mock.MagicMock(io.BufferedReader) fileobj = mock.MagicMock(io.BufferedReader)
fileobj.read.return_value = b'\x01\x02\x03\x04\xff\xfe\xfd\xfa\x00dummydata' fileobj.read.return_value = b'\x01\x02\x03\x04\xff\xfe\xfd\xfa\x00dummydata'
val = self.s.field_get(self.FakeHeader(), fileobj, b'path', val = self.s.field_get(self.FakeHeader(), fileobj, b'path', as_str=False)
as_str=False, null_terminated=True)
self.assertEqual(b'\x01\x02\x03\x04\xff\xfe\xfd\xfa', val) self.assertEqual(b'\x01\x02\x03\x04\xff\xfe\xfd\xfa', val)
fileobj.seek.assert_called_with(16, os.SEEK_CUR) fileobj.seek.assert_called_with(16, os.SEEK_CUR)
@ -217,7 +216,8 @@ class StructTest(unittest.TestCase):
fileobj = mock.MagicMock(io.BufferedReader) fileobj = mock.MagicMock(io.BufferedReader)
fileobj.read.return_value = b'\x01\x02\x03\x04\xff\xfe\xfd\xfa\x00dummydata' 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) self.assertEqual(b'\x01\x02\x03\x04\xff\xfe\xfd\xfa\x00dummydata', val)
fileobj.seek.assert_called_with(16, os.SEEK_CUR) 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') cls.blendfiles = pathlib.Path(__file__).with_name('blendfiles')
def setUp(self): def setUp(self):
self.bf = None self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend')
def tearDown(self): def tearDown(self):
if self.bf: if self.bf:
self.bf.close() self.bf.close()
def test_loading(self): def test_loading(self):
self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend')
self.assertFalse(self.bf.is_compressed) self.assertFalse(self.bf.is_compressed)
def test_some_properties(self): def test_some_properties(self):
self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend')
ob = self.bf.code_index[b'OB'][0] ob = self.bf.code_index[b'OB'][0]
self.assertEqual('Object', ob.dna_type_name) self.assertEqual('Object', ob.dna_type_name)
@ -55,7 +53,6 @@ class BlendFileBlockTest(unittest.TestCase):
self.assertEqual('MECube³', mname) self.assertEqual('MECube³', mname)
def test_get_recursive_iter(self): def test_get_recursive_iter(self):
self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend')
ob = self.bf.code_index[b'OB'][0] ob = self.bf.code_index[b'OB'][0]
assert isinstance(ob, blendfile.BlendFileBlock) assert isinstance(ob, blendfile.BlendFileBlock)
@ -72,10 +69,82 @@ class BlendFileBlockTest(unittest.TestCase):
((b'id', b'lib'), 0), ((b'id', b'lib'), 0),
((b'id', b'name'), 'OBümlaut'), ((b'id', b'name'), 'OBümlaut'),
((b'id', b'flag'), 0), ((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())