Proper fix for error 'object has no attribute 'getSlave' in EtherCAT extension
authorAndrey Skvortsov <andrej.skvortzov@gmail.com>
Fri, 24 Aug 2018 13:41:43 +0300
changeset 2297 96ca6b056c55
parent 2296 a2ab363f9e90
child 2298 10cfc280927c
Proper fix for error 'object has no attribute 'getSlave' in EtherCAT extension

traceback:
File "/home/developer/WorkData/PLC/beremiz/beremiz/IDEFrame.py", line 1433, in OnPouSelectedChanged
window.RefreshView()
File "/home/developer/WorkData/PLC/beremiz/beremiz/etherlab/ConfigEditor.py", line 837, in RefreshView
self.RefreshProcessVariables()
File "/home/developer/WorkData/PLC/beremiz/beremiz/etherlab/ConfigEditor.py", line 886, in RefreshProcessVariables
slaves = self.Controler.GetSlaves(**self.CurrentNodesFilter)
File "/home/developer/WorkData/PLC/beremiz/beremiz/etherlab/EthercatMaster.py", line 341, in GetSlaves
for slave in self.Config.getConfig().getSlave():
<type 'exceptions.AttributeError'>:_'lxml.etree._Element'_object_has_no_attribute_'getSlave'

Steps to reproduce problem:

- Add new EtherCAT master
- Add new EthercatNode to the master
- double click on


Revert commit "Dirty fix for error '_object_has_no_attribute_'getSlave' in EtherCAT extension"
[a3ac46366b86a0b237dac93be6b2281ac70b98a8].

The problem was that XML elements (proxy object) in some cases were created using custom XML
classes constructors and lxml.etree.Element() call and live python
patching. This causes that lxml backend doesn't know that custom python class
should be used for these XML elements.
Proxy object can be move/deleted and recreated by lxml
backend at any point in time or this can be done in python by copy/deepcopy operations.
If this happens, then newly created
proxy elements are using default class lxml.etree._Element. And all
custom functionality is lost.

All created XML elements should be always created through corresponding
parser and class lookup callback done by lxml backend.
It's described in more details in lxml documentation:
https://lxml.de/element_classes.html
xmlclass/xmlclass.py
--- a/xmlclass/xmlclass.py	Fri Aug 24 13:25:05 2018 +0300
+++ b/xmlclass/xmlclass.py	Fri Aug 24 13:41:43 2018 +0300
@@ -618,8 +618,9 @@
         element_name = factory.etreeNamespaceFormat % infos["name"]
         if infos["elmt_type"]["type"] == SIMPLETYPE:
             def initial_value():
-                value = etree.Element(element_name)
+                value = factory.Parser.makeelement(element_name)
                 value.text = (infos["elmt_type"]["generate"](infos["elmt_type"]["initial"]()))
+                value._init_()
                 return value
         else:
             def initial_value():
@@ -776,6 +777,7 @@
         self.SchemaNamespace = None
         self.TargetNamespace = None
         self.etreeNamespaceFormat = "%s"
+        self.Parser = None
 
         self.CurrentCompilations = []
 
@@ -1162,7 +1164,6 @@
             classmembers["get%s" % elmtname] = generateGetMethod(elmtname)
 
         classmembers["_init_"] = generateInitMethod(self, classinfos)
-        classmembers["_tmp_initial_"] = None
         classmembers["StructurePattern"] = GetStructurePattern(classinfos)
         classmembers["getElementAttributes"] = generateGetElementAttributes(self, classinfos)
         classmembers["getElementInfos"] = generateGetElementInfos(self, classinfos)
@@ -1174,9 +1175,8 @@
         class_infos = {
             "type": COMPILEDCOMPLEXTYPE,
             "name": classname,
-            "initial": generateClassCreateFunction(class_definition),
+            "initial": generateClassCreateFunction(self, class_definition),
         }
-
         if self.FileName is not None:
             self.ComputedClasses[self.FileName][classname] = class_definition
         else:
@@ -1269,12 +1269,12 @@
         raise ValueError("XSD structure not yet supported!")
 
 
-def generateClassCreateFunction(class_definition):
+def generateClassCreateFunction(factory, class_definition):
     """
     Method that generate the method for creating a class instance
     """
     def classCreatefunction():
-        return class_definition()
+        return factory.Parser.CreateElementFromClass(class_definition)
     return classCreatefunction
 
 
@@ -1387,7 +1387,7 @@
 
                     for element in reversed(value):
                         if element_infos["elmt_type"]["type"] == SIMPLETYPE:
-                            tmp_element = etree.Element(factory.etreeNamespaceFormat % name)
+                            tmp_element = factory.Parser.makeelement(factory.etreeNamespaceFormat % name)
                             tmp_element.text = element_infos["elmt_type"]["generate"](element)
                             element = tmp_element
                         self.insert(insertion_point, element)
@@ -1582,10 +1582,6 @@
             if element["type"] != CHOICE:
                 initial = GetElementInitialValue(factory, element)
                 if initial is not None:
-                    # FIXME: this is looks like dirty hack to fix strange problem with initial[0]
-                    # changing its type after returning from _init_ method to lxml.etree._Element
-                    # As a result all methods generated by class factory are lost.
-                    object.__setattr__(self, "_tmp_initial_", initial)
                     map(self.append, initial)
     return initMethod
 
@@ -1747,6 +1743,8 @@
     def __init__(self, classes, *args, **kwargs):
         etree.PythonElementClassLookup.__init__(self, *args, **kwargs)
         self.LookUpClasses = classes
+        self.ElementTag = None
+        self.ElementClass = None
 
     def GetElementClass(self, element_tag, parent_tag=None, default=DefaultElementClass):
         element_class = self.LookUpClasses.get(element_tag, (default, None))
@@ -1760,7 +1758,56 @@
             return self.GetElementClass(element_with_parent_class, default=default)
         return element_with_parent_class
 
+    def SetLookupResult(self, element, element_class):
+        """
+        Set lookup result for the next 'lookup' callback made by lxml backend.
+        Lookup result is used only if element matches with tag's name submited to 'lookup'.
+        This is done, because there is no way to submit extra search parameters for
+        etree.PythonElementClassLookup.lookup() from etree.XMLParser.makeelement()
+        It's valid only for a signle 'lookup' call.
+
+        :param element:
+            element's tag name
+        :param element_class:
+            element class that should be returned on
+            match in the next 'lookup' call.
+        :return:
+            Nothing
+        """
+        self.ElementTag = element
+        self.ElementClass = element_class
+
+    def ResetLookupResult(self):
+        """Reset lookup result, so it don't influence next lookups"""
+        self.ElementTag = None
+        self.ElementClass = None
+
+    def GetLookupResult(self, element):
+        """Returns previously set SetLookupResult() lookup result"""
+        element_class = None
+        if self.ElementTag is not None and self.ElementTag == element.tag:
+            element_class = self.ElementClass
+        self.ResetLookupResult()
+        return element_class
+
     def lookup(self, document, element):
+        """
+        Lookup for element class for given element tag.
+        If return None from this method, the fallback is called.
+
+        :param document:
+            opaque document instance that contains the Element
+        :param element:
+            lightweight Element proxy implementation that is only valid during the lookup.
+            Do not try to keep a reference to it.
+            Once the lookup is done, the proxy will be invalid.
+        :return:
+            Returns element class corresponding to given element.
+        """
+        element_class = self.GetLookupResult(element)
+        if element_class is not None:
+            return element_class
+
         parent = element.getparent()
         element_class = self.GetElementClass(
             element.tag, parent.tag if parent is not None else None)
@@ -1778,9 +1825,10 @@
 
 
 class XMLClassParser(etree.XMLParser):
-
-    def __init__(self, namespaces, default_namespace_format, base_class, xsd_schema, *args, **kwargs):
+    def __init__(self, *args, **kwargs):
         etree.XMLParser.__init__(self, *args, **kwargs)
+
+    def initMembers(self, namespaces, default_namespace_format, base_class, xsd_schema):
         self.DefaultNamespaceFormat = default_namespace_format
         self.NSMAP = namespaces
         targetNamespace = etree.QName(default_namespace_format % "d").namespace
@@ -1827,14 +1875,53 @@
             None)
 
     def CreateElement(self, element_tag, parent_tag=None, class_idx=None):
+        """
+        Create XML element based on elements and parent's tag names.
+
+        :param element_tag:
+            element's tag name
+        :param parent_tag:
+            optional parent's tag name. Default value is None.
+        :param class_idx:
+            optional index of class in list of founded classes
+            with same element and parent. Default value is None.
+        :return:
+            created XML element
+            (subclass of lxml.etree._Element created by class factory)
+        """
         element_class = self.GetElementClass(element_tag, parent_tag)
         if isinstance(element_class, ListType):
             if class_idx is not None and class_idx < len(element_class):
-                new_element = element_class[class_idx]()
+                element_class = element_class[class_idx]
             else:
                 raise ValueError("No corresponding class found!")
-        else:
-            new_element = element_class()
+        return self.CreateElementFromClass(element_class, element_tag)
+
+    def CreateElementFromClass(self, element_class, element_tag=None):
+        """
+        Create XML element instance of submitted element's class.
+        Submitted class should be subclass of lxml.etree._Element.
+
+        element_class shouldn't be used to create XML element
+        directly using element_class(), because lxml backend
+        should be aware what class handles what xml element,
+        otherwise default lxml.etree._Element will be used.
+
+        :param element_class:
+            element class
+        :param element_tag:
+            optional element's tag name.
+            If omitted it's calculated from element_class instance.
+        :return:
+            created XML element
+            (subclass of lxml.etree._Element created by class factory)
+        """
+        if element_tag is None:
+            element_tag = element_class().tag
+        etag = self.DefaultNamespaceFormat % element_tag
+        self.ClassLookup.SetLookupResult(etag, element_class)
+        new_element = self.makeelement(etag)
+        self.ClassLookup.ResetLookupResult()
         DefaultElementClass.__setattr__(new_element, "tag", self.DefaultNamespaceFormat % element_tag)
         new_element._init_()
         return new_element
@@ -1845,18 +1932,20 @@
     This function generate a xml parser from a class factory
     """
 
+    parser = XMLClassParser(strip_cdata=False, remove_blank_text=True)
+    factory.Parser = parser
+
     ComputedClasses = factory.CreateClasses()
-
     if factory.FileName is not None:
         ComputedClasses = ComputedClasses[factory.FileName]
     BaseClass = [(name, XSDclass) for name, XSDclass in ComputedClasses.items() if XSDclass.IsBaseClass]
 
-    parser = XMLClassParser(
+    parser.initMembers(
         factory.NSMAP,
         factory.etreeNamespaceFormat,
         BaseClass[0] if len(BaseClass) == 1 else None,
-        etree.XMLSchema(etree.fromstring(xsdstring)),
-        strip_cdata=False, remove_blank_text=True)
+        etree.XMLSchema(etree.fromstring(xsdstring)))
+
     class_lookup = XMLElementClassLookUp(factory.ComputedClassesLookUp)
     parser.set_element_class_lookup(class_lookup)