比較的小規模で可搬的なPython toolのスケルトン

# Python scriptの可搬性

**2021/8/31 Python2とPython3のスクリプトが混在していても使えるようにバージョンアップした**

ちょっとした作業のためにPythonでひとまとりのスクリプトを書くことが多々ある。Pythonを利用する利点は、既存のライブラリ/モジュールが豊富で、OSのパッケージシステム(rpm,yum,apt,..)やそれに準じたもの(Macports, Homebrew)で配布されていたり、PyPI(`pip`コマンド一つ)によって容易に導入して利用できる点にある。

ただし、いろいろな計算機で可搬的に利用しようとすると、環境依存を気にしなくてはいけなくなる。モジュールがOSの配布物より導入済みの場合とそうでない場合、システム管理者としてpipを使える環境とそうでない場合、一般ユーザーとしてPyPIの設定をカスタマイズしている場合とそうでない場合、virtualenvを使っている場合とそうでない場合、などなど。ちょっとしたツールをパッと使いたいのに、各環境でPythonのセットアップがどうなっていたかを気にしないといけないのは非常に煩わしい。

それを回避するための処方箋をテンプレート化した。

# 方針

 - 利用する環境ですでに導入済みかどうかは気にせず、とにかくローカルにPyPIを利用してインストールして利用する。そのためのシェルスプリプトを用意する。(内部的では`pip install --target ./lib/python/site-packages some-module`を実行する)

 - 利用するPythonを実行する前に、PyPIでインストールしたローカルのディレクトリを環境変数`PYTHONPATH`を加えて実行する必要があるので、そのためのラッパーシェルスクリプトを用意する。
 
 - ただし、Pythonスクリプト毎にラッパーシェルスクリプトを書くのは不毛な作業なので、一つのシェルスクリプトに対してシンボリックリンクを張り、シンボリック名に応じて実行するPythonスクリプトを切り替える。

# 実装 (含むサンプルpythonスクリプト)

ファイルの一覧としては、以下。 PyPIモジュールをインストールするbashスクリプトと、実際にPythonスクリプト実行するラッパースクリプトでは、ディレクトリ名などで整合性をとる必要がある。そのため実態としては1つのファイルとして、シンボリックリンクを使って実行時のコマンド名で動作を切り替える。それにより、内部のシェル変数の定義の記述は一箇所にまとめている。

**結果的に、Pythonスクリプト以外の実体としては1つのシェルスクリプトに収まっている。**

```:ファイル構成
bin/run_pyscript
bin/mng_pymodule -> run_pyscript
bin/ex_greeting -> run_pyscript
lib/python/ex_greeting.py
lib/python/site-packages/
```

  - `bin/mng_pymodule`: 指定したPyPIモジュールのインストール/一括アンインストールをする。
  - `bin/run_pyscript`: シェルスクリプトの実体。この名前で実行したときは、第一引数に指定した名前で`lib/python`以下に置かれたpython scriptを、環境変数`PYTHONPATH`に`lib/python`と`lib/python/site-packages`を加えたうえで実行する。

  - `lib/python`: Pythonスクリプトを置くディレクトリ

  - `lib/python/site-packages/`: PyPIでPythonモジュールがローカルにインストールされるディレクトリ

  以下は、サンプル。

  - `lib/python/ex_greeting.py`: サンプルスクリプト("Hello, world!"+時刻表示)。標準モジュール以外に、pytz, tzlocalを使う。

  - `bin/ex_greeting`: 上記サンプルを呼び出すためのコマンドとして使うための`bin/run_pyscript`へのシンボリックリンク

**2021/8/31追記**

Python2とPython3の混在に対応するため、下記のシンボリックリンクとサンプル例を追加した

```:追加ファイル構成
bin/mng_pymodule2 -> run_pyscript
bin/mng_pymodule3 -> run_pyscript
bin/ex_greeting2 -> run_pyscript
bin/ex_greeting3 -> run_pyscript
lib/python/ex_greeting2.py
lib/python/ex_greeting3.py
```

# ファイル置き場

- https://github.com/nanigashi-uji/pytool_skeleton 
- https://gitlab.com/nanigashi_uji/pytool_skeleton

# 使い方

- Pythonスクリプトを書き、`lib/python/`以下に置く。

```
% emacs lib/python/ex_greeting.py
```

- `bin`以下に、Pythonスクリプト名から拡張子(`.py`)をとった名前、もしくは同名で`bin/run_pyscript`へのシンボリックを作成する。

```bash:Pythonスクリプト作成
% (cd bin ; ln -s run_pyscript ex_greeting)
```

この時点でも、必要なモジュールがpython実行環境に揃っていれば問題なく実行できるはずだが、揃ってない場合には、モジュールがないというエラーがでる。
  
```bash:モジュールがインストールされていない場合の実行例
% ./bin/ex_greeting --date
Traceback (most recent call last):
  File "......../lib/python/ex_greeting.py", line 8, in <module>
    import tzlocal
ImportError: No module named tzlocal
```

- Pythonスクリプトで使用しているPyPIのモジュール（以下の例では`tzlocal`）をインストールする。

```bash:実行例
% ./bin/mng_pymodule install tzlocal pytz
Collecting tzlocal
  Using cached tzlocal-2.1-py2.py3-none-any.whl (16 kB)
Collecting pytz
  Using cached pytz-2021.1-py2.py3-none-any.whl (510 kB)
Installing collected packages: pytz, tzlocal
Successfully installed pytz-2021.1 tzlocal-2.1
% ls
./lib/python/site-packages/pytz  pytz-2021.1.dist-info  tzlocal  tzlocal-2.1.dist-info
%
```

`bin/mng_pymodule`の第一引数の`install`は省略可能である。実行後はPythonスクリプトが正常に実行できるようになる。

**2021/8/31追記**

`python2`と`python3`のファイル／モジュールを混在して使用するために、`mng_pymodule2`,`mng_pymodule3`というシンボリックリンクを用意した。`mng_pymodule2`は`pip2`があればそれを使い、なければ`pip`を実行する。`mng_pymodule3`,`mng_pymodule`は、`pip3`があれば`pip3`を使い、なければ`pip`を使う。ただし、どちらも環境変数`PIP`で使いたい`pip`が指定されている場合には、そちらが優先して使われる。 モジュールがインストールされる場所は、
`./lib/python/site-packages/X.Y/` (X,Yはそれぞれ`python`のメジャーバージョン,マイナーバージョン)となる。


```bash:モジュールをインストールした後の実行例
% ./bin/ex_greeting --date
./bin/ex_greeting --date
Hello, World! It is "Thu Jun 23 21:01:45 2021."
```

## **[2021/8/31追記]**　Python2とPython3の使い分け

スクリプトを実行する際に、どのバージョンの`python`を用いるかは、スクリプトのShebang(シェバン)に従う動作とした。サンプルファイルでは

```bash:サンプルの比較
% diff3 lib/python/ex_greeting.py lib/python/ex_greeting2.py lib/python/ex_greeting3.py
====
1:1c
  #!/usr/bin/env python
2:1c
  #!/usr/bin/env python2
3:1c
  #!/usr/bin/env python3
```

違いはShebangだけである。これらを(呼ぶシンボリックリンクを)実行すると、

```bash:実行例
% env ./bin/ex_greeting -d | head -2
Hello, World! It is "Tue Aug 31 20:49:16 2021."
Python : 2.7.18 (....)

% env ./bin/ex_greeting2 -d | head -2
Hello, World! It is "Tue Aug 31 20:49:19 2021."
Python : 2.7.18 (....)

% env ./bin/ex_greeting3 -d | head -2
Hello, World! It is "Tue Aug 31 20:49:21 2021."
Python : 3.8.2 (....)
```

といったようにPythonが使い分けられる。ただし環境変数`PYTHON`が指定された場合には、そちらを優先的に使う。下の2番目の実行例のように、環境変数`PYTHON`とスクリプトファイルのShebangの`python`のメジャーバージョンが違っている場合には警告を出すようにした。

```
% env PYTHON=python3 ./bin/ex_greeting -d | head -2
Hello, World! It is "Tue Aug 31 20:49:37 2021."
Python : 3.8.2 (....)

% env PYTHON=python3 ./bin/ex_greeting2 -d | head -3
Warning: Python version (3.8) mismatch?  (Version 2) is expexted by /..../lib/python/ex_greeting2.py)
Hello, World! It is "Tue Aug 31 20:50:07 2021."
Python : 3.8.2 (....)
```


 
## おまけ

`bin/mng_pymodule`の第一引数を`distclean`とすると、`lib/python/site-packages/`以下にあるものを全て消去する。

```bash:実行例
% ./bin/mng_pymodule distclean
% ls ./lib/python/site-packages/
%
```

**自分で作成したスクリプトなど消してはいけないファイルを`lib/python/site-packages/`以下に置いてはいけない**

**[2021/8/31追記]**

さらに、

```bash:実行例
% ./bin/mng_pymodule2 clean
% ./bin/mng_pymodule3 clean
```

と実行した場合には、対応するpythonバージョンのモジュールのみ(対応する`./lib/python/site-packages/X.Y/`以下のみ)をクリーンナップする動作とした。