Scripted objects saving attributes: Difference between revisions

From FreeCAD Documentation
(Attributes of the class used to create the object can also be saved, that is, "serialized". This can be further controlled by the __getstate__ and __setstate__ methods of the class.)
 
(Module to Workbench renaming.)
(12 intermediate revisions by 4 users not shown)
Line 1: Line 1:
{{Work_in_progress}}
<languages/>
<languages/>

{{TOCright}}
<translate>
<translate>


== Introduction ==
== Introduction == <!--T:1-->


<!--T:2-->
[[Scripted_objects|Scripted objects]] are rebuilt every time a [[File_Format_FCStd|FCStd document]] is opened. To do this the document keeps a reference to the module and Python class that were used to create the object, along with its properties.
[[Scripted_objects|Scripted objects]] are rebuilt every time a [[File_Format_FCStd|FCStd document]] is opened. To do this the document keeps a reference to the module and Python class that were used to create the object, along with its properties.


<!--T:3-->
Attributes of the class used to create the object can also be saved, that is, "serialized". This can be further controlled by the {{incode|__getstate__}} and {{incode|__setstate__}} methods of the class.
Attributes of the class used to create the object can also be saved, that is, "serialized". This can be further controlled by the {{incode|__getstate__}} and {{incode|__setstate__}} methods of the class.


== Saving all attributes ==
== Saving all attributes == <!--T:4-->


<!--T:5-->
By default, attributes saved in an object class are those from the {{incode|__dict__}} dictionary of the class.
By default, attributes saved in an object class are those from the {{incode|__dict__}} dictionary of the class.


Line 24: Line 28:
obj.Proxy = self
obj.Proxy = self


Type = dict()
Type["Version"] = "Custom"
Type["Release"] = "production"
self.Type = Type
self.Type = "Custom"
self.Type = "Custom"
self.ver = "0.18"
self.ver = "0.18"
Line 34: Line 42:
<translate>
<translate>


<!--T:6-->
An object can be created using this class, and it can be saved to {{FileName|my_document.FCstd}}. If no particular [[viewprovider|viewprovider]] is assigned to the new object, its proxy class is simply set to a value different from {{incode|None}}, in this case, to {{incode|1}}.
An object can be created using this class, and it can be saved to {{FileName|my_document.FCstd}}. If no particular [[viewprovider|viewprovider]] is assigned to the new object, its proxy class is simply set to a value different from {{incode|None}}, in this case, to {{incode|1}}.
</translate>
</translate>
Line 54: Line 63:
<translate>
<translate>


<!--T:7-->
When we re-open the file we can inspect the dictionary of the object's class.
When we re-open the file we can inspect the dictionary of the object's class.
</translate>
</translate>
Line 59: Line 69:
>>> obj = App.ActiveDocument.Custom
>>> obj = App.ActiveDocument.Custom
>>> print(obj.Proxy)
>>> print(obj.Proxy)
<various_states.VariousStates object at 0x7fc30528a748>
<various_states.VariousStates object at 0x7f0a899bde10>
>>> print(obj.Proxy.__dict__)
>>> print(obj.Proxy.__dict__)
{'Type': 'Custom', 'ver': '0.18', 'color': [0, 0, 1], 'width': 2.5}
{'Type': {'Version': 'Custom', 'Release': 'production'}, 'ver': '0.18', 'color': [0, 0, 1], 'width': 2.5}
}}
}}
<translate>
<translate>


<!--T:8-->
We see that all attributes that start with {{incode|self}} in the class were saved. These can be of different types, including string, list, float, and other dictionaries.
We see that all attributes that start with {{incode|self}} in the class were saved. These can be of different types, including string, list, float, and dictionary. The original tuple for {{incode|self.color}} was converted to a list, but otherwise all attributes were loaded the same.


== Saving specific attributes ==
== Saving specific attributes == <!--T:9-->


<!--T:10-->
We can define a class similar to the first one, that implements specific attributes to save.
We can define a class similar to the first one, that implements specific attributes to save.
</translate>
</translate>
Line 81: Line 93:
obj.Proxy = self
obj.Proxy = self


self.Type = "Custom"
Type = dict()
Type["Version"] = "Custom"
Type["Release"] = "production"
self.Type = Type
self.ver = "0.18"
self.ver = "0.18"
self.color = (0, 0, 1)
self.color = (0, 0, 1)
Line 98: Line 113:
<translate>
<translate>


<!--T:11-->
The return value of {{incode|__getstate__}} is the object that will be serialized. This can be a single value, or a tuple of values. When the object is restored, the class calls the {{incode|__setstate__}} method, passing the {{incode|state}} variable with the serialized content. In this case {{incode|state}} is a tuple that is unpacked into the respective variables to reconstruct the "state" that original existed.
The return value of {{incode|__getstate__}} is the object that will be serialized. This can be a single value, or a tuple of values. When the object is restored, the class calls the {{incode|__setstate__}} method, passing the {{incode|state}} variable with the serialized content. In this case {{incode|state}} is a tuple that is unpacked into the respective variables to reconstruct the "state" that original existed.
</translate>
</translate>
Line 106: Line 122:
<translate>
<translate>


<!--T:12-->
We can create an object with this class, and save the document, just like in the previous example. When we re-open the file we can inspect the dictionary of the object's class.
We can create an object with this class, and save the document, just like in the previous example. When we re-open the file we can inspect the dictionary of the object's class.
</translate>
</translate>
Line 117: Line 134:
<translate>
<translate>


<!--T:13-->
The original tuple for {{incode|self.color}} was converted to a list, but otherwise the information was recovered fine. Instead of restoring all attributes like in the previous case, only the attributes that we specified in {{incode|__getstate__}} and {{incode|__setstate__}} were restored.
The original tuple for {{incode|self.color}} was converted to a list, but otherwise the information was recovered fine. Instead of restoring all attributes like in the previous case, only the attributes that we specified in {{incode|__getstate__}} and {{incode|__setstate__}} were restored.


== Links ==
== Usage == <!--T:14-->


=== Identifying the type === <!--T:15-->

<!--T:16-->
Normally, [[scripted_objects|scripted objects]] should use [[property|properties]] to store information, as these are automatically restored when the document is opened.

<!--T:17-->
However, sometimes the class restore internal information which is not intended to be modified, but which is helpful to inspect.

<!--T:18-->
For example, most objects of the [[Draft_Workbench|Draft Workbench]] set up a {{incode|Type}} attribute that can be used to determine the type of object that is under use.

</translate>
{{Code|code=
class DraftObject:
def __init__(self, obj, _type):
self.Type = _type

def __getstate__(self):
return self.Type

def __setstate__(self, state):
if state:
self.Type = state
}}
<translate>

<!--T:19-->
All objects have a {{incode|TypeId}} property, but for [[scripted_objects|scripted objects]] this property is not useful, as it always refers to the parent C++ class, for example, [[Part_Part2DObject|{{incode|Part::Part2DObjectPython}}]] or [[Part_Feature|{{incode|Part::FeaturePython}}]]. Therefore, having this additional {{incode|Proxy.Type}} attribute in the class is useful to treat each object in a particular way.

=== Migrating the object === <!--T:20-->

<!--T:21-->
Version information can be stored inside the class in order to verify the origin of an object.

</translate>
{{Code|code=
class CustomObject:
def __init__(self, obj, _type):
self.Type = _type
self.version = "0.18"

def __getstate__(self):
return self.Type, self.version

def __setstate__(self, state):
if state:
self.Type = state[0]
self.version = state[1]
}}
<translate>

<!--T:22-->
If the structure of the class changes, that is, if its properties or methods change, are renamed, or are removed, we could test the version attribute in order to migrate the older object to a new set of properties or to a new class. This can be done by implementing the {{incode|onDocumentRestored}} method, as explained in [[Scripted_objects_migration|Scripted objects migration]].

</translate>
{{Code|code=
class CustomObject:
def onDocumentRestored(self, obj):
if hasattr(obj.Proxy, "version") and obj.Proxy.version:
if obj.Proxy.version == "0.18":
self.migrate_from_018(obj)

def migrate_from_018(self, obj):
...
}}
<translate>

== Links == <!--T:23-->

<!--T:24-->
* [[Scripted_objects|Scripted objects]]
* [[Scripted_objects|Scripted objects]]
* [[Scripted_objects_migration|Scripted objects migration]]
* [[Scripted_objects_migration|Scripted objects migration]]
* [https://forum.freecadweb.org/viewtopic.php?f=10&t=49120 FreeCAD Forum Discussion: Scripted Object Serialization: json or pickle?]

<!--T:25-->
* [https://forum.freecadweb.org/viewtopic.php?f=18&t=44009 obj.Proxy.Type is a dict, not a string], explanation of {{incode|__getstate__}} and {{incode|__setstate__}} in the forum.
* [https://docs.python.org/3/library/pickle.html#object.__getstate__ The Pickle module] in the Python documentation.

</translate>
</translate>
{{Powerdocnavi{{#translation:}}}}
{{Powerdocnavi{{#translation:}}}}
[[Category:Developer Documentation{{#translation:}}]]
[[Category:Python Code{{#translation:}}]]
{{clear}}

Revision as of 20:50, 24 August 2021

Introduction

Scripted objects are rebuilt every time a FCStd document is opened. To do this the document keeps a reference to the module and Python class that were used to create the object, along with its properties.

Attributes of the class used to create the object can also be saved, that is, "serialized". This can be further controlled by the __getstate__ and __setstate__ methods of the class.

Saving all attributes

By default, attributes saved in an object class are those from the __dict__ dictionary of the class.

# various_states.py
class VariousStates:
    def __init__(self, obj):
        obj.addProperty("App::PropertyLength", "Length")
        obj.addProperty("App::PropertyArea", "Area")
        obj.Length = 15
        obj.Area = 300
        obj.Proxy = self

        Type = dict()
        Type["Version"] = "Custom"
        Type["Release"] = "production"
        self.Type = Type
        self.Type = "Custom"
        self.ver = "0.18"
        self.color = (0, 0, 1)
        self.width = 2.5

    def execute(self, obj):
        pass

An object can be created using this class, and it can be saved to my_document.FCstd. If no particular viewprovider is assigned to the new object, its proxy class is simply set to a value different from None, in this case, to 1.

import FreeCAD as App
import various_states

doc = App.newDocument()
doc.FileName = "my_document.FCStd"

obj = doc.addObject("Part::FeaturePython", "Custom")
various_states.VariousStates(obj)

if App.GuiUp:
    obj.ViewObject.Proxy = 1

doc.recompute()
doc.save()

When we re-open the file we can inspect the dictionary of the object's class.

>>> obj = App.ActiveDocument.Custom
>>> print(obj.Proxy)
<various_states.VariousStates object at 0x7f0a899bde10>
>>> print(obj.Proxy.__dict__)
{'Type': {'Version': 'Custom', 'Release': 'production'}, 'ver': '0.18', 'color': [0, 0, 1], 'width': 2.5}

We see that all attributes that start with self in the class were saved. These can be of different types, including string, list, float, and dictionary. The original tuple for self.color was converted to a list, but otherwise all attributes were loaded the same.

Saving specific attributes

We can define a class similar to the first one, that implements specific attributes to save.

# various_states.py
class CustomStates:
    def __init__(self, obj):
        obj.addProperty("App::PropertyLength", "Length")
        obj.addProperty("App::PropertyArea", "Area")
        obj.Length = 15
        obj.Area = 300
        obj.Proxy = self

        Type = dict()
        Type["Version"] = "Custom"
        Type["Release"] = "production"
        self.Type = Type
        self.ver = "0.18"
        self.color = (0, 0, 1)
        self.width = 2.5

    def execute(self, obj):
        pass

    def __getstate__(self):
        return self.color, self.width

    def __setstate__(self, state):
        self.color = state[0]
        self.width = state[1]

The return value of __getstate__ is the object that will be serialized. This can be a single value, or a tuple of values. When the object is restored, the class calls the __setstate__ method, passing the state variable with the serialized content. In this case state is a tuple that is unpacked into the respective variables to reconstruct the "state" that original existed.

state = (self.color, self.width)
state = ((0, 0, 1), 2.5)

We can create an object with this class, and save the document, just like in the previous example. When we re-open the file we can inspect the dictionary of the object's class.

>>> obj2 = App.ActiveDocument.Custom2
>>> print(obj2.Proxy)
<various_states.CustomStates object at 0x7fb399496630>
>>> print(obj2.Proxy.__dict__)
{'color': [0, 0, 1], 'width': 2.5}

The original tuple for self.color was converted to a list, but otherwise the information was recovered fine. Instead of restoring all attributes like in the previous case, only the attributes that we specified in __getstate__ and __setstate__ were restored.

Usage

Identifying the type

Normally, scripted objects should use properties to store information, as these are automatically restored when the document is opened.

However, sometimes the class restore internal information which is not intended to be modified, but which is helpful to inspect.

For example, most objects of the Draft Workbench set up a Type attribute that can be used to determine the type of object that is under use.

class DraftObject:
    def __init__(self, obj, _type):
        self.Type = _type

    def __getstate__(self):
        return self.Type

    def __setstate__(self, state):
        if state:
            self.Type = state

All objects have a TypeId property, but for scripted objects this property is not useful, as it always refers to the parent C++ class, for example, Part::Part2DObjectPython or Part::FeaturePython. Therefore, having this additional Proxy.Type attribute in the class is useful to treat each object in a particular way.

Migrating the object

Version information can be stored inside the class in order to verify the origin of an object.

class CustomObject:
    def __init__(self, obj, _type):
        self.Type = _type
        self.version = "0.18"

    def __getstate__(self):
        return self.Type, self.version

    def __setstate__(self, state):
        if state:
            self.Type = state[0]
            self.version = state[1]

If the structure of the class changes, that is, if its properties or methods change, are renamed, or are removed, we could test the version attribute in order to migrate the older object to a new set of properties or to a new class. This can be done by implementing the onDocumentRestored method, as explained in Scripted objects migration.

class CustomObject:
    def onDocumentRestored(self, obj):
        if hasattr(obj.Proxy, "version") and obj.Proxy.version:
            if obj.Proxy.version == "0.18":
                self.migrate_from_018(obj)

    def migrate_from_018(self, obj):
        ...

Links