Versionamento

Esta discussão cobre todos os aspectos do versionamento de pacotes Python.

Números de versão validos

Diferentes projetos Python podem usar diferentes esquemas de versionamento com base nas necessidades daquele projeto específico, mas para serem compatíveis com ferramentas como pip, todos eles são obrigados a cumprir um formato flexível para identificadores de versão, para o qual a referência oficial é a especificação dos especificadores de versão. Aqui estão alguns exemplos de números de versão [1]:

  • Uma versão simples (versão final): 1.2.0

  • Uma versão de desenvolvimento: 1.2.0.dev1

  • Uma versão alfa: 1.2.0a1

  • Uma versão beta: 1.2.0b1

  • Um candidato a lançamento: 1.2.0rc1

  • Um pós-lançamento: 1.2.0.post1

  • Um pós-lançamento de uma versão alfa (possível, mas desencorajado): 1.2.0a1.post1

  • Uma versão simples com apenas dois componentes: 23.12

  • Uma versão simples com apenas um componente: 42

  • Uma versão com uma época: 1!1.0

Os projetos podem usar um ciclo de pré-lançamentos para dar suporte aos testes de seus usuários antes do lançamento final. Em ordem, as etapas são: versões alfa, versões beta, candidatos a lançamento, versão final. Pip e outros instaladores modernos de pacotes Python ignoram pré-lançamentos por padrão ao decidir quais versões de dependências instalar, a menos que solicitado explicitamente (por exemplo, com `pip install pkg==1.1a3 ou pip install --pre pkg).

O objetivo dos lançamentos de desenvolvimento é oferecer suporte aos lançamentos feitos no início de um ciclo de desenvolvimento, por exemplo, uma construção noturna ou uma construção a partir do código-fonte mais recente em uma distribuição Linux.

Os pós-lançamentos são usados para corrigir pequenos erros em uma versão final que não afetam o software distribuído, como corrigir um erro nas notas de versão. Eles não devem ser usados para correção de bugs; isso deve ser feito com uma nova versão final (por exemplo, incrementando o terceiro componente ao usar o versionamento semântico).

Por fim, as épocas, um recurso raramente usado, servem para corrigir a ordem de classificação ao alterar o esquema de versionamento. Por exemplo, se um projeto estiver usando versionamento de calendário, com versões como 23.12, e mudar para versionamento semântico, com versões como 1.0, a comparação entre 1.0 e 23.12 irá para o lado errado. Para corrigir isso, os novos números de versão devem ter uma época explícita, como em “1!1.0”, para serem tratados como mais recentes que os números de versão antigos.

Versionamento semântico vs. versionamento de calendário

Um esquema de versionamento é uma forma formalizada de interpretar os segmentos de um número de versão e de decidir qual deve ser o próximo número de versão para um novo lançamento de um pacote. Dois esquemas de versionamento são comumente usados para pacotes Python, versionamento semântico e versionamento de calendário.

Cuidado

A decisão de qual número de versão escolher cabe ao mantenedor do projeto. Isso efetivamente significa que os incrementos de versão refletem a visão do mantenedor. Essa visão pode diferir da percepção dos usuários finais sobre o que o referido esquema de versionamento formalizado lhes promete.

There are known exceptions for selecting the next version number. The maintainers may consciously choose to break the assumption that the last version segment only contains backwards-compatible changes. One such case is when a security vulnerability needs to be addressed. Security releases often come in patch versions but contain breaking changes inevitably.

Versionamento semântico

A ideia do versionamento semântico (ou SemVer) é usar números de versão de 3 partes, principal.menor.correção, onde o autor do projeto incrementa:

  • principal quando são feitas alterações incompatíveis à API,

  • menor quando é adicionada funcionalidade de maneira compatível com versões anteriores, e

  • correção, quando são feitas correções de bugs compatíveis com versões anteriores.

A maioria dos projetos Python usa um esquema que se assemelha ao versionamento semântico. No entanto, a maioria dos projetos, especialmente os maiores, não aderem estritamente ao versionamento semântico, uma vez que muitas alterações são tecnicamente prejudiciais, mas afetam apenas uma pequena fração dos usuários. Tais projetos tendem a aumentar o número maior quando a incompatibilidade é alta, ou para sinalizar uma mudança no projeto, ao invés de qualquer pequena incompatibilidade [2]. Por outro lado, um aumento no número da versão principal às vezes é usado para sinalizar novos recursos significativos, mas compatíveis com versões anteriores.

Para aqueles projetos que usam controle de versão semântico estrito, esta abordagem permite que os usuários façam uso de especificadores de versão de lançamento compatíveis, com o operador ~=. Por exemplo, name ~= X.Y é aproximadamente equivalente a name >= X.Y, == X.*, ou seja, requer pelo menos a versão X.Y, e permite qualquer versão posterior com Y maior, desde que X é o mesmo. Da mesma forma, name ~= X.Y.Z é aproximadamente equivalente a name >= X.Y.Z, == X.Y.*, ou seja, requer pelo menos X.Y.Z e permite uma versão posterior com o mesmo X e Y, mas Z superior.

Projetos Python que adotam versões semânticas devem obedecer às cláusulas 1-8 da especificação de Versionamento Semântico 2.0.0.

O popular gerador de documentação Sphinx é um exemplo de projeto que usa controle de versionamento semântico estrito (Política de versionamento do Sphinx). O famoso pacote de computação científica NumPy usa explicitamente versionamento semântico “folgado”, onde lançamentos incrementando a versão menor podem conter alterações de API incompatíveis com versões anteriores (Política de versionamento do NumPy).

Versionamento de calendário

Semantic versioning is not a suitable choice for all projects, such as those with a regular time-based release cadence and a deprecation process that provides warnings for a number of releases prior to removal of a feature.

Uma vantagem principal do versionamento baseado em data, ou versionamento de calendário (CalVer), é que é simples dizer quantos anos o conjunto de recursos básicos de uma determinada versão recebe apenas o número da versão.

Os números de versão do calendário normalmente assumem o formato ano.mês (por exemplo, 23.12 para dezembro de 2023).

Pip, o instalador padrão do pacote Python, usa versionamento de calendário.

Outros esquemas

O versionamento serial refere-se ao esquema de versionamento mais simples possível, que consiste em um único número incrementado a cada versão. Embora o versionamento serial seja muito fácil de gerenciar como desenvolvedor, é o mais difícil de rastrear como usuário final, pois os números de versão serial transmitem pouca ou nenhuma informação sobre compatibilidade com versões anteriores da API.

Combinations of the above schemes are possible. For example, a project may combine date-based versioning with serial versioning to create a year.serial numbering scheme that readily conveys the approximate age of a release, but doesn’t otherwise commit to a particular release cadence within the year.

Identificadores de versão local

Identificadores de versão pública são projetados para suportar distribuição via PyPI. As ferramentas de empacotamento do Python também oferecem suporte à noção de um identificador de versão local, que pode ser usado para identificar compilações de desenvolvimento local não destinadas à publicação, ou variantes modificadas de uma versão mantida por um redistribuidor.

Um identificador de versão local assume a forma de um identificador de versão pública, seguido por “+” e um rótulo de versão local. Por exemplo, um pacote com patches específicos do Fedora aplicados poderia ter a versão “1.2.1+fedora.4”. Outro exemplo são as versões calculadas pelo setuptools-scm, um plugin do setuptools que lê a versão dos dados do Git. Em um repositório Git com alguns commits desde a versão mais recente, setuptools-scm gera uma versão como “0.5.dev1+gd00980f”, ou se o repositório tiver alterações não rastreadas, como “0.5.dev1+gd00980f.d20231217”.

Accessing version information at runtime

Version information for all distribution packages that are locally available in the current environment can be obtained at runtime using the standard library’s importlib.metadata.version() function:

>>> importlib.metadata.version("cryptography")
'41.0.7'

Many projects also choose to version their top level import packages by providing a package level __version__ attribute:

>>> import cryptography
>>> cryptography.__version__
'41.0.7'

This technique can be particularly valuable for CLI applications which want to ensure that version query invocations (such as pip -V) run as quickly as possible.

Package publishers wishing to ensure their reported distribution package and import package versions are consistent with each other can review the Mantendo uma única fonte da versão do pacote discussion for potential approaches to doing so.

As import packages and modules are not required to publish runtime version information in this way (see the withdrawn proposal in PEP 396), the __version__ attribute should either only be queried with interfaces that are known to provide it (such as a project querying its own version or the version of one of its direct dependencies), or else the querying code should be designed to handle the case where the attribute is missing [3].

Some projects may need to publish version information for external APIs that aren’t the version of the module itself. Such projects should define their own project-specific ways of obtaining the relevant information at runtime. For example, the standard library’s ssl module offers multiple ways to access the underlying OpenSSL library version:

>>> ssl.OPENSSL_VERSION
'OpenSSL 3.2.2 4 Jun 2024'
>>> ssl.OPENSSL_VERSION_INFO
(3, 2, 0, 2, 0)
>>> hex(ssl.OPENSSL_VERSION_NUMBER)
'0x30200020'