概要
私はPure PythonでWindowsのCOMとやり取りするためのパッケージであるcomtypes
をメンテナンスしています。
このパッケージはPythonの標準ライブラリctypes
に依存しています。
Python 3.13からctypes
に変更が加えられ、comtypes
はPython 3.12まで動いていた実装では動かなくなってしまいました。
comtypes
のコード変更により、Pythonバージョン間の互換性を保って動くようにすることができましたが、ctypes
の変更によってまたこの実装が壊れてしまうことが予想されました。
そこでリグレッションを防止することを目的に、Pythonの本家であるcpython
リポジトリにテストを追加するコントリビュートを行いました。
この記事ではこの顛末と、私がコードを追加したときに何を考えていたか記録するために書きました。
発覚までの経緯と原因
2024年の9月なかばになり、もうすぐPython 3.13 finalがリリースされることに気が付いた私は、Python 3.13サポートの作業に着手することをコミュニティに周知するべくissueを投稿しました。
このとき私は、Python 3.12をサポートする時と同様にCIパイプラインにテスト対象となるPythonを追加するだけの単純なものになると思っていました。
しかし、Anacondaのパッケージマネージャをメンテナンスしている人から、「comtypes
がPython 3.13では動かない」ことを報告されました。
原因を調べたところ、cpython
リポジトリのメンテナーによりctypes
に変更が加えられていたことによるものらしいことがわかりました。
この変更によってIUnknown
、CoClass
、SAFEARRAY
を実装するために用いられているメタクラスが動かなくなったため、comtypes
はモジュールをロードしただけでエラーが発生するようになってしまいました。
comtypes
コードベース変更による一時的な解決までの流れ
どこかに解決のヒントがないかと思い、エラーメッセージなどをキーワードとしてGitHub内を検索すると、comtypes
と同じくメタクラスを用いてCOMインターフェースを定義するpyglet
というプロジェクトを見つけました。
そのパッケージはcomtypes
より一足早くPython 3.13をサポートしていました。
pyglet
はメタクラスの初期化処理を__new__
だけでなく一部を__init__
で行うようにすることで、Python 3.13でも動作するようにしていました。
comtypes
でも同様にコードを変更しましたが、pyglet
とは違ってその実装ではPython 3.13より下のバージョンではうまく動きませんでした。
そこでバージョンで互換性を持たせるためsys.version_info
を用いたバージョンブリッジを入れたところ、この問題が解決しうまく動くようになりました。
cpython
へのissue投稿とメンテナーによるドキュメンテーションの追加
このctypes
の変更は"What's New In Python"に記載されておらず、またそれに対応するためバージョンブリッジを用いることがctypes
を利用するプロジェクトとして正しいのかがわかりませんでした。
そこでcpython
側にissueを立てて、どのようにすればいいかをメンテナーから意見をうかがうことにしました。
issueに対してメンテナーからリアクションがあり、"What's New In Python 3.13"にctypes
への変更に言及する記載が追加されました。
最終的なcomtypes
コードベースの変更内容
ctypes
のメンテナーからはバージョンブリッジを使うよりも、エラーの原因となっている無限再起呼び出しや未定義の名前参照を止めるように早期リターンを使う実装を提案されました。
メンテナーから提案されたコードをリファクタリングしてcomtypes
にコミットし、バージョン1.4.8としてリリースしました。
cpython
へテストを追加するコンリビュートまでの流れ
それでも今後のcpython
の変更によって、また上述したメタクラスの実装が壊れてしまうかもしれないことが想定されました。
cpython
やctypes
のメンテナーに負担をかけず、comtypes
で用いられているメタクラスの定義時にエラーが送出されるようなリグレッションをこれ以上発生させないためには何が有効かを考えました。
メンテナーに対してコード変更のたびに依存しているパッケージの動作をすべて確認してもらうということはかなりの負担となります。
最新のcpython
のmain
ブランチをビルドしてテストを行うことをGitHub ActionsなどのCIサービスで定時実行することも、リグレッションの早期発見にはつながりますが発生防止には効果はありません。
いろいろと考えた結果、comtypes
で用いられているメタクラス(をシンプルにしたもの)が異常を起こさないかのテストをcpython
に追加することがもっとも低コストで効果があることだと思いつき、issueを立てました。
テスト追加について合意が取れたのでPRを行い、レビューを受けてapproveされmergeされました。
このテストはバグフィックスステータスバージョンである3.13
と3.12
にもバックポートされるため、それらのバグ修正時にもリグレッションが発生する可能性を低下させることができると考えています。
まとめ
OSSメンテナーの立場として今回の事象を振り返ってみました。
メンテナーは機能追加や変更を行った際、依存するプロジェクトが壊れないかを恐れています。
そんなときに心強いのが、ユースケースが動作することを保証しているテストです。
しかし既存のテストはこの世のすべてのユースケースを網羅しているわけではないので、今回のように変更によって壊れるプロジェクトも生じてきます。
もしプロジェクトが壊れる前にユースケースの動作を担保するテストが追加されたり、壊れてしまった後でも新しいテストが追加されることでさらなるリグレッションを防止できるフローがコミュニティに確立されていれば、メンテナーは新しいバージョンのパッケージを恐れなくリリース/デプロイすることができます。
もし自分のプロジェクトが依存しているOSSに機能追加などでコントリビュートしたいのであれば、自分のユースケースを守るためのテストをまず追加することを行えば、メンテナーもその後の提案を受け入れやすくなるのではないかと思いました。