Poetry

 

여태껏 의존성 관리, 패키징을 하기 위해 setuptools를 사용해왔는데 언제부턴가 poetry(pyproject.toml) 가 종종 보이더니 이젠 poetry가 대세인 것 같은 모양새가 되었다.

Poetry
Poetry is a tool for dependency management and packaging in Python. It allows you to declare the libraries your project depends on and it will manage (install/update) them for you. Poetry offers a lockfile to ensure repeatable installs, and can build your project for distribution.

Poetry의 사용법에 대해 간단하게 알아보자.

1. Installation

Official site: https://python-poetry.org/docs#installation

개발환경: Linux(Rocky Linux 8.6), Ubuntu 22.04

1.1 Install Poetry

Installed directory: ~/.local/share/pypoetry

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

1.2 Add Poetry to your PATH

$ echo 'export PATH=$HOME/.local/bin:$PATH' >> ~/.bashrc

1.3 Uninstall

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

2. Dependency Managing

2.1 Update Poetry

$ poetry --version
$ poetry self update

2.2 Enable tab completion for Bash, Fish, or Zsh

왜인지 poetry가 shell completion을 지원해준다.

$ poetry completions bash >> ~/.bash_completion

2.3 Project setup

2.3.1 Initializing

$ poetry new poetry-demo
poetry-demo
├── pyproject.toml
├── README.md
├── poetry_demo
│   └── __init__.py
└── tests
    └── __init__.py
$ cd pre-existing-project
$ poetry init

2.3.2 Specifying dependencies

Dependency를 알려주는 pyproject.toml 의 기본구성은 다음과 같다.
각 항목에 대한 자세한 내용은 여기로.

[tool.poetry]
name = "poetry-demo"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"
packages = [{include = "poetry_demo"}]

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


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

[tool.poetry.dependencies] 에 package dependency를 추가할 수 있다.

이를 위해 2가지 방법을 소개하고 있는데,

  1. 직접 pyproject.toml 수정
    패키지명과 버전을 형식에 맞춰 적어준다.
     # pyproject.toml
    
     [tool.poetry.dependencies]
     python = "^3.10"
     numpy = "^1.24.2"
    
  2. poetry add
    다음과 같은 명령어는 두 가지 작업을 수행한다.

     $ poetry add numpy
    
    1. 최신 패키지 버전을 찾아 설치 혹은 업데이트
    2. pyproject.toml에 해당 패키지의 버전을 추가

버전을 지정하는데 caret/tilde/wildcard/inequality/exact requirements 등을 사용할 수 있다.
이와 관련된 몇 가지 예시를 살펴보자. 자세한 내용은 여기로.

  1. Caret requirements
    가장 왼쪽의 non-zero digit을 수정하지 않는 버전을 허용
     REQUIREMENT   VERSIONS ALLOWED
     ^1.2.3        >=1.2.3  <2.0.0
     ^1.2          >=1.2.0  <2.0.0
     ^1            >=1.0.0  <2.0.0
     ^0.2.3        >=0.2.3  <0.3.0
     ^0.0.3        >=0.0.3  <0.0.4
     ^0.0          >=0.0.0  <0.1.0
     ^0            >=0.0.0  <1.0.0
    
  2. Tilde requirements
    major.minor.patch 혹은 major.minor 형식으로 지정한 경우, patch 변화만 허용
    major 형식으로 지정한 경우, minor.patch 변화만 허용

     REQUIREMENT   VERSIONS ALLOWED
     ~1.2.3        >=1.2.3  <1.3.0
     ~1.2          >=1.2.0  <1.3.0
     ~1            >=1.0.0  <2.0.0
    
  3. Wildcard requirements
    * 를 사용한다.

     REQUIREMENT   VERSIONS ALLOWED
     *             >=0.0.0
     1.*           >=1.0.0 <2.0.0
     1.2.*         >=1.2.0 <1.3.0
    
  4. Inequality requirements
     >= 1.2.0
     > 1
     < 2
     != 1.2.3
    
  5. Multiple requirements
     >= 1.2, < 1.5
    
  6. Exact requirements
    == 를 이용하나, 사실 생략해도 무방하다.
     1.2.3
     ==1.2.3
    

2.3.3 Package collision

대부분의 경우, 한 번에 모든 패키지들을 설치하지 않고 개발이 진행됨에 따라 필요한 패키지들을 차례차례 설치하게 된다.
기존 패키지에 대한 의존성 체크뿐만 아니라, 호환되는 버전까지 알고 싶다면 @* 태그를 추가하면 된다.

$ poetry add numpy    # 최신 버전 설치
$ poetry add numba    # 최신 버전 설치 (호환성 고려 X)
$ poetry add numba@*  # 호환성이 맞는 버전 설치

가령, numpy에 대한 의존성을 가지고 있는 numbanumpy 버전이 맘에 들지 않으면 poetry add numba로 설치할 수 없다.
이와 관련된 몇 가지 예제들을 살펴보자.

  1. poetry add numpypoetry add numba
    버전을 지정하지 않으면 가장 최신 버전을 설치한다.
     (tmp) root@DESKTOP-HOME:~# poetry add numpy
     Using version ^1.24.3 for numpy
    
     Updating dependencies
     Resolving dependencies... (0.1s)
    
     Writing lock file
    
     Package operations: 1 install, 0 updates, 0 removals
    
     • Installing numpy (1.24.3)
    
    
     (tmp) root@DESKTOP-HOME:~# poetry add numba
     Using version ^0.56.4 for numba
    
     Updating dependencies
     Resolving dependencies... (0.0s)
    
     Because no versions of numba match >0.56.4,<0.57.0
     and numba (0.56.4) depends on numpy (>=1.18,<1.24), numba (>=0.56.4,<0.57.0) requires numpy (>=1.18,<1.24).
     So, because computer depends on both numpy (^1.24.3) and numba (^0.56.4), version solving failed.
    
  2. poetry add numpypoetry add numba@*
    @* 태그는 호환성이 맞는 버전을 자동으로 찾아서 설치한다.
     (tmp) root@DESKTOP-HOME:~# poetry add numpy
     Using version ^1.24.3 for numpy
    
     Updating dependencies
     Resolving dependencies... (0.1s)
    
     Writing lock file
    
     Package operations: 1 install, 0 updates, 0 removals
    
     • Installing numpy (1.24.3)
    
    
     (tmp) root@DESKTOP-HOME:~# poetry add numba@*
    
     Updating dependencies
     Resolving dependencies... (0.2s)
    
     Writing lock file
    
     Package operations: 2 installs, 0 updates, 0 removals
    
     • Installing llvmlite (0.34.0)
     • Installing numba (0.51.2)
    
     # pyproject.toml
    
     ...
     [tool.poetry.dependencies]
     python = ">=3.8,<3.11"
     numpy = "^1.24.3"
     numba = "*"
     ...
    
  3. poetry add numpypoetry add numba@* --dry-runpoetry add numba=0.51.2
    위의 방법에서 의존성 정보는 poetry.lock에 저장되어 있기 때문에 큰 문제가 없지만, pyproject.toml에 패키지 버전이 나오지 않아 맘에 들지 않았다.
    호환성이 맞는 버전을 설치없이 확인만 하고 싶을 때, --dry-run 옵션을 사용할 수 있다.
     (tmp) root@DESKTOP-HOME:~# poetry add numba@* --dry-run
    
     Updating dependencies
     Resolving dependencies... (0.1s)
    
     Package operations: 2 installs, 0 updates, 0 removals, 2 skipped
    
     • Installing llvmlite (0.34.0)
     • Installing numba (0.51.2)
     • Installing numpy (1.24.3): Skipped for the following reason: Already installed
     • Installing setuptools (67.7.1): Skipped for the following reason: Already installed
    
    
     (tmp) root@DESKTOP-HOME:~# poetry add numba=0.51.2
    
     Updating dependencies
     Resolving dependencies... (0.1s)
    
     Writing lock file
    
     Package operations: 2 installs, 0 updates, 0 removals
    
     • Installing llvmlite (0.34.0)
     • Installing numba (0.51.2)
    
  4. 그래도 문제가 발생하는 녀석들이 종종 있다.
    결국, 많은 패키지 종속성을 가지고 있는 무거운 패키지부터 설치하는 것이 가장 좋다.
     (tmp) root@DESKTOP-HOME:~# python -c "import numba"
     /root/.pyenv/versions/tmp/lib/python3.8/site-packages/numba/core/types/__init__.py:108: FutureWarning: In the future `np.long` will be defined as the corresponding NumPy scalar.
     long_ = _make_signed(np.long)
     Traceback (most recent call last):
     File "<string>", line 1, in <module>
     File "/root/.pyenv/versions/tmp/lib/python3.8/site-packages/numba/__init__.py", line 16, in <module>
         from numba.core import types, errors
     File "/root/.pyenv/versions/tmp/lib/python3.8/site-packages/numba/core/types/__init__.py", line 108, in <module>
         long_ = _make_signed(np.long)
     File "/root/.pyenv/versions/tmp/lib/python3.8/site-packages/numpy/__init__.py", line 320, in __getattr__
         raise AttributeError("module {!r} has no attribute "
     AttributeError: module 'numpy' has no attribute 'long'
    
     (tmp) root@DESKTOP-HOME:~# poetry add numba
     Using version ^0.56.4 for numba
    
     Updating dependencies
     Resolving dependencies... (0.2s)
    
     Writing lock file
    
     Package operations: 6 installs, 0 updates, 0 removals
    
     • Installing zipp (3.15.0)
     • Installing importlib-metadata (6.6.0)
     • Installing llvmlite (0.39.1)
     • Installing numpy (1.23.5)
     • Installing setuptools (67.7.1)
     • Installing numba (0.56.4)
        
    
     (tmp) root@DESKTOP-HOME:~# python -c "import numba"
    

2.3.4 Specifying dependencies using PIP

선호되는 방법은 아니지만, pip로 설치된 패키지들을 직접 추가할 수도 있다.

$ poetry add $(pip freeze)

2.4 Installing dependencies

$ poetry install

pyproject.toml 에 기록한 종속성들을 install 명령어로 설치할 수 있다.
이때, poetry.lock 이 존재하는지 여부에 따라 수행되는 작업이 달라진다.

  1. poetry.lock 이 없는 경우
    pyproject.toml 의 종속성들을 설치하고 poetry.lock 에 저장하여 프로젝트를 해당 버전에 lock 한다.

    You should commit the poetry.lock file to your project repo so that all people working on the project are locked to the same versions of dependencies.

  2. poetry.lock 이 있는 경우
    pyproject.toml의 종속성들을 설치하는데 poetry.lock 에 저장된 특정 버전으로 설치한다.

2.4.1 Commit poetry.lock to version control

프로젝트를 공유하는 모든 사람들이 동일한 버전의 종속성들을 사용할 수 있도록 poetry.lock 을 commit해야 한다.

2.5 Managing environments

종속성 관리와 함께 프로젝트 환경을 분리하는 것 역시 poetry의 핵심 기능이다.
가상환경이 저장되는 기본 디렉터리는 ~/.cache/pypoetry/virtualenvs 이나 프로젝트 내부에 가상환경을 저장하도록 바꿀 수 있다.
다음 명령어로 프로젝트 내부의 .venv 위치에 가상환경을 저장할 수 있게된다. (기본값은 null)

$ poetry config virtualenvs.in-project true

참고로 다음 명령어로 poetry와 관련된 설정들을 확인할 수 있다.

$ poetry config --list
  1. Generate environment
    사용하고자 하는 python path를 이용하여 가상환경을 만들 수 있다.
     $ poetry env use $PYTHON_PATH
    
  2. List and print info of environments
    생성한 가상환경들과 activated 가상환경이 무엇인지 다음의 명령어를 통해 알 수 있다.
     $ poetry env list
     $ poetry env info
     $ poetry env info -p  # Virtualenv.Path만 출력
    
  3. Run in environment
    activated 가상환경에서 프로그램을 실행시키기 위해 poetry run을 사용한다.
     $ poetry run python -m pytest
     $ poetry run pip install numpy
     $ poetry run test.sh
    
  4. Delete environment
    가상환경을 제거하기 위해선 python interpreter path를 사용하는 것이 편하다.
     $ poetry env remove $(poetry env info -p)/bin/python  # 현재 activated 가상환경을 제거
    
  5. Change shell
    현재 activated 가상환경 기반의 shell로 넘어갈 수도 있다.
     $ poetry shell
    

2.5.1 Test code example

pyenv를 이용하여 여러 버전의 python interpreter를 설치하고 poetry로 가상환경을 생성한 후, pytest로 코드를 검증하는 shell script이다.

# test.sh

#!/bin/bash

for version in 3.8.16 3.9.16 3.10.10
do
  pyenv install -s $version
  poetry env use ~/.pyenv/versions/$version/bin/python
  poetry install --no-root
  poetry run python -m pytest
  poetry env remove $(poetry env info -p)/bin/python
done

3. Script Managing

poetry는 실행 스크립트들을 관리할 수도 있다.

# computer/main.py

import argparse

def fn(args):
    pass

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('value1', type=int, help='Value 1')
    parser.add_argument('value2', type=int, help='Value 2')
    parser.add_argument('--mode', type=str, help='Mode')
    args = parser.parse_args()
    
    fn(args)

if __name__ == '__main__':
    main()
# pyproject.toml

...

[tool.poetry.scripts]
main = 'computer.main:main'  # computer/main.py의 main()
$ poetry run main 1 2 --mode sum