Old new pywinauto: Python Windows GUI automation install / uninstall example

    image
    One day, during the search tool to automate GUI testing, I came across an interesting Python package pywinauto . And although it supports only native controls and partially Windows Forms, it is quite suitable for our tasks.
    The history of pywinauto dates back to around 1998, when Mark McMahon wrote a utility in C for his GUI Automation needs (it took two years), and then, in 2005, he rewrote it in Python in three months . The power of python showed itself in all its glory: the pywinauto interface turned out to be simple and expressive. The tool was actively developed from 2006 to 2010. During the calm years, in 2011-2012, the kind person moden-py wrote a GUI helper to view the hierarchy of windows and generate pywinauto code called SWAPY( binaries are here ).
    Meanwhile, the world was changing. Our team switched to 64-bit binaries, and the pywinauto clone earned on 64-bit Python. In the main branch, the project did not develop for four years and is out of date. In 2015, with the consent of Mark, he managed to breathe new life into the project. Now pywinauto officially lives on a github , and largely thanks to airelil comrade, unit tests run on the AppVeyor CI server .

    At the moment, we have released 3 new releases of the 0.5.x line (the last - 0.5.2). Major improvements compared to 0.4.2:

    • Support for 64-bit applications and x64 python (however, you need 32-bit Python for 32-bit binaries).
    • Python 3 support.
    • Fixed problems with PyPI package.
    • I managed to make pywinauto friends with py2exe and the like.
    • Improved support for a number of controls, especially the toolbar, tree view and list view.
    • You can enable the recording of most actions in the log through pywinauto.actionlogger.enable().
    • A whole series of minor improvements and bug fixes.


    Over the past four years, our team has successfully used pywinauto to test internal software that includes complex graphical custom controls. They have their own wrappers, using the HwndWrapper.SendMessage method and the RemoteMemoryBlock class (also, by the way, improved along the way). But this is a topic for a separate analysis, as I have not seen any open examples of custom controls for pywinauto.

    For now, let's look at some of the features of pywinauto using a specific example.

    Automation example install / uninstall


    Often there is a task to automate the installation / removal of software on 100,500 test machines. We show how this can be done with the example of 7zip (demo example!). It is assumed that the 64-bit installer is pre-downloaded from www.7-zip.org and lies, for example, in the same folder with our scripts. On test machines, User Account Control (UAC) is disabled to level zero (this is usually an isolated subnet, which does not compromise security).

    Installation


    This is the installation script install_7zip.py (the link is the updated version):

    from __future__ import print_function # for py2/py3 compaibility
    import sys, os
    # assume the installer is placed in the same folder as the script
    os.chdir(os.path.join(os.getcwd(), os.path.dirname(sys.argv[0])))
    import pywinauto
    app = pywinauto.Application().Start(r'msiexec.exe /i 7z920-x64.msi')
    Wizard = app['7-Zip 9.20 (x64 edition) Setup']
    Wizard.NextButton.Click()
    Wizard['I &accept the terms in the License Agreement'].Wait('enabled').CheckByClick()
    Wizard.NextButton.Click()
    Wizard['Custom Setup'].Wait('enabled')
    Wizard.NextButton.Click()
    Wizard.Install.Click()
    Wizard.Finish.Wait('enabled', timeout=30)
    Wizard.Finish.Click()
    Wizard.WaitNot('visible')
    if os.path.exists(r"C:\Program Files\7-Zip\7zFM.exe"):
        print('OK')
    else:
        print('FAIL')
    


    With the installation, everything is quite simple, but there are a couple of non-obvious points. To enable the check box to agree with the license, we use the CheckByClick () method, which appeared in pywinauto 0.5.0, because many check boxes do not process the WM_CHECK message, but only respond to a real click.

    A long wait for the installation process itself is provided by the Wait () method with an explicit parameter timeout = 30 (in seconds). That is, the object itself Wizard.Finishis just a description of the button, and it is not connected with the real button until the Wait () method or any other method is called. Strictly speaking, a call is Wizard.Finish.Click()equivalent to a longer call Wizard.Finish.WrapperObject().Click()(usually it occurs implicitly) and is almost equivalent Wizard.Finish.Wait('enabled').Click(). You could write in one line, but sometimes it is worth emphasizing the importance of the Wait () method.

    Delete


    Script to remove uninstall_7zip.py is somewhat more complicated, because you have to go into the control panel, in the "Add or Remove Programs" section. If desired, using explorer.exe, you can automate other tasks.

    from __future__ import print_function
    import pywinauto
    pywinauto.Application().Start(r'explorer.exe')
    explorer = pywinauto.Application().Connect(path='explorer.exe')
    # Go to "Control Panel -> Programs and Features"
    NewWindow = explorer.Window_(top_level_only=True, active_only=True, class_name='CabinetWClass')
    try:
        NewWindow.AddressBandRoot.ClickInput()
        NewWindow.TypeKeys(r'Control Panel\Programs\Programs and Features{ENTER}', with_spaces=True, set_foreground=False)
        ProgramsAndFeatures = explorer.Window_(top_level_only=True, active_only=True, title='Programs and Features', class_name='CabinetWClass')
        # wait while list of programs is loading
        explorer.WaitCPUUsageLower(threshold=5)
        item_7z = ProgramsAndFeatures.FolderView.GetItem('7-Zip 9.20 (x64 edition)')
        item_7z.EnsureVisible()
        item_7z.ClickInput(button='right', where='icon')
        explorer.PopupMenu.MenuItem('Uninstall').Click()
        Confirmation = explorer.Window_(title='Programs and Features', class_name='#32770', active_only=True)
        if Confirmation.Exists():
            Confirmation.Yes.ClickInput()
            Confirmation.WaitNot('visible')
        WindowsInstaller = explorer.Window_(title='Windows Installer', class_name='#32770', active_only=True)
        if WindowsInstaller.Exists():
            WindowsInstaller.WaitNot('visible', timeout=20)
        SevenZipInstaller = explorer.Window_(title='7-Zip 9.20 (x64 edition)', class_name='#32770', active_only=True)
        if SevenZipInstaller.Exists():
            SevenZipInstaller.WaitNot('visible', timeout=20)
        if '7-Zip 9.20 (x64 edition)' not in ProgramsAndFeatures.FolderView.Texts():
            print('OK')
    finally:
        NewWindow.Close()
    


    There are a few key points here.

    When you start explorer.exe, a short-term startup process (launcher) is created that checks that explorer.exe (worker) is already running. Such a bunch of “launcher-> worker” is sometimes found. Therefore, we separately connect to the explorer.exe workflow using the connect () method.

    After clicking on the address bar ( AddressBandRoot), the so-called in-place edit box appears (only for the time of input). When calling the method, we TypeKeys()must specify the parameter set_foreground=False(appeared in 0.5.0), otherwise the in-place edit box will disappear from the radar. For all in-place controls, it is recommended to set this parameter to False.

    Further, the list of programs is initialized for a long time, however, the ListView control itself is available and a simple callProgramsAndFeatures.FolderView.Wait('enabled')does not guarantee that it is already fully initialized. Lazy (lazy) initialization is in a separate thread, so you need to monitor the CPU activity of the entire explorer.exe process. For this, pywinauto 0.5.2 implements two methods: CPUUsage()returning the CPU load in percent, and WaitCPUUsageLower()waiting until the CPU load drops below the threshold (2.5% by default). The idea of ​​implementing these methods suggested the article comrade JOHN_16: "We monitor the completion of the process of loading the CPU . "

    By the way, the call item_7z.EnsureVisible()magically scrolls the list so that the desired item becomes visible. No special work with the scroll bar is needed.

    Several calls Wait and WaitNot mean that you need to wait for the opening or closing of a certain window for a relatively long time (longer than the default). However, some WaitNot calls are inserted just for control. This is a good practice.

    “After all, life, it is both simpler and more complicated ...”


    Of course, this was just an example. In the case of 7zip, everything is solved much easier. We start cmd.exe as Administrator and we execute a simple line (works at any level of UAC):
        wmic product where name="7-Zip 9.20 (x64 edition)" call uninstall
    

    Of course, the zoo of installers is not limited to .msi packages, and the range of automation tasks is very wide.

    What is most often asked


    If before the main question was about Python 3 and 64 bits, now support for WPF and a number of other non-native applications that support the UI Automation API are on the agenda. There is progress in this direction. Any help in adapting the various back-ends to the pywinauto interface is welcome.

    Also popular now: