From 7af137867b3c37cc57c28c9da93b797b2911235d Mon Sep 17 00:00:00 2001 From: Jonas Holzman Date: Tue, 20 May 2025 16:27:41 +0200 Subject: [PATCH] Progress on deserialization --- dumper.py | 127 +++++++++++++++++++++++++++--------------------------- 1 file changed, 64 insertions(+), 63 deletions(-) diff --git a/dumper.py b/dumper.py index a34500d..c495bd9 100644 --- a/dumper.py +++ b/dumper.py @@ -43,9 +43,9 @@ def deserialize_nodes_into_node_tree(data: dict, node_tree: bpy.types.NodeTree): class Serializer(ABC): """ - Base Serializer class, from which other Serializer are derived. + Base Serializer class. `bl_pointers_ref` corresponds to a mutable dict passed through serialize/deserialize - functions, containing Blender object pointers from the current operation from relinking. + functions, containing a map of Blender pointers IDs and their corresponding objects. """ # Whitelisted properties, applied after the blacklist prop_whitelist = None @@ -58,9 +58,9 @@ class Serializer(ABC): @classmethod def get_serializer_map(cls) -> dict[type[bpy.types.bpy_struct], type[Serializer]]: - """Store the Serializer Map in a class variable for simple caching""" + """Get the serializer map, stored in a class variable for simple caching""" if not cls.serializer_map: - for subclass in utils.all_subclasses(Dumper): + for subclass in utils.all_subclasses(Serializer): assert hasattr(subclass, "bl_type") cls.serializer_map[subclass.bl_type] = subclass @@ -68,7 +68,7 @@ class Serializer(ABC): @classmethod def get_serializer(cls, bl_object: bpy.types.bpy_struct) -> type[Serializer]: - """Get the closest corresponding dumper for a given Blender object using its MRO""" + """Get the closest corresponding serializer for a given Blender object using its MRO""" serializer_map = cls.get_serializer_map() bl_type = type(bl_object.bl_rna.type_recast()) @@ -104,7 +104,7 @@ class Serializer(ABC): @classmethod @abstractmethod def serialize(cls, obj: bpy.types.bpy_struct | Any, bl_pointers_ref: dict) -> dict: - """Base Serialize method, overridded by subclasses""" + """Base serialization method, overridden by subclasses""" # Early recursive return case # TODO: Ported as is, check the heuristics, (if there are more or attributes type to add) @@ -179,26 +179,17 @@ class Serializer(ABC): return serializer.serialize(obj, bl_pointers_ref) - @classmethod - @abstractmethod - def construct_bl_object(cls, data: dict): - """Abstract method for Serializer specificaiton to provide a way to construct their object""" - print("DEBUG: construct_bl_object called on Base Serializer, shouldn't happen") - return None - # --- Deserialization --- @classmethod @abstractmethod def deserialize(cls, data: dict, target_obj: bpy.types.bpy_struct, bl_pointers_ref: dict): - if target_obj is None: - target_obj = cls.construct_bl_object(data) - - if target_obj is None: - print("DEBUG: No method to construct object") - # Failed to construct object/no speciailization to construct object - return None - + """ + Base deserialization method. + Deserialize data into a specific Blender object, creating sub-objects as needed. + Partial data may be provided, in which case, fields not specified will be left to default. + """ + if (kit_ptr := data.get("_kit_ptr", None)): bl_pointers_ref[kit_ptr] = target_obj @@ -211,13 +202,8 @@ class Serializer(ABC): target_bl_prop = BlenderProperty(rep=target_obj.bl_rna.properties[stored_key], attr=getattr(target_obj, stored_key)) - # Skip the property if it's read-only - if target_bl_prop.rep.is_readonly: - continue - - # Unlike serialization, there's no property array case, as they are just directly assigned - # Collection case + # Unlike serialization, there's no property array case, as they are just directly assigned if isinstance(target_bl_prop.attr, bpy.types.bpy_prop_collection): cls.deserialize_collection(stored_value, target_bl_prop.attr, bl_pointers_ref) continue @@ -225,16 +211,28 @@ class Serializer(ABC): value_to_set = stored_value # Pointer case + # Dereference the value if its already present in the pointers_ref map if target_bl_prop.rep.type == "POINTER": value_to_set = cls.deserialize_pointer(stored_value, target_bl_prop.attr, bl_pointers_ref) + # Skip setting the property if it's read-only + if target_bl_prop.rep.is_readonly: + continue + # Assign the property setattr(target_obj, stored_key, value_to_set) # If supported, update the Blender property after setting it if hasattr(target_bl_prop.attr, "update"): target_bl_prop.attr.update() - + + @classmethod + @abstractmethod + def construct_bl_object(cls, data: dict): + """Abstract method to construct a Serializer's specific Blender Object""" + print("DEBUG: construct_bl_object called on Base Serializer, shouldn't happen") + return None + @classmethod def deserialize_collection(cls, stored_value: Any, bl_coll: bpy.types.bpy_prop_collection, bl_pointers_ref: dict): # Static collection case @@ -242,71 +240,74 @@ class Serializer(ABC): cls.sub_deserialize(stored_value, bl_coll, bl_pointers_ref) return - - # TODO: Code pasted as is, review later + # We need to call the collection "new" function, parse and construct its parameters new_func = bl_coll.bl_rna.functions["new"] + for i, value in enumerate(stored_value): - if value.get("_new"): - params = value["_new"] - else: - params = { + # Using a dictionary of {parameter: parameter_value} + default_new_func_params = { k: value.get(k, utils.get_bl_default(v)) for k, v in new_func.parameters.items()[:-1] } + new_func_params = value.get("_kit_new_params", default_new_func_params) - # Replace arg pointer with bl object - valid_pointers = True + solved_all_pointers = True for param in bl_coll.bl_rna.functions["new"].parameters: - if param.identifier not in params or param.type != "POINTER": + if param.identifier not in new_func_params or param.type != "POINTER": continue - # TODO: It might be possible to abstract this into deserialize_pointer somehow - pointer_id = params[param.identifier] + pointer_id = param[param.identifier] if bl_object := bl_pointers_ref.get(pointer_id): - params[param.identifier] = bl_object + new_func_params[param.identifier] = bl_object else: - print(f"No Pointer found for param {param.identifier} of {bl_coll}") - valid_pointers = False + print(f"No pointer found for param {param.identifier} of new function of {bl_coll}") + solved_all_pointers = False - if not valid_pointers: + # Bail out if we fail to solve all pointers (TODO: I'm guessing this causes a runtimerror, but double check) + if not solved_all_pointers: continue - # TODO: Ugly bad :( - try: - item = bl_coll.new(**params) - except RuntimeError as e: - try: - item = bl_coll[i] - except IndexError as e: - break - + print("Calling BL collection new with the following parameters") + print(new_func_params) + + # Creates a collection item, type from the collection type, no need to manually construct + collection_item = bl_coll.new(**new_func_params) + deserializer = cls.get_serializer(collection_item) + + # Recursively deserialize into the newly constructured object + cls.deserialize(value, collection_item, bl_pointers_ref) + + # Old code guarded by a RuntimeError before, investigate later + # Static collection case, would need to check how to detect this. + collection_item = bl_coll[i] + + # TODO: The target_bl_prop_attr terminology is unclear @classmethod def deserialize_pointer(cls, stored_value: Any, target_bl_prop_attr: bpy.types.bpy_struct, bl_pointers_ref: dict): if stored_value is None: return None - # Actual pointer + # Actual existing pointer, dereference and return if isinstance(stored_value, int): if stored_value not in bl_pointers_ref: print("DEBUG: Pointer reference hasn't been loaded yet") # Obtain a reference to a previously dereferenced object return bl_pointers_ref[stored_value] - # Create the object by passing it to the sub-serializer - cls.sub_deserialize(stored_value, target_bl_prop_attr, bl_pointers_ref) + # Pointer doesn't exist yet, create it if it doesn't exist yet, store it, and return its object + deserializer = cls.get_serializer(target_bl_prop_attr) + + # Create the Blender object if it doesn't exist yet + if target_bl_prop_attr is None: + target_bl_prop_attr = cls.construct_bl_object(stored_value) + + # Recursively deserialize into the target object + deserializer.deserialize(stored_value, target_bl_prop_attr, bl_pointers_ref) bl_pointers_ref[stored_value["_kit_ptr"]] = target_bl_prop_attr return target_bl_prop_attr - @classmethod - def sub_deserialize(cls, data: dict, target_bl_obj: bpy.types.bpy_struct, bl_pointers_ref: dict): - # Get the deserializer for the corresponding target_bl_prop - # Introspects on its type, recreateas the object even if its None - deserializer = cls.get_serializer(target_bl_obj) - - return deserializer.deserialize(data, bl_pointers_ref, target_bl_obj) - @classmethod def get_data_to_deserialize(cls, data: dict, target_obj: bpy.types.bpy_struct=None): props_to_deserialize = cls.get_serialized_properties(target_obj)