Pythonパッケージ開発フロー

今日は半日でパッケージのα版を一つリリースして、デプロイまで実施できた。

パッケージ開発のフローにだいぶ慣れてきた証拠とも言えるので、それをメモしておこう。

作成したパッケージ

https://github.com/hinihat/backupGDgithub.com

特定のファイルまたはディレクトリの.tar.gz形式の圧縮ファイルを作成して、それをGoogleDriveにアップロードするというもの。

実質ファイル一つ、クラス一つの極小パッケージであるが、cronやscheduleライブラリで定期的に実行することでサーバー内のデータをバックアップしておくことができる。

予めGoolgeCloudPlatformを操作してGoogleDriveAPIを叩けるようにしておく必要がある。

import backupGD

tarfile = backupGD.create_backup('./data') # Create gzipped tar file
with backupGD.drive('client_secret.json') as drive:
    folder_id = drive.create_folder('backupGD') # Create folder on Google Drive
    drive.upload_backup(tarfile, folder_id=folder_id) # Upload tar file

このパッケージの作成手順を追っていく。

メインパッケージの作成

まず最初にやるのはメインパッケージ作成およびインターフェースの設計だ。

メインパッケージとはimport XXXfrom XXX import YYYXXXの部分のことで、ユニークかつ簡潔な名前が求められる。

googleという文字を入れてしまうと公式パッケージと紛らわしくなるのでGoogleDriveをGDと省略することにした。

インターフェースの設計

ここら辺はまだベストプラクティスを探索中である。

現在はクラスベースでメソッドによってインターフェースを提供することが多いが、import numpy as npimport pandas as pdのような省略形があるなら関数ベースのインターフェースでも良いのかもしれない。 ただ個人のパッケージでそのような省略形を他のパッケージをバッティングしないように設定するのは難しいのではと思っている。

クラスベースの良いところは

  • カプセル化によって状態を保持できる
  • ユーザーが意識しなくて良い値をユーザーから隠すことができる
  • 複数のインスタンスを同じコード内で使用できる

今回の場合、究極的には

import backupGD

backupGD.backup('./data', client_secret='client_secret.json')

だけで良い。(記事を書き終わった後にこのインターフェースが採用された)

でもcreate_folder()とかdelete_folder()とかを後々追加した際に、そこでも引数にclient_secretを渡さないといけなくなるので、結局プロパティを使いまわせるクラスベースのほうが良いのかな。

Docstringの整備

作成したインターフェースに対してDocstringを付与していく。 これがテストコードや自動生成ドキュメントの原型となる。

Numpy StyleのDocstringの例

def add(a: int, b: int) -> int:
    """Add two integers

    Parameters
    ------------
    a : int
       integer
    b : int
       other integer
    
    Returns
    --------
    int
       Return `a` + `b`
    """
    return a + b

nnt339es.hatenablog.com

テストコードの実装

最低限の動作保証をするユニットテストを実装。

とりあえず期待する入力に対して期待する出力をテストしておけば、リファクタリングで問題が発生した時もトラブルシューティングが楽になる。

がっつり書くなら期待しない引数入力に対する例外送出もテストする。 Pythonの型ヒントは飽くまで"型ヒント"なので引数の型が異なってもインタプリターが処理できるならそのまま処理してしまう。 これは適当に書いても動作するので便利だが、一方でバグの発見が遅れる原因にもなる。

そもそも「型ヒントを付けているのにそれを無視して引数を渡すユーザーが悪い」ということであまり厳密にテストはしていない。

ただし、__init__()でプロパティを初期化するときは、後々呼び出すメソッドがエラーを吐かないように厳密に引数をチェックすべきである。そうしなければバグの発見が遅れることになる。

そういえばユニットテストに関する記事を一つも書いていなかったな。

setup.pyその他

setup.py, setup.cfg,README.md,LICENSE等書いてパッケージ化を進める。

詳細は省略。

主流はpyproject.tomlか。

ドキュメントの生成

sphinxでドキュメントを自動生成

$ pip install sphinx sphinx-rtd-theme m2r2

クイックスタート

$ sphinx-quickstart docs_src

conf.py

# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Path setup --------------------------------------------------------------

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('..'))


# -- Project information -----------------------------------------------------

project = 'backupGD'

# The full version, including alpha/beta/rc tags
import backupGD
release = backupGD.__version__


# -- General configuration ---------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.napoleon',
     'sphinx_rtd_theme',
  ]

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']


# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

source_suffix = ['.rst', '.md']

ビルド

$ sphinx-apidoc -f -o ./docs_src backupGD
$ sphinx-build docs_src docs

Git

$ git init # リポジトリ初期化
$ git add .
$ git commit -m "Initial commit" # コミット
$ git remote add origin XXX # origin 追加
$ git push -u origin master # プッシュ

あとはGitHub Pagesでドキュメント公開したりPyPIにアップロードしたりDockerイメージ作ったり。