Python object attributes lost at load

Need help, or want to share a macro? Post here!
User avatar
JMG
Posts: 278
Joined: Wed Dec 25, 2013 9:32 am
Location: Spain
Contact:

Python object attributes lost at load

Postby JMG » Mon Jan 04, 2016 9:50 pm

Hi!

I have a problem with a python object not recognizing its attributes at load time.
The object is created from this code (this is part of the NiCr workbench for FreeCAD: https://github.com/JMG1/NiCr/tree/master/Workbench ):

Code: Select all

class SimMachine:
    def __init__( self, obj ):
        # geometric properties
        obj.addProperty( 'App::PropertyFloat',
                         'XLength',
                         'Machine Geometry' ).XLength = 800.0

        obj.addProperty( 'App::PropertyFloat',
                         'YLength',
                         'Machine Geometry' ).YLength = 600.0

        obj.addProperty( 'App::PropertyFloat',
                         'ZLength',
                         'Machine Geometry' ).ZLength = 800.0

        obj.addProperty( 'App::PropertyFloat',
                         'FrameDiameter',
                         'Machine Geometry' ).FrameDiameter = 30.0

        obj.Proxy = self
        self.addMachineToDocument( obj.FrameDiameter, obj.XLength, obj.YLength, obj.ZLength, created=False )


    def onChanged( self, fp, prop):
        if prop == 'XLength' or prop == 'YLength' or prop == 'ZLength' or prop == 'FrameDiameter':
            self.addMachineToDocument( fp.FrameDiameter, fp.XLength, fp.YLength, fp.ZLength )
            #pass

    def execute( self, fp ):
        #dbm( 'e' )
        #self.addMachineToDocument( fp.FrameDiameter, fp.XLength, fp.YLength, fp.ZLength )
        pass

    def buildMachine( self, tube_diameter, w_x, w_y, w_z ):
        # machine frame
        #dbm( '2.1' )
        main_cube = Part.makeBox( w_x + 2*tube_diameter,
                                  w_y + 2*tube_diameter,
                                  w_z + 2*tube_diameter )

        xy_cutcube = Part.makeBox( w_x,
                                   w_y,
                                   w_z*1.5,
                                   FreeCAD.Vector( tube_diameter,
                                                   tube_diameter,
                                                   -tube_diameter ) )
        xz_cutcube = Part.makeBox( w_x,
                                   w_y*1.5,
                                   w_z,
                                   FreeCAD.Vector( tube_diameter,
                                                   -tube_diameter,
                                                   tube_diameter ) )

        yz_cutcube = Part.makeBox( w_x*1.5,
                                   w_y,
                                   w_z,
                                   FreeCAD.Vector( -tube_diameter,
                                                   tube_diameter,
                                                   tube_diameter ) )

        frame = main_cube.cut( xy_cutcube )
        frame = frame.cut( xz_cutcube )
        frame = frame.cut( yz_cutcube )
        #dbm( '2.2' )
        # machine x axis frame
        xa_frame = Part.makeBox( tube_diameter,
                                 w_y,
                                 tube_diameter,
                                 FreeCAD.Vector( tube_diameter*1.1,
                                                 tube_diameter,
                                                 0 ) )

        xb_frame = Part.makeBox( tube_diameter,
                                 w_y,
                                 tube_diameter,
                                 FreeCAD.Vector( tube_diameter*1.1,
                                                 tube_diameter,
                                                 w_z + tube_diameter ) )

        # machine y axis frame
        ya_frame = Part.makeBox( tube_diameter*1.2,
                                 tube_diameter*1.6,
                                 tube_diameter*1.2,
                                 FreeCAD.Vector( tube_diameter,
                                                 tube_diameter,
                                                 -tube_diameter*0.1 ) )

        yb_frame = Part.makeBox( tube_diameter*1.2,
                                 tube_diameter*1.6,
                                 tube_diameter*1.2,
                                 FreeCAD.Vector( tube_diameter,
                                                 tube_diameter,
                                                 w_z + tube_diameter*0.9 ) )
        #dbm('2.3')
        return frame, xa_frame, xb_frame, ya_frame, yb_frame


    def addMachineToDocument(self, FrameDiameter, XLength, YLength, ZLength, created=True):
        # temporal workarround until:http://forum.freecadweb.org/viewtopic.php?f=22&t=13337
        #dbm( '0' )
        mfolder = FreeCAD.ActiveDocument.getObject('SimMachine')
        #dbm( '1' )
        # Remove previous machine parts
        if created:
            FreeCAD.ActiveDocument.removeObject('Frame')
            FreeCAD.ActiveDocument.removeObject('XA')
            FreeCAD.ActiveDocument.removeObject('XB')
            FreeCAD.ActiveDocument.removeObject('YA')
            FreeCAD.ActiveDocument.removeObject('YB')

        # machine shapes
        machine_shapes = self.buildMachine(FrameDiameter,
                                           XLength,
                                           YLength,
                                           ZLength)
        # temporal workaround
        #mfolder = FreeCAD.ActiveDocument.addObject( 'App::DocumentObjectGroup','SimMachine' )
        obj_frame = FreeCAD.ActiveDocument.addObject('Part::Feature', 'Frame')
        obj_XA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'XA')
        obj_XB = FreeCAD.ActiveDocument.addObject('Part::Feature', 'XB')
        obj_YA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'YA')
        obj_YB = FreeCAD.ActiveDocument.addObject('Part::Feature', 'YB')
        obj_frame.Shape = machine_shapes[0]
        obj_XA.Shape = machine_shapes[1]
        obj_XB.Shape = machine_shapes[2]
        obj_YA.Shape = machine_shapes[3]
        obj_YB.Shape = machine_shapes[4]
        obj_frame.ViewObject.ShapeColor = (0.67, 0.78, 0.85)
        obj_XA.ViewObject.ShapeColor = (0.00, 0.67, 1.00)
        obj_XB.ViewObject.ShapeColor = (0.00, 0.67, 1.00)
        obj_YA.ViewObject.ShapeColor = (0.00, 1.00, 0.00)
        obj_YB.ViewObject.ShapeColor = (0.00, 1.00, 0.00)
        obj_frame.ViewObject.Selectable = False
        obj_XA.ViewObject.Selectable = False
        obj_XB.ViewObject.Selectable = False
        obj_YA.ViewObject.Selectable = False
        obj_YB.ViewObject.Selectable = False
        mfolder.addObject(obj_frame)
        mfolder.addObject(obj_XA)
        mfolder.addObject(obj_XB)
        mfolder.addObject(obj_YA)
        mfolder.addObject(obj_YB)


class SimMachineViewProvider:
    def __init__(self, obj):
        obj.Proxy = self

    def getDefaultDisplayMode(self):
        return "Flat Lines"

    def getIcon(self):
        import os
        __dir__ = os.path.dirname(__file__)
        return __dir__ + '/icons/CreateMachine.svg'
I create the object, save the document, close FreeCAD and open it again receiving this error:

Code: Select all

Traceback (most recent call last):
  File "/home/javier/.FreeCAD/Mod/Workbench/NiCrSimMachine.py", line 53, in onChanged
    self.addMachineToDocument( fp.FrameDiameter, fp.XLength, fp.YLength, fp.ZLength )
<type 'exceptions.AttributeError'>: 'FeaturePython' object has no attribute 'YLength'
Traceback (most recent call last):
  File "/home/javier/.FreeCAD/Mod/Workbench/NiCrSimMachine.py", line 53, in onChanged
    self.addMachineToDocument( fp.FrameDiameter, fp.XLength, fp.YLength, fp.ZLength )
<type 'exceptions.AttributeError'>: 'FeaturePython' object has no attribute 'ZLength'
The error trace points to the onChanged() function, where it complains about the python object not having the attributes it should have :|

I have no idea of what can be wrong there, the attributes should be stored in the python object and the only ones who fail are ZLength and YLength, while XLength and FrameDiameter are working OK.

The best thing is that, even with this error, the python object is behaving as expected....


Any idea is greatly appreciated. :)

Javier.
FreeCAD scripts, animations, experiments and more: http://linuxforanengineer.blogspot.com.es/
Open source CNC hot wire cutter project (NiCr): https://github.com/JMG1/NiCr
Exploded Assembly Workbench: https://github.com/JMG1/ExplodedAssembly
User avatar
DeepSOIC
Posts: 6872
Joined: Fri Aug 29, 2014 12:45 am
Location: Saint-Petersburg, Russia

Re: Python object attributes lost at load

Postby DeepSOIC » Mon Jan 04, 2016 10:03 pm

Well, my guess would be you are receiving onChanged as FreeCAD is reading out the properties from the file and writing them to object. From my experience, __init__ is never called when loading, so it might be a good idea to call it from onChanged explicitly.
I don't know, how to test if onChanged has fired because of loading, but I think checking for document object's State attribute might give something.
looo
Posts: 2747
Joined: Mon Nov 11, 2013 5:29 pm

Re: Python object attributes lost at load

Postby looo » Mon Jan 04, 2016 10:53 pm

I had similar issues with the glider workbench. Checking for the alphabetical last property works for me. But checking the State maybe the better solution.

Code: Select all

if hasattr(fp, "ZLength"):

Code: Select all

if "Restore" in fp.State:
wmayer
Site Admin
Posts: 14640
Joined: Thu Feb 19, 2009 10:32 am

Re: Python object attributes lost at load

Postby wmayer » Tue Jan 05, 2016 11:11 am

What happened is that the "Proxy" property is restored and thus an instance of "SimMachine" is restored. Later on "XLength" is restored and touched which triggers the onChanged() method. However, at this point "YLength" and "ZLength" are not yet restored.

It's correct what looo said that the properties are stored in alphabetical order which is caused by using a std::map container (which automatically sorts the key in ascending order). However, this must be considered as an implementation detail and you should not rely on this behaviour. Until now we don't know of any issues doing it this way but if it happens in the future this can be changed.
From my experience, __init__ is never called when loading,
This is correct. Internally an object of this type is created without calling the __init__ function.
so it might be a good idea to call it from onChanged explicitly
Bad idea! Internally the FeaturePython class does all the hard work for you. It saves and restores any user defined properties.
prrvchr
Posts: 117
Joined: Sun Oct 05, 2014 7:38 pm
Location: France

Re: Python object attributes lost at load

Postby prrvchr » Tue Jan 05, 2016 11:41 am

Hi and Happy New Year,

the only place where I can initialize property while the object has finished loading is attach() of ViewObject... (use of vobj.Object)
Not very great for separating App and Gui...

But if there are better I'm interested ...
wmayer
Site Admin
Posts: 14640
Joined: Thu Feb 19, 2009 10:32 am

Re: Python object attributes lost at load

Postby wmayer » Tue Jan 05, 2016 1:17 pm

What about using the __setstate__ and __getstate__ methods of your Python class?

Code: Select all

class SimMachine:
...
  def __getstate__(self):
    # return whatever you want as long as JSON is able to serialize it
    return ...
  def __setstate__(self, res):
    # 'res' is the same what you returned inside __getstate__
But note, at this stage the FeaturePython instance behind SimMachine doesn't need to be fully restored.
the only place where I can initialize property while the object has finished loading is attach() of ViewObject
There is a method onDocumentRestored() but this is not yet available for Python features.
User avatar
JMG
Posts: 278
Joined: Wed Dec 25, 2013 9:32 am
Location: Spain
Contact:

Re: Python object attributes lost at load

Postby JMG » Tue Jan 05, 2016 3:11 pm

Thank you for your replies!

After trying several solutions, the most simple I found is to handle the error directly:

Code: Select all

    def onChanged( self, fp, prop):
        try:
            if prop == 'XLength' or prop == 'YLength' or prop == 'ZLength' or prop == 'FrameDiameter':
                self.addMachineToDocument( fp.FrameDiameter, fp.XLength, fp.YLength, fp.ZLength )

        except AttributeError:
            pass
I know this only silences the error, but everything is working correctly. I think that, in this case, the problem is solved :?


Javier.
FreeCAD scripts, animations, experiments and more: http://linuxforanengineer.blogspot.com.es/
Open source CNC hot wire cutter project (NiCr): https://github.com/JMG1/NiCr
Exploded Assembly Workbench: https://github.com/JMG1/ExplodedAssembly
wmayer
Site Admin
Posts: 14640
Joined: Thu Feb 19, 2009 10:32 am

Re: Python object attributes lost at load

Postby wmayer » Tue Jan 05, 2016 6:07 pm

git commit 061c67c exposes onDocumentRestored() to Python feature classes. You can use it as

Code: Select all

class ...
...
    def onDocumentRestored(self, fp):
       ....
User avatar
microelly2
Posts: 4329
Joined: Tue Nov 12, 2013 4:06 pm
Contact:

Re: Python object attributes lost at load

Postby microelly2 » Fri Jan 08, 2016 11:42 am

wmayer wrote: onDocumentRestored() to Python feature classes.
Does this mean that I can use onSettingDocument to upgrate my python object in the case I load an old version file
for example to extend the list of properties?