poetryで管理されているプロジェクトでeditable installがしたい
まぁまれにある。work-aroundのメモ。
editable install?
通常のpythonのパッケージのインストールは、パッケージ中のファイルをsite-packages
以下にコピーすることなのだけど、editable installというのは、直接参照して利用する形式のインストールのこと。開発時にはimportして利用されるものが直接手元で管理しているリポジトリのファイルであってほしい。そういうときに使う。
関連しそうなstack overflowのページヘのリンクでも貼っておく。
通常の方法
自身のパッケージをeditable installしたい場合には以下の様にする。これが通常の方法。
$ pip install -e .
poetryでこれをやる方法
いろいろ調べてみるとそういう機能は無いようだ。というわけでwork-aroundとしては以下の様な感じになる。
- poetry buildでsdistを生成 (
dist/<package name>-<version>.tar.gz
) - 生成されたファイルからsetup.pyを抽出
- 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。
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。
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まで到達すること)は保証できそう。