初心者がPythonのチーム開発環境を構築した際の軌跡

# はじめに

自分のためのスクリプト言語としてPythonをよく使っている。
スクリプトなので「動けば十分、自分が読めれば十分」でやってきた。
そのため、新しい書き方を知らなかったり、周辺ツールについてもよくわからないまま今に至っている。
要するに、

![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82681/9b3be602-7d07-3cd8-562d-e3fd94b4d867.png)

こんな感じだ。少なくとも開発業においてはPython初心者だといってもいい。

ところで最近チームでPythonを使った開発の機運が出てきた。
不幸なことに、チーム内で一番Pythonに詳しいのは私だ。チームを組むことになる人はPythonを触ったことすらない。
今までの一人環境のままではまずいと思い、Pythonの開発環境の構築について調査を始めた。

というわけで本記事はPythonの開発環境を構築した際の軌跡のメモである。

- - -

**🔥注意🔥**

本記事は構築手順や使い方よりも「どうしてそれを選んだか、あるいは選ばなかったか？」に重きを置いて書いています。
これは、どんな技術を選んだかよりもどうしてその技術を選んだか（理由）の方が重要だと考えているためです。
たぶんこの記事に書いた環境は完璧ではないので後に再構成することになると思いますが、その時に「当時はどう考えてそう構築したか？」を振り返ることになるのでその資料というわけです。

したがいまして、本記事を手順書として使いたい場合はノイズが多いのでそのまま流用することはおすすめしません。また、手順の意味の深いところまでを説明する気はないため、知りたい場合は各自ググってください。

- - -

なお、本記事における開発環境とは以下を指す。

1. Python環境のインストール（by anyenv & pyenv）
2. エディター（Visual Studio Code）のインストールと設定
3. パッケージ管理ツール（Poetry）のインストールと設定
4. プロジェクトの作成
5. 周辺ツールのインストールと設定

# 方針

環境構築の方法や技術選定には以下のとおり前提と方針を立てて行った。

**前提**

* OSはMacOS BigSur、シェルはbash
* 要件や用途が明確な"固まった"コードの作成・保守を前提とする（⇔データをこねくり回す試行錯誤性の強いコードは対象としない）

**方針**

* Python初心者にも楽に構築できること：本質でない工程で工数を取られたくない
* フォーマットとlintを充実させること：均一なコード担保を重視したい
    * ローカルでもCIツール上でも動作できること：同上
* Dockerは使わないこと：コンテナー化は次のステップとしたい

# Python環境のインストール

## anyenvのインストール

Python環境のインストールについてはまだ [pyenv](https://github.com/pyenv/pyenv) がほぼ唯一の現実的な選択肢らしい。

最近は [anyenv](https://github.com/anyenv/anyenv) 経由のインストールが流行っているのと、後述のとおりCI環境ではNode.js環境をインストールすることになるため、はじめにanyenvをインストールすることにする。

1. anyenvをインストールする

    ```bash
    brew install anyenv
    ```

2. `anyenv init` を実行して表示される指示に従う。`anyenv 1.1.2` 時点でかつシェルにbashを使っている場合は指示内容は以下のコマンドの実行となる

    ```bash
    echo 'eval "$(anyenv init -)"' >> $HOME/.bash_profile
    ```

3. anyenvを初期設定する

    ```bash
    anyenv install --init
    ```

## pyenvのインストール

pyenvをインストールする。

```bash
anyenv install pyenv
exec $SHELL -l
```

## Pythonのインストール

今回は使用予定のモジュールの相性の関係から、Python 3.8系列の本記事執筆時点の最新である3.8.6を使う。
本記事執筆時点のpyenvからインストールできる安定版の最新は3.9.0、世間的には3.10.0も見えてきているのでご注意を。

さて、通常は、 `pyenv install 3.8.6` でインストールできるはずだがこれでは失敗する。
どうやら、MacOS BigSurにおいてPythonのインストール時ビルドに使っているC++のビルドツールが古いらしい。したがって、Pythonのインストールの前にXCodeとCommand Line Toolsを新しいバージョンにアップデートする手順が必要である。

1. [AppleのDeveloperサイト](https://developer.apple.com/jp/xcode/resources/) から以下をダウンロードし、インストールする
    * Xcode 12.2
    * Command Line Tools for Xcode 12
2. XCodeを起動し、`XCode` - `Preference` ダイアログの `Locations` タブを開き、 `Command Line Tools` が `XCode 12.2 (12B45b)` が選択されていることを確認する
3. python 3.8.6をインストールする

```bash
LDFLAGS="-L$(xcrun --show-sdk-path)/usr/lib" pyenv install 3.8.6
```

# エディターのインストールと設定

## 選定

好きなエディターで書けばいいと思うが、複数人開発では標準エディターを決めておくことは重要だ。問題が起こった時にチーム内でヘルプが受けられる可能性がある。

選定において重視したのは、以下の要素である。

* コードの補完が効く
* コードを書きながらフォーマッターが適用できる（ショートカットキー操作でフォーマットされる）
* 初期設定が面倒くさくない

調査した結果、[PyCharm](https://www.jetbrains.com/ja-jp/pycharm/) と、拡張機能をインストールした [Visual Studio Code（VSCode）](https://azure.microsoft.com/ja-jp/products/visual-studio-code/) が候補に上がった。
能力的にはどちらも同水準だと感じたが、以下の観点でVSCodeを選択した。

* 私はもともとVSCodeを使っていた：なんだかんだいってもこれが一番大きい。ごめん
* PyCharmでは書きながらフォーマッターを適用する機能がない：外部ツール連携を使えばそれっぽい挙動はするが、外部ツール呼び出しによるオーバーヘッドによって動作は遅く、PyCharm上での変更とフォーマッター適用による変更がコンフリクトすることが多々あった
* PyCharmのlint機能をCLIから動かすのが現実的ではない
    * ローカル環境：PyCharmはlint機能がPyCharm本体とくっついており、なおかつPyCharmは複数プロセス起動に対応していないため、PyCharmのlintをCLIから実行する場合、一旦PyCharmを終了しないといけない
    * CI環境：CI環境上で500MBを超えるPyCharmをインストールするのはちょっとやだ

## Visual Studio Codeのインストール

1. [VSCodeの公式ページ](https://azure.microsoft.com/ja-jp/products/visual-studio-code/) からバイナリーをダウンロードし、インストールする
2. ターミナルでVSCodeを起動できるようにする
    1. VSCodeを起動する
    2. `⌘ + shift + P` を押下する
    3. `シェル ⌘: PATH 内に 'code' コマンドをインストールします/Shell command: Install 'code' command in PATH` を選択する
3. 拡張機能をインストールする

    ```bash
    # Python Extension Pack
    code --install-extension donjayamanne.python-extension-pack
    # Pylance
    code --install-extension ms-python.vscode-pylance
    # Code Spell Checker
    code --install-extension streetsidesoftware.code-spell-checker
    ```

`Python Extension Pack` はPython系の拡張機能の詰め合わせパックだ。とりあえずこれを入れておけばいい。
`Pylance` はPythonのLanguage Serverで、これがコード補完やlintを担ってくれる。
`Code Spell Checker` はスペルチェッカーでかっこ悪いスペルミスを防いでくれる。

# パッケージ管理ツールのインストールと設定

## 選定

今までは `pip` コマンドだけでやってきたが、やはり `requirements.txt` には限界を感じていたのでモダンなパッケージ管理ツールの導入は必須だと考えていた。

調査した結果、以下のツールが候補に上がった。なお、(Ana)condaは不必要なパッケージもインストールされるのが嫌で外した。

* pipenv
* Poetry
* Pyflow

残念ながら私のPython知識/経験では決め手となるほどのパフォーマンスの差を見つけることができなかった。
もう少し正確に書くと、パフォーマンスの差自体はあるのだが、それが私のチームが取り組む現実の問題にどれぐらい影響を与えるかが、未知数だった。

結局Poetryを選んだが、その理由はGoogle検索をするとpipenvとPoetryの比較でPoetry優勢という論調の記事が多かったという消極的なものである。なお、Pyflowはそもそも日本語の情報が極端に少ないという状態だったので採用には至らなかった。

現時点のPoetryを使ってみての感想だが、概ね満足している。強いて言えば `poetry run <script>` を実行する時に引数が指定できないのは ~~難点だ。~~ **難点だが、 [Poe the Poet](https://github.com/nat-n/poethepoet) という外部プラグインを使えばこの不満も緩和できそうだ。
（この記事を書いている途中で見つけたのでまだ試せていない。）**

## Poetryのインストール・設定

[公式ページのインストール方法](https://python-poetry.org/docs/) のとおりインストールする。
一部設定が必要なので [公式ページの設定方法](https://python-poetry.org/docs/configuration/) も参照のこと。

1. Poetryをインストールする

    ```bash
    curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
    ```

2. タブ補完を効くように設定する（brewからインストールしたbashの場合。デフォルトのbash、他シェルは上記公式ページ参照のこと）

    ```bash
    poetry completions bash > $(brew --prefix)/etc/bash_completion.d/poetry.bash-completion
    ```

## プロジェクトの作成

いよいよプロジェクトを作成する。今回のプロジェクトの名前は `template` とする。

1. プロジェクトを作成する

    ```bash
    poetry new --src template
    ```

2. `template` ディレクトリーに移動する

    ```bash
    cd ＜templateディレクトリーのパス＞
    ```

3. Poetry用のプロジェクト設定ファイル `poetry.toml` を追加する

    ```poetry.toml
    [virtualenvs]
    in-project = true
    create = true
    ```

4. Pythonのバージョンを3.8.6で固定化する

    ```bash
    pyenv local 3.8.6
    ```

5. エディターで `pyproject.toml` を開き、pythonのバージョンが3.8.6（または3.8.z）になっているか確認する

    ```template/pyproject.toml
    ...

    [tool.poetry.dependencies]
    python = "^3.8"  # 3.8.zを使うことを意味する

    ...
    ```

6. ドキュメントはMarkdownを使うため、`README.rst` を `README.md` にリネームする

ソースコードは `template/src/template` フォルダーで管理することになる。
VSCodeでこのフォルダーを認識させるには環境変数PYTHONPATHを適切に設定してやる必要があり、具体的には `.vscode/settings.json` と `.env` で設定する（後述）。

## 周辺ツールのインストールと設定

ここからが本題といえるかもしれない。

### 使用ツールの選定

#### フォーマッター

調査した結果、以下のフォーマッターが候補に上がった。

* [autopep8](https://github.com/hhatto/autopep8)
* [YAPF](https://github.com/google/yapf)
* [Black](https://github.com/psf/black)

この中で一番後発でかつ異色なのはBlackだ。普通フォーマッターというのは多機能であればあるほど設定項目が大量にあるものだがBlackはこれが極端に少ない。
「Pythonは単一の書き方で書けるのが売りなのだからフォーマットだって単一のやり方に収束するはずだ」という理念があるようだ。
フォーマッターの設定はこだわりだすと工数があっという間に溶けることを知っているので、Blackを採用することにした。

問題はimport文である。linterではimport文の順番をチェックするものもあるが、Blackはimport文のソートは行わない。
これについては [isort](https://github.com/pycqa/isort) というimportソート専門のフォーマッターツールを使うことで対処することにした。

しかしまだ問題があり、Blackもisortもファイルの中央に書かれたimport文をファイル冒頭に移動することをしない。
この機能を持っているのはautopep8だけのようである。
Pythonでコードを書いているとエディターをスクロールしてファイル冒頭にimport文を書くという操作が煩わしいので、autopep8を頼りに関数やクラスの間にimport文を書くことは多々ある。
この機能は是が非でも欲しかったので、結局、

* VSCode上のフォーマッターはautopep8を使う：VSCode上は単一のフォーマッターしか使用できないためBlackではなくautopep8を優先する
* 最終的なフォーマット処理としてgitへのコミット前にlintと一緒に以下の順番で改めてフォーマッターを適用する
    1. autopep8：import文のファイル冒頭への移動
    2. isort：import文のソート
    3. Black：本文のフォーマット

ということにした。autopep8とBlackで役割が競合しているように見えるが、autopep8はあくまでimport文のファイル冒頭への移動だけが目的である。

#### linter

調査した結果、以下のlinterが候補に上がった。

* Flake8
* Pylint
* Pyright（Pylance）
* mypy

PyrightはPylanceのコア部分である。VSCodeでPylanceを採用したので必然的に採用となる。
しかしPyrightはFlake8やPylintに比べて現時点でlint機能が貧弱であるのと、Flake8のプラグイン方式による機能拡張は魅力的であるので、Flake8も採用とした。

Flake8のプラグインは大量に存在するが、
[awesome-flake8-extensions](https://github.com/DmytroLitvinov/awesome-flake8-extensions)
で一覧できるのでこのページを見ながらプロジェクトの状況に合わせて追加や削除を続けていく。
直近では、

* hacking
* flake8-annotations
* flake8-classmethod-staticmethod
* flake8-print
* flake8-variables-names
* flake8-simplify
* flake8-SQL
* flake8-use-fstring

を採用し、様子見とする。

mypyはtype hintに特化したlinterだがPyrightがtype hintingのlint機能を持っており競合するため不採用とした。

### インストール

#### Pyright以外

1. `template` ディレクトリー内で以下のパッケージをインストール対象として登録する

    ```bash
    poetry add -D
    autopep8  \
    black \
    isort \
    flake8 \
    hacking \
    flake8-annotations \
    flake8-classmethod-staticmethod \
    flake8-print \
    flake8-variables-names \
    flake8-simplify \
    flake8-SQL \
    flake8-use-fstring \
    ```

2. 登録したパッケージをインストールする

    ```bash
    poetry install
    ```

3. VSCodeの設定ファイルにPYTHONPATH、Pythonフォーマッター、lint、スペルチェッカーの設定を追加編集する（ファイルが存在しなければ作成する）

    ```template/.vscode/settings.json
    {
        "[python]": {
            "editor.codeActionsOnSave": {
                "source.organizeImports": true
            },
            "editor.insertSpaces": true,
            "editor.tabSize": 4
        },
        "cSpell.ignoreRegExpList": [
            "[０-９Ａ-Ｚａ-ｚぁ-んァ-ヶ亜-熙纊-黑]+"
        ],
        "cSpell.words": [
            "noqa",
            "pyannotate",
            "pytest"
        ],
        "files.eol": "\n",
        "python.envFile": "${workspaceFolder}/.env",
        "python.formatting.autopep8Path": "${workspaceFolder}/.venv/bin/autopep8",
        "python.formatting.provider": "autopep8",
        "python.linting.enabled": true,
        "python.linting.flake8Enabled": true,
        "python.linting.mypyEnabled": false,
        "python.linting.pylintEnabled": false,
        "python.pythonPath": "${workspaceFolder}/.venv/bin/python3.8.6",
        "python.sortImports.path": "${workspaceFolder}/.venv/bin/isort",
        "terminal.integrated.env.linux": {
            "PYTHONPATH": "${workspaceFolder}/src"
        },
        "terminal.integrated.env.osx": {
            "PYTHONPATH": "${workspaceFolder}/src"
        },
        "terminal.integrated.env.windows": {
            "PYTHONPATH": "${workspaceFolder}/src"
        }
    }
    ```

4. 実行時のPYTHONPATH用の設定ファイルを追加作成する

    ```.env
    PYTHONPATH = ./src
    ```

5. Flake8用の設定ファイルを追加作成する

    ```template/.flake8
    [flake8]
    ignore = ANN101,ANN201,ANN202,ANN203,ANN204,ANN205,E501,E722,H101,H201,H301,SIM106,VNE003,W503
    max-line-length = 999
    exclude = tests/*
    max-complexity = 10
    select_clst1 = CLST101,CLST102,CLST130,CLST131,CLST132
    ```

6. isort用の設定とBlack用の設定を追加編集する

    ```template/pyproject.toml
    ...

    [tool.isort]
    force_sort_within_sections = true
    group_by_package = true

    [tool.black]
    line-length = 99
    
    ...
    ```

補足。

手順の3のVSCode設定では、

* VSCodeで補完を効かせるためにPYTHONPATHにsrcフォルダーを設定しsrcフォルダーを使うこと
* VSCodeからPythonコードを実行するときのPYTHONPATHを.evnから読み取ること
* Python環境はプロジェクト内のvenvを使うこと
* lintを有効化しlinterにautopep8を使うこと
* falke8を使いpylintとmypyを使わないこと
* スペルチェッカーがPythonコード中の日本語コメントに誤爆しないこと

を設定している。

手順の5のFlake8設定では、

* Flake8（拡張プラグインを含む）の指摘のうち、無視してよい指摘のコード番号
* Flake8（mccabe）により計測される循環的複雑度の許容閾値
* 1行あたりの最大文字数チェックの無効化（大きな数字を当てて実質的に無効化）

を設定している。
無視してよい指摘は既存の数ファイルをチェックにかけた結果から設定しているだけなので、今後も調整が必要である。
1行あたりの最大文字数チェックを無効化しているのは、Blackによるフォーマットでコメント行以外は必ず規定文字数内に収まること、コメント行には最大文字数チェックを入れたくないことによる。

**2021/07/12追記ここから**

Flake8の設定を `.flake8` に書いているが、これは `pyproject.toml` に統合することができる。
詳細は [flake8の設定をpyproject.tomlに統合する](https://qiita.com/que9/items/7cf9f992b8decb4265c1) を参照のこと。

**2021/07/12追記ここまで**

手順の6のBlack設定では最大文字数を100文字として設定している（数少ないBlackの設定可能項目である）。

設定ファイルがいろんなファイルに飛び散っていて管理が煩雑だが、 [PEP 518](https://www.python.org/dev/peps/pep-0518/#specification) の浸透が進めば最終的には各ツールの設定はpyproject.tomlに集約されることが期待できるらしい。

#### [CI環境のみ] Pyright

問題はPyrightである。

Pyrightをコアとして持つPylanceがVSCodeにインストールしたため、ローカル環境で開発するだけならインストール不要であるが、今回はCI環境でも動かすことを要件としている。
よって、CIでPyrightを動かすため、別途Pyrightをインストールし動作可能にしておかなければならない。

PyrightはPython製ではなくNode.js製であるため、Node.js環境を構築し、そのうえでPyrightインストールしなければならない。
Node.js環境は [nodenv](https://github.com/nodenv/nodenv) でインストールする。nodeenvは最初にインストールしたanyenvでインストールする。
まとめると、

`anyenv` -> `nodenv` -> `Node.js (with npm)` -> `Pyright`

という手順である。

既にVSCode上で使えるのに再度インストールしなければならないこと、Python環境なのにNode.jsをインストールすること、インストール手順が煩雑であること、と冗長感が極まっているので、このインストールはCIツール上のみでの手順とし、個人の開発環境では実施しないものとする。

1. nodenvをインストールする

    ```bash
    anyenv install nodenv
    exec $SHELL -l
    ```

2. Node.jsをインストールする。なお、バージョンは現時点で最新の `13.10.1` とする

    ```bash
    nodenv install 13.10.1
    ```

3. `template` ディレクトリーに移動し、プロジェクト内のNode.js環境を13.0.1で固定化する

    ```bash
    cd ＜templateディレクトリーのパス＞
    nodenv local 13.10.1
    ```

4. `template` ディレクトリー内でPyrightをインストールする

    ```bash
    npm install -D pyright
    ```

5. `pyright` コマンドを定義するため、以下を `package.json` に追記する

    ```package.json
      "scripts": {
        "pyright": "pyright --version && pyright"
      },
    ```

## フォーマット・lintスクリプトの追加

CI環境上で、ソースコードがフォーマット処理が適用されているか、あるいはlintによる指摘がなくなっているかを判断する必要があるため、フォーマット処理の適用とlintはコマンドライン上でも実行できる必要がある。
また、ローカル環境上においても、前述のとおりgitへのコミット前にisortとBlackを適用して最終的なフォーマットを確定する必要がある。この操作もコマンドライン上で行いたい。

上記を実現するスクリプトを `template` ディレクトリーに追加する。

```template/lint.sh
#!/bin/bash

# ---------- Set variables

target_file_or_dir='./template'

# ---------- Change directory

script_dir=$(cd "$(dirname "$0")" && pwd -P)
pushd "$script_dir" || exit 1

# ---------- Format python files

# To move import to top.
poetry run find "$target_file_or_dir" -name '*.py' -exec autopep8 --in-place '{}' \;
# To sort import.
poetry run isort "$target_file_or_dir" && \
# To format python code.
poetry run black "$target_file_or_dir" && \

# ---------- Lint python files

if type "node_modules/.bin/pyright" >/dev/null 2>&1; then
    npm run pyright "$target_file_or_dir"
fi

poetry run flake8 --config .flake8 "$target_file_or_dir"

# ---------- Change directory

popd || exit 1
```

各コマンドの冒頭には `poetry run` を付与し、この `template` プロジェクト環境内のツールを使うことを強制している（[参考](https://cocoatomo.github.io/poetry-ja/cli/#run)）。

本当はPythonのスクリプトとして書きたかったが、各ツールのPythonコードを読み解くよりCLIの方が情報が豊富で簡単だったためシェルスクリプトとして実現している。

# おわりに

一応体裁は整えたが、まだ道半ばと感じている。ちょくちょく更新するかもしれない。

「**もっと良いやりかたがあるよ**」 というコメントをお待ちしております❗