PipenvからPoetryへのお引越しツール作った

動機

1/13にPoetry 0.12.11 がリリースされました。

poetry.eustace.io

それに伴い、poetry init で会話形式でいろいろ設定していく際に依存パッケージを事前に検索して記述する機能のバグが修正されました。

$ poetry init

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

Package name [foo]:
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] yes

Search for package:

ここのことです。

前のバージョンだと検索するとLegacyRipositoryなんちゃらってエラーで落ちてたので使わずスキップするしかなかったんですが、直ったとのことなのでさっそく試してみると…

Search for package: django

Found 100 packages matching django

Enter package # to add, or the complete package name if it is not listed:
 [ 0] django-bagou
 [ 1] django-maro
 [ 2] django-ide
 [ 3] django-hooked
 [ 4] django-six
 [ 5] django-umanage

(中略)

 [95] django-bittersweet
 [96] django-perm
 [97] django-simplemde
 [98] django-rosetta
 [99] django-pjax
 >

ああ、うん…。そっか、そういうやつなんだ…。

引っ越しツール

これだとPipfile見ながらひとつひとつ poetry add django とかしていくのとそんな大差ない気がするのでPipfileパースしていい感じに poetry init を実行してくれるツール作りました。

github.com

たぶんMac以外だと全然動かなかったりしそうなのでPyPIには登録してません。名前もイマイチ思いつかなかくて長ったらしいしかえるかもしれない(変えた*1)ので、ひとまず個人的に使うだけのつもりですが、それでも試したい人いたらどうぞ。

PyPI に登録しました。Mac以外だとまともに動かない気がするのには変わりないのであくまでアルファ版ということで。

pypi.org

概要

poetry init コマンドは --dependency--dev-dependencyオプションにパッケージ名を与えることで、pyproject.toml[tool.poetry.dependencies] および [tool.poetry.dev-dependencies] セクションにいい感じに依存バージョン解決して書き込んでくれるという機能があります。

今回作ったツールはPipfileをパースしてこの--dependency/--dev-dependencyオプションの文字列を組み立ててくれるツールになってます。

インストール

READMEに二通りのインストール方法書いてますけど、今回はこっちの $HOME/.local/bin 以下にインストールする方法にしてみます。リポジトリsetup.py がないので pip install --user git+https://github.com/kk6/poetrify.git ではインストールできません。ごめんなさい。

PyPIに登録したのでpipでインストールできます。

$ pip install poetrify

引数無しで実行して以下のように出力されていればインストールできてます。

$ poetrify
Poetrify version 0.2.0

USAGE
  poetrify [-h] [-q] [-v [<...>]] [-V] [--ansi] [--no-ansi] [-n] <command> [<arg1>] ... [<argN>]

ARGUMENTS
  <command>              The command to execute
  <arg>                  The arguments of the command

GLOBAL OPTIONS
  -h (--help)            Display this help message
  -q (--quiet)           Do not output any message
  -v (--verbose)         Increase the verbosity of messages: "-v" for normal output, "-vv" for more verbose output and
                         "-vvv" for debug
  -V (--version)         Display this application version
  --ansi                 Force ANSI output
  --no-ansi              Disable ANSI output
  -n (--no-interaction)  Do not ask any interactive question

AVAILABLE COMMANDS
  completions            Generate completion scripts for your shell.
  generate               Generate pyproject.toml from Pipfile
  help                   Display the manual of a command

使い方

こんな感じのリポジトリがあるとします。実際は他にもソースコード等あると思いますけど。

$ tree
.
├── LICENSE
└── Pipfile

0 directories, 2 files

Pipfileの中身はこんな感じだとします。

[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[dev-packages]
pytest = "*"
pytest-cov = "*"
"pytest-flake8" = "*"
responses = "*"
pytest-runner = "*"

[packages]
rauth = "*"
requests = "*"
requests-cache = "*"
furl = "*"
arrow = "*"
pytest = "*"
responses = "*"

[requires]
python_version = "3.7"

ツールの generate コマンドを実行します。

$ poetrify generate
Generated init command:

poetry init --dependency=rauth --dependency=requests --dependency=requests-cache --dependency=furl --dependency=arrow --dependency=pytest --dependency=responses --dev-dependency=pytest --dev-dependency=pytest-cov --dev-dependency=pytest-flake8 --dev-dependency=responses --dev-dependency=pytest-runner --license=MIT

Execute the above command. Also, the following output is due to Poetry.

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

Package name [foo]:

LICENSEファイルもパースしてライセンスを自動判定させてるので、これもちゃんと判定できてれば会話モードで License [MIT]: みたいな感じでデフォルト値としてパースされたライセンス名が入ってるはずです。

initコマンドを組み立てたらあとは雑に subprocess.run() に投げてるだけです…。

あとは普通に poetry init の会話モードでの入力となります。

Package name [foo]:
Version [0.1.0]:
Description []:
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]

Using version ^0.7.3 for rauth
Using version ^2.21 for requests
Using version ^0.4.13 for requests-cache
Using version ^2.0 for furl
Using version ^0.13.0 for arrow
Using version ^4.1 for pytest
Using version ^0.10.5 for responses

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

Using version ^4.1 for pytest
Using version ^2.6 for pytest-cov
Using version ^1.0 for pytest-flake8
Using version ^0.10.5 for responses
Using version ^4.2 for pytest-runner

Generated file

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

[tool.poetry.dependencies]
python = "^3.7"
rauth = "^0.7.3"
requests = "^2.21"
requests-cache = "^0.4.13"
furl = "^2.0"
arrow = "^0.13.0"
pytest = "^4.1"
responses = "^0.10.5"

[tool.poetry.dev-dependencies]
pytest = "^4.1"
pytest-cov = "^2.6"
pytest-flake8 = "^1.0"
responses = "^0.10.5"
pytest-runner = "^4.2"

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

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

基本的にgenerateコマンド叩いた後はEnter押しっぱなしでいい感じにpyproject.tomlが生成されると思います。

既知の不具合

poetry init自体がエラーで落ちるようなパッケージ名を与えると当然このツールも落ちます。

$ poetry init --dependency black

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

Package name [foo]:
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]


[ValueError]
Could not find a matching version of package black

init [--name NAME] [--description DESCRIPTION] [--author AUTHOR] [--dependency DEPENDENCY] [--dev-dependency DEV-DEPENDENCY] [-l|--license LICENSE]

たぶんpipenvで allow_prereleases = true に設定してないとインストールできないようなパッケージはこうなります。

--dev-dependency=black:18.9b0 みたいにプレリリースなバージョン番号を指定できればちゃんとインストールできるんですけど、pipenvって pipenv install black --pre とインストールしてもPipfileには black = "*" って何でもかんでもアスタリスクにしてしまうのでバージョン番号がわからないんですよね…。Pipfile.lockのほうに書いてあるけど、こっちパースするの面倒くさそうなので気が向いたらやるかもって感じです。

なのでとりあえずPipfileから該当パッケージ行を削除してやり直して、うまくpyproject.tomlが生成できたら、あとから手動で poetry add --allow-prereleases black みたいな感じで --allow-prereleases オプション付きでインストールしてください。

*1:pipenv2poetry → poetrify

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