Python Package作成備忘録

# ツールlist

1. ~~[pyenv](https://github.com/pyenv/pyenv): Python管理~~ -> asdf
1. [conda](https://docs.conda.io/projects/conda/en/latest/user-guide/install/macos.html): Pythonの環境管理
1. [poetry](https://python-poetry.org/docs/#installation): 依存パッケージの管理
1. [Python Packages (py-pkgs.org)](https://py-pkgs.org): Python Packageの作成方法
1. [cookiecutter](https://github.com/cookiecutter/cookiecutter): テンプレートからPython Projectを作成するCLIツール
1. [PyPI (Python Package Index)](https://pypi.org): Python packageを公開するハブ
1. [Test Python Package Index](https://test.pypi.org): Python packageをテスト公開するチェック用
1. [python-semantic-release](https://github.com/python-semantic-release/python-semantic-release): Pythonのリリースを自動化するツール
1. [flake8](https://flake8.pycqa.org/en/latest/), [black](https://github.com/psf/black), [isort](https://pycqa.github.io/isort/): Formater、Linter
1. [pipx](https://github.com/pypa/pipx)
1. [asdf](https://asdf-vm.com/): asdf is a tool version manager. All tool version definitions are contained within one file (.tool-versions) which you can check in to your project's Git repository to share with your team, ensuring everyone is using the exact same versions of tools.

# Install asdf

https://asdf-vm.com/

```
asdf
```

```
asdf plugin-add python
asdf install python 3.11.0
```


# Poetry (依存パッケージ管理)

Poetryのインストール

```
asdf plugin-add poetry
asdf install poetry 1.7.1
```

プロジェクト内にvirtualenv作成

```
poetry config virtualenvs.in-project true
```


# Package作成

1. cookiecutterのインストール
    ```bash
    pip install cookiecutter # Macなら brew install cookiecutterでも可
    ```
1. cookiecutterを使ってtemplateからprojectの作成
    ```
    cookiecutter https://github.com/py-pkgs/py-pkgs-cookiecutter.git
    ```
# Packageの中身の実装

自分でPackageで実現したい内容を実装する。
ディレクトリ構成は、[How to package a Python](https://py-pkgs.org/03-how-to-package-a-python)を参考にする。↓以下参照

```
pycounts
├── CHANGELOG.md               ┐
├── CONDUCT.md                 │
├── CONTRIBUTING.md            │
├── docs                       │ Package documentation
│   └── ...                    │
├── LICENSE                    │
├── README.md                  ┘
├── pyproject.toml             ┐ 
├── src                        │
│   └── pycounts               │ Package source code, metadata,
│       ├── __init__.py        │ and build instructions 
│       ├── moduleA.py         │
│       └── moduleB.py         ┘
└── tests                      ┐
    └── ...                    ┘ Package tests
```

# Packageのビルド

```
poetry build
```

`dist` (`pyproject.toml`の `dist_path`で指定されたディレクトリ)に wheelとsrcが生成される

# Version更新

`poetry version` コマンドを使ってVersionを更新する

```
poetry version <rule>
```

rule:
1. `major`
1. `minor`
1. `patch`
1. `premajor`
1. `preminor`
1. `prepatch`
1. etc

https://python-poetry.org/docs/cli/#version

:::note warn
あとで紹介するPython Semantic Release (PSR)を使用する場合には、こちらはスキップして可。
:::

# Pypi (Package公開用)

Packageを公開する方法

## Test設定 (1回のみ)

1. アカウント作成: https://test.pypi.org/account/register/ (すでに持っていればスキップ)
1. テスト用のsource追加
    ```
    poetry source add test-pypi https://test.pypi.org/simple/
    ```
1. API keyを取得し、poetry configに設定 (`test-pypi`としているのに注意)
    ```
    poetry config pypi-token.test-pypi pypi-xxxxxx
    ```


## 本番設定 (1回のみ)

1. アカウント作成: https://pypi.org/account/register (すでに持っていればスキップ)
1. API keyを取得し、poetry configに設定 (`pypi`)
    ```
    poetry config pypi-token.pypi pypi-xxxxx
    ```

## Test pypiに公開

1. buildする
    ```
    poetry build
    ```
1. test pypiにPublishする
    ```
    poetry publish -r test-pypi
    ```
    e.g. https://test.pypi.org/project/autonote/


## Test pypiのpackageを確認

test pypiで公開したバージョンをインストールする

```
poetry add --source test-pypi autonote==0.1.1a0
```

詳細: https://python-poetry.org/docs/repositories/

Test Pypiで公開したバージョンでの挙動の確認をしたりする。

## Pypiに公開

1. buildする
    ```
    poetry build
    ```
1. pypiにPublishする
    ```
    poetry publish
    ```

# [Python Semantic Release (PSR)](https://python-semantic-release.readthedocs.io/en/latest/)

[semantic commit message](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716)を見てリリースを勝手にうまい具合にやってくれるツール.

## PSRの設定

Install

```
poetry add --dev python-semantic-release
```

以下をtomlに追加

```toml
[tool.semantic_release]
version_variable = "pyproject.toml:version"
version_source = "tag"
```

## PSRのコマンド

1. Versionを確認 (コミットやTag作成などしない): 

    ```
    semantic-release print-version # 次のバージョン
    semantic-release print-version --current # 今のバージョン
    ```

1. 新しいバージョンを発行、コミット、新規タグ作成をローカルで実行。

    ```
    semantic-release version
    ```

    <details><summary>うまく行かない場合</summary>
    
    `semantic-release version -v DEBUG` `-v DEBUG`をつけてどこがおかしいのか確認。`get_current_version_by_tag`で取れてない場合は、tomlファイルに指定したTagに値が存在しているか確認。
    
    ```
    Creating new version
    debug: get_current_version_by_tag()
    debug: get_last_version(, pattern='(\d+\.\d+\.\d+(-beta\.\d+)?)')
    debug: get_last_version -> 0.1.3
    debug: get_current_version_by_tag -> 0.1.3
    debug: get_current_release_version_by_tag()
    debug: get_last_version(, pattern='v?(\d+\.\d+\.\d+(?!.*-beta\.\d+))')
    debug: get_last_version -> 0.1.3
    debug: get_current_release_version_by_tag -> 0.1.3
    Current version: 0.1.3, Current release version: 0.1.3
    debug: evaluate_version_bump('0.1.3', None)
    debug: Commits found since last release: 0
    debug: get_new_version('0.1.3', '0.1.3', None, False, True)
    debug: get_new_version -> 0.1.3
    No release will be made.
    ```
    
    </details>


1. Publish

    ```
    semantic-release publish
    ```
    このコマンドがすること:

    1. ChangeLogファイルの更新
    1. `semantic-release version`の実行 (ローカルでコミット、Tag生成).
    1. 変更をGitにPush
    1. `build_command`の実行と dist fileをアップロード
    1. `semantic-release changelog` の実行とGitHubなどVCS providerへのポスト
    1. GitHub releaseにDist fileを添付

:::note warn
個人的にはローカルからPublishしたくないので、ここらへんはGitHub Actionsに設定したほうがいいかなと思っています。
:::


# Lint

```
poetry run isort --check --diff .
poetry run black --check --diff .
poetry run flake8 .
```

flake8でlintの対象外にしたい場合は、excludeを書ける

```.flake8
[flake8]
exclude =
    # No need to traverse our git directory
    .git,
    # There's no value in checking cache directories
    __pycache__,
    # The conf file is mostly autogenerated, ignore it
    docs/source/conf.py,
    # The old directory contains Flake8 2.0
    old,
    # This contains our built documentation
    build,
    # This contains builds of flake8 that we don't want to check
    dist
    .venv
```

# GitHub Actions

cookiecutterから持ってきた[ci-cd](https://github.com/py-pkgs/py-pkgs-cookiecutter/blob/main/%7B%7B%20cookiecutter.__package_slug%20%7D%7D/.github/workflows/ci-cd.yml)だと毎回mainにpushするたびにリリースされるのでCIとReleaseに分割。

## CI

Pull Request作成時とmain branchにPushされたときに実行するもの (test, lint, codecov, docsなどを実行)

```yaml:.github/workflows/dev.yml
name: dev

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  ci:
    # Set up operating system
    runs-on: ubuntu-latest

    # Define job steps
    steps:
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.9"

      - name: Check-out repository
        uses: actions/checkout@v3

      - name: Load cached Poetry installation
        id: cached-poetry
        uses: actions/cache@v3
        with:
          path: ~/.local  # the path depends on the OS
          key: poetry  # increment to reset cache

      - name: Install poetry
        if: steps.cached-poetry.outputs.cache-hit != 'true'
        uses: snok/install-poetry@v1

      - name: Restore cached dependencies
        uses: actions/cache@v3
        with:
          path: ~/.cache/pypoetry
          key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
          restore-keys: |
            ${{ runner.os }}-poetry-
      - name: Install package
        run: poetry install

      - name: Lint
        run: |
          poetry run isort --check --diff .
          poetry run black --check --diff .
          poetry run flake8 .

      - name: Test with pytest
        run: poetry run pytest tests/ --cov=<package_name> --cov-report=xml

      - name: Use Codecov to track coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage.xml   # coverage report

      - name: Build documentation
        run: poetry run make html --directory docs/
```
:::note warn
`<package_name>`自分のものへ変更
:::

## semantic-pull-request

リリースをcommit messageベースで判定するので、commit message(pull requestのtitleをcommit messageに使うとして)をCIでチェックする

詳細: https://github.com/amannn/action-semantic-pull-request 

```yaml:.github/workflows/semantic-pull-request.yml
name: semantic-pull-request

on:
  pull_request:
    types:
      - opened
      - edited
      - synchronize

jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - uses: amannn/action-semantic-pull-request@v5
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

## Release

あまりいいやり方じゃないかもしれませんが、自分でReleaseを手動で作成するのがめんどくさい (タグをsemantic-releaseに自動で決めてほしい) ので、releaseしたいときに、 dispatch_workflowを実行してその中で `semantic-release publish`を実行するactionを作成

```yaml:.github/workflows/release.yml
name: release

on:
  workflow_dispatch:

jobs:
  releaase:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.9"

      - name: Check-out repository
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Load cached Poetry installation
        id: cached-poetry
        uses: actions/cache@v3
        with:
          path: ~/.local  # the path depends on the OS
          key: poetry  # increment to reset cache

      - name: Install poetry
        if: steps.cached-poetry.outputs.cache-hit != 'true'
        uses: snok/install-poetry@v1

      - name: "Restore cached dependencies"
        uses: actions/cache@v3
        with:
          path: ~/.cache/pypoetry
          key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
          restore-keys: |
            ${{ runner.os }}-poetry-
      - name: Install package
        run: poetry install

      - name: Use Python Semantic Release to prepare release
        env:
          # This token is created automatically by GH Actions
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
            git config user.name github-actions
            git config user.email github-actions@github.com
            poetry run semantic-release publish
      - name: Publish to TestPyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          user: __token__
          password: ${{ secrets.TEST_PYPI_API_TOKEN }}
          repository_url: https://test.pypi.org/legacy/

      - name: Test install from TestPyPI
        run: |
            pip install \
            --index-url https://test.pypi.org/simple/ \
            --extra-index-url https://pypi.org/simple \
            <package_name>
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          user: __token__
          password: ${{ secrets.PYPI_API_TOKEN }}
```

:::note warn
`<package_name>`自分のものへ変更
:::

:::note warn
ただこれだと`poetry run semantic-release publish`で更新した `toml`内のversionをpushできてないのでいつまでもtomlのバージョンが古いままになっている。（version_sourceをtagにしてかつ`version_variable = "pyproject.toml:version"`にしてるのが良くないのか）要改善。
:::

# ToDo

- GitHub Actionsを含めたリリースフロー改善
- Documentation