poetryで管理されているプロジェクトでeditable installがしたい

まぁまれにある。work-aroundのメモ。

editable install?

通常のpythonのパッケージのインストールは、パッケージ中のファイルをsite-packages以下にコピーすることなのだけど、editable installというのは、直接参照して利用する形式のインストールのこと。開発時にはimportして利用されるものが直接手元で管理しているリポジトリのファイルであってほしい。そういうときに使う。

関連しそうなstack overflowのページヘのリンクでも貼っておく。

通常の方法

自身のパッケージをeditable installしたい場合には以下の様にする。これが通常の方法。

$ pip install -e .

poetryでこれをやる方法

いろいろ調べてみるとそういう機能は無いようだ。というわけでwork-aroundとしては以下の様な感じになる。

  1. poetry buildでsdistを生成 (dist/<package name>-<version>.tar.gz)
  2. 生成されたファイルからsetup.pyを抽出
  3. pyproject.tomlを消す
$ poetry build
$ tar -O -xf dist/<package name>-<version>.tar.gz <package name>-<version>/setup.py > setup.py
$ rm pyproject.toml
$ pip install -e .

詳細(実行例)

以下は詳細。

fooパッケージを仮に作ってそのfooパッケージに対しての例をメモしておく。

# foo パッケージ作成 (通常はこれは不要)
$ poetry new foo
Created package foo in foo
$ cd foo

# dist/*.tar.gz を生成 (sdist)
$ poetry build
Building foo (0.1.0)
  - Building sdist
  - Built foo-0.1.0.tar.gz
  - Building wheel
  - Built foo-0.1.0-py3-none-any.whl

# setup.pyがあることを確認
$ tar -tf dist/foo-0.1.0.tar.gz
foo-0.1.0/README.md
foo-0.1.0/foo/__init__.py
foo-0.1.0/pyproject.toml
foo-0.1.0/setup.py
foo-0.1.0/PKG-INFO

# setup.pyを抽出
$ tar -O -xf dist/foo-0.1.0.tar.gz foo-0.1.0/setup.py > setup.py

# editable install
$ rm pyproject.toml
$ pip install -e .

成功すると foo.egg-infoができる。これでどこからでも import foo をして確認することができるようになる(editable installとして)。

なんでpyproject.tomlを消すの?

pep517はeditable installをサポートしていないので。 editable installをどうするかについては議論が進行中。

build_wheel_for_editableという欄を設けようというようなpepが準備されているみたい。

flitにこれのPOCのPRがあったりする。お試しで対応するPR。

poetry add --editable

実は自分自身ではなく依存ライブラリに関しては editable installをする機能をpoetryも追加してくれている。元々は自分自身でpyproject.tomlに develop=true とかちょっと面倒な感じで指定する必要があったのだけれど。poetry addにeditable installをする機能が追加された。1.2から利用できるようになる。

poetry add --editable <package> として依存を追加するとeditable installになる。

そんなわけでfooとbarというパッケージを作って、barで poetry add --editable foo としてあげると言う方法もありはする。

$ poetry new foo
Created package foo in foo
$ poetry new bar
Created package bar in bar
$ cd bar/
$ poetry add --editable ../foo

Updating dependencies
Resolving dependencies... (0.1s)

Writing lock file

Package operations: 1 install, 0 updates, 0 removals

  • Installing foo (0.1.0 ~/venvs/my/foo)
$ python
Python 3.9.2 (default, Mar 15 2021, 19:36:22) 
[Clang 8.0.0 (clang-800.0.42.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import foo
>>> foo.__file__
'~/venvs/my/foo/foo/__init__.py'

flit

ちなみにflitでは flit -s でインストールできる。fastapiなんかはflitを利用している

実際の実行例はこんなかんじ。

$ ghq tiangolo/fastapi
$ cd ~/ghq/github.com/tiangolo/fastapi
$ flist install -s
...
Symlinking fastapi -> ~/venvs/my/lib/python3.9/site-packages/fastapi                  I-flit.install

pythonのargparseのオプションと関数の引数の乖離を実際の処理を呼ばずに確認する方法を考えてみる

argparseをそのまま使うと以下の様な形になる。色々な書き方があるが、個人的にはflag部分はキーワード引数を使った関数定義にして、parse_args()で返ってきた値をそのまま使わずに**paramsで適用する形が好み。

def hello(*, name: str) -> None:
    print(f"hello {name}")


def main() -> None:
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument("--name", required=True)
    args = parser.parse_args()

    params = vars(args).copy()
    hello(**params)


if __name__ == "__main__":
    main()

この方法の利点として、実行してみれば、関数の引数部分とargparseのオプション名の乖離に気づける様になる事が挙げられる。あとは暗黙的な属性名でのアクセス(e.g. args.name)などが存在しないのでどのような値に依存しているかがわかりやすくなる。

とはいえ、オプションの定義部分と関数の引数部分を別個に記述しなければいけないので、両者が揃った記述になるよう注意し続けなくてはいけない。 ここで、そこそこ高価なコマンドを作っているときなどには、このオプション名と引数部分の乖離を実際に関数を呼ぶ事なく調べたい。そのようなことができるか?というのが今回の課題。

関数を呼ばずに関数の引数の正当性をチェックしたい

pythonの型は動的なので実行によってチェックされる。例えば不適切な引数を渡したときには失敗によってそれに気づく。

# hello(nam="foo")
TypeError: hello() got an unexpected keyword argument 'nam'

失敗するときにはこれで良いが、成功したときにも呼び出したくない。とはいえ個々の関数に以下のようなhookをつけていくのもバカバカしい。というか、どうかしている。

def hello(*, name: str) -> None:
    import os
    # FAKE_CALL=1 python main.py --name=foo などとして実行した場合には静かに終了したい
    if bool(os.getenv("FAKE_CALL")):
        sys.exit(0)

    print(f"hello {name}")

実はこういうsignatureに即しているかだけを確認したいときにはinspect.getcallargsが使える1。関数のsignatureなどをいい感じに見て、関数呼び出しを辞書に変換してくれる。

inspect.getcallargs(hello, name="foo")
{'name': 'foo'}

これを上手く使うことで関数を呼ばずに正当性をチェックできる。

FAKE_CALL=1 でチェックだけして終了

そんなわけで functools.partial も使って以下の様に書くと望みの挙動になる

def hello(*, name: str) -> None:
    print(f"hello {name}")


def main() -> None:
    import os
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument("--name", required=True)
    args = parser.parse_args()

    params = vars(args).copy()
    fn = hello

    if bool(os.getenv("FAKE_CALL")):
        from inspect import getcallargs
        from functools import partial

        fn = partial(getcallargs, fn)
    fn(**params)


if __name__ == "__main__":
    main()

実際に実行してみる。期待通りの挙動。

$ FAKE_CALL=1 python 01hello.py --name world

$ python 01hello.py --name world
hello world

別解

別解としてtyperなどを使うというのもありかもしれない。そもそも記述箇所が1つだけなら悩むこともない2

github.com

import typer


def hello(*, name: str = typer.Option("")) -> None:
    print(f"hello {name}")


if __name__ == "__main__":
    typer.run(hello)

補完とちょっとしたmarkdownの生成もついてくるし無限にネストしたサブコマンドも付いてくるし、便利なツールとして自分の道具箱の中に入れておくのはありだと思う3

別解2

自作したhandsofcatsも同様の形で使えるが、知名度などを気にするならtyperに絞った方が多分良い4

github.com

from handofcats import as_command


@as_command
def hello(*, name: str) -> None:
    print(f"hello {name}")

こういう事ができたりはする。

$ python 03*.py --expose --simple


def hello(*, name: str) -> None:
    print(f"hello {name}")


def main(argv=None):
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('--name', required=True, help='-')
    args = parser.parse_args(argv)
    params = vars(args).copy()
    action = hello
    return action(**params)


if __name__ == '__main__':
    main()

元々の発端は、こうやってexposeしてargparseにバラした後は引数とオプションの整合性を自分で気をつける必要があるなー、と思ったところから。 例えば、CIなどで FAKE_CALL=1 python hello.py --name foo などと試すスクリプトを動かしておけば必ず実行可能なこと(関数のbodyまで到達すること)は保証できそう。

gist


  1. 他の用途は例えばRPCのclient側でのvalidation

  2. ところで、requiredなフラグを作る方法がわかっていない。 (追記: https://typer.tiangolo.com/tutorial/options/required/)

  3. ちょっとしたLTをする機会があればスライドを作っても良いかもしれない

  4. エディタにEmacsを使っているが他人には勧めないみたいなものと似たような行動