LoginSignup
3
4

More than 3 years have passed since last update.

Python でコマンドラインアプリを作って setup.py, argparse, GitHub Actions を理解する

Posted at

setup.py, argparse, GitHub Actions あたりの技術を学ぶのに、Python でコマンドラインアプリを作ると良さそうだと思ったのでやってみました。ここでは個々の技術を詳しく解説しないので、記事中で紹介する公式ドキュメントや解説記事を参照してください。

成果物

To-Do リストアプリ を作りました。と言っても、

todo add "ほげ"
todo complete "ほげ"
todo remove "ほげ"

などで To-Do を追加、完了、削除するだけのものです。また、

todo complete --all
todo remove -a

で全て完了や全て削除ができます。To-Do リストは ~/.todo.json に保存され、

$ todo show
□ ほげ
☑ ほげほげ

のように表示できます。

コマンドラインインターフェイスの作成

argparse を使ってコマンドライン引数の処理を定義します。

argparse は機能が多過ぎて把握しきれないですが、今回使ったのは以下です。コード全体はこちら

parser = argparse.ArgumentParser()

# git commit とか docker run などの commit や run のようなもの
# をサブコマンドと名付けて追加
parser.add_argument('subcommand', help='add, complete, or remove')

# サブコマンドより後ろに来る引数を content とする。
# nargs は引数の数で、1つの場合は nargs=1
# nagrgs='*' とすると0以上の可変長引数になる
# nargs='+' は1以上の可変長引数
# nargs='?' は0または1個
# https://docs.python.org/ja/3/library/argparse.html#nargs
parser.add_argument('content', help='Content of To-Do', nargs='?')

# --all オプション (短縮形 -a) を追加
# 引数を取るオプションではなく、フラグなので、`store_true=True` を指定
parser.add_argument('-a', '--all', action='store_true', help='remove or complete all To-Dos')

なお、サブコマンドが show のときや、remove --all などのときは引数 'content' は必要ないのですが、指定できてしまいます。指定できないように argparse で書く方法が分からなかったため、引数が指定されても内部で無視するようにしています。

これを用いて、指定された引数に応じた処理を呼び出せば良いです(処理については特筆すべき知見はないのでソースコード参照)。

パッケージ化

setup.py を書いてコマンドラインアプリを自動でインストールされるようにします。

書いた setup.pyこちらにあります (書き方は pipnumpy のものを参考にしました) が、この中で肝心なのは以下の部分かと思います。

    entry_points={
        "console_scripts": [
            "todo=todo.main:main"
        ]
    },

このようにすることで、python3 setup.py install したときに OS の PATH にコマンドがインストールされます。私の環境では以下のようになりました1

$ which todo
/home/linuxbrew/.linuxbrew/bin/todo
 $ cat $(which todo)
#!/home/linuxbrew/.linuxbrew/opt/python/bin/python3.7
# EASY-INSTALL-ENTRY-SCRIPT: 'todo==0.1.0','console_scripts','todo'
__requires__ = 'todo==0.1.0'
import re
import sys
from pkg_resources import load_entry_point

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(
        load_entry_point('todo==0.1.0', 'console_scripts', 'todo')()
    )

これで、どのディレクトリにいようとも、ターミナルで todo と叩くと todo パッケージの main モジュール内の main という関数が呼ばれて、書いた処理が実行されます。

pip でインストール

先程は setup.py を実行してインストールしましたが、以下のように pip を用いて Git リポジトリからもインストールできます。汎用性のあるものコマンド、パッケージであれば PyPI に登録するのも良いでしょう。

pip3 install git+https://github.com/pn11/python-command-line-sample

GitHub Actions を用いた自動テスト

いつの間にか2 GitHub に追加されていた Actions というのを使ってみたいと思ったのが本記事執筆のきっかけです3。今回は無難に自動テストをしてみます。

とりあえず、ToDo リストを全部消してから追加するだけのクソみたいなテストを書きました。

import todo.main

def test_add():
    todo.main._remove_todo('', all_flag=True)
    todo.main._add_todo('foo')
    dic = todo.main._load_todos()
    assert dic['foo'] == 'Not Yet'

これを GitHub Actions を使って nose で実行してみたいと思います。

Action 用 YAML 作成

リポジトリの Actions のタブに行くと、以下のような画面になるので、とりあえず Python package でやってみます。

919fa3a4-176e-419d-b249-4892ffe4f18d.jpg

以下のような画面になるので、何も考えずとりあえず Start Commit を押してみます。

71a5aa3e-4fbb-4a7b-aaad-32fcee02ad5f.jpg

テストに失敗します。

9f8800a4-0658-4dc6-a264-a9aa1a1e1348.jpg

YAML を見てみると以下のようになっています。

name: Python package

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest
    strategy:
      max-parallel: 4
      matrix:
        python-version: [2.7, 3.5, 3.6, 3.7]

    steps:
    - uses: actions/checkout@v1
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v1
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    - name: Lint with flake8
      run: |
        pip install flake8
        # stop the build if there are Python syntax errors or undefined names
        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
    - name: Test with pytest
      run: |
        pip install pytest
        pytest

何となく、steps: 以下の

- name: やること
  run: |
  コマンド1
  コマンド2 

みたいなところをいじれば良さそうです。ということで、以下のような処理を書きました。

    - name: Install Package
      run: |
        python setup.py install
    - name: Test with nose
      run: |
        pip install nose
        nosetests

setup.py で To-Do のパッケージをインストールした後、nose もインストールしてテストを実行します。

これをコミットすると無事にテストが通りました。実行ディレクトリとかは特に意識しませんでしたが、リポジトリのルートで実行されているようです。また、何となく nose を使いましたが、特にライブラリに依存したテストではないので元のまま pytest でも動くはず?

2e3474ac-5bb2-49ed-a87f-c349eaf64855.jpg

今後はコミットする度にテストが走ります。テストに通ると :white_check_mark: マークがついて良い感じです。

caaef1bb-00d2-4be6-b7b8-eceac4bb906a.jpg

まとめ

以上、argparse でコマンド作成、setup.py でパッケージ化、GitHub Actions で自動テストをやってみました。今後はこの方法でクソコマンドを量産していきたい所存です。

その他

この記事では取り上げませんでしたが、コマンドラインツール作成の際には argparse 以外にも、click 等を使うのも良いかも知れません。


  1. Linuxbrew で入れた Python を使用しています。 

  2. [速報]GitHub Actionsが正式版に。GitHub内でビルド/テスト/デプロイなど実行、CI/CDを実現。GitHub Universe 2019 - Publickey 

  3. MS に買収されてからどんどん新機能が追加されていてしゅごい。。 

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4