Poesia pythonista com Poetry

O pip tem dado passos importantes em relação à sua evolução, sendo um dos mais notórios deles a adoção por padrão de backtracking. Ainda assim, por suas atribuições serem exclusivamente de um gerenciador de dependências, ele pode deixar um gostinho de "quero mais" no seu flow de desenvolvimento. E é nessas necessidades não atendidas que algumas opções se apresentam como alternativas/complementos ao simpático utilitário de linha de comando.

O Pipfile é uma delas, e já falamos sobre no "Tchau, requirements. Olá, Pipfile!". Demorou um pouco, mas hoje vamos falar do seu concorrente direto, o Poetry.

Determinismo é só o começo

I built Poetry because I wanted a single tool to manage my Python projects from start to finish. I wanted something reliable and intuitive that the community could use and enjoy.

Essas são as palavras do criador do Poetry, Sébastien Eustace. O mesmo argumento usado no artigo sobre Pipfile, acerca de build determinístico, se sustenta aqui. E assim como o Pipenv, há uma camada extra de funcionalidades que "adoçam" o build e o controle de virtualenvs.

Meme de Guerra Civil com Capitão América e Homem de Ferro
Não há razão para flamewar entre pip x {pipenv, poetry} (freethinkingministries.com)

A sua instalação pode ocorrer de maneiras diferentes, dependendo do SO que você estiver utilizando. Portanto, uma visita à sua documentação é essencial. Ainda assim, nada que o pip (que ironia) não resolva:

$ pip install --user poetry

Para compreender como a ferramenta funciona de fato, vamos seguir os mesmos passos do getting started da documentação oficial:

$ poetry new poetry-demo
$ cd poetry-demo/
$ ls
README.rst     poetry_demo    pyproject.toml tests

Aqui nos deparamos com a primeira peculiaridade: o arquivo pyproject.toml.

Pyproject? TOML?

O pip utiliza o requirements.txt, o Pipenv utiliza o Pipfile e Pipfile.lock, e o Poetry o pyproject.toml e poetry.lock. Todos os arquivos citados são utilizados pelas respectivas ferramentas de controle de pacotes para gerenciar atualizações e instalar novas dependências.

O TOML (Tom's Obvious, Minimal Language) é um formato utilizado para a escrita de arquivos de configuração. Segundo o site oficial:

TOML aims to be a minimal configuration file format that's easy to read due to obvious semantics. TOML is designed to map unambiguously to a hash table. TOML should be easy to parse into data structures in a wide variety of languages.

Se você já conhece o Pipfile, a sintaxe utilizada no arquivo é a mesma. Mas qual é a motivação por trás do arquivo pyproject?

Senta que lá vem a história.

Antes do ano de 1998 havia o vazio, quando o assunto era empacotamento em Python. A partir dos anos 2000, instalar, buildar, ou distribuir pacotes começou a ganhar um novo sentido através da inserção da biblioteca distutils ao standard lib, e da convenção de uso do entrypoint setup.py.

Diagrama de fluxo com os passos para distribuição de pacotes
Overview do workflow de desenvolvimento Python (py-generic-project.readthedocs.io)

Quatro anos depois, somos apresentados ao setuptools, que introduz o pacote no formato egg e a possibilidade de declarar e instalar dependências. Já em 2008, o pip é lançado como uma alternativa aprimorada ao seu primo mais velho, o easy_install, que possibilitava a instalação de pacotes consultando um índice centralizado na internet, chamado Python Package Index (pypi).

E após indas e vindas (e um novo formato, chamado "wheel"), em 2014 o pip e o setuptools viram finalmente o padrão para empacotamento em Python.

Confira a timeline completa no Packaging History.

O ovo ou a galinha?

A PEP 518 é quem apresenta o arquivo pyproject.toml. Segundo Brett Cannon, criador da proposta:

(...) the purpose of PEP 518 was to come up with a way for projects to specify what build tools they required. That's it, real simple and straightforward.

Antes dela, não havia uma forma (prática) de dizer quais ferramentas de build que um projeto requer para gerar um pacote wheel. Embora o setuptools possua um argumento setup_requires para este fim, você não consegue ler esse parâmetro sem antes ter o próprio setuptools instalado, e não havia um lugar onde você pudesse especificar que precisa do setuptools para ler a configuração dentro do setuptools (notou o problema do ovo-galinha?).

E como vivemos até então sem nos preocupar com esse problema? Simples! Ferramentas como o próprio pip injetam o setuptools e o wheel quando executando um arquivo setup.py.

E se então tivéssemos um lugar para dizer ao pip sobre a necessidade dessas ferramentas?

# pyproject.toml

[build-system]
requires = ["setuptools >= 40.6.0", "wheel"]
build-backend = "setuptools.build_meta"

Booomm!!! Se amanhã alternativas mais interessantes ao setuptools ou pip aparecerem, estaremos preparados.

E o que o Poetry tem a ver com isso?

O Poetry declara qual será o build backend através da utilização das PEPs 518 (citada acima) e 517:

# pyproject.toml

[tool.poetry]
name = "poetry-demo"
version = "0.1.0"
description = ""
authors = ["Klaus Laube <mail@mail.com>"]

[tool.poetry.dependencies]
python = "^3.7"

[tool.poetry.dev-dependencies]
pytest = "^5.2"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

O cliente, também instalado através do pip install anterior, sabe se comunicar com o poetry.core, explicitamente declarado na seção [build-system].

E com isso o Poetry não apenas cuidará das dependências do seu projeto, como também se responsabilizará por buildar o seu app ou lib, e distribuí-lo no Python Package Index.

Continuando a poesia

Para adicionar uma dependência ao projeto, passamos a utilizar o comando poetry add:

$ poetry add django

Com isso, o Django passa a ser adicionado ao "requirements":

# pyproject.toml

...

[tool.poetry.dependencies]
python = "^3.7"
Django = "^3.1.4"

...

Por consequência um arquivo poetry.lock foi criado, e é ele quem garantirá o determinismo durante a instalação de dependências do projeto:

$ poetry install

Installing dependencies from lock file

Outra consequência do comando add foi a criação automática de um virtualenv. Agora, através do comando poetry run, podemos executar dependências Python diretamente do ambiente virtual:

$ poetry run django-admin.py -h

Ou ainda através do comando shell:

$ poetry shell
$ which django-admin.py
/Users/klauslaube/Library/Caches/pypoetry/virtualenvs/poetry-demo-IqTsQBT7-py3.9/bin/django-admin

Note que em um ambiente diferente de development, talvez você precise do parâmetro --no-root:

$ poetry install --no-root

Conclusão

Há cerca de 3 meses eu retornei do Java ao Python, e tinha esquecido (ou nunca de fato notado) o quão não intuitivo é a dança entre pyenv, virtualenv e pip.

Nisso, tanto Pipenv quanto Poetry são fenomenais. O Pipenv leva certa vantagem, uma vez que é capaz de gerenciar até mesmo a versão do Python (com Poetry, ainda necessitamos do pyenv).

Aparentemente há planos do pip suportar um argumento --pipfile, o que faria do Pipfile (e por consequência Pipenv) uma opção ainda mais atraente que o concorrente.

Mas tenho certeza que o Poetry será uma mão na roda caso você esteja escrevendo libs.

Até a próxima.

Referências