# HG changeset patch # User Andrey Skvortsov # Date 1535107303 -10800 # Node ID 96ca6b056c55595f71bfaca9f54b9e8646460c23 # Parent a2ab363f9e90a7481d3f57be60db1b49710e86b2 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(): :_'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 diff -r a2ab363f9e90 -r 96ca6b056c55 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)