Caos na dependência do Python

Você está familiarizado com a história dos pacotes Python? Você navega em formatos de pacotes? Você sabe que terá que desvendar o emaranhado de dependências, mesmo que pareça um milagre - dependência zero? Estou certo de que eles não estão tão familiarizados com tudo isso quanto o autor da biblioteca DepHell.



Consegui conversar com Nikita Voronov , mais conhecido como Gram ou orsinium , e perguntar a ele sobre o tópico de um relatório futuro, as dores das soluções de resolução de dependências ruins, DepHell, pip, o primeiro fósforo vence o princípio, desenvolvimento incremental Guido, Pipfile, Python e o futuro do ecossistema.

- No Moscow Python Conf ++, você conversará sobre dependências e tudo o que está ao lado delas. Por que você escolheu exatamente esse tópico para o relatório?

Porque esta pergunta passa por toda a minha experiência com Python. Quando criei meu primeiro pacote, escrevi o primeiro código, pensei em como ajudar outras pessoas para que eles pudessem instalá-lo e fiz o setup.py. Depois, ele trabalhou em uma empresa, em outra, em um terceiro, a tarefa foi complicada e desenvolvida. No começo, havia apenas um arquivo requirements.txt, então percebi que precisava corrigir as dependências, pip-tools, um arquivo de bloqueio apareceu. Mais tarde, adquirimos Pipenv e depois Poesia.

Gradualmente, mais e mais problemas foram se abrindo, eu estava me afundando mais neste caos. Como resultado, comecei a implementar o DepHell , um projeto para gerenciar dependências que podem resolvê-las e lê-las em um formato diferente. Enquanto eu trabalhava com todos os tipos de formatos, já tinha visto o interior deles o suficiente e agora sei o quanto está organizado por dentro, mas todos os dias aprendo algo novo. Portanto, posso lhe contar muitas coisas interessantes sobre dor e más decisões.

- A dor é sempre interessante. Quais são os problemas agora nesta parte do Python?

JS possui um diretório node_modules e cada dependência possui suas próprias dependências empilhadas dentro dele. Em Python, este não é o caso. Por exemplo, um pacote está instalado no mesmo ambiente e todos os pacotes que o utilizam usam a mesma versão deste pacote. Para fazer isso, você precisa resolver corretamente as dependências - escolha a versão deste pacote que atenderá geralmente a todos os pacotes desse ambiente. A tarefa é bastante trivial: os pacotes dependem um do outro, tudo está interligado e a resolução de dependências é difícil. Praticamente não há resolvedores no Python. O resolvedor inteligente está apenas em Poesia e DepHell.

Tudo isso é muito complicado pelo fato de o pypi.org geralmente não fornecer informações sobre dependências de pacotes, porque essas informações devem ser especificadas pelo cliente, o servidor PyPI não pode descobrir sozinho. Portanto, quando o PyPI diz que o pacote não tem dependências, você não pode confiar nele. Você precisa baixar o release inteiro, descompactar e analisar as dependências do pacote em setup.py. Como é um processo longo, o resolvedor em Python não pode ser rápido.

Além de haver poucos resolvedores no Python, eles também são lentos por design.

No meu relatório , quero dizer como a resolução do DepHell funciona: como criar um gráfico de dependência, como esse gráfico se parece, por que a maioria dos artigos científicos se baseia em como resolver dependências e trabalhar com esse gráfico. Obviamente, existem documentos técnicos sobre como tudo isso deve funcionar. Pessoas inteligentes escreveram artigos com algoritmos, mas na maioria das vezes não funcionam no Python. Portanto, descreverei como trabalho com resolução de dependência na prática no DepHell.

- Costumo ouvir de programadores que eles usam pip, e tudo funciona bem para eles. O que eles estão fazendo de errado?

Eles têm sorte, não se deparam com um conflito de dependências. Embora o problema possa surgir quando você coloca apenas dois pacotes em um ambiente limpo. Recentemente, houve um lançamento do pacote de cobertura 5.0 e, se você especificar apenas os pip install pytest-cov coveralls pip, o pip ficará em ordem e o primeiro pacote selecionará a versão mais recente da cobertura, ou seja, 5.0. O princípio da primeira partida ganha funciona no pip, portanto, mesmo que a versão não seja compatível com o segundo pacote, ela já será corrigida para o primeiro pacote. Muitas vezes, essa abordagem funciona, mas nem sempre.

Além disso, há uma pergunta com ambientes reproduzíveis. Como o pip sempre coloca a versão mais recente, as versões no ambiente local e na produção podem ser diferentes. Para resolver esse problema, é habitual corrigir as dependências. E quando as dependências já estiverem corrigidas, as versões específicas que o pip deve instalar são indicadas e o pip já funciona bem. O Pip não possui um resolvedor, mas o faz quando outra pessoa resolve dependências para ele, como DepHell ou Poetry.

- Por que esse tópico agora está ganhando tanta relevância, como você pensa? Por que nada aconteceu antes, mas agora se foi, e mesmo em direções diferentes?

Primeiro, o ecossistema Python está crescendo. Existem mais pacotes, eles precisam ser instalados mais e muito mais problemas surgem. Em segundo lugar, os problemas com os formatos de arquivo existem há muito tempo e são discutidos há muito tempo.

Setup.py é geralmente impossível de analisar, só pode ser executado. Se quisermos, por exemplo, escrever um servidor no Go para distribuir rapidamente pacotes para o Python, não podemos simplesmente pegar e ler o setup.py, porque é um arquivo executável. Dessa forma, para executá-lo, você precisa de Python e de um ambiente completo, e geralmente também para que todo o projeto esteja próximo e que algumas dependências específicas sejam instaladas. Além de todas essas dificuldades, a execução de setup.py pode ser perigosa, porque algum outro código será executado no seu computador. Na verdade, é assustador até executar código de baixo do usuário atual, porque, por exemplo, se ele recebe minha chave SSH privada e a envia para algum lugar, será uma grande tragédia.

A segunda opção para definir dependências, que existe há muito tempo e todos trabalham com ela, é requirements.txt. Também é quase impossível analisar da mesma maneira. O Pip pode, mas é muito, muito difícil: as funções que chamam de funções, iteradores, tudo é misturado. Além disso, o pip pode ler algumas de suas chaves em requirements.txt, por exemplo, um índice para download pode ser especificado. Mas isso não funciona com todas as chaves.

Portanto, para analisar o arquivo requirements.txt, é necessário usar o pip ou alguma solução de terceiros. Todas as soluções de terceiros são essencialmente garfos e usam algum tipo de suposição sobre o arquivo. Nem todo arquivo de requisitos exigidos do.txt.txt pode ler esses garfos.

O próprio Pip não se destina a ser usado como uma biblioteca. Essa é uma ferramenta exclusivamente da CLI que pode ser usada apenas no console. Todo o código-fonte do pip está oculto por trás do _internal , e os desenvolvedores dizem diretamente: “Não use isso!”. E toda versão quebra a compatibilidade com versões anteriores. Eles honestamente não garantem compatibilidade e podem mudar qualquer coisa a qualquer momento. E é isso que acontece - toda vez que um novo lançamento é lançado, eu aprendo sobre isso com um IC quebrado no DepHell.

- Que tal em outros idiomas? É tão ruim lá ou todos esses problemas estão resolvidos em algum lugar?

Guido van Rossum recebeu recentemente o Prêmio Dijkstra. Participei de suas palestras e perguntei sobre as dependências do Python. Guido disse que o vício em todas as línguas é um caos, ele tenta não entrar lá e confia na comunidade para resolver esse problema.

Assim, no Python, o trabalho com dependências é gradualmente organizado pela comunidade. Novas soluções estão surgindo. Depois que o Distutils foi construído em Python, as pessoas perceberam que havia muitos problemas, ferramentas de instalação complementares. easy_Install foi desenvolvido posteriormente para instalar pacotes, mas também teve problemas. Para resolvê-los, criou pip. Agora o pip tem muitos problemas. Suas fontes estão mudando constantemente, não há arquitetura, não há interface.

A comunidade está tentando inventar alguma coisa. Por exemplo, houve uma longa discussão sobre o problema chamado requisitos 2.0 sobre como tornar os requisitos compreensíveis para as pessoas (aqui está a versão, aqui estão os marcadores) e programaticamente a partir de outros idiomas.

Eles criaram um Pipfile, mas como o Pip é muito confuso, eles não puderam adicionar suporte ao Pipfile.

Os desenvolvedores querem fazer isso, é claro. Provavelmente, um dia eles serão capazes, mas até agora o pip não pode suportar o Pipfile. Portanto, criamos o pipenv para trabalhar com o Pipfile e o ambiente virtual, alguns outros invólucros com o ambiente. Mas no pipenv também tudo é confuso e confuso.

Para outros idiomas, gosto de como o gerenciamento de dependências é implementado no Go. Anteriormente, não havia versão, havia go get , no qual você indica de qual repositório qual pacote baixar. Do ponto de vista de um iniciante, isso é conveniente: você simplesmente escreve go get e o pacote já está no sistema. E quando você começa a trabalhar com o Python, um monte de tudo entra em colapso: algumas versões, PyPI, pip, requirements.txt, setup.py, agora também Pipfile, Poetry, __pymodules__ , etc.

À medida que o Python evolui de forma incremental e com a ajuda da comunidade, o legado se acumula no ecossistema. Go foi apenas go get , mas novamente surgiu o problema de que as dependências precisavam ser corrigidas para que, em particular, o ambiente fosse reprodutível.

Um ambiente reproduzível pode ser criado usando um contêiner de docker com todas as dependências instaladas. Mas às vezes você precisa atualizar dependências individuais. Por exemplo, podemos não estar prontos para atualizar tudo, porque o projeto não possui testes suficientes para provar que, após a atualização, tudo ainda funciona. Mas uma certa dependência pode precisar ser atualizada, porque, digamos, foi encontrada uma vulnerabilidade. Para fazer isso, é melhor não ter uma imagem do docker, mas um arquivo que diz: "Instale uma versão específica de um pacote específico".

O Go não existia e a venda apareceu: todas as dependências foram obtidas e colocadas em um diretório. Esta é uma solução suja, semelhante ao node_modules , que no Go foi implementado há algum tempo usando soluções de terceiros. No Python, essa abordagem também é usada, por exemplo, o pip possui um diretório de vendor . Quando você instala o pip, as dependências não são feitas e você pode pensar que tudo é muito legal e não há dependências, mas, na verdade, elas estão todas dentro do vendor .

Há um ano, o go.mod (Go Modules) apareceu em Go. Esta é uma nova ferramenta interna, mas o go get também go get suportado. O projeto contém dois arquivos:

  • um descreve as dependências com as quais o projeto trabalha diretamente;
  • o outro é um arquivo de bloqueio, que descreve absolutamente todas as dependências e suas versões específicas.

Esta é uma solução centralizada legal.

O que é importante, eles insistem que certas coisas devem parecer de uma certa maneira. Por exemplo, no Go, a versão deve ser semântica.

O Python também tem uma especificação sobre a aparência da versão. Para isso, existe o PEP 440. Mas, primeiro, a especificação é muito complicada: não há apenas três componentes de versão (números), mas também pré-lançamento, pós-lançamento e época (quando o modo de versionamento muda). Em segundo lugar, o PEP 440 não foi aceito imediatamente, eles também chegaram a ele de forma incremental, portanto, a versão legada é suportada, o que significa que qualquer coisa pode ser usada como a versão - qualquer linha como "Hello world!".

- Você disse que a comunidade desenvolve a linguagem de forma incremental, para que haja um grande número de soluções. Mas por que não se livrar de todo esse lixo? Por que não jogar fora os Distutils, abandonar o antigo e desnecessário que ninguém está usando e introduzir ativamente novas práticas e ferramentas?

Manter tudo isso faz sentido, para que você ainda possa instalar os pacotes antigos. É impossível insistir que é necessário fazer exatamente isso, e não o contrário, porque a decisão é tomada pela comunidade. Nenhum dos desenvolvedores do Core Python vem e diz: "É isso, estamos fazendo tudo agora e sem pregos".

O Go tem tudo o que você precisa para trabalhar com dependências imediatamente. No Python, você precisa reinstalar tudo de fora e ainda precisa entender o que exatamente. Na maioria das vezes, o pip é suficiente, mas agora outras opções estão aparecendo.

No site com recomendações oficiais de pacotes da Python Packaging Authority, o grupo que produz pip, pipenv, PyPI, ele foi escrito para usar o pipenv. Com pipenv é outra história. Em primeiro lugar, tem baixa resolução. Em segundo lugar, não há lançamentos há muito tempo e a comunidade já está esperando que os criadores admitam honestamente que este projeto está morto. O terceiro problema com o pipenv é que ele é adequado apenas para projetos, mas não para pacotes: você pode especificar dependências do projeto no pipenv, mas não pode especificar seu nome, versão e, portanto, colocá-lo em um pacote para download no PyPI. Acontece que seguir as recomendações da Python Packaging Authority e usar o pipenv ainda não é suficiente para descobrir isso.

A poesia está tentando ser revolucionária. Basicamente, ele não gera o arquivo setup.py, o que seria útil para compatibilidade com versões anteriores, porque o Poetry deseja ser o novo e único formato para tudo. Ele sabe como coletar pacotes e possui um arquivo de bloqueio, necessário para os projetos. No entanto, a poesia tem muitas coisas estranhas, muitos recursos familiares não são suportados.

- O que você acha que é o futuro do ecossistema em termos de trabalhar com dependências? Sua previsão.

Tudo está mais ou menos melhorando. Por exemplo, vi uma vaga no pip e um desenvolvedor que a coloca em ordem recebe muito dinheiro. Talvez o pip se torne uma solução mais universal. Mas você precisa de alguém para levar a sério: venha e diga que estamos fazendo dessa maneira, agora estamos seguindo um PEP mais rigoroso e insistimos em sua observância (porque o PEP é apenas uma recomendação de que ninguém realmente não é necessário seguir).

Por exemplo, tivemos uma história: uma certa versão do PyYAML foi bloqueada no arquivo de bloqueio. Um dia, os testes no IC são aprovados, implantamos na produção e tudo cai lá, porque a versão PyYAML não foi encontrada. O problema era que a versão bloqueada foi excluída do pypi.org. Todo mundo ficou indignado, atualizou o arquivo de bloqueio, de alguma forma sobreviveu, mas o sedimento permaneceu.

Há pouco tempo, o PEP 592 apareceu; ele já foi adotado e é mantido em pip, no qual apareceram lançamentos arrancados. Yank significa que a versão ainda não foi completamente removida do pypi.org - está oculta. Ou seja, se você especificar que precisa, por exemplo, da versão PyYAML maior que 3.0, o pip ignorará as versões arrancadas e instalará as últimas disponíveis. Mas se uma versão específica for indicada no arquivo de bloqueio e essa versão for arrancada, o pip o instalará de qualquer maneira. Portanto, os arquivos de bloqueio e a implantação não serão interrompidos, mas, se possível, a versão antiga não será usada.

A segunda coisa interessante é o PEP para __pymodules__ . Estes são ambientes virtuais leves: você abre o diretório do projeto, grava o pip install PyYAML e o PyYAML é instalado não globalmente, mas no diretório __pymodules__ . Quando o Python é iniciado neste diretório, ele importa o PyYAML não globalmente, mas desse diretório.

Eu chamo isso de ambientes virtuais no mínimo, porque há menos isolamento. Por exemplo, não há acesso aos arquivos binários. Quando um ambiente virtual com o pytest instalado é ativado, ele pode ser usado no console: basta escrever o pytest e fazer alguma coisa. Com __pymodules__ estarão disponíveis para importação, mas não os binários, pois eles não serão realmente instalados.

Este PEP foi projetado para facilitar para iniciantes. Para que eles não precisem lidar com todos os meandros dos ambientes virtuais, basta instalar tudo o que você precisa em __pymodules__ por meio da instalação do pip.

- Bem, o futuro em sua previsão é mais brilhante do que agora.

Sim, mas como eu disse, se ninguém vier e disser que estamos refazendo e tentando jogar fora o legado, os problemas permanecerão. Agora estamos acumulando e acumulando ferramentas, e será impossível nos livrarmos completamente de qualquer uma delas no futuro próximo.

- O que você acha, por que nenhum dos desenvolvedores pode atualizar dependências Quase em qualquer lugar - nem nas empresas nem no código aberto - o processo de trabalhar com versões de segurança, em princípio, com novas versões menores ou maiores, foi construído. Onde você vê os problemas aqui?

No mínimo, quando você deseja atualizar dependências, é assustador atualizar todas as dependências, porque não é fato que, mesmo se você passar nos testes, tudo funcionará. Por exemplo, muitas vezes essa situação surge com o aipo, porque o aipo não pode ser totalmente testado em testes. Você pode bloquear algo, simplificar algo, mas o fato de os trabalhadores estarem em execução não pode ser verificado.

O trabalho Ir com testes está bem implementado, mesmo nos tutoriais do Go Modules está escrito como atualizar dependências: você atualiza determinadas dependências e executa testes. Além disso, os testes executam não apenas o seu, mas também essa dependência.

Um aspecto interessante ainda vale a pena mencionar: os testes devem estar em pacotes em Python? Quando você baixa um pacote do pypi.org, deve haver testes? Em teoria, eles devem e até têm um mecanismo para executá-los: em setup.py, você pode especificar como executar testes, quais dependências eles têm.

Mas, primeiro, muitas pessoas não sabem como executá-las e não executam testes dependentes. Portanto, eles geralmente não são necessários. Em segundo lugar, geralmente esses testes têm equipamentos muito difíceis e, portanto, incluir testes em um pacote significa torná-lo 6 a 10 vezes maior.

Seria ótimo poder baixar um pacote com testes e sem testes. Mas agora não existe essa possibilidade, portanto os testes geralmente não são incluídos nos pacotes. Há um caos, e eu nem sei se é possível executar testes dessas dependências ao atualizar dependências.

Esse aspecto parece ser negligenciado principalmente. Porém, em alguns outros idiomas, em particular, o Go é considerado uma boa prática, atualizando um pacote no ambiente, execute imediatamente testes para garantir que esse pacote funcione bem nesse ambiente.

- Por que, na sua opinião, no Python, as ferramentas para controle de versão semântico automático não são populares?

Eu acho que um dos problemas é que a versão pode ser descrita em muitos lugares. Na maioria das vezes, existem três deles: o formato de descrição dos metadados do projeto (pypi.org, poety, setup.py etc.), dentro do próprio projeto e na documentação. Atualizar uma versão em três lugares não é muito difícil, mas fácil de esquecer.

O DepHell possui uma equipe para atualizações de versão. DepHell , , . semantic version, compatible version .. , , .

Flit. Flit — , . : init , build , publish install . , , PyPI — . Flit , . docstring . , .

DepHell Flit . description, , , .

, .

DepHell import , , , , . , , .

, Moscow Python Conf++ 27 . DepHell backend, web, , AI/ML, , DevOps, , IoT, infosec . , , Moscow Python Conf++.

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


All Articles