Pilão de dentro para fora. Como ele faz isso

Vários ajudantes na escrita de códigos legais nos cercam, linter, typekchera, utilitário para encontrar vulnerabilidades, todos conosco. Estamos acostumados a usá-lo sem entrar em detalhes como uma "caixa preta". Por exemplo, poucas pessoas entendem os princípios do Pylint, uma dessas ferramentas indispensáveis ​​para otimizar e melhorar o código Python.

Mas Maxim Mazaev sabe o quanto é importante entender suas ferramentas e ele nos disse no Moscow Python Conf ++ . Usando exemplos da vida real, ele mostrou como o conhecimento do dispositivo interno da Pylint e de seus plug-ins ajudou a reduzir o tempo de revisão do código, melhorar a qualidade do código e, geralmente, melhorar a eficiência do desenvolvimento. Abaixo está uma instrução de descriptografia.



Por que precisamos do Pylint?


Se você já o usa, pode surgir a pergunta: "Por que saber o que há dentro do Pylint, como esse conhecimento pode ajudar?"

Normalmente, os desenvolvedores escrevem código, iniciam o linter, recebem mensagens sobre o que melhorar, como tornar o código mais bonito e fazer as alterações propostas. Agora o código é mais fácil de ler e não tem vergonha de mostrar aos colegas.

Por um longo tempo, eles trabalharam exatamente da mesma maneira com Pylint no Cyan Institute, com pequenas adições: eles mudaram configurações, removeram regras desnecessárias e aumentaram o comprimento máximo da string.

Mas em algum momento eles se depararam com um problema, pelo qual eu tive que me aprofundar no Pylint e descobrir como ele funciona. Qual é esse problema e como resolvê-lo, continue lendo.


Sobre o palestrante: Maxim Mazaev ( barra invertida ), 5 anos em desenvolvimento, trabalha no CIAN. Aprende profundamente Python, assincronia e programação funcional.

Sobre cyan


Muitos acreditam que o CIAN é uma agência imobiliária com corretores de imóveis e ficam muito surpresos ao descobrir que, em vez de corretores, temos programadores.

Somos uma empresa técnica na qual não existem corretores de imóveis, mas existem muitos programadores.

  • 1 milhão de usuários únicos por dia.
  • O maior quadro de avisos para a venda e aluguel de imóveis em Moscou e São Petersburgo. Em 2018, eles entraram no nível federal e trabalham em toda a Rússia.
  • Quase 100 pessoas na equipe de desenvolvimento, das quais 30 escrevem código Python diariamente.

Todos os dias, centenas e milhares de linhas de novo código entram em produção. Os requisitos para o código são bem simples:

  • Código de qualidade decente.
  • Homogeneidade estilística. Todos os desenvolvedores devem escrever código aproximadamente semelhante, sem "vinagrete" nos repositórios.

Para conseguir isso, é claro, você precisa de uma revisão de código.

Revisão de código


A revisão de código no CIAN ocorre em dois estágios:

  1. O primeiro estágio é automatizado . O robô Jenkins executa testes, executa Pylint e verifica a consistência da API entre microsserviços, uma vez que usamos microsserviços. Se, nesse estágio, os testes falharem ou o linter mostrar algo estranho, será uma ocasião para rejeitar a solicitação de recebimento e enviar o código para revisão.
  2. Se o primeiro estágio foi bem-sucedido, o segundo vem - a aprovação de dois desenvolvedores . Eles podem avaliar a qualidade do código em termos de lógica de negócios, aprovar uma solicitação pull ou retornar o código para revisão.


Problemas de revisão de código


A solicitação de recebimento pode não passar na revisão do código devido a:

  • erros na lógica de negócios quando um desenvolvedor resolveu um problema de maneira ineficaz ou incorreta;
  • problemas de estilo de código.

Quais poderiam ser os problemas de estilo se o linter verifica o código?

Todo mundo que escreve em Python sabe que existe um guia para escrever código PEP-8 . Como qualquer padrão, o PEP-8 é bastante geral e, para nós, como desenvolvedores, isso não é suficiente. Quero especificar o padrão em alguns lugares e expandir em outros.

Portanto, criamos nossos arranjos internos sobre a aparência e o funcionamento do código e os chamamos de "Recusar propostas Cian" .



“Recusar propostas Cian” - um conjunto de regras, agora existem cerca de 15. Cada uma dessas regras é a base para que a solicitação de recebimento seja rejeitada e enviada para revisão.

O que dificulta uma revisão produtiva do código?


Há um problema com nossas regras internas - o linter não sabe sobre elas, e seria estranho se ele soubesse - elas são internas.
O desenvolvedor que executa a tarefa deve sempre lembrar e manter as regras em mente. Se ele esquecer uma das regras, no processo de revisão de código os revisores apontarão o problema, a tarefa passará por revisão e o tempo de liberação da tarefa aumentará. Após a conclusão e a correção dos erros, os testadores precisam lembrar o que estava na tarefa, para mudar o contexto.

Isso cria um problema para o desenvolvedor e os revisores. Como resultado, a velocidade da revisão de código é reduzida drasticamente. Em vez de analisar a lógica do código, os testadores começam a analisar o estilo visual, ou seja, realizam o trabalho do linter: digitalizam o código linha por linha e procuram inconsistências na indentação no formato de importação.

Gostaríamos de nos livrar desse problema.

Mas não nos escreva seu linter?


Parece que o problema será resolvido por uma ferramenta que conhecerá todos os acordos internos e poderá verificar o código para sua implementação. Então, precisamos do nosso próprio linter?

Na verdade não. A ideia é estúpida, porque já usamos o Pylint. Este é um interface conveniente, apreciado pelos desenvolvedores e incorporado em todos os processos: é executado no Jenkins, gera relatórios bonitos que são completamente satisfeitos e vêm na forma de comentários na solicitação pull. Está tudo bem, não é necessário um segundo ponteiro .

Então, como resolver o problema se não queremos escrever nosso próprio linter?

Escreva um plug-in Pylint


Você pode escrever plugins para o Pylint, eles são chamados de damas. Sob cada regra interna, você pode escrever seu próprio verificador, que será verificado.

Considere dois exemplos de tais damas.

Exemplo No. 1


Em algum momento, verificou-se que o código contém muitos comentários no formato “TODO” - promete refatorar, excluir código desnecessário ou reescrevê-lo lindamente, mas não agora, mas mais tarde. Há um problema com esses comentários - eles absolutamente não o obrigam a nada.

O problema


O desenvolvedor escreveu uma promessa, exalou e ficou tranqüilo para executar a próxima tarefa.


Em resumo:

  • comentários com promessas são suspensos ao longo dos anos e não são seguidos;
  • código está cheio;
  • a dívida técnica vem se acumulando há anos.

Por exemplo, um desenvolvedor há 3 anos prometeu remover algo após um lançamento bem-sucedido, mas o lançamento aconteceu em 3 anos? Talvez sim. Devo excluir o código neste caso? Esta é uma grande questão, mas provavelmente não.

Solução: escreva seu verificador para a Pylint


Você não pode proibir os desenvolvedores de escrever esses comentários, mas pode fazê-los fazer um trabalho extra: crie uma tarefa no rastreador para finalizar a promessa. Então definitivamente não vamos esquecê-la.

Precisamos encontrar todos os comentários do formulário TODO e garantir que cada um deles tenha um link para uma tarefa no Jira. Vamos escrever

O que é um verificador em termos de Pylint? Esta é uma classe que herda da classe base do verificador e implementa uma certa interface.

class TodoIssueChecker(BaseChecker): _ _implements_ _ = IRawChecker 

No nosso caso, este é o IRawChecker - o chamado verificador "bruto".

Um verificador bruto itera sobre as linhas de um arquivo e pode executar uma determinada ação em uma linha. No nosso caso, em cada linha, o verificador procurará algo semelhante a um comentário e um link para uma tarefa.

Para o verificador, você precisa determinar a lista de mensagens que ele emitirá:

 msgs = { '9999': ('  TODO    ', issue-code-in-todo', ' ')} 

A mensagem tem:

  • a descrição é curta e longa;
  • código do verificador e um nome mnemônico curto que determina que tipo de mensagem é.

O código da mensagem tem o formato "C1234", no qual:

  • A primeira letra é claramente padronizada para diferentes tipos de mensagens: [C] onvention; [W] arning; [E] yog; [F] atal; [R] efactoring. Graças à carta, o relatório mostra imediatamente o que está acontecendo: um lembrete dos acordos ou problemas fatais que precisam ser abordados com urgência.
  • 4 números aleatórios exclusivos do Pylint.

O código é necessário para desativar a verificação se for desnecessária. Você pode escrever Pylint: disable e um código alfanumérico curto ou nome mnemônico:

 # Pylint: disable=C9999 # Pylint: disable=issue-code-in-todo 

Os autores do Pylint recomendam abandonar o código alfanumérico e usar o mnemônico, é mais visual.

O próximo passo é definir um método chamado process_module .



O nome é muito importante. O método deve ser chamado dessa maneira, porque o Pylint o chamará.

O parâmetro do é passado para o módulo. Nesse caso, não importa o que é ou o tipo, é importante lembrar que o nó possui um método de fluxo que retorna um arquivo linha por linha.

Você pode percorrer o arquivo e, para cada linha, verificar comentários e links para a tarefa. Se houver um comentário, mas nenhum link, emita um aviso no formulário 'emitir código-em-todo' com o código do verificador e o número da linha. O algoritmo é bastante simples.

Registre o verificador para que o Pylint saiba sobre ele. Isso é feito pela função de registro :

 def register(linter: Pylinter) -> None: linter. register_checker ( TodoIssueChecker(linter) ) 

  • Uma instância do Pylint entra na função.
  • Ele chama o método register_checker.
  • Passamos o verificador para o método

Um ponto importante: o módulo verificador deve estar no PYTHONPATH para que o Pylint possa importá-lo mais tarde.

Um verificador registrado é verificado por um arquivo de teste com comentários sem links para tarefas.

 $ cat work. # T0D0:   , -! $ pylint work. --load-plugins todo_checker … 

Para o teste, execute o Pylint, passe o módulo para ele, use o parâmetro load-plugins para passar no verificador e, dentro do linter, execute duas fases.

Fase 1. Inicialização do Plugin


  • Todos os módulos com plugins são importados. O Pylint possui verificadores internos e externos. Todos eles se reúnem e são importados.
  • Nós registramos - module.register (self) . Para cada verificador, a função de registro é chamada, onde a instância do Pylint é passada.
  • As verificações são realizadas: para a validade dos parâmetros, para a presença de mensagens, opções e relatórios no formato correto.

Fase 2. Analisar o conjunto de damas


Após a fase 1, permanece uma lista completa de diferentes tipos de damas:

  • Verificador AST;
  • Verificador bruto;
  • Verificador de token.



Na lista, selecionamos aqueles relacionados à interface bruta do verificador: verificamos quais verificadores implementam a interface IRawChecker e os tomamos por conta própria.

Para cada verificador selecionado, chame o método checker.process_module (module) e execute a verificação.

Resultado


Execute o verificador no arquivo de teste novamente:

 $ cat work. # T0D0:   , -! $ pylint work,  --load-plugins todo_checker : 0,0:   T0D0     (issue-code-in-todo) 

Aparecerá uma mensagem informando que há um comentário com TODO e nenhum link para a tarefa.

O problema foi resolvido e, agora, no processo de revisão do código, os desenvolvedores não precisam escanear o código com os olhos, encontrar comentários, escrever um lembrete ao autor do código de que existe um acordo e é recomendável deixar um link. Tudo acontece automaticamente e a revisão do código é um pouco mais rápida.

Exemplo No. 2. argumentos-chave


Existem funções que aceitam argumentos posicionais. Se houver muitos argumentos, quando eles chamam a função, não está muito claro onde está o argumento e por que ele é necessário.

O problema


Por exemplo, temos uma função:

 get_offer_by_cian_id( "sale", rue, 859483, ) 

O código tem venda e True, e não está claro o que eles significam. É muito mais conveniente quando funções nas quais existem muitos argumentos seriam chamadas apenas com argumentos nomeados:

 get_offer_by_cian_id( deal_type="sale", truncate=True, cian_id=859483, ) 

Este é um bom código, no qual fica imediatamente claro onde está o parâmetro e não confundiremos sua sequência. Vamos tentar escrever um verificador que verifique esses casos.

O verificador "bruto" usado no exemplo anterior é muito difícil de escrever para esse caso. Você pode adicionar expressões regulares super complexas, mas esse código é difícil de ler. É bom que o Pylint permita escrever outro tipo de verificador com base na árvore de sintaxe abstrata do AST , e nós o usaremos.

Letras sobre AST


Uma árvore de sintaxe AST ou abstrata é uma representação em árvore do código, onde o vértice são os operandos e as folhas são operadores.

Por exemplo, uma chamada de função, onde há um argumento posicional e dois argumentos nomeados, é transformada em uma árvore abstrata:


Existe um vértice com o tipo Call e ele possui:

  • atributos de função chamados func;
  • uma lista de argumentos posicionais args, onde há um nó com o tipo Const e um valor de 112;
  • lista de argumentos nomeados Palavras-chave.

A tarefa neste caso:

  • Encontre no módulo todos os nós com o tipo Chamada (chamada de função).
  • Calcule o número total de argumentos que a função leva.
  • Se houver mais de 2 argumentos, verifique se não há argumentos posicionais no nó.
  • Se houver argumentos posicionais, mostre um aviso.


 ll( func=Name(name='get_offer'), args=[Const(value=1298880)], keywords=[ … ]))] 

Do ponto de vista do Pylint, um verificador baseado em AST é uma classe que herda da classe verificador base e implementa a interface IAstroidChecker :

 class NonKeywordArgsChecker(BaseChecker): -_ _implements_ _ = IAstroidChecker 

Como no primeiro exemplo, a descrição do verificador, o código da mensagem e o nome mnemônico curto são indicados na lista de mensagens:

 msgs = { '9191': (' ', keyword-only-args', ' ')} 

A próxima etapa é definir o método visit_call :

 def visit_call(self, node: Call) 

O método não precisa ser chamado assim. O mais importante é o prefixo visit_, e depois vem o nome do vértice que nos interessa, com uma pequena letra.

  • O analisador AST percorre a árvore e, para cada vértice, verifica se a interface checkr visit_ <Name> está definida.
  • Se sim, então chame.
  • Recursivamente passa por todos os seus filhos.
  • Ao sair de um nó, ele chama o método leave_ <Name>.

Neste exemplo, o método visit_call receberá um nó do tipo Call como uma entrada e verá se ele possui mais de dois argumentos e se argumentos posicionais estão presentes para emitir um aviso e passar o código para o próprio nó.

 def visit_call(self, n): if node.args and len(node.args + node.keywords) > 2: self.add_message( 'keyword-only-args', node=node ) 

Registramos o verificador, como no exemplo anterior: transferimos a instância do Pylint, chamamos register_checker, passando o próprio verificador e iniciando-o.

 def register(linter: Pylinter) -> None: linter.register_checker( TodoIssueChecker(linter) ) 

Este é um exemplo de uma chamada de função de teste em que existem 3 argumentos e apenas um deles é nomeado:

 $ cat work. get_offers(1, True, deal_type="sale") $ Pylint work.py --load-plugins non_kwargs_checker … 

Essa é uma função potencialmente chamada incorretamente do nosso ponto de vista. Inicie o Pylint.

A fase de inicialização do plug-in 1 é completamente repetida, como no exemplo anterior.

Fase 2. Módulo analisando na AST


O código é analisado em uma árvore AST. A análise é realizada pela biblioteca Astroid .

Por que Astroid, não AST (stdlib)


O Astroid usa internamente não o módulo AST padrão do Python, mas o analisador AST do tipo typed_ast , caracterizado por suportar as dicas do tipo PEP 484. Typed_ast é um ramo do AST, um fork que se desenvolve em paralelo. Curiosamente, existem os mesmos erros que estão no AST e são reparados em paralelo.

 from module import Entity def foo(bar): # type: (Entity) -> None return 

Anteriormente, o Astroid usava o módulo AST padrão, no qual era possível encontrar o problema de usar as dicas definidas nos comentários usados ​​no segundo Python. Se você verificar esse código através do Pylint, até um certo ponto, ele juraria na importação não utilizada, porque a classe Entity importada está presente apenas no comentário.

Em algum momento, Guido Van Rossum chegou ao Astroid no GitHub e disse: “Pessoal, você tem o Pylint, que jura nesses casos, e temos um analisador AST digitado que suporta tudo isso. Vamos ser amigos!

O trabalho começou a ferver! Dois anos se passaram, nesta primavera, a Pylint mudou para um analisador AST digitado e parou de xingar essas coisas. As importações de taiphints não são mais marcadas como não utilizadas.

O Astroid usa um analisador AST para analisar o código em uma árvore e, em seguida, faz algumas coisas interessantes ao construí-lo. Por exemplo, se você usar import * , ele importa tudo com um asterisco e adiciona aos locais para evitar erros com importações não utilizadas.

Os plug-ins de transformação são usados ​​nos casos em que existem alguns modelos complexos baseados em meta classes, quando todos os atributos são gerados dinamicamente. Nesse caso, o Astroid é muito difícil de entender o que se entende. Ao verificar, o Pylint jurará que os modelos não têm esse atributo quando for acessado e, usando os plugins Transform, você pode resolver o problema:

  • Ajude o Astroid a modificar a árvore abstrata e entender a natureza dinâmica do Python.
  • Complemente o AST com informações úteis.

Um exemplo típico é o pylint-django . Ao trabalhar com modelos complexos de django, o linter geralmente jura por atributos desconhecidos. O Pylint-django apenas resolve esse problema.

Fase 3. Analise o conjunto de damas


Voltamos ao verificador. Novamente, temos uma lista de verificadores, a partir dos quais encontramos aqueles que implementam a interface do verificador AST.

Fase 4. Analisar verificadores por tipos de nós


Em seguida, encontramos métodos para cada verificador, eles podem ser de dois tipos:

  • visit_ <Nome do nó>
  • leév_ <Nome do nó>.

Seria bom saber quais nós você precisa chamar para um nó enquanto caminhava em uma árvore. Portanto, eles entendem o dicionário, onde a chave é o nome do nó, o valor é uma lista dos verificadores que estão interessados ​​no fato de acessar esse nó.

 _visit_methods = dict( < > : [checker1, checker2 ... checkerN] ) 

O mesmo com os métodos leave: uma chave na forma de um nome de nó, uma lista de verificadores que estão interessados ​​no fato de sair desse nó.

 _leave_methods = dict( < >: [checker1, checker2 ... checkerN] ) 

Inicie o Pylint. Ele mostra um aviso de que temos uma função em que há mais de dois argumentos e há um argumento posicional:

 $ cat work. get_offers(1, True, deal_type="sale") $ Pylint work.py --load-plugins non_kwargs_checker C: 0, 0:  c >2      (keyword-only-args) 

O problema está resolvido. Agora, os programadores de revisão de código não precisam ler os argumentos da função; o linter fará isso por eles. Economizamos nosso tempo , tempo para revisão de código e as tarefas são mais rápidas na produção.

E para escrever testes?


O Pylint permite realizar testes de unidade de damas e é muito simples. Do ponto de vista do linter, o verificador de teste se parece com uma classe que herda do abstrato CheckerTestCase . É necessário indicar o verificador que está sendo verificado nele.

 class TestNonKwArgsChecker(CheckerTestCase): CHECKER_CLASS = NonKeywordArgsChecker 

Etapa 1. Criamos um nó AST de teste a partir da parte do código que estamos verificando.

 node = astroid.extract_node( "get_offers(3, 'magic', 'args')" ) 

Etapa 2. Verifique se o verificador, entrando no nó, lança ou não lança a mensagem correspondente:

 with self.assertAddsMessages(message): self.checker.visit_call(node) 

Tokenchecker


Há outro tipo de verificador chamado TokenChecker . Ele trabalha com o princípio de um analisador lexical. O Python possui um módulo de tokenize que faz o trabalho de um scanner lexical e divide o código em uma lista de tokens. Pode ser algo como isto:


Nomes de variáveis, nomes de funções e palavras-chave se tornam tokens do tipo NAME e delimitadores, colchetes e dois pontos se tornam tokens do tipo OP. Além disso, existem tokens separados para recuo, avanço de linha e conversão reversa.

Como o Pylint funciona com o TokenChecker:

  • O módulo em teste é tokenizado.
  • Uma lista enorme de tokens é passada para todos os verificadores que implementam ITokenChecker e o método process_tokens (tokens) é chamado .

Não encontramos o uso do TokenChecker, mas existem alguns exemplos que o Pylint usa:

  • Verificação ortográfica . Por exemplo, você pode pegar todos os tokens com o texto do tipo e examinar a alfabetização lexical, verificar palavras nas listas de palavras de parada, etc.
  • Verifique recuos , espaços.
  • Trabalhe com strings . Por exemplo, você pode verificar se o Python 3 não usa literais Unicode ou se apenas caracteres ASCI estão presentes na cadeia de bytes.

Conclusões


Ocorreu um problema com a revisão de código. Os desenvolvedores executaram o trabalho do linter, gastaram seu tempo em varreduras inúteis de código e informaram o autor sobre erros. Com a Pylint, nós:

  • Transferiu verificações de rotina para o linter, implementou acordos internos nele.
  • Maior velocidade e revisão do código de qualidade.
  • Reduzido o número de solicitações de recebimento rejeitadas, e o tempo para passar tarefas na produção tornou-se menor.

Um simples verificador é escrito em meia hora e outro complexo em poucas horas. O verificador economiza muito mais tempo do que leva para escrever e luta por várias solicitações de recebimento não rejeitadas.

Você pode aprender mais sobre o Pylint e como escrever damas na documentação oficial , mas em termos de escrever damas é bastante ruim. Por exemplo, sobre o TokenChecker, há apenas uma menção, mas não sobre como escrever o próprio verificador. Mais informações estão disponíveis nas fontes Pylint no GitHub . Você pode ver o que as damas estão no pacote padrão e se inspirar para escrever suas próprias.

O conhecimento do design interno da Pylint economiza horas de trabalho, simplifica
desempenho e melhora o código. Economize seu tempo, escreva um bom código e
use linter.
A próxima conferência Moscow Python Conf ++ será realizada em 5 de abril de 2019 e você já pode reservar um ingresso antecipado de birf agora. É ainda melhor coletar suas opiniões e solicitar um relatório, pois a visita será gratuita e os pães agradáveis ​​serão um bônus, incluindo treinamento na preparação do relatório.

Nossa conferência é uma plataforma para encontrar pessoas que pensam da mesma forma, mecanismos do setor, para comunicar e discutir o que os desenvolvedores de Python adoram: back-end e Web, coleta e processamento de dados, AI / ML, testes, IoT. Como foi no outono, veja a reportagem em vídeo em nosso canal Python e assine o canal - em breve publicaremos os melhores relatórios da conferência para acesso gratuito.

Source: https://habr.com/ru/post/pt433474/


All Articles