何をしようとしたのか?
タイトルの通り、グローバルコマンドをして使えるPython製のCLIプログラミングをつくろうとしていた。
日本語がおかしいかもしれないので、具体的に説明するとmy-cli <sub-command> <option>
みたいなものだ。
例えばgitコマンドでgit add -p
のようなものを作りたい。
最終的にはそれを配布できるパッケージにしてインストールすることを目指そうと思う。
path周りでちょっとハマってしまったので、その備忘録も兼ねて記事にする。
CLIを作成する順序
作成に必要な構成は以下2点
- デフォルトのPythonパッケージを作成する。(Cookiecutter)
- CLI用のスクリプトを作成する。(Click)
- Pythonの配布用パッケージの設定する。(setup.py)
Clickはサードパーティのモジュールでデコレータを用いることで非常に簡単にCLIを作成できる。
1. デフォルトのPythonパッケージを作成する。(Cookiecutter)
Python用のパッケージを作成する必要はいくつかあるが、著名なサードパーティライブラリとしてcookiecutterがある。 このライブラリはPythonパッケージを作成に必要なディレクトリ構造をテンプレートとして作成できる。
cookiecutterについては詳しく述べないが、以下のようにして導入&プロジェクト作成が可能
$ pip install cookiecutter $ coockiecutter <cookiecutter template>
coockiecutterテンプレート一覧
- cookiecutter-pypackage: @audreyr's ultimate Python package project template.
- cookiecutter-pipproject: Minimal package for pip-installable projects.
- cookiecutter-pypackage-minimal: A minimal Python package template.
- cookiecutter-flask : A Flask template with Bootstrap 3, starter templates, and working user registration.
- cookiecutter-pyvanguard: A template for cutting edge Python development. Invoke, pytest, bumpversion, and Python 2/3 compatibility.
- Python-iOS-template: A template to create a Python project that will run on iOS devices.
- Python-Android-template: A template to create a Python project that will run on Android devices.
- cookiecutter-pytest-plugin: Minimal Cookiecutter template for authoring pytest plugins that help you to write better programs.
- cookiecutter-tox-plugin: Minimal Cookiecutter template for authoring tox plugins to change or extend the behavior of your test automation.
- cookiecutter-python-app: A template to create a Python CLI application with subcommands, logging, YAML
上記のテンプレートはcookiecutterデフォルトでは保持していないため、githubリポジトリを
もちろんローカルのテンプレートリポジトリを使用する場合は上記の様に事前にクローンしておく必要があるが、以下のように~/.coockiecutter
へgit clone
して使用する必要がある。(詳しくはここのテンプレートをおいているリポジトリを見よう)gh:
を接頭辞として指定することで直接リモートリポジトリを指定できる。
また今回はgithubをCVSとして指定しているが、bitbucket等いくつか対応しているようにみえる。
今回は基本のcookiecutter-pypackage
でこれを使用する。
$ cookiecutter -o my_cli/ gh:audreyr/cookiecutter-pypackage // ...対話的な設定入力が始まる。
ここで-o
をアウトプット先を決定している。
コマンドを実行すると、対話的に設定入力が始まるがその時に以下のようにCLIライブラリの選択画面が現れるので、ここでは1.Click
を選択する。
それ以外はCIツールやテストに関する情報等各自に応じて必要な情報を入力する。
Select command_line_interface: 1 - Click 2 - Argparse 3 - No command-line interface
インストールが完了すると、設定にもよるが大体以下のようなディレクトリ構造が生成される。
my_cli/ ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── my_cli │ ├── __init__.py │ ├── cli.py │ └── my_cli.py ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── tests └── tox.ini
実を言うとここまでで2のCLIモジュールについても大体できてしまっている。
my_cli
の中にできて生成されているモジュールを確認してみる。
一つ注意として、この段階ではパッケージ名にハイフン(ダッシュ)を使用しないこと。理由は詳細には述べないが、__init__.py
に余計な処理を書く手間が増える。
後にsetup.py
を編集するときにグローバルコマンド名はmy-cli
の様にダッシュをつけることができるので心配は不要だ。
# -*- coding: utf-8 -*- """Console script for my_cli.""" import sys import click @click.command() def main(args=None): """Console script for my_cli.""" click.echo("Replace this message by putting your code into " "my_cli.cli.main") click.echo("See click documentation at https://click.palletsprojects.com/") return 0 if __name__ == "__main__": sys.exit(main()) # pragma: no cover
このスクリプトがCLIを構成する部分となる。
2. CLI用のスクリプトを作成する。(Clickを使用する)
Clickは非常に直感的で基本的に以下の3つのデコレータで今回の要求はクリアできる。
@click.command()
コマンドを指定するデコレータ。このデコレータを付けられた関数がグローバルコマンドとして実行される。@click.option()
オプションを生成するデコレータ。いくつか引数を指定することで@click.group()
サブコマンド作成時に使用するデコレータ。このデコレータを使用した関数名、つまり@<groupデコレータの関数名>.command()
とすることで複数のコマンドをグルーピングしてサブコマンドとして利用できる。
早速いくつかのサブコマンドとオプションを持つCLIを作ってみよう。
my_cli.py
# -*- coding: utf-8 -*- """Console script for my_cli.""" import sys import click @click.group() def cli(): click.echo("Please select sub command. You can see all sub commands 'my-cli --help'") @cli.command() @click.option('--name', help='input file name', required=True) @click.option('--path', help='file path') def read_file(name ,path): click.echo("reading file") click.echo("name:{}, path:{}".format(name, path)) @cli.command() @click.option('--text',default="default value", help='echo text', required=True) def print_text(text): click.echo(text)
上記は以下のような構成だ。
read_file
とprint_text
という2つのサブコマンドを作成した。read_file
コマンドの方はname
とpath
オプションを保持しており、そのうちname
オプションの方は指定が必須となっている。print_text
コマンドの方は、オプションで値を渡さない場合デフォルト値としてprint default value
が引き渡される。
これだけでもいつものようにif __name__=="__main__":
でエントリポイントを指定後にスクリプトとして指定して使用することもできるが、Clickモジュールの詳細についてはまた別の記事で紹介する。
次にsetup.py
を編集してエントリポイントの設定をインストールを行う。
3. Pythonの配布用パッケージの設定する。(setup.py)
デフォルトでは以下のよう大体なっているだろう。
#!/usr/bin/env python # -*- coding: utf-8 -*- """The setup script.""" from setuptools import setup, find_packages with open('README.rst') as readme_file: readme = readme_file.read() with open('HISTORY.rst') as history_file: history = history_file.read() requirements = ['Click>=7.0', ] setup_requirements = [ ] test_requirements = [ ] setup( author="Takuya Kodama", author_email='example@test.co.jp', python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', classifiers=[ 'Development Status :: 2 - Pre-Alpha', 'Intended Audience :: Developers', 'Natural Language :: English', "Programming Language :: Python :: 2", 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', ], description="test_cli", entry_points={ 'console_scripts': [ 'my_cli=my_cli.cli:main', ], }, install_requires=requirements, long_description=readme + '\n\n' + history, include_package_data=True, keywords='my_cli', name='my_cli', packages=find_packages(include=['my_cli', 'my_cli.*']), setup_requires=setup_requirements, test_suite='tests', tests_require=test_requirements, url='https://github.com/takuya0412/my_cli', version='0.1.0', zip_safe=False, )
今回は最小限の部分だけ説明する。確認が必要な部分は以下の2点。
- packages
- entry_points
1. packages
この項目では配布(pip installできる)パッケージに含める項目を選択する。
ここで使われているfind_package()
では__init__.py
を含むディレクトリを自動で探索するが、上記のようにinclude()
で探索するディレクトリを指定することもできる。
最低限含めるべきなのはソースコードを含むディレクトリなので今回の場合はこのままで良い。
2. entry_points
この項目でグローバルコマンド名を決定する。
'my_cli=my_cli.cli:main'
これはコマンド名がmy_cli
、実行される関数はmy_cli/cli.py
のmain()
が実行されることを示している。
今回はmy-cli
をコマンド名としたいので、編集したcli.py
にあわせて次の様に変更する。
entry_points={ 'console_scripts': [ 'my-cli=my_cli.cli:cli', ], },
これで準備は完了した。実際にインストールして実行してみよう。 パッケージの形式には色々あるが今回は簡単としてtarで圧縮したパッケージとしてインストールする。
$ python setup.py sdist $ pip install dist/my_cli-0.1.0.tar.gz
これで準備が完了したので動作を確認してみよう。
$ my-cli Usage: my-cli [OPTIONS] COMMAND [ARGS]... Options: --help Show this message and exit. Commands: print-text read-file
サブコマンドを選択せずにmy-cli
をそのまま実行すると、my-cli --help
を実行した結果が得られる。(--help
を無くす方法もあるが、今回は割愛する。)
ちなみにClick7.0.0以降から、サブコマンド名は関数のアンダースコアがハイフンに変更された名前となっていることに注意しよう。(def print_text
--> print-text
)
結果を見ると、サブコマンドが表示されていることがわかる。それぞれのサブコマンドについても--help
オプションをつけて見てみよう。
$ my-cli print-text --help Please select sub command. You can see all sub commands 'my-cli --help' Usage: my-cli print-text [OPTIONS] Options: --text TEXT echo text [required] --help Show this message and exit. $ my-cli read-file --help Please select sub command. You can see all sub commands 'my-cli --help' Usage: my-cli read-file [OPTIONS] Options: --name TEXT input file name [required] --path TEXT file path --help Show this message and exit.
先程設定したオプションがちゃんと設定されていることがわかる。
help
に記載した内容が説明として記載され、required=True
としたものについては[required]
がついている。
試しにそれぞれに付いてオプションをつけて実行してみよう。
$ my-cli print-text Please select sub command. You can see all sub commands 'my-cli --help' default value $ my-cli print-text --text="INPUT TEXT" Please select sub command. You can see all sub commands 'my-cli --help' INPUT TEXT
オプション有無でデフォルト値と引数の値が切り替わっている。
$ my-cli read-file --path . Please select sub command. You can see all sub commands 'my-cli --help' Usage: my-cli read-file [OPTIONS] Try "my-cli read-file --help" for help. Error: Missing option "--name". $ my-cli read-file --path . --name test Please select sub command. You can see all sub commands 'my-cli --help' reading file name:test, path:.
必須のオプションがない場合はエラーとなり、付けた場合は想定通りの動作となっている。
最後に
今回でサブコマンド持ちのグローバルコマンドの作成がPythonでできた。 Clickとsetuptoolsにはそれぞれ色々機能があり、リリースパッケージも本来は今回の形式ではあまり配布しない。 また検索状況をみて需要があれば書こうと思う。
ほんとに最後に…
今回はcli.pyしか編集しなかったが、当然本来は別のモジュールを作成し、importすることになる。
このときデフォルトのPYTHONPAHTの特徴で以下のように同一パッケージ内のモジュール(上記の例でmy-cli.py
とする)をimportしようとすると失敗(unable import mycli, not found ~)してしまう。
from mycli import *
このときはパッケージ名からinstallする必要があるようだ。(もしかしたら環境によっては解決できない場合もあるかもしれない)
from my_cli.mycli import *
私の場合はこれで解決できた。
参考
以下は古いがClickコマンドについて詳しく記載があるので、凝ったものを作りたい場合は参考にすると良いだろう。