Biblioteca Python — é um projeto de código aberto para automação de aplicativos GUI de desktop no Windows. Nos últimos dois anos, novas funcionalidades significativas foram adicionadas:
- Suporte à tecnologia MS UI Automation. A interface permaneceu a mesma, e agora suporta: WinForms, WPF, Qt5, Windows Store (UWP) e assim por diante — quase tudo que existe no Windows.
- Sistema de backends/plugins (atualmente são dois em funcionamento: o padrão
"win32"e o novo"uia"). A seguir, estamos avançando na direção da multiplataforma. - Ganchos Win32 para mouse e teclado (teclas de atalho no estilo do pyHook).
Além disso, faremos uma breve visão geral do que existe em código aberto para automação de desktop (sem pretensões de comparações sérias).
Este artigo é parte de uma transcrição da palestra na conferência SQA Days 20 em Minsk ( e ), parcialmente a versão em português do para pywinauto.
- Abordagens principais
- Principais tecnologias de acessibilidade de desktop
Vamos começar com uma breve visão geral do código aberto nesta área. Para aplicativos GUI de desktop, tudo é um pouco mais complicado do que para a web, que tem o Selenium. Aqui estão as principais abordagens:
Método de coordenadas
Codificamos pontos de clique, esperando que consigamos acertar.
[+] Multiplataforma, facilmente implementável.
[+] Fácil de fazer gravação e repetição de testes.
[-] O mais instável em relação a mudanças na resolução da tela, temas, fontes, tamanhos de janelas, etc.
[-] Exige enormes esforços para manutenção, muitas vezes é mais fácil regenerar os testes do zero ou testá-los manualmente.
[-] Automatiza apenas ações; existem outros métodos para verificação e extração de dados.
Ferramentas (multiplataforma): , , e muitas outras. Normalmente, ferramentas mais complexas incluem essa funcionalidade (nem sempre de forma multiplataforma).
Vale a pena mencionar que o método de coordenadas pode complementar as outras abordagens. Por exemplo, para gráficos personalizados, você pode clicar em coordenadas relativas (a partir do canto superior esquerdo da janela/elemento, e não de toda a tela) — geralmente isso é bastante confiável, especialmente se considerar a altura/largura de todo o elemento (então mesmo a resolução de tela diferente não será um impedimento).
Outra opção: dedicar apenas uma máquina com configurações estáveis para testes (não é multiplataforma, mas em alguns casos é adequado).
Reconhecimento de imagens de referência
[+] Multiplataforma
[+-] Relativamente confiável (melhor do que o método coordenado), mas ainda requer algumas manhas.
[-+] Relativamente lento, pois exige recursos de CPU para algoritmos de reconhecimento.
[-] Sobre reconhecimento de texto (OCR), geralmente não se fala em questões => não é possível obter dados textuais. Pelo que sei, as soluções OCR existentes não são muito confiáveis para esse tipo de tarefa e não têm uma aplicação ampla (sinta-se à vontade para comentar se isso já mudou).
Ferramentas: , (compatível com Sikuli, em puro Python), .
Tecnologias de acessibilidade
[+] O método mais confiável, pois permite buscar por texto, independentemente de como foi renderizado pelo sistema ou framework.
[+] Permite extrair dados textuais => mais fácil verificar os resultados dos testes.
[+] Geralmente, o mais rápido, pois quase não consome recursos de CPU.
[-] Difícil fazer uma ferramenta multiplataforma: todas as bibliotecas open-source suportam uma ou duas tecnologias de acessibilidade. Ninguém suporta Windows/Linux/MacOS completamente, exceto soluções pagas como TestComplete, UFT ou Squish.
[-] Nem sempre essa tecnologia está disponível. Por exemplo, testar a tela de boot dentro do VirtualBox — aqui não há como evitar o reconhecimento de imagens. Mas em muitos casos clássicos, o método de acessibilidade ainda pode ser aplicado. É sobre isso que vamos falar a seguir.
Ferramentas: em C#, em C# (compatível com Selenium), em C# (compatível com Appium), , (compatível com LDTP), , em Ruby, (Linux Desktop Testing Project) e sua versão para Windows .
LDTP — talvez a única ferramenta open-source multiplataforma (mais precisamente, um conjunto de bibliotecas) baseada em tecnologias de acessibilidade. No entanto, não é muito popular. Eu não o utilizei, mas segundo os comentários, sua interface não é a mais amigável. Se houver comentários positivos, peço que compartilhem.
Backdoor de teste (a.k.a. bicicleta interna)
Para aplicativos multiplataforma, os próprios desenvolvedores costumam criar um mecanismo interno para garantir a testabilidade. Por exemplo, criam um servidor TCP auxiliar no aplicativo, os testes se conectam a ele e enviam comandos de texto: o que clicar, de onde obter dados, etc. Confiável, mas não universal.
Principais tecnologias de acessibilidade de desktop
O bom e velho Win32 API
A maioria dos aplicativos Windows, desenvolvidos antes do lançamento do WPF e depois da Windows Store, foram construídos de uma forma ou de outra sobre a API Win32. Especificamente, MFC, WTL, C++ Builder, Delphi, VB6 – todas essas ferramentas utilizam a API Win32. Mesmo o Windows Forms é, em grande parte, compatível com a API Win32.
Ferramentas: (semelhante ao VB) e uma camada Python , (linguagem própria, possui interface IDispatch COM), (Python), (Ruby), (Ruby).
Microsoft UI Automation
A principal vantagem: a tecnologia MS UI Automation suporta a imensa maioria dos aplicativos GUI no Windows, com raras exceções. O problema: não é muito mais fácil de aprender do que a API Win32. Caso contrário, ninguém faria camadas sobre ela.
Na verdade, é um conjunto de interfaces COM personalizadas (principalmente, UIAutomationCore.dll), e também possui um wrapper .NET na forma de namespace System.Windows.Automation. Ela, a propósito, tem um bug introduzido que pode fazer com que alguns elementos da UI sejam ignorados. Portanto, é melhor usar a UIAutomationCore.dll diretamente (se você ouviu falar do UiaComWrapper em C#, é isso).
Variações das interfaces COM:
(1) IUknown básico — "a raiz de todo o mal". O mais de baixo nível, nada amigável ao usuário.
(2) IDispatch e derivados (por exemplo, Excel.Application), que podem ser utilizados em Python com o pacote win32com.client (incluído no pyWin32). A opção mais conveniente e elegante.
(3) Interfaces personalizadas, com as quais o pacote Python de terceiros .
Ferramentas: em C#, 0.6.0+, em C#, (cujas wrappers em C sobre a UIAutomationCore.dll não tiveram seu código-fonte revelado), em Ruby.
AT-SPI
Apesar de quase todas as distribuições da família Linux serem baseadas no X Window System (no Fedora 25 os "X" foram trocados pelo Wayland), os "X" permitem operar apenas janelas de nível superior e mouse/teclado. Para uma análise detalhada em botões, caixas de lista e assim por diante — existe a tecnologia AT-SPI. Os gestores de janelas mais populares têm um chamado demônio de registro AT-SPI, que fornece para os aplicativos uma GUI automatizável (pelo menos Qt e GTK são suportados).
Ferramentas: .
Na minha opinião, pyatspi2 contém muitas dependências como o PyGObject. A própria tecnologia está disponível como uma biblioteca dinâmica comum libatspi.so. Ela está disponível . Para a biblioteca pywinauto, planejamos implementar o suporte ao AT-SPI precisamente assim: através do carregamento da libatspi.so e do módulo ctypes. Há apenas um pequeno problema em usar a versão certa, pois para aplicativos GTK+ e Qt elas são um pouco diferentes. Espera-se que a versão 0.7.0 do pywinauto com suporte completo ao Linux seja lançada na primeira metade de 2018.
Apple Accessibility API
No MacOS, existe uma linguagem de automação chamada AppleScript. Para implementar algo semelhante em Python, é claro que é necessário usar funções do ObjectiveC. Começando, parece, ainda com o MacOS 10.6, o pacote pyobjc é incluído no Python pré-instalado. Isso também facilitará a lista de dependências para suporte futuro no pywinauto.
Ferramentas: Além da linguagem Apple Script, vale a pena prestar atenção a , também conhecido como pyatom. Ele é compatível em interface com LDTP, mas também é uma biblioteca independente. Há um , escrito pelo meu estudante. Existe um problema conhecido: os tempos flexíveis não funcionam (métodos waitFor*). Mas, no geral, é algo interessante.
Como começar a trabalhar com pywinauto
Primeiro, deve-se armar-se com um inspetor de objetos GUI (o que chamam de Spy tool). Ele ajuda a estudar o aplicativo por dentro: como a hierarquia de elementos está estruturada, quais propriedades estão disponíveis. Os inspetores de objetos mais conhecidos são:
- Spy++ — vem junto com o Visual Studio, incluindo a edição Express ou Community. Usa a API Win32. Também é conhecido seu clone AutoIt Window Info.
- Inspect.exe — faz parte do Windows SDK. Se você o tiver instalado, poderá encontrá-lo na pasta
C:Program Files (x86)Windows Kits<winver>binx64. Dentro do inspetor, você deve selecionar o modo UI Automation em vez de MS AA (Active Accessibility, antecessor do UI Automation).
Após analisar o aplicativo, escolhemos o backend que iremos utilizar. Basta indicar o nome do backend ao criar o objeto Application.
- backend=»win32″ — ainda é utilizado por padrão, funciona bem com MFC, WTL, VB6 e outros aplicativos legados.
- backend=»uia» — novo backend para MS UI Automation: funciona perfeitamente com WPF e WinForms; também é bom para aplicativos Delphi e Windows Store; funciona com Qt5 e alguns aplicativos Java. E, de maneira geral, se o Inspect.exe consegue ver os elementos e suas propriedades, isso significa que esse backend é adequado. Na verdade, a maioria dos navegadores também suporta UI Automation (Mozilla por padrão, e para o Chrome ao iniciar, é necessário passar a chave de linha de comando
--force-renderer-accessibility, para ver os elementos nas páginas no Inspect.exe). Claro, a competição com o Selenium nesta área é improvável. É apenas mais uma maneira de trabalhar com o navegador (pode ser útil para cenários interprodutos).
Pontos de entrada para automação
O aplicativo já foi suficientemente estudado. É hora de criar o objeto Application e iniciá-lo ou se conectar a uma instância já em execução. Esta não é apenas uma cópia da classe padrão subprocess.Popen, ou seja, um objeto de entrada que limita todas as suas ações dentro dos limites do processo. Isso é muito útil se vários instâncias do aplicativo estiverem em execução, e você não quiser interferir nas outras.
from pywinauto.application import Application
app = Application(backend="uia").start('notepad.exe')
# Descreva a janela que queremos encontrar no processo Notepad.exe
dlg_spec = app.UntitledNotepad
# espera até que a janela realmente apareça
actionable_dlg = dlg_spec.wait('visible')Se você deseja gerenciar vários aplicativos ao mesmo tempo, a classe Desktop. Por exemplo, no calculadora do Win10, a hierarquia dos elementos está espalhada por vários processos (não apenas calc.exe). Portanto, um objeto Desktop é indispensável.
from subprocess import Popen
from pywinauto import Desktop
Popen('calc.exe', shell=True)
dlg = Desktop(backend="uia").Calculator
dlg.wait('visible')O objeto raiz (Aplicação ou Desktop) — é o único lugar onde você precisa especificar o backend. Todo o resto se integra de forma transparente ao conceito de "especificação->wrapper", sobre o qual falaremos a seguir.
Especificações de janelas/elementos
Esta é a principal lógica sobre a qual a interface pywinauto é construída. Você pode descrever a janela/elemento de forma aproximada ou mais detalhada, mesmo que ainda não exista ou já esteja fechada. A especificação da janela (objeto WindowSpecification) armazena os critérios com os quais devemos procurar a janela ou elemento real.
Exemplo de especificação detalhada da janela:
>>> dlg_spec = app.window(title='Untitled - Notepad')
>>> dlg_spec
>>> dlg_spec.wrapper_object()A busca da janela é realizada chamando o método .wrapper_object(). Ele retorna um "wrapper" para a janela/elemento real ou lança ElementNotFoundError (às vezes ElementAmbiguousError, se vários elementos forem encontrados, ou seja, é necessário especificar os critérios de busca mais detalhadamente). Este "wrapper" já pode realizar algumas ações com o elemento ou obter dados dele.
Python pode ocultar a chamada .wrapper_object(), fazendo com que o código final fique mais curto. Recomendamos usar isso apenas para depuração. As duas linhas a seguir fazem exatamente a mesma coisa:
dlg_spec.wrapper_object().minimize() # debugging
dlg_spec.minimize() # productionExistem muitos critérios de busca para a especificação da janela. Aqui estão apenas alguns exemplos:
# могут иметь несколько уровней
app.window(title_re='.* - Notepad$').window(class_name='Edit')
# можно комбинировать критерии (как AND) и не ограничиваться одним процессом приложения
dlg = Desktop(backend="uia").Calculator
dlg.window(auto_id='num8Button', control_type='Button')A lista de todos os possíveis critérios está na documentação da função .
A mágica do acesso por atributo e por chave
Python simplifica a criação de especificações de janela e reconhece os atributos do objeto dinamicamente (o método __getattribute__ foi sobrecarregado)). Naturalmente, o nome do atributo possui as mesmas restrições que o nome de qualquer variável (não é permitido inserir espaços, vírgulas e outros caracteres especiais). Felizmente, o pywinauto utiliza um algoritmo de busca chamado «best match», que é resistente a erros de digitação e pequenas variações.
app.UntitledNotepad
# o mesmo que
app.window(best_match='UntitledNotepad')Se você realmente precisa de strings Unicode (por exemplo, para o idioma russo), espaços, etc., é possível acessar por chave (como se fosse um dicionário comum):
app['Untitled - Notepad']
# o mesmo que
app.window(best_match='Untitled - Notepad')Cinco regras para nomes mágicos
Como descobrir os nomes mágicos de referência? Aqueles que são atribuídos ao elemento antes da busca. Se você forneceu um nome que é suficientemente semelhante ao padrão, o elemento será encontrado.
- Por título (texto, nome):
app.Properties.OK.click() - Por texto e pelo tipo de elemento:
app.Properties.OKButton.click() - Por tipo e número:
app.Properties.Button3.click()(nomesButton0eButton1estão vinculados ao primeiro elemento encontrado,Button2— ao segundo, e assim por diante — assim se estabeleceu historicamente) - Por texto estático (à esquerda ou acima) e pelo tipo:
app.OpenDialog.FileNameEdit.set_text("")(útil para elementos com texto dinâmico) - Por tipo e pelo texto interno:
app.Properties.TabControlSharing.select("General")
Normalmente, duas ou três regras são aplicadas ao mesmo tempo, raramente mais. Para verificar quais nomes específicos estão disponíveis para cada elemento, pode-se usar o método print_control_identifiers(). Ele pode imprimir a árvore de elementos tanto na tela quanto em um arquivo. Para cada elemento, seus nomes mágicos de referência são impressos. Também é possível copiar e colar a partir daí especificações mais detalhadas dos elementos filhos. O resultado no script ficará assim:
app.Properties.child_window(title="Contains:", auto_id="13087", control_type="Edit")A própria árvore de elementos — geralmente um grande monte de informações.
>>> app.Properties.print_control_identifiers()
Identificadores de Controle:
Diálogo - 'Propriedades do Windows NT' (L688, T518, R1065, B1006)
[u'Propriedades do Windows NT', u'Diálogo', u'Propriedades do Windows NT']
child_window(title="Propriedades do Windows NT", control_type="Window")
|
| Imagem - '' (L717, T589, R749, B622)
| [u'', u'0', u'Imagem1', u'Imagem0', 'Imagem', u'1']
| child_window(auto_id="13057", control_type="Image")
|
| Imagem - '' (L717, T630, R1035, B632)
| ['Imagem2', u'2']
| child_window(auto_id="13095", control_type="Image")
|
| Editar - 'Nome da pasta:' (L790, T596, R1036, B619)
| [u'3', 'Editar', u'Editar1', u'Editar0']
| child_window(title="Nome da pasta:", auto_id="13156", control_type="Edit")
|
| Estático - 'Tipo:' (L717, T643, R780, B658)
| [u'Tipo:Estático', u'Estático', u'Estático1', u'Estático0', u'Tipo:']
| child_window(title="Tipo:", auto_id="13080", control_type="Text")
|
| Editar - 'Tipo:' (L790, T643, R1036, B666)
| [u'4', 'Editar2', u'Tipo:Editar']
| child_window(title="Tipo:", auto_id="13059", control_type="Edit")
|
| Estático - 'Localização:' (L717, T669, R780, B684)
| [u'Localização:Estático', u'Localização:', u'Estático2']
| child_window(title="Localização:", auto_id="13089", control_type="Text")
|
| Editar - 'Localização:' (L790, T669, R1036, B692)
| ['Editar3', u'Localização:Editar', u'5']
| child_window(title="Localização:", auto_id="13065", control_type="Edit")
|
| Estático - 'Tamanho:' (L717, T695, R780, B710)
| [u'Tamanho:Estático', u'Tamanho:', u'Estático3']
| child_window(title="Tamanho:", auto_id="13081", control_type="Text")
|
| Editar - 'Tamanho:' (L790, T695, R1036, B718)
| ['Editar4', u'6', u'Tamanho:Editar']
| child_window(title="Tamanho:", auto_id="13064", control_type="Edit")
|
| Estático - 'Tamanho em disco:' (L717, T721, R780, B736)
| [u'Tamanho em disco:', u'Tamanho em disco:Estático', u'Estático4']
| child_window(title="Tamanho em disco:", auto_id="13107", control_type="Text")
|
| Editar - 'Tamanho em disco:' (L790, T721, R1036, B744)
| ['Editar5', u'7', u'Tamanho em disco:Editar']
| child_window(title="Tamanho em disco:", auto_id="13106", control_type="Edit")
|
| Estático - 'Contém:' (L717, T747, R780, B762)
| [u'Contém:1', u'Contém:0', u'Contém:Estático', u'Estático5', u'Contém:']
| child_window(title="Contém:", auto_id="13088", control_type="Text")
|
| Editar - 'Contém:' (L790, T747, R1036, B770)
| [u'8', 'Editar6', u'Contém:Editar']
| child_window(title="Contém:", auto_id="13087", control_type="Edit")
|
| Imagem - 'Contém:' (L717, T773, R1035, B775)
| [u'Contém:Imagem', 'Imagem3', u'Contém:2']
| child_window(title="Contém:", auto_id="13096", control_type="Image")
|
| Estático - 'Criado:' (L717, T786, R780, B801)
| [u'Criado:', u'Criado:Estático', u'Estático6', u'Criado:1', u'Criado:0']
| child_window(title="Criado:", auto_id="13092", control_type="Text")
|
| Editar - 'Criado:' (L790, T786, R1036, B809)
| [u'Criado:Editar', 'Editar7', u'9']
| child_window(title="Criado:", auto_id="13072", control_type="Edit")
|
| Imagem - 'Criado:' (L717, T812, R1035, B814)
| [u'Criado:Imagem', 'Imagem4', u'Criado:2']
| child_window(title="Criado:", auto_id="13097", control_type="Image")
|
| Estático - 'Atributos:' (L717, T825, R780, B840)
| [u'Atributos:Estático', u'Estático7', u'Atributos:']
| child_window(title="Atributos:", auto_id="13091", control_type="Text")
|
| Caixa de Seleção - 'Somente leitura (Aplica-se apenas a arquivos na pasta)' (L790, T825, R1035, B841)
| [u'Caixa de Seleção0', u'Caixa de Seleção1', 'Caixa de Seleção', u'Somente leitura (Aplica-se apenas a arquivos na pasta)Caixa de Seleção', u'Somente leitura (Aplica-se apenas a arquivos na pasta)']
| child_window(title="Somente leitura (Aplica-se apenas a arquivos na pasta)", auto_id="13075", control_type="CheckBox")
|
| Caixa de Seleção - 'Oculto' (L790, T848, R865, B864)
| ['Caixa de Seleção2', u'OcultoCaixa de Seleção', u'Oculto']
| child_window(title="Oculto", auto_id="13076", control_type="CheckBox")
|
| Botão - 'Avançado...' (L930, T845, R1035, B868)
| [u'Avançado...', u'BotãoAvançado...', 'Botão', u'Botão1', u'Botão0']
| child_window(title="Avançado...", auto_id="13154", control_type="Button")
|
| Botão - 'OK' (L814, T968, R889, B991)
| ['Botão2', u'OK', u'BotãoOK']
| child_window(title="OK", auto_id="1", control_type="Button")
|
| Botão - 'Cancelar' (L895, T968, R970, B991)
| ['Botão3', u'BotãoCancelar', u'Cancelar']
| child_window(title="Cancelar", auto_id="2", control_type="Button")
|
| Botão - 'Aplicar' (L976, T968, R1051, B991)
| ['Botão4', u'BotãoAplicar', u'Aplicar']
| child_window(title="Aplicar", auto_id="12321", control_type="Button")
|
| Controle de Aba - '' (L702, T556, R1051, B962)
| [u'10', u'ControleAbaCompartilhamento', u'ControleAbaVersões Anteriores', u'ControleAbaSegurança', u'ControleAba', u'ControleAbaPersonalizar']
| child_window(auto_id="12320", control_type="Tab")
| |
| | Item da Aba - 'Geral' (L704, T558, R753, B576)
| | [u'ItemAbaGeral', 'ItemAba', u'Geral', u'ItemAba0', u'ItemAba1']
| | child_window(title="Geral", control_type="TabItem")
| |
| | Item da Aba - 'Compartilhamento' (L753, T558, R801, B576)
| | [u'Compartilhamento', u'ItemAbaCompartilhamento', 'ItemAba2']
| | child_window(title="Compartilhamento", control_type="TabItem")
| |
| | Item da Aba - 'Segurança' (L801, T558, R851, B576)
| | [u'Segurança', 'ItemAba3', u'ItemAbaSegurança']
| | child_window(title="Segurança", control_type="TabItem")
| |
| | Item da Aba - 'Versões Anteriores' (L851, T558, R947, B576)
| | [u'ItemAbaVersões Anteriores', u'Versões Anteriores', 'ItemAba4']
| | child_window(title="Versões Anteriores", control_type="TabItem")
| |
| | Item da Aba - 'Personalizar' (L947, T558, R1007, B576)
| | [u'ItemAbaPersonalizar', 'ItemAba5', u'Personalizar']
| | child_window(title="Personalizar", control_type="TabItem")
|
| Barra de Título - 'Nenhum' (L712, T521, R1057, B549)
| ['BarraDeTítulo', u'11']
| |
| | Menu - 'Sistema' (L696, T526, R718, B548)
| | [u'Sistema0', u'Sistema', u'Sistema1', u'Menu', u'MenuSistema']
| | child_window(title="Sistema", auto_id="MenuBar", control_type="MenuBar")
| | |
| | | Item de Menu - 'Sistema' (L696, T526, R718, B548)
| | | [u'Sistema2', u'ItemMenu', u'ItemMenuSistema']
| | | child_window(title="Sistema", control_type="MenuItem")
| |
| | Botão - 'Fechar' (L1024, T519, R1058, B549)
| | [u'BotãoFechar', u'Fechar', 'Botão5']
| | child_window(title="Fechar", control_type="Button")Em alguns casos, imprimir toda a árvore pode ser lento (por exemplo, no iTunes, em uma aba há até três mil elementos!), mas é possível usar o parâmetro depth (profundidade): depth=1 — o próprio elemento, depth=2 — apenas os filhos imediatos, e assim por diante. Também pode ser especificado nas especificações ao criar child_window.
Exemplos
Estamos constantemente atualizando . Recentemente, vale destacar a automação do analisador de rede WireShark (um bom exemplo de aplicação Qt5; embora essa tarefa possa ser resolvida sem GUI, pois há scapy.Sniffer do pacote Python ). Também há um exemplo de automação do MS Paint com sua barra de ferramentas Ribbon.
Outro ótimo exemplo, escrito por um dos meus alunos: (ele será transferido para o repositório principal um pouco mais tarde).
E, claro, um exemplo de assinatura em eventos de teclado (hot keys) e mouse:
.
Agradecimentos
Agradecimentos especiais a todos que constantemente ajudam a desenvolver o projeto. Para mim e , isso é um hobby constante. Dois dos meus alunos da NNGU recentemente se formaram com trabalhos de graduação sobre esse tema. fez uma grande contribuição no suporte à Automatização da Interface do Usuário do MS e recentemente começou a fazer um gerador automático de código baseado no princípio "gravação-reprodução" com propriedades textuais (este é o recurso mais complexo), por enquanto apenas para o backend "uia". está desenvolvendo um novo backend para Linux baseado em AT-SPI (módulos mouse e keyboard baseado em — já nas versões 0.6.x).
Como há bastante tempo leciono um curso especial sobre automação em Python, parte dos alunos de mestrado realizam tarefas de casa implementando pequenos recursos ou exemplos de automação. Algumas questões-chave na fase de pesquisa também foram descobertas por estudantes. Apesar disso, às vezes, é preciso ficar de olho na qualidade do código. Isso é muito ajudado por analisadores estáticos (QuantifiedCode, Codacy e Landscape) e testes automáticos na nuvem (serviço AppVeyor) com cobertura de código em torno de 95%.
Obrigado também a todos que deixam feedback, reportam bugs e enviam pull requests!
Recursos adicionais
Estamos de olho nas perguntas através do (recentemente apareceu ) e . Há .
Atualizamos a cada mês Em número de estrelas no GitHub, só crescem mais rapidamente o Autohotkey (com uma comunidade muito grande e uma longa história) e o PyAutoGUI (principalmente devido à popularidade dos livros de seu autor Al Sweigart: "Automate the Boring Stuff with Python" e outros).
Fonte: habr.com
