過去に作ったpythonの関数をコマンドとして扱えるようにするライブラリを更新しました

github.com

過去に作ったpythonの関数をコマンドとして扱えるようにするライブラリを更新しました。

なんでhandfcats?

猫の手も借りたいときに使いたかったからです。痒いところに手が届くという意味では、猫の手よりまごの手の方が近いのかもしれないですが。まぁでも猫の手の方が可愛いので。 ちなみに実は名前の正しさについては悩んでいて以下のどれがふさわしい名前なのかわかっていなかったりします。

  • handofcat
  • handofcats
  • handsofcat
  • handsofcats

どういう機能?

pythonの関数定義を無料でコマンドとして扱えるようにするライブラリです。基本的な使いかたはhandofcats.as_commandデコレータをつけるだけです。

例えば以下の様な定義を書いてあげると、すぐにコマンドとして利用できます(型定義を見るので型アノテーションは書いてください)。

from handofcats import as_command


@as_command
def greeting(message: str, is_surprised: bool = False, name: str = "foo") -> None:
    """greeting message"""
    suffix = "!" if is_surprised else ""
    print("{name}: {message}{suffix}".format(name=name, message=message, suffix=suffix))

以下の様に扱われます。

  • 通常の引数(positional arguments)は、通常のコマンドライン引数に(必須)
  • キーワードオンリーの引数(keyword only arguments)は、オプション引数に(必須)
  • デフォルト値を持った引数は、オプション引数(省略可能)

また、int,float,boolについてだけですが型をよしなに取り扱ってくれます。

実行例

$ python greeting.py --is-surprised hello
foo: hello!

help message

$ python greeting.py -h
usage: greeting.py [-h] [--expose] [--is-surprised] [--name NAME] message

greeting message

positional arguments:
  message

optional arguments:
  -h, --help      show this help message and exit
  --expose
  --is-surprised
  --name NAME     (default: 'foo')

ちなみにhandofcatsコマンドというものも扱えてこれはデコレータ無しに任意の関数を指定してあげると、それがコマンドとして扱われるというようなものです(詳細はリンク先など)。

なんでライブラリの更新をしたかったの?

元々の実装でも期待通りには動いていたのですが。書き換えました。概ね書き直しです。書き直した理由は幾つかあるのですが大きく2つです。

  • sphinx形式のdoc commentを見るより、型アノテーションを見たほうが良い
  • 以前から個人的にテーマとしている「やがて消えてなくなるライブラリ」という概念を実装できそうだったため

書き直しのタイミングで幾つか過剰に保持していた機能も一部捨て去りました(ミドルウェア的な機能など)。

sphinx形式のdoc commentを見るより、型アノテーションを見たほうが良い

これはそのままの意味で、当時はたしかか型アノテーションも一般的ではなく、どちらかと言えばまだsphinxのautodocで生成する時に利用するdoc commentの形式の方が主流だった記憶があります。ただ、これを実装した当時もオプションという扱いで、あまりアピールしたい機能として実装はしていませんでした。

というのも、基本的には、実装の初期段階の試行錯誤の間で、消しては作ってを繰り返し、最終的には残るかどうかもわからないコマンド(スクリプト)の使用感を試すというようなタイミングで、横着をするためにこのライブラリがあるわけなのですが。まじめにsphinx形式のドキュメント文字列を書いている時点で、コマンドラインパーサーの定義くらい書いてしまうことが多いからです。これは自分だけかもしれませんが、試行錯誤のコマンドラインパーサーの定義(argparse)の方がドキュメント文字列より先になることが多いです。また、まじめにコマンドとして作り込みたいという場合には、それこそ自分でコードを書くと言うことが多かったりします。

過去のバージョンで冒頭に紹介したコードと同様のことを行う例は以下です。

from handofcats import as_command


@as_command
def greeting(message, is_surprised=False, name="foo"):
    """ greeting message

    :param message: message of greeting
    :param is_surprised: surprised or not (default=False)
    :param name: name of actor
    """
    suffix = "!" if is_surprised else ""
    print("{name}: {message}{suffix}".format(name=name, message=message, suffix=suffix))

ちなみに、上の例からわかると思いますが、以前のバージョン(0.4.4)と現在のバージョン(2.0.0)では機能に違いがあります。引数毎のコメントをつけることができなくなっています。

ただ、機能が増えた面もあって、デフォルト引数なしに、数値として扱うことができるようになっています(記憶が確かなら過去の版はsphinxのautodoc用のコメントのうちの型定義の読み込みは省略していたような記憶があります)。以前のバージョンではデフォルト引数の型を見て数値や論理値として扱うかどうか判断していた記憶があります。

以前から個人的にテーマとしている「やがて消えてなくなるライブラリ」という概念を実装できそうだったため

どちらかと言えば、これが主題です。

これについては別のところで詳しく話したりしたいのですが、自分の中でのライブラリの理想として、「やがて消えてなくなるライブラリ」というものがあります。これは、不要になったら淡雪のように溶けてなくなるようなライブラリが理想ということです。

最近のOSS界隈で有名なソフトウェアを見てみると個人で作ったものが市民権を得ると言うよりは、何らかのメンテナンスを行う母体がある所のOSSが市民権を得ているような気がします。特にドキュメンテーションとメンテナンスに関してはけっこう多大なりソースが必要そうで、それを個人で賄っている人たちもいますが(尊敬しています)どこか辛そうな印象も受けたりしています。

加えて、何らかの概念や世界観を元に作るようなある種opinionatedなソフトウェアは、ドキュメントを手厚くする必要があったり、チュートリアルのための動画を作ったり、コミュニケーションのための場を設けたり、コントリビューションのガイドラインを設けたり、広報活動的なことが必須になっていっている気がします。ある種のアイドル活動のような振る舞いを迫られるというようなイメージです。やりたいのはコードを書くことなのでそちらにはあまり個人のリソースを割きたくありません(個人的には)。

一方、標準(他の人々がメンテナンスをしてくれるであろう環境)からは外れず、やがて来るであろう公式の未来だけを信じて先に利用しつつ、不要になったら取り外すというのが良い生活習慣なのではないか?というような認識の人も多くなっているような気がします。polyfillを使うということです。より政治的な手腕を発揮したい人々(?)など、標準として提案しつつ、これは標準だという体でガンガン依存していくみたいな活動(prolyfil)を行う人も居ましたが、個人的にはそこまで主張することが好きというわけではないので逆の立場を選びました。

自分の作ったものが標準(主流)というわけではなく、自分の作ったものは脇道(支流)であるということを前提にすることにしたのです。そして、必要なときには、必要なときだけ、自分が作ったライブラリをしれっと使い便利に過ごし、標準が追いついてきたら(あるいは不要になったら)、またしれっと溶けてなくなったかのように消してしまうという振る舞いが理想なのではないかと思うようになりました。これが「やがて消えてなくなるライブラリ」です。

create-reat-appの --expose から

この「やがて消えてなくなる」というテーマは、ずっと心の中にあったのですが、たまたま、create-react-appなどを試してみたりする中で、--exposeというオプションを設けていることに気づきました(例えば、create-react-appは--exposeを実行するとwebpackを使ったbuildに変換される)。どのように取り外すかという部分の体験についてこの--exposeというオプションを使うというのはありかもしれないと思い試しに自分でも作ってみるかと思ったのがそもそもライブラリを更新した始まりでした(ただし、create-react-appのexposeはけっこうメンテナンスが大変そうな印象がある(中をまじめに覗いてはいない))。

作業としては、いきなりコードを書き換えるのではなく、アイデアレベルでスケッチのような形でgistに幾つかコードを書いてみていけそうかどうか試してみたりしていました。

そして、これはけっこう手軽にできそうということになり、書き換えに至ったという形です。

実際の --expose

全てのhandofcatsでコマンド化した関数は、--exposeというオプションを持っています。例えば冒頭のコードにおいても、--exposeというオプションをつけることで、標準ライブラリ以外に依存していない形式のコードが出力されます。

$ python greeting.py --expose


def greeting(message: str, is_surprised: bool = False, name: str = "foo") -> None:
    """greeting message"""
    suffix = "!" if is_surprised else ""
    print("{name}: {message}{suffix}".format(name=name, message=message, suffix=suffix))

def main(argv=None):
    import argparse
    parser = argparse.ArgumentParser(description='greeting message')
    parser.print_usage = parser.print_help
    parser.add_argument('message')
    parser.add_argument('--is-surprised', action='store_true')
    parser.add_argument('--name', default='foo', help="(default: 'foo')", required=False)
    args = parser.parse_args(argv)
    greeting(**vars(args))


if __name__ == '__main__':
    main()

なので以下の様にしてhandofcatsへの依存を外すことができます(--in-place みたいなオプションがあっても良いのかもしれません)。

$ python greeting.py --expose > tmp && mv tmp greeting.py

何が嬉しいのか

--expose への対応は個人的にも常用したくなるような嬉しいことがあります。個人的な話かもしれませんが。

個人的にはけっこう頻繁にgistを使っています(先程挙げたスケッチレベルと言ったコードもそうでした)。そのgistを共有する時に依存を外したいということはたびたびあります。

バグの再現手順の作成であっても、基本的には最小のサブセットを目指すことが多いと思います。そのようなところでの試行錯誤の最中では一時的にこのhandofcats(今回書き換えたライブラリのこと)を使い、後に共有のためにargparseの実装に書き換えるということをやっていました。この不毛な作業から開放されるのはけっこう嬉しさがあります。気が乗らないときにはどうせ書き直すのだからとはじめからargparseを使った実装にしていました。それがこれからは気にする必要がなくなるのでとても良い感じです。