Pular para o conteúdo principal

Gerenciamento de Versões, Ambientes Virtuais e Dependências com Pyenv e Poetry

Quem é desenvolvedor de software sempre tem vários projetos no computador usando diferentes linguagens, versões dessas linguagens, bibliotecas e ferramentas. Para que um projeto não interfira no outro, é importante que sejam isolados de alguma forma. No caso de projetos Python, vamos precisar de ferramentas que gerenciem versões do Python, ambientes virtuais e dependências de projeto. Existem várias opções, mas vamos nos concentrar em duas:

  1. pyenv gerencia diferentes versões do Python na mesma máquina
  2. poetry gerencia ambientes virtuais e as dependências do projeto dentro desses ambientes virtuais.

Como Funcionam Ambientes Virtuais do Python

A não ser que você especifique o caminho completo, um comando precisa ser pesquisado pelo sistema operacional para ser executado. A pesquisa é feita em uma lista de diretórios registrada na variável de ambiente chamada PATH. Quando a primeira ocorrência é encontrada, a busca é interrompida e o programa é executado.

Por exemplo, se PATH contém $HOME/.local/bin:/usr/local/bin:/usr/bin e você executa o comando python --version, então o shell buscará por um executável de nome python primeiro em $HOME/.local/bin, depois em /usr/local/bin e por último em /usr/bin. O primeiro arquivo executável de nome python que for encontrado interrompe a busca e é executado com o parâmetro --version. Se nenhum arquivo for encontrado, então uma mensagem de erro é exibida.

O próximo conceito é que um ambiente virtual no Python é um apenas um diretório que contém a versão desejada do Python e das bibliotecas necessárias para o projeto. A ativação ou desativação de um ambiente virtual é feita, basicamente, manipulando a lista de caminhos contida em PATH. A ativação põe o diretório do ambiente virtual no começo lista de caminhos, e a desativação o remove da lista da sessão atual.

Instalando e Gerenciando Versões do Python com pyenv

Com vários projetos antigos e novos no mesmo computador, é bem provável que cada um use uma versão diferente do Python. A ferramenta que vai nos permitir instalar e escolher onde e qual versão do Python usar é o pyenv.

Instalação do pyenv

No Ubuntu/Debian/Mint, a instalação do pyenv é feita por um instalador próprio:

$ curl https://pyenv.run | bash

No MacOs, use o brew para instalar o pyenv:

$ brew update
$ brew install pyenv

Para que o pyenv funcione corretamente, é necessário adicionar as linhas a seguir no seu arquivo de configuração do shell. Para Bash, este arquivo é ~/.bashrc:

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"

Em seguida é necessário iniciar uma nova sessão do terminal ou executar exec $SHELL para reiniciar a sessão corrente.

Instalação de Versões do Python

Para ver todas as versões do Python disponíveis para instalação, use o comando pyenv install --list. Como são muitas opções, melhor filtrar usando grep:

$ pyenv install --list | grep ' 3.9'
  3.9.0
  3.9-dev
  3.9.1
  3.9.2
  3.9.3
  3.9.4

Para instalar as versões 3.7.10 e 3.9.4, os comandos são:

$ pyenv install 3.7.10
$ pyenv install 3.9.4

As versões instaladas disponíveis são listadas pelo comando pyenv versions:

$ pyenv versions
* system (set by $HOME/.pyenv/version)
  3.7.10
  3.9.4

Como Funciona o pyenv

O pyenv insere um caminho $HOME/.pyenv/shims no começo de PATH para interceptar chamadas a comandos python e a outros comandos relacionados. Esta interceptação leva a executáveis intermediários, que redirecionam as chamadas para uma versão específica do Python de acordo com a primeira configuração encontrada, nesta ordem:

  1. Shell. Versão registrada na variável de ambiente PYENV_VERSION. Você pode usar o comando pyenv shell <versão> para configurar essa variável na sua sessão atual do shell, ou usar outro comando equivalente, tal como export PYENV_VERSION=<versão>.
  2. Local. Versão registrada no arquivo .python-version, que é procurado recursivamente a partir do diretório atual até chegar no diretório-raiz. Você pode usar o comando pyenv local <versão> para gerar este arquivo.
  3. Global. Versão registrada no arquivo $(pyenv root)/version, que pode ser gerado pelo comando pyenv global <versão>.
  4. Sistema. Se nenhuma configuração anterior for encontrada, é usada a versão do Python instalada no sistema operacional.

Ambientes Virtuais e Gerenciamento de Dependências com poetry

Instalação do poetry

O poetry pode ser instalado de várias maneiras. A mais recomendada é através do instalador próprio, que evita que as dependências do poetry se misturem com as de outras bibliotecas:

$ curl https://install.python-poetry.org | python -

No Linux, a instalação é feita no diretório $HOME/.local/bin. Se este diretório não está no seu PATH, então é necessário adicioná-lo manualmente no seu arquivo de configuração do shell (~/.bashrc ou ~/.profile) com as seguintes linhas:

if [ -d "$HOME/.local/bin" ] ; then
    PATH="$HOME/.local/bin:$PATH"
fi

# a configuração do pyenv entra aqui

Para testar a instalação, execute:

$ poetry --version

Se o comando não retornar nenhuma mensagem de erro, então a instalação foi concluída com sucesso!

Configuração Inicial do poetry

A configuração do poetry é mantida no arquivo $HOME/.config/pypoetry/config.toml. Mas ao invés de acessar este arquivo diretamente, você deve usar o comando poetry config e seus subcomandos.

Para listar as configurações, execute:

$ poetry config --list

Uma das configurações que você deve modificar logo após a instalação é a que define o local de instalação dos ambientes virtuais. Ao invés de manter a configuração padrão que cria ambientes virtuais dentro do diretório $HOME/.cache/pypoetry, é melhor que cada ambiente virtual seja criado dentro da raiz do projeto com o nome de .venv porque editores tais como VSCode consigam achá-lo automaticamente e configurar o intellisense.

Para mudar o local de instalação do ambiente virtual para dentro do projeto, execute:

$ poetry config virtualenvs.in-project true

Comandos do poetry

Para exibir os comandos disponíveis, execute:

$ poetry

Vamos nos concentrar em alguns relacionados com as tarefas mais comuns:

  1. Criar um novo projeto
  2. Gerenciar as dependências
  3. Gerenciar o ambiente virtual

Criando Um Novo Projeto

O comando poetry new cria um novo projeto já com uma estrutura básica de diretórios e arquivos. Por exemplo:

$ poetry new projeto-x --name app

resulta na seguinte estrutura:

projeto-x
├── app
│   └── __init__.py
├── pyproject.toml
├── README.rst
└── tests
    ├── __init__.py
    └── test_app.py

--name é opcional e permite definir um nome diferente para o diretório onde ficarão os arquivos do pacote do projeto. O padrão do poetry é usar o mesmo nome do projeto.

O arquivo pyproject.toml é o arquivo de configuração do projeto. Contém informações como o nome, versão, dependências, etc.

Gerenciando as Dependências

Os principais comandos relacionados com o gerenciamento de dependências são:

  1. poetry add. Adiciona uma dependência ao projeto.
  2. poetry remove. Remove uma dependência do projeto.
  3. poetry show. Exibe as dependências do projeto.
  4. poetry update. Atualiza as dependências do projeto.

As dependências do projeto podem ser divididas em dois grupos: de pacotes essenciais para o funcionamento do projeto e de pacotes usados apenas para o desenvolvimento do projeto, em atividades tais como teste e linting. Na implantação do projeto em um ambiente de estágio ou de produção, é necessário instalar os pacotes do primeiro grupo, mas os do segundo devem ser evitados para diminuir o tamanho da instalação.

A adição dos pacotes principais é feita pelo comando poetry add. Por exemplo:

$ poetry add fastapi jinja2 aioredis[hiredis] databases loguru passlib[argon2]
Creating virtualenv projeto-x in /tmp/projeto-x/.venv
Using version ^0.70.0 for fastapi
Using version ^3.0.3 for Jinja2
Using version ^2.0.0 for aioredis
Using version ^0.5.3 for databases
Using version ^0.5.3 for loguru
Using version ^1.7.4 for passlib

Updating dependencies
Resolving dependencies... (8.8s)

Writing lock file

Package operations: 28 installs, 0 updates, 0 removals

• Installing idna (3.3)
• Installing pycparser (2.21)
• Installing sniffio (1.2.0)
• Installing anyio (3.4.0)
• Installing cffi (1.15.0)
• Installing greenlet (1.1.2)
...

Uma vez que as versões não foram especificadas explicitamente, as versões mais recentes disponíveis dos pacotes e suas dependências são usadas.

Para adicionar os pacotes de desenvolvimento, acrescente a opção --dev ao comando poetry add:

$ poetry add --dev pytest alt-pytest-asyncio pytest-cov asgi-lifespan isort blue mypy httpx
The following packages are already present in the pyproject.toml and will be skipped:

• pytest

If you want to update it to the latest compatible version, you can use `poetry update package`.
If you prefer to upgrade it to the latest available version, you can use `poetry add package@latest`.

Using version ^0.6.0 for alt-pytest-asyncio
Using version ^3.0.0 for pytest-cov
Using version ^1.0.1 for asgi-lifespan
Using version ^5.10.1 for isort
Using version ^0.7.0 for blue
Using version ^0.910 for mypy
Using version ^0.21.1 for httpx

Updating dependencies
Resolving dependencies... (10.3s)

Writing lock file

Package operations: 25 installs, 0 updates, 0 removals

• Installing appdirs (1.4.4)
• Installing certifi (2021.10.8)
• Installing click (8.0.3)
• Installing h11 (0.12.0)
...

À medida que os pacotes são adicionados, o arquivo pyproject.toml é atualizado com as informações sobre as dependências. Note que os pacotes de desenvolvimento ficam em uma seção separada:

[tool.poetry.dependencies]
python = "^3.10"
fastapi = "^0.70.0"
Jinja2 = "^3.0.3"
aioredis = {extras = ["hiredis"], version = "^2.0.0"}
databases = "^0.5.3"
loguru = "^0.5.3"
passlib = {extras = ["argon2"], version = "^1.7.4"}

[tool.poetry.dev-dependencies]
pytest = "^5.2"
alt-pytest-asyncio = "^0.6.0"
pytest-cov = "^3.0.0"
asgi-lifespan = "^1.0.1"
isort = "^5.10.1"
blue = "^0.7.0"
mypy = "^0.910"
httpx = "^0.21.1"

O poetry aceita versões baseadas no padrão SemVer (Major.Minor.Patch) e usa algumas convenções para a especificação da atualização de versões. A notação ^ indica que uma atualização de versão é permitida se o novo número da versão não modificar o dígito diferente de zero mais à esquerda no agrupamento Major.Minor.Patch. Por exemplo, poetry update fastapi aceitaria qualquer versão >=0.70.0 e <0.71.0.

Durante a instalação das dependências de desenvolvimento, você deve ter notado que o pacote pytest não foi instalado porque já tinha sido incluído pelo comando poetry new durante a criação do projeto. No entanto, a versão 5.2 não é a versão mais recente disponível. O primeiro impulso é tentar atualizar a versão usando o comando poetry update pytest, mas a especificação ^5.2 não permite que uma versão 6 seja usada. A solução é adicionar explicitamente a versão mais recente de pytest como dependência de desenvolvimento:

$ poetry add --dev pytest@latest
Using version ^6.2.5 for pytest

Updating dependencies
Resolving dependencies... (0.7s)

Writing lock file

Package operations: 1 install, 1 update, 2 removals

• Removing more-itertools (8.12.0)
• Removing wcwidth (0.2.5)
• Installing iniconfig (1.1.1)
• Updating pytest (5.4.3 -> 6.2.5)

Isto instala o pacote e também atualizada a seção [tool.poetry.dev-dependencies] em pyproject.toml com a nova versão.

Note que, além dos pacotes especificados, várias outras dependências indiretas são instaladas. A relação exata com todos os pacotes instalados, suas versões e outras informações adicionais são armazenadas no arquivo poetry.lock. Este arquivo garante que todos no projeto usam exatamente as mesmas versões de pacotes.

Ao invés de examinar o arquivo poetry.lock diretamente, a melhor forma de ver a relação completa de dependências do projeto é através do comando poetry show --tree:

$ poetry show --tree
aioredis 2.0.0 asyncio (PEP 3156) Redis support
├── async-timeout *
│   └── typing-extensions >=3.6.5
├── hiredis >=1.0
└── typing-extensions *
alt-pytest-asyncio 0.6.0 Alternative pytest plugin to pytest-asyncio
└── pytest >=3.0.6
    ├── atomicwrites >=1.0
    ├── attrs >=19.2.0
    ├── colorama *
    ├── iniconfig *
    ├── packaging *
    │   └── pyparsing >=2.0.2,<3.0.5 || >3.0.5
    ├── pluggy >=0.12,<2.0
    ├── py >=1.8.2
    └── toml *
asgi-lifespan 1.0.1 Programmatic startup/shutdown of ASGI apps.
└── sniffio *
blue 0.7.0 Blue -- Some folks like black but I prefer blue.
├── black 21.7b0
│   ├── appdirs *
│   ├── click >=7.1.2
│   │   └── colorama *
│   ├── mypy-extensions >=0.4.3
│   ├── pathspec >=0.8.1,<1
│   ├── regex >=2020.1.8
│   └── tomli >=0.2.6,<2.0.0
└── flake8 3.8.4
    ├── mccabe >=0.6.0,<0.7.0
    ├── pycodestyle >=2.6.0a1,<2.7.0
    └── pyflakes >=2.2.0,<2.3.0
...

Habilitando o Ambiente Virtual

Se você acabou de clonar o projeto, deve usar poetry install para criar o diretório do ambiente virtual e instalar as dependências.

Uma vez instalado em .venv, há duas opções de habilitar o ambiente virtual pelo terminal. A primeira é ativar o ambiente virtual com o comando poetry shell:

$ poetry shell

(projeto-x) $

O prompt da linha de comando é alterado para mostrar a ativação. Para desativar, você pode executar exit, apertar CTRL+D, ou apenas abrir um novo terminal.

A segunda opção é rodar um comando dentro do ambiente virtual mas sem ativá-lo permanentemente. Isso é feito pelo comando poetry run <comando>, que ativa o ambiente virtual, executa o comando internamente e depois desativa. É particularmente útil para ser rodados em scripts.

Seja, por exemplo, um projeto contendo um Makefile com a seguinte tarefa:

test:
    pytest --cov-report term-missing --cov-report html --cov-branch \
        --cov app/

Você pode ativar o ambiente virtual e depois executar make test para executar os testes, ou pode executar poetry run make test em um passo só.

Considerações Finais

Este artigo abordou os principais pontos que você precisa saber para usar a combinação pyenv + poetry no gerenciamento de ecossistemas independentes de projetos Python. Algumas considerações finais:

  1. Não compartilhe ambientes virtuais entre projetos diferentes.
  2. O diretório do ambiente virtual não deve ser mantido no controle de versão porque pode ser reconstruído sempre que necessário através dos arquivos pyproject.toml e poetry.lock. Estes, sim, devem fazer rastreados.
  3. Não manipule o diretório do ambiente virtual manualmente. Sempre use os comandos do poetry para isso.
  4. Para outros detalhes e comandos do poetry, visite a seção sobre comandos na documentação do projeto.

Referências Complementares

1 pyenv: How It Works
2 Managing Multiple Python Versions With pyenv
3 Modern Python Environments - dependency and workspace management
4 Pyenv + Poetry
5 Overview of python dependency management tools
6 Pipenv and Poetry: Benchmarks & Ergonomics
7 Pipenv and Poetry: Benchmarks & Ergonomics II

Comentários

Comments powered by Disqus