これまでpythonプロジェクトにおいては、pipでパッケージ管理しvenvで仮想環境を構築するというスタンスだったのですが、いい加減もっと便利なツールに乗り換えることにしました。
調べてみるとpipenvpoetryという二つの良さそうな候補があり、ちょっと迷いましたがpoetryに決めました。
ドキュメントが分かりやすいので不要かもしれませんが、この記事では知っておいた方が良さそうな部分を抜粋して動作確認しつつ、poetryの使い方をまとめています。

poetryを選んだ理由

pipenvもpoetryも調べた範囲では、担当プロジェクトでやりたいことを考えると、機能的には不足を感じませんでしたが、先駆者の皆さんのブログを読むと、以下のような点でpoetryの方が優位性が高く、githubのstar数の推移を見ても、将来的にはpoetryがはやる可能性が高そうな気がするので、poetryを採用することにしました。

  • poetryの方はPEP-0518で提案されたfile formatであるpyproject.tomlを使うのに対し、pipenvは独自のPipfileを使う
  • パッケージ公開に必要な情報を、poetryはpyproject.tomlに書けるが、pipenvは別でsetup.py/setup.cfgに書く必要がある
  • pipenvはpipfile.lockの生成にとても時間がかかるらしい
  • pipenvでは解決できない依存関係をpoetryは解決できるらしい

セットアップ

インストール

pipでインストール

昔はできなかったですが、いつのまにかpipでインストールできるようになってました。

pip install --user poetry

get-poetry.pyでインストール

curlで取ってきたget-poetry.pyをpythonコマンドで実行する形でインストールします。

pyenv local 3.7.5 # 3系のpythonで実行した方が無難そう
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python

また、.bash_profile.bashrcに以下を記載して、poetryコマンドにPATHを通します。

export PATH=~/.poetry/bin:$PATH

poetryはユーザのHOMEディレクトリにインストールされるので、LINUXでもユーザ毎にインストールする想定のようです。

アンインストール

# get-poetry.pyをダウンロード
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py > get-poetry.py
# 以下のいずれかを実行
python get-poetry.py --uninstall
POETRY_UNINSTALL=1 python get-poetry.py

# 上記を実行すると、以下が行われているっぽい
rm -rf ~/.poetry
rm -rf ~/Library/Caches/pypoetry # macの場合
rm -rf ~/.cache/pypoetry # linuxの場合

アップデート

# 通常
poetry self update
# バージョン指定
poetry self update 1.0.9
# pre-releaseバージョンをinstallしたい場合
poetry self update --preview

TAB補完

こちらにTAB補完の定義の設定方法が載っています。
すでにシェルのTAB補完を使っている人は自分の使っているシェルに対応する物を選んで設定するといいと思います。
自分はbashを使っていますがタブ補完は使ってなかったのでそこから設定しました。

# poetryのTAB補完定義を出力
poetry completions bash > $(brew --prefix)/etc/bash_completion.d/poetry.bash-completion

### ここから下はpoetry関係ありません ###
# bashをbrewでinstall
brew install bash

# ログインシェルで指定可能なシェルを追加
sudo vi /etc/shells
+ /usr/local/bin/bash

# ログインシェルを変更
chsh -s /usr/local/bin/bash

# タブ補完機能追加
brew install bash-completion

# タブ補完の有効化
vi ~/.bash_profile
+ # bash-completions
+ if [ -f $(brew --prefix)/etc/bash_completion ]; then
+   source $(brew --prefix)/etc/bash_completion
+ fi

# ついでに、VSコードのTerminalの起動シェルもbrewのbashに変更
# Code > Preference > Settings > settings.json
+ "terminal.integrated.shell.osx": "/usr/local/bin/bash",

pyproject.tomlの記述

pyproject.tomlはプロジェクトの依存関係を統括する重要なファイルです。

ファイル作成

以下のコマンドでファイルを作成できます。

poetry init

依存関係を記載

本番で利用するものは[tool.poetry.dependencies]に、テスト・開発時のみ利用するものは[tool.poetry.dev-dependencies]に記載します。

[tool.poetry.dependencies]
python = "^3.7"
jinja2 = "^2.11.2"
pandas = "^0.25.3"
:

コマンドを使っても追加削除ができます。コマンドを使う場合、poetry.lockにもそのタイミングで反映されます。

# dependenciesに追加
poetry add jinja2 
# dev-dependenciesに追加
poetry add jinja2 --dev

# dependenciesから削除
poetry remove jinja2
# dev-dependenciesから削除
poetry remove jinja2 --dev

依存関係の解決

poetry install

poetry.lockに記載されたパッケージをインストールするコマンドです。
ただし、poetry.lockが存在しない場合だけ、pyproject.tomlを元に依存関係を解決してパッケージをインストールし、poetry.lockを作成してくれます。
本番環境のデプロイ手順としては、このコマンドを利用することになります。また、開発時も他の人が更新したパッケージを反映する場合に利用します。

# 開発時
poetry install
# 本番
poetry install --no-dev

poetry update

pyproject.tomlを元に依存関係を解決・パッケージをインストールし、poetry.lockを更新するコマンドです。
開発者が手でpyproject.tomlを変更した際には、commit前に忘れずにpoetry updateを実行しておく必要があります。
(普段からpoetry addpoetry removeを使うようにしておけば、poetry.lockも同時に更新されているので、あまり気にしなくて良さそう)

# 全部まとめて更新
poetry update
# 特定のパッケージだけ更新
poetry update jinja2 pandas

インストールされたパッケージの確認

現在インストールされているパッケージを確認するには、poetry showを使います。
パッケージの概要やそのパッケージが依存するパッケージまで確認できるのがいいですね。

# リストアップ
poetry show
> boto                     2.49.0     Amazon Web Services Library
> boto3                    1.14.4     The AWS SDK for Python
> botocore                 1.17.4     Low-level, data-driven core of boto 3.
> cachetools               4.1.0      Extensible memoizing collections and decorators
(省略)

# パッケージの詳細を確認
poetry show boto3
> name         : boto3
> version      : 1.14.4
> description  : The AWS SDK for Python
> 
> dependencies
>  - botocore >=1.17.4,<1.18.0
>  - jmespath >=0.7.1,<1.0.0
>  - s3transfer >=0.3.0,<0.4.0

仮想環境の構築

poetryはすでに仮想環境がある場合はその仮想環境を使い、存在しなければ新たに仮想環境を作ります。

poetry installで依存関係解決のついでに構築

デフォルト(virtualenvs.create=true)では、poetry installpoetry addした際に、仮想環境がすでに存在してなければ、自動的にpythonコマンドのバージョンを元に仮想環境が作成されます。

poetry install
> Creating virtualenv hoge-project-4FNRMnIZ-py3.8 in /Users/xxxxx/Library/Caches/pypoetry/virtualenvs
> Installing dependencies from lock file
> 
> Package operations: 52 installs, 0 updates, 0 removals
> 
>   - Installing six (1.15.0)
>   - Installing docutils (0.15.2)
>   (省略)

poetry env use で明示的に構築

指定したpythonコマンドのバージョンを元に仮想環境を作成する方法もあります。こちらもすでに仮想環境があれば利用されません。

pyenv local 3.8.2
poetry env use python
> Creating virtualenv hoge-project-4FNRMnIZ-py3.8 in /Users/xxxxx/Library/Caches/pypoetry/virtualenvs
> Using virtualenv: /Users/xxxxx/Library/Caches/pypoetry/virtualenvs/hoge-project-4FNRMnIZ-py3.8

確認

アクティブな仮想環境の詳細を確認することもできます。

# リストアップ
poetry env list
> hoge-project-4FNRMnIZ-py3.7
> hoge-project-4FNRMnIZ-py3.8 (Activated)

# アクティブな仮想環境の詳細を確認
poetry env info
> Virtualenv
> Python:         3.8.2
> Implementation: CPython
> Path:           /Users/xxxxx/Library/Caches/pypoetry/virtualenvs/hoge-project-4FNRMnIZ-py3.8
> Valid:          True
> 
> System
> Platform: darwin
> OS:       posix
> Python:   /Users/xxxxx/.pyenv/versions/3.8.2

削除

# コマンドで削除
poetry env remove hoge-project-4FNRMnIZ-py3.7
> Deleted virtualenv: /Users/xxxxx/Library/Caches/pypoetry/virtualenvs/hoge-project-4FNRMnIZ-py3.7

# フォルダを直接削除
rm -rf /Users/xxxxx/Library/Caches/pypoetry/virtualenvs/hoge-project-4FNRMnIZ-py3.7

venvと併用する意味はなさそう

たまにvenvで仮想環境を作ってから、poetry installするような記事を見かけたので、意味はあるのだろうか?と疑問を持ちました。もしかしてvenvで作った仮想環境があったらそれを使い回すのか?と。ので、ちょっと試してみます。

pyenv local 3.8.2
python -m venv env # venv仮想環境を作成
source env/bin/activate
poetry env use python # このpythonは`path_to_project/env/bin/python`。`poetry install`でも同じ結果
> Creating virtualenv hoge-project-4FNRMnIZ-py3.8 in /Users/xxxxx/Library/Caches/pypoetry/virtualenvs
> Using virtualenv: /Users/xxxxx/Library/Caches/pypoetry/virtualenvs/hoge-project-4FNRMnIZ-py3.8
deactivate
rm -rf env # venv仮想環境を削除
poetry run python --version
> Python 3.8.2

結果としては、poetry仮想環境を構築する際にコピーされるpythonランタイムのコピー元としてvenv仮想環境のpythonランタイムが使われるようですが、コピー後はvenv仮想環境を削除してもpoetryは正常に動作するので、venv仮想環境は特に必要がありません。
pythonのバージョンを変更したいだけなら、venvを使って仮想環境を作るまでもなく、pyenvでバージョンを変えればいいだけなので、venvで仮想環境を作る意味はなさそうです。

プログラムの実行

以下のような超シンプルなscripts/hello.pyを実行するケースを考えます。

def main():
    print('hello poetry!')
if __name__ == "__main__":
    main()

直接pythonプログラムを実行

poetry runの後にpythonプログラムを実行するコマンドを記載するだけです。引数つけても大丈夫です。

poetry run python scripts/hello.py 
> hello poetry!

スクリプトとして実行する

pyproject.tomlにスクリプト定義とスクリプトを配置するフォルダを記載する必要があります。

[tool.poetry]
packages = [
    { include="scripts" },
]

[tool.poetry.scripts]
hello = "scripts.hello:main"

あとは、poetry run <スクリプト名>で実行します。

poetry run hello
> hello poetry!

プロジェクト直下だと[ModuleOrPackageNotFound]が発生してうまく動きません。

コマンドライン引数を渡して実行する

コマンドライン引数を扱うためにargparseをインストールします。

poetry add argparse

scripts:/hello.pyを、messageというコマンドライン引数にとって、printするように変更します。

import argparse

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('message')
    args = parser.parse_args()
    message: str = args.message

    print(f'hello {message}!')

if __name__ == "__main__":
    main()

pythonコマンドで直接実行してみると、普通に動きます。

python scripts/hello.py xxxxx
> hello xxxxx!

当然ながら、poetry runで直接起動することもできます。

poetry run python  scripts/hello.py xxxxx
> hello xxxxx!

スクリプトとして実行することもできます。

poetry run hello xxxxx
> hello xxxxx!

ちなみに、スクリプトの設定時にhello = "scripts.hello:main"と指定してるので、もしかしてmain()関数に直接引数を渡したりできるのかな?とも思って試してみたのですが、それはできませんでした。

VS Code上でデバッグ実行する

Pythonプラグインを使ってデバッグすることもできます。とても便利です。

一点だけ注意点として、VS Codeがpoetryの仮想環境を認識できる必要があるため、virtualenvs.in-projecttrueにして、プロジェクト配下に仮想環境が作成されるようにする必要があります。

poetry config virtualenvs.in-project true --local

もしくは、.vscode/settings.jsonpython.pythonPathに仮想環境のpathを書いても良いみたいです。

あとは、こちらに書いてある通り、launch.jsonを作って、実行対象のpythonファイルを開いて、Runするだけです。

poetry自体の設定

poetryではconfigコマンドで設定を行うことができます。

設定はpoetry config --listで確認できます。

poetry config --list
> cache-dir = "/Users/xxxxx/Library/Caches/pypoetry"
> virtualenvs.create = true
> virtualenvs.in-project = false
> virtualenvs.path = "{cache-dir}/virtualenvs"  # /Users/xxxxx/Library/Caches/pypoetry/virtualenvs

設定できるのは以下の5つのようです。個人的にはvirtualenvs.in-project: trueだけは設定した方が良いと感じています。

  • cache-dir
    • Poetryが使うキャッシュディレクトリのpathです。defaultは以下
      • mac: ~/Library/Caches/pypoetry
      • Linux: ~/.cache/pypoetry
  • virtualenvs.create
    • まだ仮想環境が存在していない場合、新しい仮想環境を作成するか。defaultはtrue
  • virtualenvs.in-project
    • プロジェクトのルートディレクトリに仮想環境を作成するか。defaultはfalse
    • 仮想環境の消し忘れがありえると思うので、この設定はtrueにしておいた方が良よさそう
  • virtualenvs.path
    • 仮想環境が作成されるディレクトリ。defaultは{cache-dir}/virtualenvs
  • repositories.{name}
    • PyPI以外のプライベートリポジトリを利用したい場合などに設定。

プロジェクトローカルでも設定できます。

# グローバルの設定
poetry config {項目名} {値}
poetry config virtualenvs.in-project true

# プロジェクトローカルの設定
poetry config {項目名} {値} --local
poetry config virtualenvs.in-project true --local

トラブル対応

ほとんどトラブルらしいトラブルが起こったことはないですが、Linuxにinstallした際に少しハマったので載せておきす。

/home/xxx/.poetry/lib/poetry/_vendor/py2.7/subprocess32.py:149: RuntimeWarning: The _posixsubprocess module is not being used. Child process reliability may suffer if your program uses threads.

2020/06/16現在、obuntuにpoetryをインストールすると、このエラーが発生します。
このissueで対応されるまでは手でパッチを当てる必要があるので要注意です。

curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py > get-poetry.py
vi get-poetry.py
-        allowed_executables = ["python", "python3"]
+        allowed_executables = ["python3", "python"]

python3 get-poetry.py

[RecursionError] maximum recursion depth exceeded while calling a Python object

poetry add scrapyをした際に、このエラーが発生しました。
同様のエラーがgithubのissueに報告されており、開発中の1.1.0b2で対応されているとのことなので

poetry self update --preview
> Updating to 1.1.0b2
>  - Downloading poetry-1.1.0b2-darwin.tar.gz 100%OS
> Poetry (1.1.0b2) is installed now. Great!

で、poetry自体をこのバージョンにあげてみたところ、問題なくscrapyをインストールできました。

[2020/10/20追記] リリースの#2342で修正済みのようで、現在はpoetryのバージョンUPをするだけで解決されます。

poetry self update

その他Tips

poetryではincompatibleなバージョンを無理やりインストールすることはできない

例えば、pyppeteerというスクレイピングライブラリが特定のケースでwebsocketの最新バージョンだと上手く動かないことが分かったので、過去バージョンをインストールしてみようとしたところ、以下のようにpyppeteer (0.2.2) depends on websockets (>=8.1,<9.0)だから、依存関係を解決できないよ、というエラーが出ました。

poetry add websockets==6.0
Updating dependencies
Resolving dependencies... (0.0s)
  SolverProblemError
  Because pyppeteer (0.2.2) depends on websockets (>=8.1,<9.0)
   and no versions of pyppeteer match >0.2.2,<0.3.0, pyppeteer (>=0.2.2,<0.3.0) requires websockets (>=8.1,<9.0).
  So, because python-scraping-sample depends on both pyppeteer (^0.2.2) and websockets (6.0), version solving failed.

どうやら、強制的にインストールするオプションなども無いみたいですし、 stackoverflow でもできないよと書いてあります。

ここを見ると、poetry installはresolverを含まないのでpipと同じようにできると書いてあるのですが、poetry.lockを手で編集してpoetry installを実行するのはちょっと厳しいので自分は断念しました。
なお、pyproject.tomlwebsockets = 6.0を追加して、poetry updateしてみたのですが、こちらはpoetry addと同様にSolverProblemErrorが出ました。

ちなみに、どうしても一時的にincompatibleな過去バージョンにして検証したくなった場合は、自分はpipを使うようにしてます。