[x3d-public] x3d.py problem

Don Brutzman brutzman at nps.edu
Sat May 16 22:25:15 PDT 2020


Thanks for all the help.  Yes it was pretty involved to untangle some things but it all appears to be working now.  Special thanks to Loren for help through the thicket.

Immutable "constants" such as DEFAULT_VALUE etc. are now exposed as functions.  Default values are being inserted wherever appropriate.  Hopefully a lot fewer mysteries.

Also upgraded tooltips documentation to X3Dv4.

Also added a smoke test for the original problem you reported Andreas.

A lot changed internally, but little should change when using x3d.py package.  Deployed on PyPi as version 0.0.30.

[1] Python package x3d
     https://pypi.org/project/x3d

[2] X3DPSAIL, X3D Python Scene Access Interface Library
     https://www.web3d.org/x3d/stylesheets/python/python.html

PythonX3dSmokeTests.py excerpts with corresponding build.examples.log.txt outputs:
================================================================
print ('SFBool().NAME()         = ' +     SFBool().NAME());
print ('SFBool().DEFAULT_VALUE()= ' + str(SFBool().DEFAULT_VALUE()));
print ('SFBool().ARRAY_TYPE()   = ' + str(SFBool().ARRAY_TYPE()));
print ('SFBool().TUPLE_SIZE()   = ' + str(SFBool().TUPLE_SIZE()));
-----
SFBool().NAME()         = SFBool
SFBool().DEFAULT_VALUE()= True
SFBool().ARRAY_TYPE()   = False
SFBool().TUPLE_SIZE()   = 1
================================================================

# smoke test adapted from Andreas Plesch 12 May 2020
# http://web3d.org/pipermail/x3d-public_web3d.org/2020-May/012596.html
# x3d.py solution adapted from Vince Marchetti and Loren Peitso
print()
testScene1 = Scene()
testScene1.children.append(Group())
testScene2 = Scene()
if testScene2.hasChild():
     print('*** empty default children() test failed:')
else:
     print('*** empty default children() test passed:')
print('initialization testScene1.XML()=')
print(testScene1.XML())
print('initialization testScene2.XML()=')
print(testScene2.XML())
-----
*** empty default children() test passed:
initialization testScene1.XML()=
<Scene>
   <Group/>
</Scene>

initialization testScene2.XML()=
<Scene/>
================================================================

Here is example pattern for Group.  Pure python throughout.

Feedback and test reports welcome.  Have fun with X3D Python!  8)

================================================================
class Group(_X3DGroupingNode):
     """
     Group is a Grouping node that can contain most nodes.
     """
     def NAME(self):
         return 'Group'
     def SPECIFICATION_URL(self):
         return ''
     def TOOLTIP_URL(self):
         return 'https://www.web3d.org/x3d/tooltips/X3dTooltips.html#Group'
     def FIELD_DECLARATIONS(self):
         return [ # name, defaultValue, type, accessType, inheritedFrom
         ('bboxCenter', (0, 0, 0), FieldType.SFVec3f, AccessType.initializeOnly, 'X3DGroupingNode'),
         ('bboxSize', (-1, -1, -1), FieldType.SFVec3f, AccessType.initializeOnly, 'X3DGroupingNode'),
         ('displayBBox', False, FieldType.SFBool, AccessType.inputOutput, 'X3DGroupingNode'),
         ('visible', True, FieldType.SFBool, AccessType.inputOutput, 'X3DGroupingNode'),
         ('children', list(), FieldType.MFNode, AccessType.inputOutput, 'X3DGroupingNode'),
         ('DEF', '', FieldType.SFString, AccessType.inputOutput, 'X3DNode'),
         ('USE', '', FieldType.SFString, AccessType.inputOutput, 'X3DNode'),
         ('class_', '', FieldType.SFString, AccessType.inputOutput, 'X3DNode'),
         ('IS', None, FieldType.SFNode, AccessType.inputOutput, 'X3DNode'),
         ('metadata', None, FieldType.SFNode, AccessType.inputOutput, 'X3DNode')]
     def __init__(self,
                  bboxCenter=(0, 0, 0),
                  bboxSize=(-1, -1, -1),
                  displayBBox=False,
                  visible=True,
                  children=None,
                  DEF='',
                  USE='',
                  class_='',
                  IS=None,
                  metadata=None):
         # if _DEBUG: print('... in ConcreteNode Group __init__ calling super.__init__(' + str(DEF) + ',' + str(USE) + ',' + str(class_) + ',' + str(metadata) + ',' + str(IS) + ')', flush=True)
         super().__init__(DEF, USE, class_, IS, metadata) # fields for _X3DNode only
         self.bboxCenter = bboxCenter
         self.bboxSize = bboxSize
         self.displayBBox = displayBBox
         self.visible = visible
         self.children = children
     @property # getter - - - - - - - - - -
     def bboxCenter(self):
         """Bounding box center accompanies bboxSize and provides an optional hint for bounding box position offset from origin of local coordinate system."""
         return self.__bboxCenter
     @bboxCenter.setter
     def bboxCenter(self, bboxCenter):
         if  bboxCenter is None:
             bboxCenter = (0, 0, 0) # default
         assertValidSFVec3f(bboxCenter)
         self.__bboxCenter = bboxCenter
     @property # getter - - - - - - - - - -
     def bboxSize(self):
         """Bounding box size is usually omitted, and can easily be calculated automatically by an X3D player at scene-loading time with minimal computational cost."""
         return self.__bboxSize
     @bboxSize.setter
     def bboxSize(self, bboxSize):
         if  bboxSize is None:
             bboxSize = (-1, -1, -1) # default
         assertValidSFVec3f(bboxSize)
         assertBoundingBox('bboxSize', bboxSize)
         self.__bboxSize = bboxSize
     @property # getter - - - - - - - - - -
     def displayBBox(self):
         """Whether to display bounding box for associated geometry, aligned with world coordinates."""
         return self.__displayBBox
     @displayBBox.setter
     def displayBBox(self, displayBBox):
         if  displayBBox is None:
             displayBBox = False # default
         assertValidSFBool(displayBBox)
         self.__displayBBox = displayBBox
     @property # getter - - - - - - - - - -
     def visible(self):
         """Whether or not renderable content within this node is visually displayed."""
         return self.__visible
     @visible.setter
     def visible(self, visible):
         if  visible is None:
             visible = True # default
         assertValidSFBool(visible)
         self.__visible = visible
     @property # getter - - - - - - - - - -
     def children(self):
         """[X3DChildNode] Grouping nodes contain an ordered list of children nodes."""
         return self.__children
     @children.setter
     def children(self, children):
         if  children is None:
             children = MFNode.DEFAULT_VALUE(self)
         assertValidMFNode(children)
         self.__children = children
     # hasChild() function - - - - - - - - - -
     def hasChild(self):
         ''' Whether or not this node has any child node, statement or comment '''
         return (self.children) or (self.IS) or (self.metadata)
     # output function - - - - - - - - - -
     def XML(self, indentLevel=0, syntax="XML"):
         """ Provide Canonical X3D output serialization using XML encoding. """
         result = ''
         indent = '  ' * indentLevel
         result = indent ### confirm
         # if _DEBUG: result += indent + '# invoked class function Group.XML(self=' + str(self) + ', indentLevel=' + str(indentLevel) + '), indent="' + indent + '"'
         # print(result)
         result += '<Group'
         if self.DEF:
             result += " DEF='" + self.DEF + "'"
         if self.USE:
             result += " USE='" + self.USE + "'"
         if self.bboxCenter != (0, 0, 0):
             result += " bboxCenter='" + SFVec3f(self.bboxCenter).XML() + "'"
         if self.bboxSize != (-1, -1, -1):
             result += " bboxSize='" + SFVec3f(self.bboxSize).XML() + "'"
         if self.class_:
             result += " class_='" + self.class_ + "'"
         if self.displayBBox != False:
             result += " displayBBox='" + SFBool(self.displayBBox).XML() + "'"
         if self.visible != True:
             result += " visible='" + SFBool(self.visible).XML() + "'"
         if not self.hasChild():
             if syntax.upper() == "HTML5":
                 result += '></Group>' + '\n' # no self-closing tags allowed by HTML5
             elif syntax.upper() == "XML":
                 result += '/>' + '\n' # singleton element
             else:
                 raise X3DValueError('.toXML(syntax=' + syntax + ') is incorrect, allowed values are "HTML5" and "XML"')
         else:
             result += '>' + '\n'
             if self.IS: # output this SFNode
                 result += self.IS.XML(indentLevel=indentLevel+1,syntax=syntax)
             if self.metadata: # output this SFNode
                 result += self.metadata.XML(indentLevel=indentLevel+1,syntax=syntax)
             ### if self.children: # walk each child in MFNode list, if any
             ### print('* Group found self.children with self.hasChild()=' + str(self.hasChild()) + ' and len(children)=' + str(len(self.children)) + ', now invoking XML(' + str(indentLevel+1) + ')', flush=True)
             for each in self.children:
                result += each.XML(indentLevel=indentLevel+1,syntax=syntax)
             result += indent + '</Group>' + '\n'
#       print('XML serialization complete.', flush=True)
         return result
     # output function - - - - - - - - - -
     def HTML5(self, indentLevel=0):
         """ Provide HTML5 output serialization using XML encoding with no singleton self-closing elements. """
         return self.XML(indentLevel, syntax="HTML5")
     # output function - - - - - - - - - -
     def VRML(self, indentLevel=0, VRML97=False):
         """ Provide X3D output serialization using VRML encoding. """
         result = ''
         indent = '  ' * indentLevel
         # if _DEBUG: result += indent + '# invoked class function Group.VRML(self=' + str(self) + ', indentLevel=' + str(indentLevel) + '), indent="' + indent + '"'
         # print(result)
         if indentLevel == 0:
             result += '\n'
         if self.DEF:
             result += 'DEF ' + self.DEF + ' ' + 'Group' + ' {'
         elif self.USE:
             result += 'USE ' + self.USE # no node name, nothing follows
         else:
             result += 'Group' + ' {'
         if self.bboxCenter != (0, 0, 0):
             result += " bboxCenter " + SFVec3f(self.bboxCenter).VRML() + ""
         if self.bboxSize != (-1, -1, -1):
             result += " bboxSize " + SFVec3f(self.bboxSize).VRML() + ""
         if self.class_:
             result += " class_ " +  '"' + self.class_ + '"' + ""
         if self.displayBBox != False:
             result += " displayBBox " + SFBool(self.displayBBox).VRML() + ""
         if self.visible != True:
             result += " visible " + SFBool(self.visible).VRML() + ""
         if self.IS: # output this SFNode
             result += '\n' + '  ' + indent + 'IS ' + self.IS.VRML(indentLevel=indentLevel+1,VRML97=VRML97)
         if self.metadata: # output this SFNode
             result += '\n' + '  ' + indent + 'metadata ' + self.metadata.VRML(indentLevel=indentLevel+1,VRML97=VRML97)
         if self.children: # walk each child in MFNode list, if any
             for each in self.children:
                 result += each.VRML(indentLevel=indentLevel+1,VRML97=VRML97)
         if not self.USE:
             result += ' }'
#       print('VRML serialization complete.', flush=True)
         return result
================================================================


On 5/14/2020 3:14 PM, Peitso, Loren (CIV) wrote:
> Don,
> 
> Here is an even clearer explanation:
> 
> https://docs.python-guide.org/writing/gotchas/
> 
> OK, I have not run into this previously because my style heavily favors use of the property/setter pairs for instance attributes, and when you do it that way it naturally make you write the code more like vice has it. 
> 
> You already have those for children in Scene but in the setter you have a non-standerd implementation:
> 
> current:
>         def children(self, children=None):
>             if  children is None:
>                 children = MFNode.DEFAULT_VALUE
>             isValidMFNode(children)
>             self.__children = children
> 
> change to this:  
> #runs when self.children is accessed on the LHS of an assignment operator
>         #  I am in favor of giving parameters unique names to avoid confusion between the arrtibute that is being managed and the values being passed
> #also note that a setter NEVER needs a default argument, it is only EVER called if the attribute is being assigned, and the result of the RHS is passed as the single argument
> 
>         def children(self, childrenArg):
>             if  childrenArg is None:
>                 self.__children = MFNode.DEFAULT_VALUE
>     else:
>                 isValidMFNode(childrenArg)        #don't need this check on MFNode.DEFAULT_VALUE because it had better be correct or else...
>                 self.__children = childrenArg

> This manages getting children properly set and does not require an empty list default argument at all.   So make the Scene init method start like this as vince suggests
> 
>>>     def __init__(self, children=None):
>>>         self.children = children
> 
> and use the above modified property/setter pair and you can leave off the rest of what vince put because the setup property/setter pair is overall superior.  
> 
> v/r  Loren



On 5/13/2020 4:35 AM, Andreas Plesch wrote:
> Thanks for recognizing the issue, and suggesting a solution. All nodes with children, or other mutable valued parameters, are likely affected.
> 
> Andreas
> 
> ---on the phone---
> 
> On Tue, May 12, 2020, 9:26 PM vmarchetti at kshell.com <mailto:vmarchetti at kshell.com> <vmarchetti at kshell.com <mailto:vmarchetti at kshell.com>> wrote:
> 
>     This is an unfortunate and common bug that occurs in Python.
> 
>     In the definition of the class Scene (around line 8795 of x3d.py) is this definition of the __init__ function for Scene:
> 
>          def __init__(self, children=list()):
>              self.children = children
> 
>     This code is read once when the x3d.py module is loaded, and it creates a class-level default value for the children argument.
>     If you ever use the default value by calling Scene() {with no parameter}, this class-level instance of list is modified, and this SAME INSTANCE of list
>     if used for any further calls of Scene()
> 
>     The way to avoid this is to define the __init__ function this way:
> 
>     def __init__(self, children=None):
>     if children is None:
>     self.children = list() # creates a new empty list for each Scene instance
>     else:
>     self.children = children
> 
> 
>     Vince Marchetti
> 
> 
>>     On May 12, 2020, at 5:59 PM, Andreas Plesch <andreasplesch at gmail.com <mailto:andreasplesch at gmail.com>> wrote:
>>
>>     I discovered a simple workaround. Always pass a children parameter, even if empty.
>>
>>     On Tue, May 12, 2020 at 5:16 PM Andreas Plesch <andreasplesch at gmail.com <mailto:andreasplesch at gmail.com>> wrote:
>>
>>         Hi,
>>
>>         after some debugging I discovered another issue with x3d.py. It can be boiled down t o a few lines of code:
>>
>>         import x3d.x3d as x
>>
>>         my_scene = x.Scene()
>>         group = x.Group()
>>         my_scene.children.append(group)
>>         print(my_scene.XML())
>>
>>         => output
>>         => <Scene>
>>         =>  <Group/>
>>         => </Scene>
>>
>>         my_other_scene = x.Scene()
>>         print(my_other_scene.XML())
>>
>>         => output
>>         => <Scene>
>>         =>  <Group/>
>>         => </Scene>
>>
>>         I think my_other_scene should be empty but remembers the content of the first scene.
>>         Can you reproduce that ?
>>         On the other hand this works (in a new session):
>>
>>         my_scene = x.Scene( children = [x.Group()] )
>>         print(my_scene.XML())
>>
>>         => output
>>         => <Scene>
>>         =>  <Group/>
>>         => </Scene>
>>
>>         my_other_scene = x.Scene()
>>         print(my_other_scene.XML())
>>
>>         => output
>>         => <Scene>
>>         => </Scene>
>>
>>         -- 
>>         Andreas Plesch
>>         Waltham, MA 02453
>>
>>
>>
>>     -- 
>>     Andreas Plesch
>>     Waltham, MA 02453
>>     _______________________________________________
>>     x3d-public mailing list
>>     x3d-public at web3d.org <mailto:x3d-public at web3d.org>
>>     http://web3d.org/mailman/listinfo/x3d-public_web3d.org
> 

all the best, Don
-- 
Don Brutzman  Naval Postgraduate School, Code USW/Br       brutzman at nps.edu
Watkins 270,  MOVES Institute, Monterey CA 93943-5000 USA   +1.831.656.2149
X3D graphics, virtual worlds, navy robotics http://faculty.nps.edu/brutzman



More information about the x3d-public mailing list