Poetry + Read the Docs でハマった話

pythonhosted.org でのホスティングはオワコン

以前は pythonhosted.org でホスティングしていた

Annictというアニメの視聴記録等ができるWebサービスがある。

annict.jp

REST APIが提供されているのでそのPython wrapperを作っていた。

github.com

で、このライブラリのドキュメント、Pipenvすら導入以前の requirements.txt で管理していた頃に一度 Read the Docs でホスティングしようと試したことがあるが、ビルドが上手くいってなかった。当時は色々試してみたもののよくわからなくて諦め、他の方法を探していたところ pythonhosted.org でホスティングする方法を見つけたのでそうしていた。

ホスティングサポート終了

前述のpython-annict、細々とメンテしたり放置したりしつつも、この度ひさびさに機能追加してリリースした。なのでドキュメントを更新したいのだけれど、どうにもそれらしきUIが見当たらない。

PyPI のドキュメントの管理画面を開くと、削除ボタンのところに「一度削除すると再アップロードはもはやサポートしていないので復元できない」とある。

Destroy documentation

Warning If you would like to DESTROY any existing documentation hosted athttps://pythonhosted.org/annict/

There is no undo as uploading new documentation is no longer supported.

同様の話題が pipy-legacy の issue でも見つかった。それによると代わりにRead the Docsとか使って欲しいとのこと。

github.com

どうやらお引越しが必要のようだ。

Read the Docs を再び試す

依存ライブラリの ImportError が発生する

Read the Docs に久々にログインし、ビルドを再び試すも ImportError が発生。furlが無いとのこと。

Running Sphinx v1.7.9

Traceback (most recent call last):
  File "/home/docs/checkouts/readthedocs.org/user_builds/python-annict/envs/latest/local/lib/python2.7/site-packages/sphinx/cmdline.py", line 303, in main
    args.warningiserror, args.tags, args.verbosity, args.jobs)
  File "/home/docs/checkouts/readthedocs.org/user_builds/python-annict/envs/latest/local/lib/python2.7/site-packages/sphinx/application.py", line 163, in __init__
    confoverrides or {}, self.tags)
  File "/home/docs/checkouts/readthedocs.org/user_builds/python-annict/envs/latest/local/lib/python2.7/site-packages/sphinx/config.py", line 167, in __init__
    raise ConfigError(CONFIG_ERROR % traceback.format_exc())
ConfigError: There is a programable error in your configuration file:

Traceback (most recent call last):
  File "/home/docs/checkouts/readthedocs.org/user_builds/python-annict/envs/latest/local/lib/python2.7/site-packages/sphinx/config.py", line 161, in __init__
    execfile_(filename, config)
  File "/home/docs/checkouts/readthedocs.org/user_builds/python-annict/envs/latest/local/lib/python2.7/site-packages/sphinx/util/pycompat.py", line 150, in execfile_
    exec_(code, _globals)
  File "/home/docs/checkouts/readthedocs.org/user_builds/python-annict/envs/latest/local/lib/python2.7/site-packages/six.py", line 709, in exec_
    exec("""exec _code_ in _globs_, _locs_""")
  File "<string>", line 1, in <module>
  File "conf.py", line 24, in <module>
    from annict import __version__, __author__
  File "/home/docs/checkouts/readthedocs.org/user_builds/python-annict/checkouts/latest/annict/__init__.py", line 16, in <module>
    from .api import API  # noqa
  File "/home/docs/checkouts/readthedocs.org/user_builds/python-annict/checkouts/latest/annict/api.py", line 2, in <module>
    from .services import APIMethod
  File "/home/docs/checkouts/readthedocs.org/user_builds/python-annict/checkouts/latest/annict/services.py", line 2, in <module>
    from furl import furl
ImportError: No module named furl

これはもしや requirements.txt が必要なやつなのでは…。

あとPython2.7でビルドしようとしているのでRead the Docsのプロジェクトの管理画面より使用するPythonのバージョンを CPython3.x に変更しておいた。

この辺いろいろやってて記憶が定かではないが、たしか「管理 > 高度な設定」の 「setup.py installでvirtualenv環境にあなたのプロジェクトをインストールします」のチェックを入れるとエラー内容が代わり、Python3.6から使えるようになっているf-stringsのところでSyntaxErrorで落ちた。CPython3.xとは3.5のことだったのだ…。これどうしたらいいんだ。

他のリポジトリを参考にする

poetry はどうやってるんだろうと思ったら Sphinx ではなく Mkdocs を使ってたので参考にならず。blackはPoetry使っててドキュメントはRead the Docs を使っていたので参考になりそう。

github.com

リポジトリ内を念入りに見て回ると、どうやら readthedocs.ymldocs/environment.yml というファイルに秘密がありそう。

ググると Read the Docs のドキュメントがヒットした。

Read the Docs YAML Config — Read the Docs 2.7 documentation

どうやらWEBの管理画面でできるのと同じような設定をYamlから指定することもできるらしい。しかもこちらだとPython3.6を使うよう指定したりcondaを利用したりできる模様。

Yamlにビルドの設定を書く

なんとなく把握したところで black の readthedocs.ymldocs/environment.yml を参考に以下のようにした

readthedocs.yml

name: annict
type: sphinx
conda:
     file: docs/environment.yml
python:
  version: 3

condaを使用するよう指定する。ちなみにPythonのバージョンはyaml経由だと3.6ではなく3でも問題なかった。

docs/environment.yml

name: annict_docs
channels:
  - conda-forge
dependencies:
- python>=3.6
- Sphinx==1.8.2
- pip:
  - sphinx-rtd-theme==0.4.2
  - arrow==0.12.1
  - furl==2.0.0
  - rauth==0.7.3
  - requests==2.21.0
  - requests-cache==0.4.13
  - annict==0.7.0

condaがインストールするライブラリたちを指定する。そうかここでPython3.6以上を指定してるからそれがはいるのか。

dependenciesのトップレベルに指定できるのはcondaでインストールできるものだけ。最初 sphinx-rtd-theme をトップレベル(Sphinxとpipの間の行)に記載したらResolvePackageNotFound というエラーでビルド失敗した。condaよく知らないけどcondaはPyPIからなんでもインストールできるわけではないんだったような気がする。condaでインストールできないものは pip のしたにインデントして列挙する。これでpip経由でのインストールになるのでpipでインストールできるものなら何でも入る。

無事ビルド成功

2つのYamlをmasterにcommit&pushしたら無事Read the Docsでビルド成功した。というわけで requirements.txt が無くてもビルドすることができた。たぶん setup.py が無くてもいけると思う。

まとめ

Poetryでパッケージ管理していると setup.py とか requirements.txt ファイルが無いので Read the Docs 側がPoetryに対応してくれるまで利用できないのかと思ったけど、そんなことはなくてRead the Docsのドキュメントしっかり読むと結構色々できそうな感じだった、というお話でした。

Poetryでインストール時にTooManyRedirectsが起こるときの対処法

問題

二日前は成功していた pre-commit というライブラリのインストールが失敗するようになった。

$ poetry add -D pre-commit
Creating virtualenv demo-py3.7 in /Users/kk6/sandbox/demo/.venv

[TooManyRedirects]
Exceeded 30 redirects.

add [-D|--dev] [--git GIT] [--path PATH] [-E|--extras EXTRAS] [--optional] [--python PYTHON] [--platform PLATFORM] [--allow-prereleases] [--dry-run] [--] <name> (<name>)...

-vvv を付けて再実行しtracebackを確認すると、どうやら requests が発生させている例外の模様。ただ、それ以上はよくわからなかった。

$ poetry add -D pre-commit -vvv
Using virtualenv: /Users/kk6/sandbox/python/precommit-demo/.venv

[TooManyRedirects]
Exceeded 30 redirects.

Exception trace:
 /Users/kk6/.poetry/lib/poetry/_vendor/py3.7/cleo/application.py in run() at line 94
   status_code = self.do_run(input_, output_)
   ...
 /Users/kk6/.poetry/lib/poetry/_vendor/py3.7/requests/sessions.py in resolve_redirects() at line 165
   raise TooManyRedirects('Exceeded %s redirects.' % self.max_redirects, response=resp)

Issueを探る

とりあえずこういうときはIssueを覗いてみるのが一番。PoetryのIssueで TooManyRedirects で検索してみる。

github.com

似たような問題を発見。これの下の方を読んでるとキャッシュ消したら解決したという情報が。

Poetryのキャッシュを削除する

Poetry でキャッシュを消すには cache:clear [--all] [--] <cache> を実行する。<cache> には対象のサーバー名を指定する。PyPIのキャッシュを消したいので以下のようにする。

$ poetry cache:clear pypi --all

Delete 297 entries? (yes/no) [no] yes

キャッシュ削除後、ちゃんとインストールできた。

$ poetry add -D pre-commit
Using version ^1.13 for pre-commit

Updating dependencies
Resolving dependencies... (4.8s)


Package operations: 17 installs, 0 updates, 0 removals

Writing lock file

  - Installing pyyaml (3.13)
  - Installing six (1.12.0)
  - Installing aspy.yaml (1.1.1)
  - Installing atomicwrites (1.2.1)
  - Installing attrs (18.2.0)
  - Installing cached-property (1.5.1)
  - Installing cfgv (1.1.0)
  - Installing identify (1.1.7)
  - Installing importlib-metadata (0.7)
  - Installing more-itertools (4.3.0)
  - Installing nodeenv (1.3.3)
  - Installing pluggy (0.8.0)
  - Installing py (1.7.0)
  - Installing toml (0.10.0)
  - Installing virtualenv (16.1.0)
  - Installing pre-commit (1.13.0)
  - Installing pytest (3.10.1)

TooManyRedirects に限らず、インストールで問題が起きたらとりあえずキャッシュを消してみるといいかもしれない。

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のほうが賢いという問題はありますが