pyWinAuto: c:\.projects\py_pywinauto\pywinauto\XMLHelpers.py

0001# GUI Application automation and testing library
0002# Copyright (C) 2006 Mark Mc Mahon
0003#
0004# This library is free software; you can redistribute it and/or
0005# modify it under the terms of the GNU Lesser General Public License
0006# as published by the Free Software Foundation; either version 2.1
0007# of the License, or (at your option) any later version.
0008#
0009# This library is distributed in the hope that it will be useful,
0010# but WITHOUT ANY WARRANTY; without even the implied warranty of
0011# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
0012# See the GNU Lesser General Public License for more details.
0013#
0014# You should have received a copy of the GNU Lesser General Public
0015# License along with this library; if not, write to the
0016#    Free Software Foundation, Inc.,
0017#    59 Temple Place,
0018#    Suite 330,
0019#    Boston, MA 02111-1307 USA
0020
0021"""Module containing operations for reading and writing dialogs as XML
0022"""
0023
0024__revision__ = "$Revision: 606 $"
0025
0026
0027# how should we read in the XML file
0028# NOT USING MS Components (requirement on machine)
0029# maybe using built in XML
0030# maybe using elementtree
0031# others?
0032
0033#import elementtree
0034try:
0035    # Python 2.5 (thanks to Daisuke Yamashita)
0036    from xml.etree.ElementTree import Element, SubElement, ElementTree
0037    from xml.etree.cElementTree import Element, SubElement, ElementTree
0038except ImportError:
0039    from elementtree.ElementTree import Element, SubElement, ElementTree
0040    from cElementTree import Element, SubElement, ElementTree
0041
0042import ctypes
0043import re
0044import PIL.Image
0045import controls
0046
0047# reported that they are not used - but in fact they are
0048# through a search of globals()
0049from win32structures import LOGFONTW, RECT
0050
0051class XMLParsingError(RuntimeError):
0052    "Wrap parsing Exceptions"
0053    pass
0054
0055
0056
0057#DONE: Make the dialog reading function not actually know about the
0058# types of each element (so that we can read the control properties
0059# without having to know each and every element type)
0060# probably need to store info on what type things are.
0061#
0062# - if it is a ctypes struct then there is a __type__ field
0063#   which says what kind of stuct it is
0064# - If it is an image then a "_IMG" is appeded to the the element tag
0065# - if it is a long then _LONG is appended to attribute name
0066# everything else is considered a string!
0067
0068
0069#-----------------------------------------------------------------------------
0070def _SetNodeProps(element, name, value):
0071    "Set the properties of the node based on the type of object"
0072
0073    # if it is a ctypes structure
0074    if isinstance(value, ctypes.Structure):
0075
0076        # create an element for the structure
0077        struct_elem = SubElement(element, name)
0078        #clsModule = value.__class__.__module__
0079        cls_name = value.__class__.__name__
0080        struct_elem.set("__type__", "%s" % cls_name)
0081
0082        # iterate over the fields in the structure
0083        for prop_name in value._fields_:
0084            prop_name = prop_name[0]
0085            item_val = getattr(value, prop_name)
0086
0087            if isinstance(item_val, (int, long)):
0088                prop_name += "_LONG"
0089                item_val = unicode(item_val)
0090
0091            struct_elem.set(prop_name, _EscapeSpecials(item_val))
0092
0093    elif isinstance(value, PIL.Image.Image):
0094        try:
0095            # if the image is too big then don't try to
0096            # write it out - it would probably product a MemoryError
0097            # anyway
0098            if value.size[0] * value.size[1] > (5000*5000):
0099                raise MemoryError
0100
0101            image_data = value.tostring().encode("bz2").encode("base64")
0102            _SetNodeProps(
0103                element,
0104                name + "_IMG",
0105                {
0106                    "mode": value.mode,
0107                    "size_x":value.size[0],
0108                    "size_y":value.size[1],
0109                    "data":image_data
0110                })
0111
0112        # a system error is raised from time to time when we try to grab
0113        # the image of a control that has 0 height or width
0114        except (SystemError, MemoryError):
0115            pass
0116
0117
0118    elif isinstance(value, (list, tuple)):
0119        # add the element to hold the values
0120        # we do this to be able to support empty lists
0121        listelem = SubElement(element, name + "_LIST")
0122
0123        for i, attrval in enumerate(value):
0124            _SetNodeProps(listelem, "%s_%05d"%(name, i), attrval)
0125
0126    elif isinstance(value, dict):
0127        dict_elem = SubElement(element, name)
0128
0129        for item_name, val in value.items():
0130            _SetNodeProps(dict_elem, item_name, val)
0131
0132    else:
0133        if isinstance(value, bool):
0134            value = long(value)
0135
0136        if isinstance(value, (int, long)):
0137            name += "_LONG"
0138
0139        element.set(name, _EscapeSpecials(value))
0140
0141
0142#-----------------------------------------------------------------------------
0143def WriteDialogToFile(filename, props):
0144    """Write the props to the file
0145
0146    props can be either a dialog of a dictionary
0147    """
0148    # if we are passed in a wrapped handle then
0149    # get the properties
0150    try:
0151        props[0].keys()
0152    except (TypeError, AttributeError):
0153        props = controls.GetDialogPropsFromHandle(props)
0154
0155    # build a tree structure
0156    root = Element("DIALOG")
0157    root.set("_version_", "2.0")
0158    for ctrl in props:
0159        ctrlelem = SubElement(root, "CONTROL")
0160        for name, value in sorted(ctrl.items()):
0161            _SetNodeProps(ctrlelem, name, value)
0162
0163    # wrap it in an ElementTree instance, and save as XML
0164    tree = ElementTree(root)
0165    tree.write(filename, encoding="utf-8")
0166
0167
0168
0169#-----------------------------------------------------------------------------
0170def _EscapeSpecials(string):
0171    "Ensure that some characters are escaped before writing to XML"
0172
0173    # ensure it is unicode
0174    string = unicode(string)
0175
0176    # escape backslashs
0177    string = string.replace('\\', r'\\')
0178
0179    # escape non printable characters (chars below 30)
0180    for i in range(0, 32):
0181        string = string.replace(unichr(i), "\\%02d"%i)
0182
0183    return string
0184
0185
0186#-----------------------------------------------------------------------------
0187def _UnEscapeSpecials(string):
0188    "Replace escaped characters with real character"
0189
0190    # Unescape all the escape characters
0191    for i in range(0, 32):
0192        string = string.replace("\\%02d"%i, unichr(i))
0193
0194    # convert doubled backslashes to a single backslash
0195    string = string.replace(r'\\', '\\')
0196
0197    return unicode(string)
0198
0199
0200
0201#-----------------------------------------------------------------------------
0202def _XMLToStruct(element, struct_type = None):
0203    """Convert an ElementTree to a ctypes Struct
0204
0205    If struct_type is not specified then element['__type__']
0206    will be used for the ctypes struct type"""
0207
0208
0209    # handle if we are passed in an element or a dictionary
0210    try:
0211        attribs = element.attrib
0212    except AttributeError:
0213        attribs = element
0214
0215    # if the type has not been passed in
0216    if not struct_type:
0217        # get the type and create an instance of the type
0218        struct = globals()[attribs["__type__"]]()
0219    else:
0220        # create an instance of the type
0221        struct = globals()[struct_type]()
0222
0223    # get the attribute and set them upper case
0224    struct_attribs = dict([(at.upper(), at) for at in dir(struct)])
0225
0226    # for each of the attributes in the element
0227    for prop_name in attribs:
0228
0229        # get the value
0230        val = attribs[prop_name]
0231
0232        # if the value ends with "_long"
0233        if prop_name.endswith("_LONG"):
0234            # get an long attribute out of the value
0235            val = long(val)
0236            prop_name = prop_name[:-5]
0237
0238        # if the value is a string
0239        elif isinstance(val, basestring):
0240            # make sure it if Unicode
0241            val = unicode(val)
0242
0243        # now we can have all upper case attribute name
0244        # but structure name will not be upper case
0245        if prop_name.upper() in struct_attribs:
0246            prop_name = struct_attribs[prop_name.upper()]
0247
0248            # set the appropriate attribute of the Struct
0249            setattr(struct, prop_name, val)
0250
0251    # reutrn the struct
0252    return struct
0253
0254
0255
0256#====================================================================
0257def _OLD_XMLToTitles(element):
0258    "For OLD XML files convert the titles as a list"
0259    # get all the attribute names
0260    title_names = element.keys()
0261
0262    # sort them to make sure we get them in the right order
0263    title_names.sort()
0264
0265    # build up the array
0266    titles = []
0267    for name in title_names:
0268        val = element[name]
0269        val = val.replace('\\n', '\n')
0270        val = val.replace('\\x12', '\x12')
0271        val = val.replace('\\\\', '\\')
0272
0273        titles.append(unicode(val))
0274
0275    return titles
0276
0277
0278#====================================================================
0279# TODO: this function should be broken up into smaller functions
0280#       for each type of processing e.g.
0281#       ElementTo
0282def _ExtractProperties(properties, prop_name, prop_value):
0283    """Hmmm - confusing - can't remember exactly how
0284    all these similar functions call each other"""
0285
0286    # get the base property name and number if it in the form
0287    #  "PROPNAME_00001" = ('PROPNAME', 1)
0288    prop_name, reqd_index = _SplitNumber(prop_name)
0289
0290    # if there is no required index, and the property
0291    # was not already set - then just set it
0292
0293    # if this is an indexed member of a list
0294    if reqd_index == None:
0295        # Have we hit a property with this name already
0296        if prop_name in properties:
0297            # try to append current value to the property
0298            try:
0299                properties[prop_name].append(prop_value)
0300
0301            # if that fails then we need to make sure that
0302            # the curruen property is a list and then
0303            # append it
0304            except AttributeError:
0305                new_val = [properties[prop_name], prop_value]
0306                properties[prop_name] = new_val
0307        # No index, no previous property with that name
0308        #  - just set the property
0309        else:
0310            properties[prop_name] = prop_value
0311
0312    # OK - so it HAS an index
0313    else:
0314
0315        # make sure that the property is a list
0316        properties.setdefault(prop_name, [])
0317
0318        # make sure that the list has enough elements
0319        while 1:
0320            if len(properties[prop_name]) <= reqd_index:
0321                properties[prop_name].append('')
0322            else:
0323                break
0324
0325        # put our value in at the right index
0326        properties[prop_name][reqd_index] = prop_value
0327
0328
0329#====================================================================
0330def _GetAttributes(element):
0331    "Get the attributes from an element"
0332
0333    properties = {}
0334
0335    # get all the attributes
0336    for attrib_name, val in element.attrib.items():
0337
0338        # if it is 'Long' element convert it to an long
0339        if attrib_name.endswith("_LONG"):
0340            val = long(val)
0341            attrib_name = attrib_name[:-5]
0342
0343        else:
0344            # otherwise it is a string - make sure we get it as a unicode string
0345            val = _UnEscapeSpecials(val)
0346
0347        _ExtractProperties(properties, attrib_name, val)
0348
0349    return properties
0350
0351
0352#====================================================================
0353number = re.compile(r"^(.*)_(\d{5})$")
0354def _SplitNumber(prop_name):
0355    """Return (string, number) for a prop_name in the format string_number
0356
0357    The number part has to be 5 digits long
0358    None is returned if there is no _number part
0359
0360    e.g.
0361    >>> _SplitNumber("NoNumber")
0362    ('NoNumber', None)
0363    >>> _SplitNumber("Anumber_00003")
0364    ('Anumber', 3)
0365    >>> _SplitNumber("notEnoughDigits_0003")
0366    ('notEnoughDigits_0003', None)
0367    """
0368    found = number.search(prop_name)
0369
0370    if not found:
0371        return prop_name, None
0372
0373    return found.group(1), int(found.group(2))
0374
0375
0376
0377#====================================================================
0378def _ReadXMLStructure(control_element):
0379    """Convert an element into nested Python objects
0380
0381    The values will be returned in a dictionary as following:
0382
0383     - the attributes will be items of the dictionary
0384       for each subelement
0385
0386       + if it has a __type__ attribute then it is converted to a
0387         ctypes structure
0388       + if the element tag ends with _IMG then it is converted to
0389         a PIL image
0390
0391     - If there are elements with the same name or attributes with
0392       ordering e.g. texts_00001, texts_00002 they will be put into a
0393       list (in the correct order)
0394    """
0395
0396    # get the attributes for the current element
0397    properties = _GetAttributes(control_element)
0398
0399    for elem in control_element:
0400        # if it is a ctypes structure
0401        if "__type__" in elem.attrib:
0402            # create a new instance of the correct type
0403
0404            # grab the data
0405            propval = _XMLToStruct(elem)
0406
0407        elif elem.tag.endswith("_IMG"):
0408            elem.tag = elem.tag[:-4]
0409
0410            # get image Attribs
0411            img = _GetAttributes(elem)
0412            data = img['data'].decode('base64').decode('bz2')
0413
0414            propval = PIL.Image.fromstring(
0415                img['mode'],
0416                (img['size_x'], img['size_y']),
0417                data)
0418
0419        elif elem.tag.endswith("_LIST"):
0420            # All this is just to handle the edge case of
0421            # an empty list
0422            elem.tag = elem.tag[:-5]
0423
0424            # read the structure
0425            propval = _ReadXMLStructure(elem)
0426
0427            # if it was empty then convert the returned dict
0428            # to a list
0429            if propval == {}:
0430                propval = list()
0431
0432            # otherwise extract the list out of the returned dict
0433            else:
0434                propval = propval[elem.tag]
0435
0436        else:
0437            propval = _ReadXMLStructure(elem)
0438
0439        _ExtractProperties(properties, elem.tag, propval)
0440
0441    return properties
0442
0443
0444
0445
0446#====================================================================
0447def ReadPropertiesFromFile(filename):
0448    """Return an list of controls from XML file filename"""
0449
0450    # parse the file
0451    parsed = ElementTree().parse(filename)
0452
0453    # Return the list that has been stored under 'CONTROL'
0454    props =  _ReadXMLStructure(parsed)['CONTROL']
0455    if not isinstance(props, list):
0456        props = [props]
0457
0458
0459    # it is an old XML so let's fix it up a little
0460    if not parsed.attrib.has_key("_version_"):
0461
0462        # find each of the control elements
0463        for ctrl_prop in props:
0464
0465            ctrl_prop['Fonts'] = [_XMLToStruct(ctrl_prop['FONT'], "LOGFONTW"), ]
0466
0467            ctrl_prop['Rectangle'] =                   _XMLToStruct(ctrl_prop["RECTANGLE"], "RECT")
0469
0470            ctrl_prop['ClientRects'] = [
0471                _XMLToStruct(ctrl_prop["CLIENTRECT"], "RECT"),]
0472
0473            ctrl_prop['Texts'] = _OLD_XMLToTitles(ctrl_prop["TITLES"])
0474
0475            ctrl_prop['Class'] = ctrl_prop['CLASS']
0476            ctrl_prop['ContextHelpID'] = ctrl_prop['HELPID']
0477            ctrl_prop['ControlID'] = ctrl_prop['CTRLID']
0478            ctrl_prop['ExStyle'] = ctrl_prop['EXSTYLE']
0479            ctrl_prop['FriendlyClassName'] = ctrl_prop['FRIENDLYCLASS']
0480            ctrl_prop['IsUnicode'] = ctrl_prop['ISUNICODE']
0481            ctrl_prop['IsVisible'] = ctrl_prop['ISVISIBLE']
0482            ctrl_prop['Style'] = ctrl_prop['STYLE']
0483            ctrl_prop['UserData'] = ctrl_prop['USERDATA']
0484
0485            for prop_name in [
0486                'CLASS',
0487                'CLIENTRECT',
0488                'CTRLID',
0489                'EXSTYLE',
0490                'FONT',
0491                'FRIENDLYCLASS',
0492                'HELPID',
0493                'ISUNICODE',
0494                'ISVISIBLE',
0495                'RECTANGLE',
0496                'STYLE',
0497                'TITLES',
0498                'USERDATA',
0499                ]:
0500                del(ctrl_prop[prop_name])
0501
0502    return props