[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

Pythonでgulp watchみたいにファイル変更があったらリロードするコマンド作った(pywatcher)

gulpとかは指定したファイルの変更があると、それに応じてアクションを実行してくれる機能がありますね。またbottleやdjangoのDEVサーバ等でもコードの変更を検知して自動でプロセスを再起動してくれる機能があります。

先日作業中に、WEBサーバではないものの常駐型プロセスの開発をするケースがあり、コード書き換えごとに再起動するのが面倒だったので、変更を検知するとプロセスを自動で再起動してくれるコードを書きました。

今回はそれをもう少し整えてPyPIに登録したのでまとめておきます。

PyWatcher

pywatcherというライブラリを作製しました。

.
├── LICENSE
├── MANIFEST.in
├── README.md
├── pywatcher
│   ├── __init__.py
│   ├── command.py
│   └── pywatcher.py
├── requirements.txt
└── setup.py

実処理が書いてあるpywatcher.pyとコマンドラインツールとしてのインターフェースをもつcommand.pyだけの作りです。pywatcherをライブラリとして利用することは出来ると思いますが、本ライブラリ自体が提供するpywatcherというコマンドラインツールを使うだけでいいと思います。

インストール

pipでいれるだけです。

$ pip install pywatcher

これでpywacherコマンドが使用可能になります。

(pywatcher) denzownoMacBook-Pro:pywatcher denzow$ pywatcher -v
usage: pywatcher [-h] -t TARGET_DIR_PATH -c TARGET_COMMAND_STR
                 [-i RELOAD_THRESHOLD_SECONDS] [--disable-capture-stdout]
pywatcher: error: the following arguments are required: -t/--target-dir, -c/--command
(pywatcher) denzownoMacBook-Pro:pywatcher denzow$ pywatcher -h
usage: pywatcher [-h] -t TARGET_DIR_PATH -c TARGET_COMMAND_STR
                 [-i RELOAD_THRESHOLD_SECONDS] [--disable-capture-stdout]

-----------------------------------------------------------------------
PyWatcher:

monitor file and reload process. like gulp watch

e.g:

pywatcher -t .  -c 'ping localhost'
-> if some file on current dir changed, restart process 'ping localhost'.

-----------------------------------------------------------------------

optional arguments:
  -h, --help            show this help message and exit
  -t TARGET_DIR_PATH, --target-dir TARGET_DIR_PATH
                        target directory for watching.
  -c TARGET_COMMAND_STR, --command TARGET_COMMAND_STR
                        target command. this command execute and restart when file changed.
  -i RELOAD_THRESHOLD_SECONDS, --reload-interval-seconds RELOAD_THRESHOLD_SECONDS
                        reload threshold seconds.
  --disable-capture-stdout
                        is_disable_capture_stdout

使い方

基本的にはREADME通りですが、以下のように使用します。

$ pywatcher -t .  -c 'ping localhost'

-tで監視ディレクトリを指定し、-cで常駐するプロセスのコマンドを指定します。以降は監視ディレクトリ配下で変更が発生するたびに指定しているプロセスが停止され、再起動します。

(pywatcher) denzownoMacBook-Pro:pywatcher denzow$ pywatcher -t. -c 'ping localhost'
2018-03-11 23:35:37,114 - INFO - [start process]: ping localhost
2018-03-11 23:35:37,124 - DEBUG - [subprocess_output]: PING localhost (127.0.0.1): 56 data bytes
2018-03-11 23:35:37,124 - DEBUG - [subprocess_output]: 64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.043 ms
2018-03-11 23:35:38,129 - DEBUG - [subprocess_output]: 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.132 ms
2018-03-11 23:35:39,133 - DEBUG - [subprocess_output]: 64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.161 ms
2018-03-11 23:35:40,138 - DEBUG - [subprocess_output]: 64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.152 ms
2018-03-11 23:35:41,142 - DEBUG - [subprocess_output]: 64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.062 ms
2018-03-11 23:35:42,147 - DEBUG - [subprocess_output]: 64 bytes from 127.0.0.1: icmp_seq=5 ttl=64 time=0.095 ms
2018-03-11 23:35:43,141 - INFO - [reload process]: ping localhost  <-- ファイルを変更したので再起動された
2018-03-11 23:35:43,141 - DEBUG - [subprocess_output]: b''
2018-03-11 23:35:43,141 - INFO - [start process]: ping localhost
2018-03-11 23:35:43,149 - DEBUG - [subprocess_output]: PING localhost (127.0.0.1): 56 data bytes
2018-03-11 23:35:43,149 - DEBUG - [subprocess_output]: 64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.044 ms
2018-03-11 23:35:44,152 - DEBUG - [subprocess_output]: 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.063 ms
2018-03-11 23:35:45,152 - DEBUG - [subprocess_output]: 64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.048 ms

こんな感じで呼び出されているプロセス側の標準出力もロギングされています。終了時はctrl+cで停止します。

また、指定したディレクトリのすべてのファイルではなく特定のパターンにマッチする場合のみ監視したいという要件もあるかと思いますので-pオプションを設けています。本オプションでGlobパターンを指定することで、それに該当するファイルの変更のみをトリガーとすることができます。

$ # *.pyファイルの変更のみを監視する
$ python pywatcher/command.py -t ./ -c 'ping localhost' -p '*.py'

ちょっと込み入った事

pywatcherはwatchdogというライブラリを使用しています。このライブラリはファイルの変更を検知してそのイベントに応じて何かをするといった処理を簡単にかけるようにしてくれます。

from watchdog.events import FileSystemEventHandler

class PyWatcher(FileSystemEventHandler):

    def __init__(self, process_command, reload_threshold_seconds, is_capture_subprocess_output, pattern_list=None, logger=None):
        """
        :param str process_command: process command string
        :param int reload_threshold_seconds: reload min threshold seconds.
        :param bool is_capture_subprocess_output: capture subprocess output flag.
        """
        super().__init__()
    :

    def on_created(self, event):
        """create時の処理"""

    def on_modified(self, event):
        """modify時の処理"""

    def on_deleted(self, event):
        """delete時の処理"""

抜粋ですが、基本的にFileSystemEventHandlerを継承しon_xxxメソッドを実装するだけで処理がかけます。引数で渡ってくるeventFileSystemEventHandlerの場合watchdog.events.FileSystemEventのサブクラスとなっており、ファイルパス等の情報が取得できますのでパターンマッチも容易でした。

まとめ

とりあえず作れるかな?とおもって作り始めてみたらさっとできたので個人的には満足しています。

$ pip install pywatcher