Python-bibliotek — är ett open source-projekt för att automatisera desktop GUI-applikationer på Windows. Under de senaste två åren har det fått nya stora funktioner:
- Stöd för MS UI Automation-teknologi. Gränssnittet är fortfarande detsamma, och nu stöds: WinForms, WPF, Qt5, Windows Store (UWP) och mer — nästan allt som finns på Windows.
- Backend/plugin-system (just nu finns det två under huven: standard
"win32"och den nya"uia"). Vi rör oss framåt mot plattformsoberoende. - Win32-hooks för mus och tangentbord (hotkeys i stil med pyHook).
Vi kommer också att ge en kort översikt över vad som finns inom open source för desktopautomatisering (utan anspråk på seriös jämförelse).
Denna artikel är delvis en transkription av en presentation från konferensen SQA Days 20 i Minsk ( och ), delvis en svensk version för pywinauto.
- Ämnen
- Huvudprinciperna för desktop-tillgänglighetsteknologier
Vi börjar med en kort översikt över öppen källkod inom detta område. För desktop GUI-applikationer är det något mer komplicerat än för webben, som har Selenium. Här är de viktigaste metoderna:
Koordinatmetoden
Hårdprogrammering av klickpunkter, vi hoppas på tur.
[+] Plattformsoberoende, lätt att implementera.
[+] Lätt att göra "record-replay" testinspelningar.
[-] Mest instabilt vid ändringar av skärmupplösning, teman, typsnitt, fönsterstorlekar etc.
[-] Kräver stora insatser för underhåll, ofta lättare att regenerera tester från början eller testa manuellt.
[-] Automatiserar endast åtgärder, för verifiering och datainsamling finns andra metoder.
Verktyg (plattformsoberoende): , , och många fler. Som regel inkluderar mer avancerade verktyg denna funktionalitet (inte alltid plattformsoberoende).
Det är värt att nämna att koordinatmetoden kan komplettera andra tillvägagångssätt. Till exempel, för anpassad grafik kan man klicka på relativa koordinater (från det övre vänstra hörnet av fönstret/elementet, inte hela skärmen) — detta är vanligtvis tillförlitligt, särskilt med hänsyn till längden/bredden av hela elementet (då kan olika skärmupplösningar inte störa).
Ett annat alternativ: att endast välja en maskin för tester med stabila inställningar (inte plattformsoberoende, men i vissa fall passande).
Identifiering av referensbilder
[+] Plattformsoberoende
[+-] Relativt tillförlitlig (bättre än koordinatmetoden), men kräver fortfarande viss finess.
[-+] Relativt långsam, eftersom den kräver CPU-resurser för igenkänningsalgoritmer.
[-] Om textigenkänning (OCR) talas det vanligtvis inte om => kan inte hämta textdata. Såvitt jag vet är befintliga OCR-lösningar inte särskilt tillförlitliga för denna typ av uppgifter och har inte omfattande användning (välkommen i kommentarerna om detta har förändrats).
Verktyg: , (Sikuli-kompatibel, i ren Python), .
Tillgänglighetsteknologier
[+] Den mest pålitliga metoden, eftersom den tillåter att söka genom text, oavsett hur den ritas av systemet eller ramverket.
[+] Gör det möjligt att extrahera textdata => lättare att verifiera testresultat.
[+] Vanligtvis den snabbaste, eftersom den nästan inte använder CPU-resurser.
[-] Det är svårt att skapa ett plattformsoberoende verktyg: absolut alla open-source-bibliotek stöder en eller två tillgänglighetstekniker. Ingen stöder Windows/Linux/MacOS helt, förutom betalprogram såsom TestComplete, UFT eller Squish.
[-] Denna teknik är inte alltid tillgänglig. Till exempel, testning av startskärmen inom VirtualBox — här kan man inte undvika bildigenkänning. Men i många klassiska fall är tillgänglighetsmetoden ändå tillämplig. Om detta kommer vi att diskutera vidare.
Verktyg: i C#, i C# (Selenium-kompatibel), i C# (Appium-kompatibel), , (kompatibel med LDTP), , i Ruby, (Linux Desktop Testing Project) och dess Windows-version .
LDTP - är nog den enda plattformsoberoende open-source-verktyget (närmare bestämt en familj av bibliotek) baserat på tillgänglighetstekniker. Men det är inte särskilt populärt. Jag har inte använt det själv, men enligt recensioner är det inte det mest användarvänliga gränssnittet. Om det finns positiva recensioner, tveka inte att dela dem i kommentarerna.
Testbackdoor (även känd som internt cykel)
För plattformsöverskridande applikationer skapar utvecklarna ofta en intern mekanism för att säkerställa testbarhet. Till exempel, de skapar en tjänstefunktion som en TCP-server i applikationen, som tester ansluter till och skickar textkommandon: vad de ska klicka på, var de ska hämta data osv. Pålitligt, men inte universellt.
Huvudprinciperna för desktop-tillgänglighetsteknologier
Den gamla goda Win32 API
De flesta Windows-applikationer skrivna före WPF och sedan Windows Store är på ett eller annat sätt byggda på Win32 API. Det vill säga, MFC, WTL, C++ Builder, Delphi, VB6 — alla dessa verktyg använder Win32 API. Även Windows Forms är till stor del kompatibla med Win32 API.
Verktyg: (ser ut som VB) och Python-wrap , (eget språk, har IDispatch COM-gränssnitt), (Python), (Ruby), (Ruby).
Microsoft UI Automation
Den största fördelen: teknologin MS UI Automation stöder nästan alla GUI-applikationer på Windows med några få undantag. Problemet: det är inte mycket lättare att lära sig än Win32 API. Annars skulle ingen skapa wraps över den.
Faktiskt är detta en uppsättning anpassade COM-gränssnitt (i huvudsak, UIAutomationCore.dll), och har dessutom en .NET-wrapper i form av namespace System.Windows.Automation. Den har för övrigt ett introducerat bug, vilket gör att vissa UI-element kan missas. Därför är det bättre att använda UIAutomationCore.dll direkt (om du har hört talas om UiaComWrapper på C#, så är det den).
Varianter av COM-gränssnitt:
(1) Grundläggande IUknown — "the root of all evil". Det mest lågnivå, inte vänligt för användare.
(2) IDispatch och härledda (till exempel, Excel.Application), som kan användas i Python med hjälp av paketet win32com.client (som ingår i pyWin32). Den mest bekväma och snygga varianten.
(3) Anpassade gränssnitt som kan hanteras av tredje parts Python-paket .
Verktyg: i C#, 0.6.0+, i C#, (de har inte avslöjat källkoden för de C-baserade wraps över UIAutomationCore.dll), i Ruby.
AT-SPI
Trots att nästan alla distros i Linux-familjen är baserade på X Window System (i Fedora 25 har "X" bytts ut mot Wayland), tillåter "X" endast att hantera fönster i översta nivån och mus/tangentbord. För detaljerad analys av knappar, listboxar och så vidare — finns det teknologin AT-SPI. De mest populära fönsterhanterarna har så kallade AT-SPI registry-demoner som tillhandahåller automatiserbar GUI för applikationer (minst Qt och GTK stöds).
Verktyg: .
pyatspi2, enligt min mening, innehåller för många beroenden typ PyGObject. Själva teknologin är tillgänglig i form av ett vanligt dynamiskt bibliotek libatspi.so. Det finns tillgängligt . För biblioteket pywinauto planerar vi att implementera stöd för AT-SPI på detta sätt: genom att ladda libatspi.so och modulen ctypes. Det finns ett litet problem med att använda rätt version, eftersom de är något olika för GTK+ och Qt-applikationer. En trolig utgivning av pywinauto 0.7.0 med fullständigt stöd för Linux kan förväntas under första halvan av 2018.
Apple Accessibility API
I MacOS finns ett eget automatiseringsspråk, AppleScript. För att implementera något liknande på Python måste man självklart använda funktioner från ObjectiveC. Från och med MacOS 10.6 ingår paketet pyobjc i den förinstallerade Python. Detta underlättar också listan över beroenden för framtida stöd i pywinauto.
Verktyg: Förutom AppleScript-språket bör man också titta på , även känd som pyatom. Den är kompatibel med gränssnittet LDTP, men är också ett fristående bibliotek. Det finns ett , skrivet av min student. Det finns ett välkänt problem: flexibla tidsinställningar fungerar inte (metoderna waitFor*). Men, överlag, en ganska bra sak.
Hur man börjar arbeta med pywinauto
Först och främst bör man beväpna sig med ett GUI-objektsinspektor (det som kallas Spy tool). Det hjälper till att studera applikationen inifrån: hur hierarkin av element är konstruerad, vilka egenskaper som är tillgängliga. De mest kända objektinspektorerna är:
- Spy++ — ingår i Visual Studio, inklusive Express eller Community Edition. Använder Win32 API. Dess klon är också känd som AutoIt Window Info.
- Inspect.exe — ingår i Windows SDK. Om den är installerad hittar du den på 64-bitars Windows i mappen
C:Program Files (x86)Windows Kitsbinx64. I själva inspektorn måste man välja läget UI Automation istället för MS AA (Active Accessibility, föregångaren till UI Automation).
Efter att ha inspekterat applikationen noggrant väljer vi backenden som vi ska använda. Det räcker med att ange namnet på backenden när du skapar objektet Application.
- backend=»win32″ — används fortfarande som standard, fungerar bra med MFC, WTL, VB6 och andra legacy-applikationer.
- backend=»uia» — en ny backend för MS UI Automation: fungerar perfekt med WPF och WinForms; också bra för Delphi- och Windows Store-applikationer; fungerar med Qt5 och vissa Java-applikationer. Generellt, om Inspect.exe ser element och deras egenskaper, betyder det att denna backend passar. I princip stöder de flesta webbläsare också UI Automation (Mozilla som standard, medan Chrome vid start måste få nyckeln för kommandoraden
--force-renderer-accessibility, för att se elementen på sidorna i Inspect.exe). Självfallet är konkurrensen med Selenium på detta område knappast möjlig. Bara ett annat sätt att arbeta med webbläsaren (kan vara användbart för korsproduktionsscenarier).
Ingångspunkter för automatisering
Applikationen har studerats tillräckligt. Det är dags att skapa ett Application-objekt och starta det eller ansluta till ett redan startat. Detta är inte bara en klon av standardklassen subprocess.Popen, utan en introduktionsobjekt som begränsar alla dina åtgärder inom processens gränser. Detta är mycket användbart om flera instanser av applikationen körs och man inte vill påverka de andra.
from pywinauto.application import Application
app = Application(backend="uia").start('notepad.exe')
# Vi beskriver fönstret som vi vill hitta i processen Notepad.exe
dlg_spec = app.UntitledNotepad
# vänta tills fönstret verkligen dyker upp
actionable_dlg = dlg_spec.wait('visible')Om man vill hantera flera applikationer på en gång, hjälper klassen Desktop. Till exempel i kalkylatorn på Win10 är hierarkin av element utspridd över flera processer (inte bara calc.exe). Så utan objektet Desktop går det inte.
from subprocess import Popen
from pywinauto import Desktop
Popen('calc.exe', shell=True)
dlg = Desktop(backend="uia").Calculator
dlg.wait('visible')Rotobjektet (Application eller Desktop) — är den enda platsen där man behöver ange backend. Allt annat faller transparant in i konceptet "specifikation->wrapper", som beskrivs nedan.
Specifikationer för fönster/element
Detta är den grundläggande koncept som pywinauto-gränssnittet bygger på. Du kan beskriva ett fönster/element mer eller mindre detaljerat, även om det ännu inte finns eller redan är stängt. Fönsterspecifikationen (objektet WindowSpecification) lagrar kriterierna för att söka efter det verkliga fönstret eller elementet.
Exempel på detaljerad fönsterspecifikation:
>>> dlg_spec = app.window(title='Untitled - Notepad')
>>> dlg_spec
>>> dlg_spec.wrapper_object()Själva sökningen efter fönstret sker genom att anropa metoden .wrapper_object(). Den returnerar en slags "wrapper" för det verkliga fönstret/elementet eller kastar ElementNotFoundError (ibland ElementAmbiguousError, om flera element har hittats, vilket innebär att kriteriet för sökningen kräver precisering). Denna "wrapper" kan redan utföra vissa åtgärder med elementet eller hämta data från det.
Python kan dölja anropet .wrapper_object(), så den slutliga koden blir kortare. Vi rekommenderar att använda det endast för felsökning. De närmaste två raderna gör exakt samma sak:
dlg_spec.wrapper_object().minimera() # felsökning
dlg_spec.minimera() # produktionDet finns många sökkriterier för fönsterspecifikation. Här är bara några exempel:
# могут иметь несколько уровней
app.window(title_re='.* - Notepad$').window(class_name='Edit')
# можно комбинировать критерии (как AND) и не ограничиваться одним процессом приложения
dlg = Desktop(backend="uia").Calculator
dlg.window(auto_id='num8Button', control_type='Button')Listan över alla möjliga kriterier finns i funktionsdokumentationen .
Magin i attributhantering och nycklar
Python förenklar skapandet av fönsterspecifikationer och känner igen objektattribut dynamiskt (metoden är överskriven __getattribute__). Självklart gäller samma begränsningar för attributnamnet som för namnet på vilken variabel som helst (inga mellanslag, kommatecken eller andra specialtecken). Lyckligtvis använder pywinauto en så kallad "best match"-sökalgoritm som är motståndskraftig mot stavfel och små variationer.
app.OuntitledNotepad
# detsamma som
app.window(best_match='UntitledNotepad')Om Unicode-strängar (t.ex. för ryska språket), mellanslag osv. behövs, kan man få åtkomst via nyckel (som om det vore ett vanligt ordbok):
app['Untitled - Notepad']
# detsamma som
app.window(best_match='Untitled - Notepad')Fem regler för magiska namn
Hur får man reda på referensens magiska namn? De som tilldelas elementet före sökningen. Om du angav ett namn som är tillräckligt likt referensen, kommer elementet att hittas.
- Efter titel (text, namn):
app.Properties.OK.click() - Efter text och typ av element:
app.Properties.OKButton.click() - Efter typ och nummer:
app.Properties.Button3.click()(namnButton0ochButton1är kopplade till det första hittade elementet,Button2— till det andra, och därefter i ordning — så har det historiskt blivit) - Efter statisk text (till vänster eller ovan) och typ:
app.OpenDialog.FileNameEdit.set_text("")(användbart för element med dynamisk text) - Efter typ och text inuti:
app.Properties.TabControlSharing.select("Allmänt")
Ofta tillämpas två till tre regler samtidigt, sällan fler. För att kontrollera vilka specifika namn som är tillgängliga för varje element kan man använda metoden print_control_identifiers(). Den kan skriva ut trädet av element både på skärmen och i en fil. För varje element skrivs dess referensens magiska namn ut. Det går också att kopiera mer detaljerade specifikationer av underliggande element därifrån. Resultatet i skriptet kommer att se ut så här:
app.Properties.child_window(title="Innehåller:", auto_id="13087", control_type="Edit")Själva elementträdet — är vanligtvis ganska stort.
>>> app.Properties.print_control_identifiers()
Kontrollidentifierare:
Dialog - 'Windows NT Egenskaper' (L688, T518, R1065, B1006)
[u'Windows NT EgenskaperDialog', u'Dialog', u'Windows NT Egenskaper']
child_window(title="Windows NT Egenskaper", control_type="Window")
|
| Bild - '' (L717, T589, R749, B622)
| [u'', u'0', u'Bild1', u'Bild0', 'Bild', u'1']
| child_window(auto_id="13057", control_type="Bild")
|
| Bild - '' (L717, T630, R1035, B632)
| ['Bild2', u'2']
| child_window(auto_id="13095", control_type="Bild")
|
| Redigera - 'Mappenamn:' (L790, T596, R1036, B619)
| [u'3', 'Redigera', u'Redigera1', u'Redigera0']
| child_window(title="Mappenamn:", auto_id="13156", control_type="Edit")
|
| Statisk - 'Typ:' (L717, T643, R780, B658)
| [u'Typ:Statisk', u'Statisk', u'Statisk1', u'Statisk0', u'Typ:']
| child_window(title="Typ:", auto_id="13080", control_type="Text")
|
| Redigera - 'Typ:' (L790, T643, R1036, B666)
| [u'4', 'Redigera2', u'Typ:Redigera']
| child_window(title="Typ:", auto_id="13059", control_type="Edit")
|
| Statisk - 'Plats:' (L717, T669, R780, B684)
| [u'Plats:Statisk', u'Plats:', u'Statisk2']
| child_window(title="Plats:", auto_id="13089", control_type="Text")
|
| Redigera - 'Plats:' (L790, T669, R1036, B692)
| ['Redigera3', u'Plats:Redigera', u'5']
| child_window(title="Plats:", auto_id="13065", control_type="Edit")
|
| Statisk - 'Storlek:' (L717, T695, R780, B710)
| [u'Storlek:Statisk', u'Storlek:', u'Statisk3']
| child_window(title="Storlek:", auto_id="13081", control_type="Text")
|
| Redigera - 'Storlek:' (L790, T695, R1036, B718)
| ['Redigera4', u'6', u'Storlek:Redigera']
| child_window(title="Storlek:", auto_id="13064", control_type="Edit")
|
| Statisk - 'Storlek på disk:' (L717, T721, R780, B736)
| [u'Storlek på disk:', u'Storlek på disk:Statisk', u'Statisk4']
| child_window(title="Storlek på disk:", auto_id="13107", control_type="Text")
|
| Redigera - 'Storlek på disk:' (L790, T721, R1036, B744)
| ['Redigera5', u'7', u'Storlek på disk:Redigera']
| child_window(title="Storlek på disk:", auto_id="13106", control_type="Edit")
|
| Statisk - 'Innehåller:' (L717, T747, R780, B762)
| [u'Innehåller:1', u'Innehåller:0', u'Innehåller:Statisk', u'Statisk5', u'Innehåller:']
| child_window(title="Innehåller:", auto_id="13088", control_type="Text")
|
| Redigera - 'Innehåller:' (L790, T747, R1036, B770)
| [u'8', 'Redigera6', u'Innehåller:Redigera']
| child_window(title="Innehåller:", auto_id="13087", control_type="Edit")
|
| Bild - 'Innehåller:' (L717, T773, R1035, B775)
| [u'Innehåller:Bild', 'Bild3', u'Innehåller:2']
| child_window(title="Innehåller:", auto_id="13096", control_type="Bild")
|
| Statisk - 'Skapad:' (L717, T786, R780, B801)
| [u'Skapad:', u'Skapad:Statisk', u'Statisk6', u'Skapad:1', u'Skapad:0']
| child_window(title="Skapad:", auto_id="13092", control_type="Text")
|
| Redigera - 'Skapad:' (L790, T786, R1036, B809)
| [u'Skapad:Redigera', 'Redigera7', u'9']
| child_window(title="Skapad:", auto_id="13072", control_type="Edit")
|
| Bild - 'Skapad:' (L717, T812, R1035, B814)
| [u'Skapad:Bild', 'Bild4', u'Skapad:2']
| child_window(title="Skapad:", auto_id="13097", control_type="Bild")
|
| Statisk - 'Attribut:' (L717, T825, R780, B840)
| [u'Attribut:Statisk', u'Statisk7', u'Attribut:']
| child_window(title="Attribut:", auto_id="13091", control_type="Text")
|
| CheckBox - 'Skrivskyddad (Gäller endast filer i mappen)' (L790, T825, R1035, B841)
| [u'CheckBox0', u'CheckBox1', 'CheckBox', u'Skrivskyddad (Gäller endast filer i mappen)CheckBox', u'Skrivskyddad (Gäller endast filer i mappen)']
| child_window(title="Skrivskyddad (Gäller endast filer i mappen)", auto_id="13075", control_type="CheckBox")
|
| CheckBox - 'Dold' (L790, T848, R865, B864)
| ['CheckBox2', u'DoldCheckBox', u'Dold']
| child_window(title="Dold", auto_id="13076", control_type="CheckBox")
|
| Knapp - 'Avancerat...' (L930, T845, R1035, B868)
| [u'Avancerat...', u'Avancerat...Knapp', 'Knapp', u'Knapp1', u'Knapp0']
| child_window(title="Avancerat...", auto_id="13154", control_type="Button")
|
| Knapp - 'OK' (L814, T968, R889, B991)
| ['Knapp2', u'OK', u'OKKnapp']
| child_window(title="OK", auto_id="1", control_type="Button")
|
| Knapp - 'Avbryt' (L895, T968, R970, B991)
| ['Knapp3', u'AvbrytKnapp', u'Avbryt']
| child_window(title="Avbryt", auto_id="2", control_type="Button")
|
| Knapp - 'Tillämpa' (L976, T968, R1051, B991)
| ['Knapp4', u'TillämpaKnapp', u'Tillämpa']
| child_window(title="Tillämpa", auto_id="12321", control_type="Button")
|
| TabControl - '' (L702, T556, R1051, B962)
| [u'10', u'TabControlDelning', u'TabControlTidigare versioner', u'TabControlSäkerhet', u'TabControl', u'TabControlAnpassa']
| child_window(auto_id="12320", control_type="Tab")
| |
| | TabItem - 'Allmänt' (L704, T558, R753, B576)
| | [u'AllmäntTabItem', 'TabItem', u'Allmänt', u'TabItem0', u'TabItem1']
| | child_window(title="Allmänt", control_type="TabItem")
| |
| | TabItem - 'Delning' (L753, T558, R801, B576)
| | [u'Delning', u'DelningTabItem', 'TabItem2']
| | child_window(title="Delning", control_type="TabItem")
| |
| | TabItem - 'Säkerhet' (L801, T558, R851, B576)
| | [u'Säkerhet', 'TabItem3', u'SäkerhetTabItem']
| | child_window(title="Säkerhet", control_type="TabItem")
| |
| | TabItem - 'Tidigare versioner' (L851, T558, R947, B576)
| | [u'Tidigare versionerTabItem', u'Tidigare versioner', 'TabItem4']
| | child_window(title="Tidigare versioner", control_type="TabItem")
| |
| | TabItem - 'Anpassa' (L947, T558, R1007, B576)
| | [u'AnpassaTabItem', 'TabItem5', u'Anpassa']
| | child_window(title="Anpassa", control_type="TabItem")
|
| TitelRad - 'Ingen' (L712, T521, R1057, B549)
| ['TitelRad', u'11']
| |
| | Meny - 'System' (L696, T526, R718, B548)
| | [u'System0', u'System', u'System1', u'Meny', u'SystemMeny']
| | child_window(title="System", auto_id="MenyBar", control_type="MenuBar")
| | |
| | | MenyObjekt - 'System' (L696, T526, R718, B548)
| | | [u'System2', u'MenyObjekt', u'SystemMenyObjekt']
| | | child_window(title="System", control_type="MenuItem")
| |
| | Knapp - 'Stäng' (L1024, T519, R1058, B549)
| | [u'StängKnapp', u'Stäng', 'Knapp5']
| | child_window(title="Stäng", control_type="Button")I vissa fall kan utskrivning av hela trädet saktas ner (till exempel i iTunes, där det på en flik finns hela tre tusen objekt!), men man kan använda parametern depth (djup): depth=1 — själva elementet, depth=2 — endast direkta barn, och så vidare. Den kan också anges i specifikationerna när man skapar child_window.
Exempel
Vi uppdaterar ständigt . Bland de nya exemplen märks automatiseringen av nätverksanalysatorn WireShark (det är ett bra exempel på en Qt5-applikation; även om den här uppgiften kan lösas utan GUI, för det finns scapy.Sniffer från Python-paketet ). Här finns också ett exempel på automatisering av MS Paint med dess Ribbon-verktygsfält.
Ett annat utmärkt exempel, skrivet av min student: (det kommer att flyttas till huvudarkivet lite senare).
Och naturligtvis ett exempel på prenumeration på tangentbords- (snabbtangenter) och musaktiviteter:
.
Tack.
Ett särskilt tack till dem som ständigt hjälper till att utveckla projektet. För mig och är detta en ständig hobby. Två av mina studenter från NNUG har nyligen avslutat sina kandidatexamen inom detta ämne. har bidragit stort till stödet för MS UI Automation och har nyligen börjat utveckla en automatisk kodgenerator baserad på principen "inspelning-uppspelning" med hjälp av textattribut (det är den mest komplexa funktionen), för närvarande bara för "uia"-backenden. utvecklar en ny backend för Linux baserad på AT-SPI (moduler mouse och keyboard baserat på — redan med versioner 0.6.x).
Eftersom jag har undervisat en specialkurs i automatisering med Python under en längre tid, utför en del masterstudenter sina hemuppgifter genom att implementera små funktioner eller exempel på automatisering. Vissa nyckelfunktioner under forskningsfasen har också en gång grävts fram av studenter. Även om det ibland krävs noggrann övervakning av kodens kvalitet. Statisk analys (QuantifiedCode, Codacy och Landscape) och automatiska tester i molnet (tjänsten AppVeyor) med en kodtäckning runt 95% hjälper mycket.
Stort tack också till alla som lämnar feedback, rapporterar buggar och skickar pull requests!
Ytterligare resurser
För frågor följer vi (nyligen tillkom en ) och . Det finns .
Vi uppdaterar varje månad . När det gäller antal stjärnor på GitHub växer endast Autohotkey snabbare (de har en mycket stor gemenskap och en lång historia) och PyAutoGUI (mycket tack vare populariteten hos böckerna av dess författare Al Sweigart: «Automate the Boring Stuff with Python» och andra).
Källa: habr.com
