Poetryを使ったPythonパッケージ開発からPyPI公開まで

この記事は BeProud Advent Calender 201820日目の記事です。そのためいつもよりボリュームたっぷり、文体も丁寧にお送りします。

adventar.org

本記事ではPoetryを使ってパッケージ開発→PyPIへ登録するまでの流れを紹介します。

github.com

プロジェクト作成からPyPI登録までわずか30秒

$ poetry new kk6-demo
Created package kk6-demo in kk6-demo
$ cd kk6-demo/
$ poetry build
Creating virtualenv kk6-demo-py3.7 in /Users/kk6/sandbox/kk6-demo/.venv
Building kk6-demo (0.1.0)
 - Building sdist
 - Built kk6-demo-0.1.0.tar.gz

 - Building wheel
 - Built kk6_demo-0.1.0-py3-none-any.whl
$ poetry publish

Publishing kk6-demo (0.1.0) to PyPI
 - Uploading kk6-demo-0.1.0.tar.gz 100%
 - Uploading kk6_demo-0.1.0-py3-none-any.whl 100%

newbuildpublishの3コマンドでプロジェクト作成からPyPIに登録まで、約25秒ほどでした。簡単ですね。(やったあとでTestPyPIの存在思い出したんですが、こういうテスト用途にはそちらを使いましょう…)

実際にはパッケージのコードを書いたりテストコード書いたりが必要ですが、それ以外の諸々はPoetryがやってくれるので、本質的なコードを書くことに集中できます。

Poetry について

基本的な使い方

Poetryの基本的な使い方については公式ドキュメントの他にも日本語で有用な記事がすでに2つありますのでそちらを紹介しておきます。

Poetry と Pipenv

Pipenvは確かに便利だけど

Pythonのパッケージ管理ツールとしては最近ではPipenvが使われるようになってきていると思いますが、あちらはあくまでも依存関係の管理までです。Poetryはそれに加えてこうしたパッケージのビルドや公開までもが守備範囲となっています。

また、先程紹介した記事によるとPipenvはちょっと雲行きが怪しい感じみたいです。

Pipenv は当初大きな期待を持って迎えられましたが(私も期待しましたが)、さまざまな問題が出ており最近はどうも先行きが怪しい感じです。私自身 Pipenv の導入によって便利になったところがある一方で Pipenv を使っていなかったら抱えていなかった新たなストレスも増えた感じがします。 Poetry は私のような「脱 Pipenv 」を検討している方にとって有力な移行先候補のひとつです。

Pipenv の問題点とそれにまつわる議論については次のページ等が参考になります。興味のある方は読んでみてください。

www.lifewithpython.com

PoetryのREADMEでもPipenvについて言及しています。それによるとPipenvで依存関係の解決に失敗するようなケースでもPoetryなら大丈夫とのこと。

What about Pipenv? | sdispater/poetry

またライブラリのインストールもPipenvは結構遅いですが、Poetryはだいぶましです。Pipほど速くはないですが…。

Pipenvも良いツールだと思います。ただ、「これからの時代は何も考えずにとりあえずPipenvいれとけばいい」という状況ではないと思います。

Pipenv から Poetry に乗り換える

先日書いた記事を参考にしてください。

Pipenv から Poetry への乗り換え - PYTHONIC BOOM BOOM HEAD

今をときめくソースコード自動整形ツール black もPipenvからPoetryへの移行が進行中のようです(現在のところは両方サポート)。こちらも参考になるでしょう。

github.com

Poetry と Pyenv

Poetryの作者自身、Pyenvと組み合わせて利用しているようです。Pyenvを使う場合はこんな感じでインストールすると良いでしょう。以下Issueのオーナーコメントより引用。

Basically:

  • Install pyenv
  • Install a specific Python version: pyenv install 3.7.0
  • Install Poetry for this Python version: pyenv global 3.7.0 && curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python

github.com

Pyenvといえば日本人しか使ってないニッチなツールというイメージはもはや過去のものとなり、PipenvもPoetryもPyenvとの連携が想定されていますし、CircleCIも内部でPyenvを使っています。

以前は僕もPyenvのハマりどころの多さに嫌気が差して使うのをやめていましたが、あれから徐々に海外でも知名度を上げコミットする人間も増えたみたいで、今では使っていても問題は特に起きないので日常的に使っています。

ただし、こういうパッケージ開発なんかでは細かいバージョン違いまで管理する必要がありますが、「これからPythonはじめたいのでPyenvいれてみるかー」みたいなのは避けたほうがいいです。Python2と3の使い分け程度ならhomebrewで十分ですし、Python3.6と3.7の使い分けとかも公式からバイナリ落としてきてそれぞれインストールすればいいだけです。「よくわからないけどとりあえずPyenvいれてみる」というケースが一番ハマるパターンだと思っています。

PEP517 と PEP518

www.python.org

Poetryにも使われているpyproject.tomlPipfileのようなものですが、あちらが独自定義のファイルなのに対し、こちらはPEPで仕様が決められています。

PEP518によると、pyproject.tomlでビルドツールを指定、もしそのツールが無ければsetuptoolsによるビルドが行われます。Poetryだとこんな感じですが、例えば flit に乗り換えたければここだけ直せば後の設定は一切書き換えること無くビルドツールを乗り換えることができる、というのが本来の目的だと思います。

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

ただ、現状では [tool.poetry]のようなPoetry独自設定を書き込まなければならないため、「一見PEPに従ってるように見えるが蓋を開けてみればまだまだPipfileと同じような独自定義の設定ファイル」となっています。これは同じく pyproject.toml に対応しているflitも同様のようです。

また、ビルドツール用の標準APIの仕様策定は、PEP517で行われています。

www.python.org

PoetryもすでにPEP517に準拠しています。

The pyproject.toml file | Documentation | Poetry - Python dependency management and packaging made easy.

ちなみにpipもPEP517対応済みで、pip buildコマンドの追加へ向けてすでに動いているようです。

github.com

将来的にすべてがpipに収束するのかはわかりませんが、少なくともユースケースに応じて適切なツールを自由に選択できるようになっているんじゃないかと思います。

Poetryの各種設定

個人的に仮想環境(venv)をプロジェクト内に作ってほしかったりするのでそれらの設定を先に行います。このあたりは必要に応じて行ってください。

venvの作成先をプロジェクト内にしたい

デフォルトだと仮想環境は、macだと$HOME/Library/Caches/pypoetry/virtualenvs以下に作られます。これは $ poetry config --listで確認できます。

$ poetry config --list
settings.virtualenvs.create = true
settings.virtualenvs.in-project = false
settings.virtualenvs.path = "/Users/kk6/Library/Caches/pypoetry/virtualenvs"

これらの設定は公式ドキュメントに記載されている通り、macなら~/Library/Application Support/pypoetry以下のconfig.tomlというファイルに記載されています。もしまだconfig.tomlが無ければ、一度 $ poetry config --list を実行すると自動で作成されます。

現状だとsettings.virtualenvs.in-projectfalseなので、settings.virtualenvs.pathに設定されている"~/Library/Caches/pypoetry/virtualenvs"へ仮想環境が作られるようになっています。

仮想環境をプロジェクト配下に作りたい場合は、以下のようにsettings.virtualenvs.in-projecttrueを指定します。

$ poetry config settings.virtualenvs.in-project true

これによりvirtualenvs.pathの設定は無視して各プロジェクト直下に仮想環境が作られるようになります。

TestPyPIへアップロードできるようにする

パッケージを公開する publish コマンドはデフォルトではPyPIへのアップロードになっています。練習なのでPyPIではなくTestPyPIのほうにアップロードできるように設定しましょう。まずリポジトリの設定から。

$ poetry config repositories.testpypi https://test.pypi.org/legacy/

このような感じで、repositories.XXX のXXXの部分に好きな名前を指定して、URLを紐づけます。

余談ですが、TestPyPIへのアップロードは https://test.pypi.org/legacy のほうじゃないとダメです。https://test.pypi.org/simple だと失敗します。(実は記事執筆時点の最新版のPoetryだと成功してる風な出力になりますが実際にはアップロードできてなくて、試しにtwineでアップロードしてみると405エラーになります。)

また、TestPyPIに限り、pyproject.toml にhomepageの設定がないとアップロードできません。先ほどと同様Poetryだと成功しているように見えるんですが、twineでやってみるとHome-PageにNoneは設定できないよという感じのエラーが返ってきます。

$ poetry run twine upload --repository-url https://test.pypi.org/legacy/ dist/*
...
HTTPError: 400 Client Error: 'None' is an invalid value for Home-Page. Error: Invalid URI See https://packaging.python.org/specifications/core-metadata for url: https://test.pypi.org/legacy/

とりあえずIssueは出しておきました。オーナーからバグなので対処すると返事も来てました。

github.com

TestPyPIのユーザー名とパスワードを設定する

publishコマンドを実行すると通常は対話形式でログインのためにユーザー名とパスワードを要求されます。対話形式での入力以外の方法としては以下の2通りがあります。

  • publishコマンドに--username--passwordのオプションとしてユーザー名とパスワードを渡す方法
  • configコマンドでユーザー名とパスワードを恒久的に設定する方法

今回は後者を設定してみます。

$ poetry config http-basic.testpypi username password

以上です。なお、PyPIの場合はこう。

poetry config http-basic.pypi username password

ちなみにここで入力した値は ~/Library/Application\ Support/pypoetry/auth.tomlに保存されています。twine.pypircのようなものですね。そちら同様パスワードは平文で保存されてるので扱いには十分注意してください。

あ、そもそもTestPyPIにアカウント登録していない場合はまずそちらを済ませておいてください。

ここまでの設定

ここまでで設定はこんな感じになりました。

$ poetry config --list
settings.virtualenvs.create = true
settings.virtualenvs.in-project = true
settings.virtualenvs.path = "/Users/kk6/Library/Caches/pypoetry/virtualenvs"
repositories.testpypi.url = "https://test.pypi.org/legacy/"

プロジェクトを用意する

新規作成

標準的なレイアウト

それではまずプロジェクトを作ってみます。プロジェクトを作るには newコマンドを実行します。

$ poetry new howoldru
Created package howoldru in howoldru

作成されたプロジェクトの構成はこうなっています。

$ tree howoldru/
howoldru/
├── README.rst
├── howoldru
│   └── __init__.py
├── pyproject.toml
└── tests
    ├── __init__.py
    └── test_howoldru.py

2 directories, 5 files

新規作成時点でのpyproject.tomlです。

[tool.poetry]
name = "howoldru"
version = "0.1.0"
description = ""
authors = ["kk6 <hiro.ashiya@gmail.com>"]

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

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

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

srcレイアウト

パッケージのルートディレクトリを howoldru のような名前ではなく src という名前を使う、いわゆる src レイアウトでプロジェクトを作成したい場合は --src オプションを指定します。

$ poetry new --src foo
$ tree foo
foo
├── README.rst
├── pyproject.toml
├── src
│   └── foo
│       └── __init__.py
└── tests
    ├── __init__.py
    └── test_foo.py

3 directories, 5 files

srcレイアウトの有効性についてはPytestの公式ドキュメントの翻訳記事があったのでこちらを参考にしてください。

しかし、これだとちょっとした問題を引き起こす。それは、testsディレクトリからモジュールをロードするために、pytestがsys.pathの先頭にレポジトリのルートを追加するが、これがmypkgもインポート可能となるということを引き起こすからである。

もし仮想環境においてパッケージをテストするためにtoxのようなツールを利用しているならば、これは問題となる。なぜなら、テストしたいのはインストールされたバージョンのパッケージであり、レポジトリのローカルコードではないからだ。

このようなシチュエーションではsrcレイアウトを使用することが強く推奨される。

qiita.com

本家公式ドキュメントの同じ記述の部分はこちら

Good Integration Practices — pytest documentation

おまけ:既存のプロジェクトに適用する

もし既存のプロジェクトをPoetryで管理したい場合は initコマンドを使用します。パッケージ名やバージョンなどを対話形式で入力すると、その内容を元にpyproject.tomlを生成してくれます。

$ poetry init

This command will guide you through creating your pyproject.toml config.

Package name [howoldru]:
Version [0.1.0]:
Description []:
Author [kk6 <hiro.ashiya@gmail.com>, n to skip]:

License []: Compatible Python versions [^3.7]:

Would you like to define your dependencies (require) interactively? (yes/no) [yes]


Search for package:


Would you like to define your dev dependencies (require-dev) interactively (yes/no) [yes]


Search for package:

Generated file

[tool.poetry]
name = "howoldru"
version = "0.1.0"
description = ""
authors = ["kk6 <hiro.ashiya@gmail.com>"]

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

[tool.poetry.dev-dependencies]

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


Do you confirm generation? (yes/no) [yes]

newの場合はtool.poetry.dev-dependenciesに自動的にpytestが記載されていましたがこちらの場合はそれがないです。

必要なライブラリのインストール

今回は作ろうと思っているパッケージは、コマンドラインツールを簡単に実装できるfireと日時操作を簡単に行えるpendulumを使いますのでそれらをインストールします。

インストールにはaddコマンドを使用します。

$ cd howoldru
$ poetry add fire pendulum
Creating virtualenv howoldru-py3.7 in /Users/kk6/sandbox/python/bp_advent_calendar_2018/howoldru/.venv
Using version ^0.1.3 for fire
Using version ^2.0 for pendulum

Updating dependencies
Resolving dependencies... (0.7s)


Package operations: 11 installs, 0 updates, 0 removals

Writing lock file

  - Installing six (1.12.0)
  - Installing atomicwrites (1.2.1)
  - Installing attrs (18.2.0)
  - Installing more-itertools (4.3.0)
  - Installing pluggy (0.8.0)
  - Installing py (1.7.0)
  - Installing python-dateutil (2.7.5)
  - Installing pytzdata (2018.7)
  - Installing fire (0.1.3)
  - Installing pendulum (2.0.4)
  - Installing pytest (3.10.1)

新たに依存パッケージをインストールする add コマンドや、pyproject.tomlに記載された依存パッケージをインストールする install コマンドを実行すると初回は自動的に仮想環境が作られます。続いてその環境に先程指定したパッケージがインストールされます。

なお、add で自ら追加したパッケージはpyproject.tomlに追記されています。

[tool.poetry.dependencies]
python = "^3.7"
fire = "^0.1.3"
pendulum = "^2.0"

依存関係は poetry.lock のほうに記載されています。結構な行数なのでここでは割愛します。

公開予定のプログラムを書く

今回は1ファイルのみのコマンドラインツールを作ろうと思うので、howoldruパッケージは削除してhowoldru.pyを用意します。

$ ls
README.rst     howoldru       poetry.lock    pyproject.toml tests
$ rm -rf howoldru/
$ touch howoldru.py
$ nvim howoldru.py

howoldru.pyはこんな感じで実装しました。

import fire
import pendulum


def show_age(birthday):
    age = pendulum.parse(birthday).age
    print(f"あなたは現在{age}歳です。")

def main():
    fire.Fire(show_age)


if __name__ == "__main__":
    main()

生年月日を入力すると現在の年齢を教えてくれるだけのコマンドラインツールです。

動作確認してみましょう。poetry run pythonで仮想環境のほうのPythonが実行されます。

$ poetry run python howoldru.py  2001-08-25
あなたは現在17歳です。

ちゃんと動いてますね。

スクリプトの設定

python howolru.py とするのではなく、howoldru コマンドで実行できるようにしてみましょう。

pyproject.tomlに以下の設定を追加します。

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

これで poetry run howoldruコマンドが実行できるようになりました。

$ poetry run howoldru 2001-08-25
あなたは現在17歳です。

poetry run howoldru だとまだ長い?では howoldruコマンドだけで済むようにしてみましょう。

仮想環境をactivateする

poetryコマンドを経由しないで python コマンドを実行しても当然 pyproject.toml の内容は読み込んでくれません。なのでまず shell コマンドで仮想環境をactivateします。

$ poetry shell
Spawning shell within /Users/kk6/sandbox/python/bp_advent_calendar_2018/howoldru/.venv
Welcome to fish, the friendly interactive shell

これで source .venv/bin/activate.fishしたのと同じ状態になりました。

※ 筆者はfish shellを利用しています。bashzshだと表示が若干違うと思いますが適宜読み替えてください。

開発途中のパッケージをインストールする

develop コマンドで開発途中のパッケージを仮想環境にインストールします。 pip install -e . のことですね。

$ poetry develop
develop is deprecated use install instead.

Installing dependencies from lock file

Nothing to install or update

  - Installing howoldru (0.1.0)

これで howoldruコマンドだけで実行できるようになりました。

$ howoldru
Fire trace:
1. Initial component
2. ('The function received no value for the required argument:', 'birthday')

Type:        function
String form: <function show_age at 0x108f81048>
File:        /Users/kk6/sandbox/python/bp_advent_calendar_2018/howoldru/howoldru.py
Line:        5

Usage:       howoldru BIRTHDAY
             howoldru --birthday BIRTHDAY

パッケージのビルド

ビルドする

build コマンドを実行すると pyproject.toml[tool.poetry] セクションの name に設定されている名前と同じPythonファイルまたはパッケージ、あるいは src ディレクトリを探し、ビルドを行います。 buildコマンドでパッケージをビルドする際にpyproject.tomlの内容に基づいてsetup.pyを自動生成してくれます。では実際に確認してみましょう。

まずビルドします。(事情によりhowoldruのバージョンがちょっと上がってますが気にしないでください…)

$ poetry build
Building howoldru (0.1.2)
 - Building sdist
 - Built howoldru-0.1.2.tar.gz

 - Building wheel
 - Built howoldru-0.1.2-py3-none-any.whl

dist/以下にtar.gzwhlファイルが作成されました。

setup.py/setup.cfg/MANIFEST.inはもう書かなくて良い

生成されたsetup.pyを確認してみます。

$ cd dist/
$ tar zxvf howoldru-0.1.2.tar.gz
x howoldru-0.1.2/howoldru.py
x howoldru-0.1.2/pyproject.toml
x howoldru-0.1.2/setup.py
x howoldru-0.1.2/PKG-INFO
$ nvim howoldru-0.1.2/setup.py

内容がこちら。

# -*- coding: utf-8 -*-
from distutils.core import setup

modules = \
['howoldru']
install_requires = \
['fire>=0.1.3,<0.2.0', 'pendulum>=2.0,<3.0']

entry_points = \
{'console_scripts': ['howoldru = howoldru:main']}

setup_kwargs = {
    'name': 'howoldru',
    'version': '0.1.2',
    'description': '',
    'long_description': None,
    'author': 'kk6',
    'author_email': 'hiro.ashiya@gmail.com',
    'url': 'https://github.com/kk6',
    'py_modules': modules,
    'install_requires': install_requires,
    'entry_points': entry_points,
    'python_requires': '>=3.7,<4.0',
}


setup(**setup_kwargs)

install_requiresconsole_scriptsなんかもpyproject.tomlに基づいて自動的に設定してくれているのがわかると思います。

また、setup.cfgMANIFEST.inに書いていたような内容もpyproject.tomlがその役目を負ってくれているのでもう書く必要はありません。

PyPIへの公開

TestPyPIに公開する

PyPI(TestPyPI)への公開はpublishコマンドで行います。今回はTestPyPIのほうにアップロードしたいので poetry publish -r testpypi のようにアップロード先を指定します。オプションを忘れるとPyPIのほうへアップロードされてしまうので注意。

configコマンドでPyPIの認証情報を設定していない場合は、実行時にユーザー名とパスワードの入力を求められますので入力します。認証に成功すると先程ビルドしたものがPyPIへアップロードされます。

$ poetry publish -r testpypi

Publishing howoldru (0.1.2) to testpypi
 - Uploading howoldru-0.1.2-py3-none-any.whl 100%
 - Uploading howoldru-0.1.2.tar.gz 100%

PyPIの自身のプロジェクト一覧を見るとアップロードされているのが確認できると思います。

https://pypi.org/manage/projects/

パッケージのページもできてますね。

howoldru · TestPyPI

付加情報の追加

パッケージの公開ページにdescriptionがなかったりと寂しいので追加してみましょう。

パッケージ公開ページのdescriptionはREADMEとして書く

setup.pyを書く場合のテクニックとして、README.rst等をパースしてlong_descriptionに設定するという方法がありますが、Poetryの場合 pyproject.toml に1行追加すればそれを勝手にやってくれます。

[tool.poetry]
...
readme = "README.rst"
...

README.rst自体はpoetry newした時に作られてますが、空のファイルのためdescriptnも空になっています。なのでREADME.rstを編集しましょう。以下は例です。

howoldru
========

It's demo package.

ライセンスファイルを用意する

プロジェクト内にLICENSEファイルがあればPoetryが自動的に検出してビルド成果物に含めてくれます。

pyproject.tomlに以下を書くのが慣習ぽいのですが、無くても勝手に含めてくれてました。

[tool.poetry]
...
license = "MIT"
...

CHANGELOG 等も含めたい場合

ライセンスは自動で検出してくれるようですが、自分で含めたいファイルを指定する場合は tool.poetry セクションに include で指定します。

[tool.poetry]
# ...
include = ["CHANGELOG.md"]

逆に除外したいファイルがあれば exclude を指定します。

exclude = ["my_package/excluded.py"]

poetry.eustace.io

その他諸々

その他の項目も pyproject.tomlに追記してこういう感じになりました。

[tool.poetry]
name = "howoldru"
version = "0.1.3"
description = "How old are you?"
readme = "README.rst"
authors = ["kk6 <hiro.ashiya@gmail.com>"]
homepage = "https://github.com/kk6"
repository = "https://github.com/kk6/howoldru"
documentation = "https://github.com/kk6/howoldru/wiki"
license = "MIT"
classifiers = [
    "Development Status :: 3 - Alpha",
    "Environment :: Console",
    "Intended Audience :: End Users/Desktop",
    "License :: OSI Approved :: MIT License",
    'Programming Language :: Python :: 3',
    "Programming Language :: Python :: 3.7",
    "Topic :: Utilities",
]

homepageとは別途、リポジトリのURLを設定したり、ドキュメントのURLを設定したり、classifiersのようなメタデータを記載しました。これらは公開されたパッケージのページ上で表示されます。

classifiersについては以下のページからそれらしいものを選んで記載しましょう。

https://pypi.org/pypi?%3Aaction=list_classifiers

より詳しい pyproject.toml の書き方はドキュメントを参考に。

poetry.eustace.io

なお、PyPIの仕様上、再アップロード時にバージョン番号がアップロード済みのものだと受け付けてくれないのでバージョンを version = "0.1.3" みたいな感じで上げておきましょう。

公開パッケージをアップデートする

パッケージを更新したのでアップロードし直します。今回はpublishコマンドに--buildオプションを指定してビルドと公開を同時に行ってみましょう。

$ poetry publish -r testpypi --build
Building howoldru (0.1.5)
 - Building sdist
 - Built howoldru-0.1.5.tar.gz

 - Building wheel
 - Built howoldru-0.1.5-py3-none-any.whl

Publishing howoldru (0.1.5) to testpypi
 - Uploading howoldru-0.1.5-py3-none-any.whl 100%
 - Uploading howoldru-0.1.5.tar.gz 100%

パッケージのページを見てみるとメタ情報等が表示されているのが確認できます。

howoldru · TestPyPI

また、手元でビルドした成果物を確認してみましょう。

$ cd dist/
$ tar zxvf howoldru-0.1.5.tar.gz
x howoldru-0.1.5/LICENSE
x howoldru-0.1.5/README.rst
x howoldru-0.1.5/howoldru.py
x howoldru-0.1.5/pyproject.toml
x howoldru-0.1.5/setup.py
x howoldru-0.1.5/PKG-INFO

最初のビルドとの違いはまず、 LICENSEREADME.rst が追加されていることです。それからsetup.pyの内容も変わっています。long_descriptionREADME.rstをもとに設定されていますね。

# -*- coding: utf-8 -*-
from distutils.core import setup

modules = \
['howoldru']
install_requires = \
['fire>=0.1.3,<0.2.0', 'pendulum>=2.0,<3.0']

entry_points = \
{'console_scripts': ['howoldru = howoldru:main']}

setup_kwargs = {
    'name': 'howoldru',
    'version': '0.1.5',
    'description': '',
    'long_description': "howoldru\n========\n\nIt's demo package.\n\n",
    'author': 'kk6',
    'author_email': 'hiro.ashiya@gmail.com',
    'url': 'https://github.com/kk6',
    'py_modules': modules,
    'install_requires': install_requires,
    'entry_points': entry_points,
    'python_requires': '>=3.7,<4.0',
}


setup(**setup_kwargs)

また、PKG-INFO を見ると先程pyproject.tomlに記載したClassifier等が記述されています。

Metadata-Version: 2.1
Name: howoldru
Version: 0.1.5
Summary:
Home-page: https://github.com/kk6
Author: kk6
Author-email: hiro.ashiya@gmail.com
Requires-Python: >=3.7,<4.0
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Topic :: Utilities
Requires-Dist: fire (>=0.1.3,<0.2.0)
Requires-Dist: pendulum (>=2.0,<3.0)
Project-URL: Documentation, https://github.com/kk6/howoldru/wiki
Project-URL: Repository, https://github.com/kk6/howoldru

インストールして使ってみる

こちらは素のvenv環境にpipでインストールするというごく普通のユースケースでやってみます。

$ mkdir foo
$ cd foo
$ python3 -m "venv" .venv
$ source .venv/bin/activate.fish

ではTestPyPIからインストールしてみましょう。PyPIではなくTestPyPIからインストールしたいので --index-urlでURLを指定します。

(.venv) $ pip install howoldru --index-url https://test.pypi.org/simple
Looking in indexes: https://test.pypi.org/simple
Collecting howoldru
  Downloading https://test-files.pythonhosted.org/packages/35/28/db1a4755d30fed9135cb9136e2151a0c2cf84421f6fed8c3a9f56919d534/howoldru-0.1.5-py3-none-any.whl
Collecting fire<0.2.0,>=0.1.3 (from howoldru)
  Could not find a version that satisfies the requirement fire<0.2.0,>=0.1.3 (from howoldru) (from versions: )
No matching distribution found for fire<0.2.0,>=0.1.3 (from howoldru)

おっと、失敗しました。どうやら依存ライブラリであるfireがTestPyPIに存在しないようです。

複数のサーバーから検索してくるには --extra-index-url を使うのが正解です。これはまず指定したURL(今回はTestPyPI)からインストールを試み、無ければPyPIのほうから探してきてくれます。

(.venv) $ pip install howoldru --extra-index-url https://test.pypi.org/simple
Looking in indexes: https://pypi.org/simple, https://test.pypi.org/simple
Collecting howoldru
  Using cached https://test-files.pythonhosted.org/packages/35/28/db1a4755d30fed9135cb9136e2151a0c2cf84421f6fed8c3a9f56919d534/howoldru-0.1.5-py3-none-any.whl
Collecting fire<0.2.0,>=0.1.3 (from howoldru)
  Using cached https://files.pythonhosted.org/packages/5a/b7/205702f348aab198baecd1d8344a90748cb68f53bdcd1cc30cbc08e47d3e/fire-0.1.3.tar.gz
Collecting pendulum<3.0,>=2.0 (from howoldru)
  Using cached https://files.pythonhosted.org/packages/1c/03/7b84db3d21741dc93ac0e89545010a143fc71437b24a7abdf9eff8a7e483/pendulum-2.0.4-cp37-cp37m-macosx_10_14_x86_64.whl
Collecting six (from fire<0.2.0,>=0.1.3->howoldru)
  Using cached https://files.pythonhosted.org/packages/73/fb/00a976f728d0d1fecfe898238ce23f502a721c0ac0ecfedb80e0d88c64e9/six-1.12.0-py2.py3-none-any.whl
Collecting pytzdata>=2018.3 (from pendulum<3.0,>=2.0->howoldru)
  Using cached https://files.pythonhosted.org/packages/46/8b/d7fe0c5b24870438d5ed59ceab72d537c5374d7db3cc57107e4a922b3a23/pytzdata-2018.7-py2.py3-none-any.whl
Collecting python-dateutil<3.0,>=2.6 (from pendulum<3.0,>=2.0->howoldru)
  Using cached https://files.pythonhosted.org/packages/74/68/d87d9b36af36f44254a8d512cbfc48369103a3b9e474be9bdfe536abfc45/python_dateutil-2.7.5-py2.py3-none-any.whl
Installing collected packages: six, fire, pytzdata, python-dateutil, pendulum, howoldru
  Running setup.py install for fire ... done
Successfully installed fire-0.1.3 howoldru-0.1.5 pendulum-2.0.4 python-dateutil-2.7.5 pytzdata-2018.7 six-1.12.0

無事インストールできました。ログを見ると Looking in indexes: https://pypi.org/simple, https://test.pypi.org/simple というようにTestPyPIとPyPIの両方を参照してくれているのがわかります。

それではインストールした howoldru を使ってみましょう。

(.venv) $ howoldru 2001-08-25
あなたは現在17歳です。

ちゃんと howoldru コマンドとして動作していますね。

まとめ

このようにPoetryはパッケージの依存関係の管理から公開まで一括で管理してくれます。パッケージ開発者にとってはとても便利なツールだと思います。

ただ、非常に強力で便利なツールではありますが、「これからはPoetryの時代だ!Pipenvなんか捨ててしまえ!」などと言うつもりはありません。Poetryがいかなるユースケースにおいても最適とは言い切れません。buildpublishも不要ならPipenvでも十分*1ですし、それ以上に素のpip+venvだけで十分なことも多いでしょう。公式なツールで事足りるなら、余計なものを入れるのはトラブルの素になりかねません。

というわけで目的に合わせて最適なツールを使い分けるのが良いと思います。

参考文献

*1:依存関係の解決すらPipenvよりPoetryのほうが賢いという問題はありますが

Pipenv から Poetry への乗り換え

乗り換えるリポジトリ

GitHub - kk6/aeroplast: Transparent PNG conversion (Mainly for Twitter)

twitterにアップロードしたPNGがJPGに強制変換されて見栄えが悪いという問題がある。それを回避するために画像の四隅どこかに1pxの透過ドットを打ち込むという方法がある。いちいちペイントソフトでそれを毎回やるのが面倒なのでコマンドラインで画像のPath渡したら変換してくれるコマンドラインツールを以前作った。こいつをPipenvで管理しているので、Poetryへの乗り換えを試してみる。

Poetry については数日中にもうちょっと突っ込んだ記事書きます。

pyproject.toml の生成

init コマンドで対話形式で情報を入力していくだけ。

$ poetry init

This command will guide you through creating your pyproject.toml config.

Package name [aeroplast]:
Version [0.1.0]:
Description []: Transparent PNG conversion.
Author [kk6 <hiro.ashiya@gmail.com>, n to skip]:
License []: MIT
Compatible Python versions [^3.7]:

Would you like to define your dependencies (require) interactively? (yes/no) [yes] no



Would you like to define your dev dependencies (require-dev) interactively (yes/no) [yes] no

Generated file

[tool.poetry]
name = "aeroplast"
version = "0.1.0"
description = "Transparent PNG conversion."
authors = ["kk6 <hiro.ashiya@gmail.com>"]
license = "MIT"

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

[tool.poetry.dev-dependencies]

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


Do you confirm generation? (yes/no) [yes] yes

これで pyproject.toml が生成される。

依存パッケージのインストール

続いて Pipfile を見ながらパッケージを poetry add で追加する。

(追記) だるいのでツール作った

kk6.hateblo.jp

(追記おわり)

$ poetry add pillow fire logzero
Using version ^5.3 for pillow
Using version ^0.1.3 for fire
Using version ^1.5 for logzero

Updating dependencies
Resolving dependencies... (3.2s)


Package operations: 0 installs, 1 update, 0 removals

Writing lock file

  - Updating six (1.11.0 -> 1.12.0)

インストールしたパッケージは tool.poetry.dependencies セクションに追記されている。

[tool.poetry]
name = "aeroplast"
version = "0.1.0"
description = "Transparent PNG conversion."
authors = ["kk6 <hiro.ashiya@gmail.com>"]
license = "MIT"

[tool.poetry.dependencies]
python = "^3.7"
pillow = "^5.3"
fire = "^0.1.3"
logzero = "^1.5"

[tool.poetry.dev-dependencies]

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

と同時に poetry.lock ファイルが生成されている。依存関係はこちらに記述されている。

pytestのインストールを忘れていた。

$ poetry add -D pytest
Using version ^4.0 for pytest

Updating dependencies
Resolving dependencies... (0.5s)


Package operations: 0 installs, 1 update, 0 removals

Writing lock file

  - Updating pytest (4.0.1 -> 4.0.2)

今回は -D オプションを付けてインストールしたので、pyproject.toml の tool.poetry.dev-dependencies セクションの方に追記されている。

[tool.poetry.dependencies]
python = "^3.7"
pillow = "^5.3"
fire = "^0.1.3"
logzero = "^1.5"

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

コマンドラインツールとして実行できるようにする

PipenvのときはPipfileに以下のように記述して使っていた。

[scripts]
cli = "pipenv run python -m aeroplast"

こんな感じで使っていた

$ pipenv run cli convert ...

Potryの場合は pyproject.tomltool.poetry.scripts セクションに記述する。

[tool.poetry.scripts]
aeroplast = "aeroplast.cli:main"

Poetryの便利なところは、パッケージをビルドした際にこの設定がそのまま setup.pyconsole_scripts として記述される。ビルドする時にコマンドとして実行できるようにあらためてsetup.pyを書く必要がない。

...
entry_points = \
{'console_scripts': ['aeroplast = aeroplast.cli:main']}
...

とまあそれはさておき poetry run aeroplast で実行できるようになった。

$ poetry run aeroplast
Type:        CLI
String form: <aeroplast.cli.CLI object at 0x100f6e978>
Docstring:   Transparent PNG conversion

Usage:       aeroplast
             aeroplast convert
             aeroplast resize

また、poetry developpip install -e . するのと同様に開発途中のパッケージをvenvにインストールできる。インストールした上で poetry shell でvenvをactivateすると aeroplast コマンドのみで実行可能になる。

$ poetry shell
Spawning shell within /Users/kk6/PycharmProjects/aeroplast/.venv
Welcome to fish, the friendly interactive shell
$ poetry develop
develop is deprecated use install instead.

Installing dependencies from lock file

Nothing to install or update

  - Installing aeroplast (0.1.0)
$ aeroplast
Type:        CLI
String form: <aeroplast.cli.CLI object at 0x107dfa908>
Docstring:   Transparent PNG conversion

Usage:       aeroplast
             aeroplast convert
             aeroplast resize
<aeroplast.cli.CLI object at 0x107dfa908>

以上で移行完了。わりと簡単。しかも今すぐにでも以下の2行を実行するだけでPyPIに公開可能な状態となっている。

$ poetry build
$ poetry publish

Poetry便利。

またPyenv環境に戻してみた

動機

ライブラリ開発とかしてるとどうしてもhomebrewだけではつらくなる(python3.6が入ってる状態でbrew upgradeしてpython3.7に上がると、brew cleanup叩いた時点でpython3.6が消される)

公式からバージョンごとにインストーラー落としてきて入れるというのも考えたが、pipenvにはpyenvと連携するオプションが用意されてるし、CircleCIも内部でpyenv使ってるしpoetryの作者もとあるissueに対する返信で「pyenvを使うことを強く推奨する」なんて言ってたのでもう海外でもpyenvが市民権得たのかなーとか思った次第。じゃあもうpyenvでいいやーと。

事前確認

brewで入れているPython

  • 2.7.10
  • 3.7.0

pyenvのインストール

~ $ brew install pyenv
==> Installing dependencies for pyenv: autoconf
==> Installing pyenv dependency: autoconf
==> Downloading https://homebrew.bintray.com/bottles/autoconf-2.69.mojave.bottle.4.tar.gz
######################################################################## 100.0%
==> Pouring autoconf-2.69.mojave.bottle.4.tar.gz
==> Caveats
Emacs Lisp files have been installed to:
  /usr/local/share/emacs/site-lisp/autoconf
==> Summary
🍺  /usr/local/Cellar/autoconf/2.69: 71 files, 3.0MB
==> Installing pyenv
==> Downloading https://homebrew.bintray.com/bottles/pyenv-1.2.8.mojave.bottle.tar.gz
######################################################################## 100.0%
==> Pouring pyenv-1.2.8.mojave.bottle.tar.gz
🍺  /usr/local/Cellar/pyenv/1.2.8: 612 files, 2.4MB
==> Caveats
==> autoconf
Emacs Lisp files have been installed to:
  /usr/local/share/emacs/site-lisp/autoconf

pyenvを使えるようにするために、bash等の場合は.bash_profile等にパスの追記等が必要だが、自分はfish shell & fisherman を使っているので、このへんはプラグインを入れるだけでOK.

~ $ fisher pyenv
Installing 1 plugin/s
OK Fetch pyenv github.com/fisherman/pyenv
Done in 2s 26ms

python3.6をインストールしてみるも失敗する

~ $ pyenv install 3.6.7
python-build: use openssl from homebrew
python-build: use readline from homebrew
Downloading Python-3.6.7.tar.xz...
-> https://www.python.org/ftp/python/3.6.7/Python-3.6.7.tar.xz
Installing Python-3.6.7...
python-build: use readline from homebrew

BUILD FAILED (OS X 10.14 using python-build 20180424)

Inspect or clean up the working tree at /var/folders/kb/knxvk74s36s20fd1n2sh7g200000gq/T/python-build.20181107053928.7189
Results logged to /var/folders/kb/knxvk74s36s20fd1n2sh7g200000gq/T/python-build.20181107053928.7189.log

Last 10 log lines:
  File "/private/var/folders/kb/knxvk74s36s20fd1n2sh7g200000gq/T/python-build.20181107053928.7189/Python-3.6.7/Lib/ensurepip/__main__.py", line 5, in <module>
    sys.exit(ensurepip._main())
  File "/private/var/folders/kb/knxvk74s36s20fd1n2sh7g200000gq/T/python-build.20181107053928.7189/Python-3.6.7/Lib/ensurepip/__init__.py", line 204, in _main
    default_pip=args.default_pip,
  File "/private/var/folders/kb/knxvk74s36s20fd1n2sh7g200000gq/T/python-build.20181107053928.7189/Python-3.6.7/Lib/ensurepip/__init__.py", line 117, in _bootstrap
    return _run_pip(args + [p[0] for p in _PROJECTS], additional_paths)
  File "/private/var/folders/kb/knxvk74s36s20fd1n2sh7g200000gq/T/python-build.20181107053928.7189/Python-3.6.7/Lib/ensurepip/__init__.py", line 27, in _run_pip
    import pip._internal
zipimport.ZipImportError: can't decompress data; zlib not available
make: *** [install] Error 1

最近Mojaveにアップグレードしたのが原因だった

mac os Mojave にアップグレードしたら bundle install が失敗する事象を解決した - Qiita

~ $ open /Library/Developer/CommandLineTools/Packages/
The file /Library/Developer/CommandLineTools/Packages does not exist.

上記リンクのQiita記事を参考にopenしてみるも、無いって言われる。記事の一番下の参照元リンク先みるとxcode-selectが必要っぽい。

~ $ xcode-select --install
xcode-select: note: install requested for command line developer tools

インストール完了後、再びopenコマンド実行すると今度はちゃんとフォルダが開くので、macOS_SDK_headers_for_macOS_10.14.pkgをダブルクリックでインストールする。

再びpyenvでpython3.6をインストールすると、今度は無事成功。

それぞれのPythonを呼び出せるようにする

simlink作成(非推奨)

tox等で使えるよう、python3.6コマンドで呼び出せるようにしたい。何も考えずに最初はsimlinkで対応した。

$ cd /usr/local/bin
$ ln -s ~/.pyenv/versions/3.6.7/bin/python3.6 .

でも後で気づいたが公式のやりかたがあった

pyenv global コマンド

~ $ pyenv versions
* system (set by /Users/kk6/.pyenv/version)
  2.7.15
  3.4.9
  3.5.6
  3.6.7

上記のような状態で、とりあえず3系全部動くようにしたいとする。pyenv globalに使いたいバージョンを列挙すればいいだけ。

~ $ pyenv global 3.4.9 3.5.6 3.6.7
~ $ pyenv versions
  system
  2.7.15
* 3.4.9 (set by /Users/kk6/.pyenv/version)
* 3.5.6 (set by /Users/kk6/.pyenv/version)
* 3.6.7 (set by /Users/kk6/.pyenv/version)
~ $ python3.5
Python 3.5.6 (default, Nov  7 2018, 06:07:53)
[GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.11.45.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 

これだけ。いちいちsimlink張るより楽。ただ、simlinkのほうが起動は速い。

【追記】

pyenv global するとき、バージョンが新しいものから列挙してやらないとpython3コマンドが3.4を見に行ったりするので以下のようにする。

$ pyenv global 3.7.1 3.6.7 3.5.6 3.4.9 2.7.15

brewで入れたpython3.7をどうするか問題

brewpythonとpyenvのpython混在とか地獄を見そうなのでbrewpythonを消そうと思ったが…

~ $ brew uninstall python3
Error: Refusing to uninstall /usr/local/Cellar/python/3.7.0
because it is required by thefuck and vim, which are currently installed.
You can override this and force removal with:
  brew uninstall --ignore-dependencies python3

python3に依存してるやつらがいるから消せないとのこと。

thefuckに関してはわりとどうでもいい。vimもいい機会だしneovimにするのもありかもしれない。pyenvのpython3.7インストールしたら問題無さそうな気もするし、問題があればpythonインストール後にこいつらをbrewでインストールし直すのでもいけそうではある。

brewpython消してpyenvで入れ直した

やっぱり3.7だけbrewで他のバージョンがpyenvなの気持ち悪すぎるのでpyenvに統一する。

$ brew uninstall thefuck vim python3したあと$ pyenv install 3.7.1した。

で、既存のvenvが壊れるのは想定内で、pipenvが壊れるのも想定内(brewのpython3.7で入れていた)。ただ、pipも動かないとは思ってなかった。

$ pipenv
Failed to execute process '/Users/kk6/.local/bin/pipenv'. Reason:
The file '/Users/kk6/.local/bin/pipenv' specified the interpreter '/usr/local/opt/python/bin/python3.7', which is not an executable command.
$ pip
Failed to execute process '/usr/local/bin/pip'. Reason:
The file '/usr/local/bin/pip' specified the interpreter '/usr/local/opt/python/bin/python3.7', which is not an executable command.
$ which pip
/usr/local/bin/pip

エラーメッセージ見れば分かる通り、/usr/local/bin/にゴミが残っている。ついでにpip3も消しておく。

$ which pip
/Users/kk6/.pyenv/shims/pip
$ pip

Usage:
  pip <command> [options]

Commands:
  install                     Install packages.
  download                    Download packages.
  ...

これで動くようになった。

あとはpipenvを入れ直す。

$ pip install --user pipenv

以上でひとまずpyenvへの移行完了です。