Oferecendo suporte para empacotamento em downstream

Status da página:

Esboço

Última revisão:

2025-?

Embora o PyPI e as ferramentas de empacotamento Python, como pip, sejam os principais meios de distribuição de pacotes Python, eles também são frequentemente disponibilizados como parte de outros ecossistemas de empacotamento. Esses esforços de reempacotamento são chamados coletivamente de empacotamento downstream (seus próprios esforços são chamados de empacotamento upstream) e incluem projetos como distribuições Linux, Conda, Homebrew e MacPorts. Geralmente, eles visam fornecer suporte aprimorado para casos de uso que não podem ser tratados apenas por ferramentas de empacotamento Python, como integração nativa com um sistema operacional específico ou compatibilidade garantida com versões específicas de software não Python.

Esta discussão tenta explicar como o empacotamento downstream geralmente é feito e quais desafios adicionais os empacotadores downstream normalmente enfrentam. O objetivo é fornecer algumas diretrizes opcionais que os mantenedores do projeto podem optar por seguir, o que ajuda a tornar o empacotamento downstream significativamente mais fácil (sem impor grandes problemas de manutenção ao projeto upstream). Observe que esta não é uma proposta do tipo “tudo ou nada” — qualquer coisa que os mantenedores upstream possam fazer é útil, mesmo que seja apenas uma pequena parte. Os mantenedores downstream também estão dispostos a preparar patches para resolver esses problemas. Mesclar esses patches pode ser muito útil, pois elimina a necessidade de diferentes downstreams carregarem e continuarem rebaseando os mesmos patches, além do risco de aplicar soluções inconsistentes ao mesmo problema.

Estabelecer um bom relacionamento entre mantenedores de software e empacotadores downstream pode trazer benefícios mútuos. Os downstreams geralmente estão dispostos a compartilhar sua experiência, tempo e hardware para aprimorar seu pacote. Às vezes, eles estão em melhor posição para ver como seu pacote é usado na prática e fornecer informações sobre suas relações com outros pacotes que, de outra forma, exigiriam um esforço significativo para serem obtidas. Os empacotadores geralmente conseguem encontrar bugs antes que seus usuários os encontrem em produção, fornecem relatórios de bugs de boa qualidade e fornecem patches sempre que possível. Por exemplo, eles são regularmente ativos para garantir que os pacotes que redistribuem sejam atualizados para quaisquer problemas de compatibilidade que surjam quando uma nova versão do Python é lançada.

Observe que as construções downstream incluem não apenas a redistribuição binária, mas também construções de código-fonte feitas em sistemas de usuários (em distribuições que priorizam o código-fonte, como o Gentoo Linux, por exemplo).

Forneça distribuições fonte completas

Por que?

A grande maioria dos empacotadores downstream prefere construir pacotes a partir do código-fonte, em vez de usar os pacotes binários fornecidos pelo upstream. Em alguns casos, o uso do código-fonte é realmente necessário para que o pacote seja incluído na distribuição. Isso também se aplica a pacotes Python puros que fornecem rodas universais. Os motivos para usar distribuições de código-fonte podem incluir:

  • Ser possível auditar o código-fonte de todos os pacotes.

  • Ser capaz de executar o conjunto de testes e construir a documentação.

  • Ser capaz de aplicar patches facilmente, incluindo fazer backporting de commits do repositório do projeto e enviar patches de volta ao projeto.

  • Ser capaz de construir em uma plataforma específica que não é coberta por construções upstream.

  • Ser capaz de construir em versões específicas de bibliotecas do sistema.

  • Term um processo de construção consistente em todos os pacotes Python.

Enquanto é possível construir pacotes através do repositório Git, há razões importantes pelas quais é incentivado prover um arquivo estático:

  • Obter um único arquivo costuma ser mais eficiente, confiável e com melhor suporte do que, por exemplo, usar um clone do Git. Isso pode ajudar usuários com baixa conectividade com a internet.

  • Os downstreams costumam usar hashes para verificar a autenticidade dos arquivos fonte em construções subsequentes, o que exige que eles permaneçam idênticos bit a bit ao longo do tempo. Por exemplo, arquivos Git gerados automaticamente não garantem isso, pois os dados compactados podem mudar se o gzip for atualizado no servidor.

  • Arquivos compactados podem ser espelhados, reduzindo o uso de largura de banda tanto no upstream quanto no downstream. As construções podem ser executadas posteriormente em ambientes com firewall ou offline, que só podem acessar os arquivos fonte fornecidos pelo mirror local ou redistribuídos anteriormente.

  • Publicar explicitamente arquivos compactados pode garantir que quaisquer dependências nos metadados do sistema de controle de versão sejam resolvidas ao criar o arquivo fonte. Por exemplo, arquivos Git gerados automaticamente omitem todas as informações da tag de commit, o que pode resultar em detalhes de versão incorretos nas compilações resultantes.

Como?

O ideal é que um arquivo de distribuição de código-fonte publicado no PyPI inclua todos os arquivos do repositório Git do pacote que são necessários para construir o pacote em si, executar seu conjunto de testes, construir e instalar sua documentação e quaisquer outros arquivos que possam ser úteis para usuários finais, como conclusões de shell, arquivos de suporte do editor e assim por diante.

Este ponto se aplica apenas aos arquivos pertencentes ao próprio pacote. O processo de empacotamento downstream, assim como os gerenciadores de pacotes Python, provisionará as dependências Python, ferramentas de sistema e bibliotecas externas necessárias para o seu pacote e seus scripts de compilação. No entanto, os arquivos que listam essas dependências (por exemplo, arquivos requirements*.txt) também devem ser incluídos para ajudar os downstreams a determinar as dependências necessárias e verificar se há alterações nelas.

Alguns projetos apresentam preocupações relacionadas aos gerenciadores de pacotes Python que utilizam distribuições de código-fonte do PyPI. Eles não desejam aumentar seu tamanho com arquivos que não são usados por essas ferramentas, ou não desejam publicar distribuições de código-fonte, pois elas permitem um fallback problemático ou totalmente não funcional para a construção do projeto específico a partir do código-fonte. Nesses casos, um bom meio-termo pode ser publicar um arquivo de código-fonte separado para uso posterior em outro lugar, por exemplo, anexando-o a uma versão do GitHub. Como alternativa, arquivos grandes, como dados de teste, podem ser divididos em arquivos separados.

Por outro lado, alguns projetos (NumPy, por exemplo) decidem incluir testes em seus pacotes instalados. Isso tem a vantagem adicional de permitir que os usuários executem testes após a instalação, por exemplo, para verificar regressões após a atualização de uma dependência. Outra abordagem é dividir os testes ou os dados de teste em um pacote Python separado. Essa abordagem foi adotada pelo projeto cryptography, com os grandes vetores de teste sendo divididos no pacote cryptography-vectors.

Uma boa ideia é usar sua distribuição de código-fonte no fluxo de trabalho de lançamento. Por exemplo, a ferramenta construir faz exatamente isso — primeiro cria uma distribuição de código-fonte e, em seguida, a utiliza para criar uma wheel. Isso garante que a distribuição de código-fonte realmente funcione e que ela não instale acidentalmente menos arquivos do que as wheels oficiais.

O ideal é usar também a distribuição fonte para executar testes, compilar documentação e assim por diante, ou adicionar testes específicos para garantir que todos os arquivos necessários foram realmente incluídos. É compreensível que isso exija mais esforço, então não há problema em não fazer isso — os empacotadores posteriores reportarão imediatamente quaisquer arquivos ausentes.

Não utilize a internet durante o processo de construção

Por que?

As construções downstream são frequentemente realizadas em ambientes isolados, sem acesso à internet. Os códigos-fonte dos pacotes são descompactados nesse ambiente e todas as dependências necessárias são instaladas.

Mesmo este não sendo o caso, e assumindo que você teve cuidado suficiente para autenticar os downloads, usar a internet é desencorajado pelas seguintes razões:

  • A conexão a internet pode ser instável (ex. devido a má recepção) or apresentar problemas temporários que podem causar a falha ou atraso no processo.

  • Os recursos remotos podem ficar indisponíveis temporariamente ou até permanente, tornando a construção impossível. Isto é especialmente problemático quando alguém precisa construir um pacote de versão antiga.

  • Os recursos remotos podem mudar, tornando a construção não reprodutível.

  • O acesso a servidores remotos representa um problema de privacidade e um possível problema de segurança, pois expõe informações sobre o sistema que cria o pacote.

  • O usuário pode estar usando um serviço com um plano de dados limitado, no qual o acesso descontrolado à Internet pode resultar em cobranças adicionais ou outros inconvenientes.

Como?

Se o pacote estiver implementando quaisquer ações de backend de construção personalizadas que usem a Internet, por exemplo, baixando automaticamente dependências fornecidas ou buscando submódulos do Git, sua distribuição fonte deverá incluir todos esses arquivos ou permitir o provisionamento externo, e a Internet não deverá ser usada se os arquivos já estiverem presentes.

Observe que este ponto não se aplica às dependências do Python especificadas nos metadados do pacote e obtidas durante o processo de construção e instalação por frontends (como construir ou pip). Os downstreams utilizam frontends que utilizam provisionamento local para dependências do Python.

O ideal é que scripts de construção personalizados nem tentem acessar a internet, a menos que explicitamente solicitados. Se algum recurso estiver faltando e precisar ser recuperado, eles devem primeiro solicitar a permissão do usuário. Se isso não for viável, a melhor solução é fornecer uma opção de desativação para desabilitar todo o acesso à internet. Isso pode ser feito, por exemplo, verificando se uma variável de ambiente NO_NETWORK está definida com um valor não vazio.

Como os downstreams frequentemente também executam testes e criam documentação, o ideal é que o exposto acima se estenda também a esses processos.

Lembre-se também de que, se você estiver buscando recursos remotos, será absolutamente necessário verificar a autenticidade deles (geralmente por meio de um hash) para evitar que o arquivo seja substituído por uma parte mal-intencionada.

Suporte à construção com dependências do sistema

Por que?

Alguns projetos Python possuem dependências que não são Python, como bibliotecas escritas em C ou C++. Tentar usar as versões de sistema dessas dependências em pacotes upstream pode causar uma série de problemas para os usuários finais:

  • As wheels publicadas exigem que uma versão binária compatível da biblioteca utilizada esteja presente no sistema do usuário. Se a biblioteca estiver ausente ou uma versão incompatível estiver instalada, o pacote Python poderá falhar com erros que não são claros para usuários inexperientes, ou até mesmo apresentar mau funcionamento em tempo de execução.

  • Construir a partir de uma distribuição fonte requer que uma versão compatível com a origem da dependência esteja presente, juntamente com seus cabeçalhos de desenvolvimento e outros arquivos auxiliares que alguns sistemas empacotam separadamente da própria biblioteca.

  • Mesmo para um usuário experiente, instalar uma versão de dependência compatível pode ser difícil. Por exemplo, a distribuição Linux utilizada pode não prover a versão necessária, ou um pacote diferente pode exigir uma versão incompatível.

  • A ligação entre o pacote Python e sua dependência do sistema não é registrada pelo sistema de empacotamento. A próxima atualização do sistema pode atualizar a biblioteca para uma versão mais recente, o que quebra a compatibilidade binária com o pacote Python e requer intervenção do usuário para correção.

Por esses motivos, você pode decidir, com razão, vincular suas dependências estaticamente ou fornecer cópias locais no pacote instalado. Você também pode vender a dependência na sua distribuição fonte. Às vezes, essas dependências também são reempacotadas no PyPI e podem ser declaradas como dependências de projeto, como qualquer outro pacote Python.

No entanto, nenhuma dessas questões se aplica ao empacotamento downstream, e os downstreams têm bons motivos para preferir vincular dinamicamente às dependências do sistema. Em particular:

  • Em muitos casos, o compartilhamento confiável de dependências dinâmicas entre componentes é uma grande parte do propósito de um ecossistema de empacotamento downstream. Ajudar a dar suporte a isso facilita o acesso dos usuários desses sistemas a projetos upstream em seu formato preferido.

  • A vinculação estática e a venda obscurecem o uso de dependências externas, dificultando a auditoria da fonte.

  • A vinculação dinâmica possibilita a substituição rápida e sistemática das bibliotecas usadas em todo um ecossistema de empacotamento downstream, o que pode ser particularmente importante quando elas contêm uma vulnerabilidade de segurança ou um bug crítico.

  • O uso de dependências do sistema faz com que o pacote se beneficie da personalização posterior, o que pode melhorar a experiência do usuário em uma plataforma específica, sem que os mantenedores posteriores precisem aplicar patches constantemente nas dependências fornecidas em diferentes pacotes. Isso pode incluir melhorias de compatibilidade e reforço da segurança.

  • A vinculação estática e a venda de versões podem resultar no carregamento de várias versões diferentes da mesma biblioteca no mesmo processo (por exemplo, ao tentar importar dois pacotes Python vinculados a versões diferentes da mesma biblioteca). Isso às vezes funciona sem incidentes, mas também pode levar a erros de carregamento de bibliotecas, bugs sutis de tempo de execução e falhas catastróficas (como travamentos repentinos e perda de dados).

  • Por último, mas não menos importante, a ligação estática e o fornecimento resulta em duplicação, e pode aumentar o uso de espaço e memória de disco.

Como?

Um bom meio-termo entre as necessidades de ambas as partes é fornecer uma alternância entre o uso de dependências do fornecedor e do sistema. Idealmente, se o pacote tiver várias dependências do fornecedor, ele deve fornecer alternâncias individuais para cada dependência e uma alternância geral para controlar o padrão para elas, por exemplo, por meio de uma variável de ambiente USE_SYSTEM_DEPS.

Se o usuário solicitar usando dependências do sistema e uma dependência específica estiver ausente ou for incompatível, a construção deverá falhar com uma mensagem explicativa, em vez de retornar a uma versão do fornecedor. Isso dá ao empacotador a oportunidade de perceber o erro e decidir conscientemente como resolvê-lo.

É razoável que projetos upstream deixem os testes de construção com dependências do sistema para seus reempacotadores downstream. O objetivo destas diretrizes é facilitar uma colaboração mais eficaz entre projetos upstream e reempacotadores downstream, e não sugerir que projetos upstream assumam tarefas que os reempacotadores downstream estão mais bem equipados para lidar.

Ofereça suporte a testes no downstream

Por que?

Diversos projetos downstream realizam algum grau de teste nos projetos Python empacotados. Dependendo do caso específico, isso pode variar de testes de fumaça mínimos a execuções abrangentes do conjunto de testes completo. Pode haver vários motivos para isso, por exemplo:

  • Verificar se o empacotamento posterior não introduziu nenhum bug.

  • Testar em plataformas adicionais que não estão cobertas por testes upstream.

  • Encontrando bugs sutis que só podem ser reproduzidos com hardware específico, certas versões de pacotes de sistemas, e assim por diante.

  • Testar o pacote lançado em relação a versões de dependência mais recentes (ou mais antigas) do que as presentes durante os testes de versão upstream.

  • Testar o pacote em um ambiente muito semelhante à configuração de produção. Isso pode detectar problemas causados por interações não triviais entre diferentes pacotes instalados, incluindo pacotes que não são dependências do seu pacote, mas que, mesmo assim, podem causar problemas.

  • Testar o pacote lançado em novas versões do Python (incluindo os pontos mais recentes), ou implementações menos testadas do Python, como o PyPy.

É verdade que, às vezes, testes downstream podem gerar falsos positivos ou relatórios de bugs sobre cenários que o projeto upstream não tem interesse em suportar. No entanto, talvez com ainda mais frequência, eles notificam problemas com antecedência ou encontram bugs não triviais que, de outra forma, causariam problemas para os usuários do projeto upstream. Embora erros aconteçam, a maioria dos empacotadores downstream faz o possível para verificar seus resultados e ajudar os mantenedores upstream a triar e corrigir os bugs relatados.

Como?

Há uma série de medidas que os projetos upstream podem tomar para ajudar os reempacotadores downstream a testar seus pacotes de forma eficiente e eficaz, incluindo algumas das sugestões já mencionadas. Essas são, em geral, melhorias que tornam o conjunto de testes mais confiável e fácil de usar para todos, não apenas para os empacotadores downstream. Algumas sugestões específicas são:

  • Inclua os arquivos de teste e acessórios na distribuição fonte ou torne possível baixá-los facilmente separadamente.

  • Não escreva nos diretórios de pacotes durante os testes. Às vezes, as configurações de teste posteriores executam testes sobre o pacote instalado, e modificações realizadas durante os testes e arquivos de teste temporários podem acabar fazendo parte do pacote instalado!

  • Make the test suite work offline. Mock network interactions, using packages such as responses or vcrpy. If that is not possible, make it possible to easily disable the tests using Internet access, e.g. via a pytest marker. Use pytest-socket to verify that your tests work offline. This often makes your own test workflows faster and more reliable as well.

  • Make your tests work without a specialized setup, or perform the necessary setup as part of test fixtures. Do not ever assume that you can connect to system services such as databases — in an extreme case, you could crash a production service!

  • If your package has optional dependencies, make their tests optional as well. Either skip them if the needed packages are not installed, or add markers to make deselecting easy.

  • More generally, add markers to tests with special requirements. These can include e.g. significant space usage, significant memory usage, long runtime, incompatibility with parallel testing.

  • Do not assume that the test suite will be run with -Werror. Downstreams often need to disable that, as it causes false positives, e.g. due to newer dependency versions. Assert for warnings using pytest.warns() rather than pytest.raises()!

  • Aim to make your test suite reliable and reproducible. Avoid flaky tests. Avoid depending on specific platform details, don’t rely on exact results of floating-point computation, or timing of operations, and so on. Fuzzing has its advantages, but you want to have static test cases for completeness as well.

  • Split tests by their purpose, and make it easy to skip categories that are irrelevant or problematic. Since the primary purpose of downstream testing is to ensure that the package itself works, downstreams are not generally interested in tasks such as checking code coverage, code formatting, typechecking or running benchmarks. These tests can fail as dependencies are upgraded or the system is under load, without actually affecting the package itself.

  • If your test suite takes significant time to run, support testing in parallel. Downstreams often maintain a large number of packages, and testing them all takes a lot of time. Using pytest-xdist can help them avoid bottlenecks.

  • Ideally, support running your test suite via pytest. pytest has many command-line arguments that are truly helpful to downstreams, such as the ability to conveniently deselect tests, rerun flaky tests (via pytest-rerunfailures), add a timeout to prevent tests from hanging (via pytest-timeout) or run tests in parallel (via pytest-xdist). Note that test suites don’t need to be written with pytest to be executed with pytest: pytest is able to find and execute almost all test cases that are compatible with the standard library’s unittest test discovery.

Aim for stable releases

Por que?

Many downstreams provide stable release channels in addition to the main package streams. The goal of these channels is to provide more conservative upgrades to users with higher stability needs. These users often prefer to trade having the newest features available for lower risk of issues.

While the exact policies differ, an important criterion for including a new package version in a stable release channel is for it to be available in testing for some time already, and have no known major regressions. For example, in Gentoo Linux a package is usually marked stable after being available in testing for a month, and being tested against the versions of its dependencies that are marked stable at the time.

However, there are circumstances which demand more prompt action. For example, if a security vulnerability or a major bug is found in the version that is currently available in the stable channel, the downstream is facing a need to resolve it. In this case, they need to consider various options, such as:

  • putting a new version in the stable channel early,

  • adding patches to the version currently published,

  • ou até retornando o canal estável a uma versão anterior ao lançamento.

Cada opção envolve certos riscos e certo trabalho, e os empacotadoes devem ponderar suas opções para determinar seu curso de ação .

Como?

There are some things that upstreams can do to tailor their workflow to stable release channels. These actions often are beneficial to the package’s users as well. Some specific suggestions are:

  • Adjust the release frequency to the rate of code changes. Packages that are released rarely often bring significant changes with every release, and a higher risk of accidental regressions.

  • Avoid mixing bug fixes and new features, if possible. In particular, if there are known bug fixes merged already, consider making a new release before merging feature branches.

  • Considere fazer pré-lançamentos após grandes mudanças, provendo assim mais oportunidade de teste para usuários e pessoas interessadas.

  • If your project is subject to very intense development, consider splitting one or more branches that include a more conservative subset of commits, and are released separately. For example, Django currently maintains three release branches in addition to main.

  • Even if you don’t wish to maintain additional branches permanently, consider making additional patch releases with minimal changes to the previous version, especially when a security vulnerability is discovered.

  • Split your changes into focused commits that address one problem at a time, to make it easier to cherry-pick changes to earlier releases when necessary.