Ported BlendFileBlock.get_recursive_iter()

Also simplified get_file_offset(), because its optional parameters are
never used anyway.
This commit is contained in:
Sybren A. Stüvel 2018-02-22 16:45:16 +01:00
parent 87300df6a3
commit 7165d121bd
4 changed files with 91 additions and 44 deletions

View File

@ -326,32 +326,15 @@ class BlendFileBlock:
assert (type(dna_type_id) is bytes) assert (type(dna_type_id) is bytes)
self.refine_type_from_index(self.bfile.sdna_index_from_id[dna_type_id]) self.refine_type_from_index(self.bfile.sdna_index_from_id[dna_type_id])
def get_file_offset(self, path, def get_file_offset(self, path: bytes) -> (int, int):
default=..., """Return (offset, length)"""
sdna_index_refine=None,
base_index=0,
):
"""
Return (offset, length)
"""
assert isinstance(path, bytes) assert isinstance(path, bytes)
# TODO: refactor to just return the length, and check whether this isn't actually
# simply the same as self.size.
ofs = self.file_offset ofs = self.file_offset
if base_index != 0: field, _ = self.dna_type.field_from_path(self.bfile.header.pointer_size, path)
assert (base_index < self.count) return ofs, field.name.array_size
ofs += (self.size // self.count) * base_index
self.bfile.fileobj.seek(ofs, os.SEEK_SET)
if sdna_index_refine is None:
sdna_index_refine = self.sdna_index
else:
self.bfile.ensure_subtype_smaller(self.sdna_index, sdna_index_refine)
dna_struct = self.bfile.structs[sdna_index_refine]
field = dna_struct.field_from_path(
self.bfile.header, self.bfile.fileobj, path)
return self.bfile.fileobj.tell(), field.dna_name.array_size
def get(self, def get(self,
path: dna.FieldPath, path: dna.FieldPath,
@ -360,7 +343,7 @@ class BlendFileBlock:
null_terminated: typing.Optional[bool] = None, null_terminated: typing.Optional[bool] = None,
as_str=True, as_str=True,
base_index=0, base_index=0,
): ) -> typing.Any:
"""Read a property and return the value. """Read a property and return the value.
:param path: name of the property (like `b'loc'`), tuple of names :param path: name of the property (like `b'loc'`), tuple of names
@ -395,12 +378,20 @@ class BlendFileBlock:
null_terminated=null_terminated, as_str=as_str, null_terminated=null_terminated, as_str=as_str,
) )
def get_recursive_iter(self, path, path_root=b"", def get_recursive_iter(self,
path: dna.FieldPath,
path_root: dna.FieldPath = b'',
default=..., default=...,
sdna_index_refine=None, sdna_index_refine=None,
use_nil=True, use_str=True, null_terminated: typing.Optional[bool] = None,
as_str=True,
base_index=0, base_index=0,
): ) -> typing.Iterator[typing.Tuple[bytes, typing.Any]]:
"""Generator, yields (path, property value) tuples.
If a property cannot be decoded, a string representing its DNA type
name is used as its value instead, between pointy brackets.
"""
if path_root: if path_root:
path_full = ( path_full = (
(path_root if type(path_root) is tuple else (path_root,)) + (path_root if type(path_root) is tuple else (path_root,)) +
@ -409,22 +400,26 @@ class BlendFileBlock:
path_full = path path_full = path
try: try:
# Try accessing as simple property
yield (path_full, yield (path_full,
self.get(path_full, default, sdna_index_refine, use_nil, use_str, base_index)) self.get(path_full, default, sdna_index_refine, null_terminated, as_str,
except NotImplementedError as ex: base_index))
msg, dna_name, dna_type = ex.args except exceptions.NoReaderImplemented as ex:
struct_index = self.bfile.sdna_index_from_id.get(dna_type.dna_type_id, None) # This was not a simple property, so recurse into its DNA Struct.
dna_type = ex.dna_type
struct_index = self.bfile.sdna_index_from_id.get(dna_type.dna_type_id)
if struct_index is None: if struct_index is None:
yield (path_full, "<%s>" % dna_type.dna_type_id.decode('ascii')) yield (path_full, "<%s>" % dna_type.dna_type_id.decode('ascii'))
else: return
struct = self.bfile.structs[struct_index]
for f in struct.fields: # Recurse through the fields.
yield from self.get_recursive_iter( for f in dna_type.fields:
f.dna_name.name_only, path_full, default, None, use_nil, use_str, 0) 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): def items_recursive_iter(self):
for k in self.keys(): for k in self.keys():
yield from self.get_recursive_iter(k, use_str=False) yield from self.get_recursive_iter(k, as_str=False)
def get_data_hash(self): def get_data_hash(self):
""" """

View File

@ -2,7 +2,7 @@ import typing
import os import os
from . import dna_io, header from . import dna_io, header, exceptions
# Either a simple path b'propname', or a tuple (b'parentprop', b'actualprop', arrayindex) # Either a simple path b'propname', or a tuple (b'parentprop', b'actualprop', arrayindex)
FieldPath = typing.Union[bytes, typing.Iterable[typing.Union[bytes, int]]] FieldPath = typing.Union[bytes, typing.Iterable[typing.Union[bytes, int]]]
@ -115,6 +115,14 @@ class Struct:
self._fields.append(field) self._fields.append(field)
self._fields_by_name[field.name.name_only] = field self._fields_by_name[field.name.name_only] = field
@property
def fields(self) -> typing.List[Field]:
"""Return the fields of this Struct.
Do not modify the returned list; use append_field() instead.
"""
return self._fields
def field_from_path(self, def field_from_path(self,
pointer_size: int, pointer_size: int,
path: FieldPath) \ path: FieldPath) \
@ -231,8 +239,9 @@ class Struct:
try: try:
simple_reader = simple_readers[dna_type.dna_type_id] simple_reader = simple_readers[dna_type.dna_type_id]
except KeyError: except KeyError:
raise NotImplementedError("%r exists but isn't pointer, can't resolve field %r" % raise exceptions.NoReaderImplemented(
(path, dna_name.name_only), dna_name, dna_type) "%r exists but isn't pointer, can't resolve field %r" % (path, dna_name.name_only),
dna_name, dna_type) from None
if isinstance(path, tuple) and len(path) > 1 and isinstance(path[-1], int): if isinstance(path, tuple) and len(path) > 1 and isinstance(path[-1], int):
# The caller wants to get a single item from an array. The offset we seeked to already # The caller wants to get a single item from an array. The offset we seeked to already

View File

@ -37,3 +37,18 @@ class BlendFileError(Exception):
class NoDNA1Block(BlendFileError): class NoDNA1Block(BlendFileError):
"""Raised when the blend file contains no DNA1 block.""" """Raised when the blend file contains no DNA1 block."""
class NoReaderImplemented(NotImplementedError):
"""Raised when reading a property of a non-implemented type.
This indicates that the property should be read using some dna.Struct.
:type dna_name: blender_asset_tracer.blendfile.dna.Name
:type dna_type: blender_asset_tracer.blendfile.dna.Struct
"""
def __init__(self, message: str, dna_name, dna_type):
super().__init__(message)
self.dna_name = dna_name
self.dna_type = dna_type

View File

@ -6,7 +6,7 @@ import os
from blender_asset_tracer import blendfile from blender_asset_tracer import blendfile
class BlendLoadingTest(unittest.TestCase): class BlendFileBlockTest(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cls.blendfiles = pathlib.Path(__file__).with_name('blendfiles') cls.blendfiles = pathlib.Path(__file__).with_name('blendfiles')
@ -18,10 +18,12 @@ class BlendLoadingTest(unittest.TestCase):
if self.bf: if self.bf:
self.bf.close() self.bf.close()
def test_some_properties(self): def test_loading(self):
self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend') self.bf = blendfile.BlendFile(self.blendfiles / 'basic_file.blend')
self.assertFalse(self.bf.is_compressed) self.assertFalse(self.bf.is_compressed)
self.assertEqual(1, len(self.bf.code_index[b'OB']))
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)
@ -51,3 +53,29 @@ class BlendLoadingTest(unittest.TestCase):
mesh = self.bf.block_from_addr[mesh_ptr] mesh = self.bf.block_from_addr[mesh_ptr]
mname = mesh.get((b'id', b'name')) mname = mesh.get((b'id', b'name'))
self.assertEqual('MECube³', mname) 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)
# No recursing, just an array property.
gen = ob.get_recursive_iter(b'loc')
self.assertEqual([(b'loc', [2.0, 3.0, 5.0])], list(gen))
# Recurse into an object
gen = ob.get_recursive_iter(b'id')
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'), '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.