Pythonパッケージ開発からPyPI公開までの道のりと実践知見

に公開

1. はじめに

今回、Pythonで作成したパッケージをPyPI(Python Package Index)に初めて公開しました。この記事では、その際に行った作業や工夫した点、今後のために気をつけたいことをまとめます。
今回作成した作成物はGithubPyPIに掲載しております。

2. 今回実装した機能

このパッケージの中核は hmscalc/hms_time.py に実装されている HMSTime クラスです。
このクラスは「時:分:秒」形式の文字列を扱い、直感的な時間計算や変換を可能にします。

HMSTimeクラスの主な特徴と設計

  • HH:MMHH:MM:SS形式の文字列で表現された時刻の加算・減算ができる
  • 負の時間(マイナス値)も自然に扱える
  • 時刻を秒・分・時間・タプル・辞書形式に変換可能
  • 例外処理(不正なフォーマットや型)も独自クラスで堅牢に対応
  • 比較演算子(==, <, >, <=, >=, !=)もサポート
  • すべての時間は「秒」に正規化して保持し、演算や変換時に必要な形式へ変換
  • 正規表現で入力文字列をパースし、柔軟かつ厳密なバリデーションを実現

主要な実装例

from hmscalc import HMSTime

a = HMSTime("1:30:15")
b = HMSTime("2:15:45")

print(a + b)            # "3:46:00"
print(a - b)            # "-0:45:30"
print(a.to_seconds())   # 5415
print(a.to_tuple())     # (1, 30, 15)
print(a.to_dict())      # {'hh': 1, 'mm': 30, 'ss': 15}

クラス定義の一部抜粋

class HMSTime:
    def __init__(self, time_str: str):
        self.total_seconds = self._parse_time_string(time_str)

    def __add__(self, other: "HMSTime") -> "HMSTime":
        return HMSTime.from_seconds(self.total_seconds + other.total_seconds)

    def __sub__(self, other: "HMSTime") -> "HMSTime":
        return HMSTime.from_seconds(self.total_seconds - other.total_seconds)

    def __str__(self) -> str:
        total = abs(self.total_seconds)
        hh = total // 3600
        mm = (total % 3600) // 60
        ss = total % 60
        sign = "-" if self.total_seconds < 0 else ""
        return f"{sign}{hh}:{mm:02}:{ss:02}"

    @staticmethod
    def _parse_time_string(time_str: str) -> int:
        match = re.fullmatch(r"(-)?(\d+):(\d{1,2})(?::(\d{1,2}))?", time_str)
        if not match:
            raise InvalidTimeFormatError(time_str)
        neg, hh, mm, ss = match.groups()
        hh = int(hh)
        mm = int(mm)
        ss = int(ss) if ss is not None else 0
        total = hh * 3600 + mm * 60 + ss
        return -total if neg else total

3. テストの実装とポイント

  • テストフレームワーク: pytestを利用し、主要な関数やクラスに対してユニットテストを実装
  • CI連携: GitHub Actionsでプッシュ時に自動テストを実行
  • テスト用データ: テストケースごとに異常系・正常系を用意

4. CI/CD(GitHub Actions)による自動化

  • 自動テスト: プルリクエスト時に、複数Pythonバージョンで自動的にテスト(pytest)・Lint・型チェック(ruff, black, mypy)が実行される
  • ビルド&パッケージング: タグpush時にpoetry publish --buildでwheelとsdistを作成し、そのままPyPIへ公開

5. PyPI公開までの流れ

  1. パッケージ構成の整理

    • ディレクトリ構成を明確にし、hmscalc/配下に実装、tests/配下にテストコードを配置。
    • __init__.pyを忘れずに設置。
      hmscalc/                # パッケージ本体(実装)
      │  ├── __init__.py
      │  └── hms_time.py      # 時間計算の主要ロジック
      │  └── exceptions.py    # 例外系
      │
      tests/                  # ユニットテスト
      │  └── ...              # テストコード群
      │
      README.md               # パッケージの説明・使い方
      pyproject.toml          # パッケージ管理・ビルド設定(Poetry等)
      Dockerfile              # 開発・テスト用Docker設定
      runtests.sh             # テスト一括実行スクリプト
      lint.sh                 # Lint一括実行スクリプト
      LICENSE                 # ライセンス
      
    • ブランチ戦略は以下とした。
      • mainブランチ: リリース用の安定したコードのみをマージ。
      • developブランチ: 日々の開発はこちらで行い、動作確認後にmainへマージ。
      • feature/xxxブランチ: 新機能や修正ごとに作成し、developへマージ。
      • release/xxxブランチ: リリース前の最終調整やバージョンアップ用。
  2. pyproject.tomlsetup.cfgの作成

    • パッケージ名、バージョン、説明、依存パッケージなどを正確に記載。
    • long_descriptionlong_description_content_typeでREADMEを反映。
    • Poetryやsetuptoolsなど、ビルドツールに合わせて記述。
  3. テストの実装・実行

    • pytestなどでユニットテストを作成し、ローカルやCIで必ず実行。
    • 異常系・正常系のテストケースを網羅。
  4. ビルド

    • Poetryならpoetry build、setuptoolsならpython -m builddist/配下にパッケージを生成。
    • 生成物(whl, tar.gz)が正しくできているか確認。
  5. テスト公開(TestPyPI)

    • twine upload --repository testpypi dist/*poetry publish -r testpypiでテスト用PyPIにアップロード。
    • 実際にpip install --index-url https://test.pypi.org/simple/ ...でインストール検証。
  6. 本番公開(PyPI)

    • 本番用APIトークンを使い、twine upload dist/*poetry publishで公開。
    • バージョンの重複に注意(PyPIは同一バージョンの再アップロード不可)。
  7. GitHub Actionsによる自動化

    • タグpushやmainブランチへのマージで自動ビルド・テスト・公開を実施。
    • Secrets(PYPI_API_TOKEN)の設定や、CIの成否チェックを必ず行う。
    • ワークフローの失敗時はログを確認し、依存やパス、権限設定を見直す。

6. 気をつけること

  • バージョン管理: PyPIは同じバージョンで再アップロード不可。バージョン番号の更新を忘れずに。
  • GitHub Secretsの設定: PYPI_API_TOKENなどの機密情報はGitHubリポジトリのSettings > Secrets and variables > Actionsで登録し、ワークフロー内で${{ secrets.PYPI_API_TOKEN }}として利用。
  • branch protectの設定:
    • mainブランチやreleaseブランチに対して「プルリクエスト経由でのみマージ可能」「レビュー必須」「CI成功必須」などの保護ルールを設定することで、誤ったコードや未検証のコードが本番リリースされるのを防ぐ。
    • 必要に応じて「force push禁止」「管理者も保護ルールを無効化できない」なども有効にする。
    • セキュリティや品質担保の観点から、branch protectはCI/CD運用・PyPI公開の自動化とセットで必須の運用とするのがおすすめ。
    • 保護ルールの設定はGitHubリポジトリの「Settings > Branches」から行う。

7. まとめ

初めてのPyPI公開は不安も多かったですが、機能実装・テスト・CI/CD・ドキュメント整備を徹底することでスムーズに進めることができました。
色々学べることも多かったので、便利系ツール何か思いつきましたら、また他の公開も挑戦してみたいなと思います。
※ちなみに英語系のREADMEの記載、コメント等、英語苦手なので、ほぼLLMに任せてしまいました。テストコードなどもある程度書いてくれるので、かなり助かりました。


ご質問やフィードバックがあれば、ぜひコメントでお知らせください!

GitHubで編集を提案

Discussion