株式会社CenterWaveでは、Webからの情報収集などをSeleniumなどを使って、自動化しています。

その際、定期実行するためのCUIでの起動と、起動してから細かい設定を気楽にいじれるGUIでの起動をサポートすることが多いです。

GUIは手軽なのでtkinterを利用しています。

その際、様々なエラーが起きます。

社内アプリとはいえ、より高いクオリティを目指すためには、定期実行した際に起きたエラーや、自分以外の人間が起きたエラーをキャッチしてログを残したり、メールやチャットなどで、製作者に送る必要があります。

我が社では情報共有にChatWorkを利用しているので、https://tonari-it.com/python-chatwork/を参考に、ChatWorkのAPIに送信するクラスChatBotを作成し、製作者にチャットルームにログを送っています。

しかし、tkinterはデフォルトでは、内部で起きた例外をターミナルに出力して、握りつぶしてしまいます。

つまり、


from tkinter import *

from chatbot import ChatBot


bot = ChatBot("APIキー", "ルームID")

try:

    root = Tk()

    root.mainloop()

except:

    import traceback

    import sys

    bot.send(" ",join(sys.argv)  + "\n" + traceback.format_exc())

    raise

としても、mainloop内で例外が起きても何も起きません。
tkinterにはCallWrapperというクラスがあり、これがtkinter内での関数のコールをすべてラップして、アプリ終了以外の例外を握りつぶしているのです。
これに関してWeb上の日本語情報は見つからず、英語情報もPython 2時代の頃のものばかりでした。
そこで、http://mgltools.scripps.edu/api/DejaVu/Tkinter-pysrc.html#CallWrapperで見つけたpython 2時代のコードを書きます。


class CallWrapper: 
      """Internal class. Stores function to call when some user 
      defined Tcl function is called e.g. after an event occurred.""" 
      def __init__(self, func, subst, widget): 
          """Store FUNC, SUBST and WIDGET as members.""" 
          self.func = func 
          self.subst = subst 
          self.widget = widget 
      def __call__(self, *args): 
          """Apply first function SUBST to arguments, than FUNC.""" 
          try: 
              if self.subst: 
                  args = self.subst(*args) 
              return self.func(*args) 
          except SystemExit, msg: 
              raise SystemExit, msg 
          except: 
              self.widget._report_exception()

これを自作クラスで上書きする必要があります。
python 3で書いたコードが次です。


import tkinter as tk
from tkinter import *

from chatbot import ChatBot

bot = ChatBot("APIキー", "ルームID")

class Catcher:
    """例外の情報をChatWorkに送る"""
    def __init__(self, func, subst, widget):
        self.func = func
        self.subst = subst
        self.widget = sidget
    
    def __call__(self, *args):
        try:
            if self.subst:
               args = self.subst(*args)
            return self.func(*args)
        except SystemExit as e:
            raise e
        except:
            import traceback
            import sys
            bot.send(" ",join(sys.argv)  + "\n" + traceback.format_exc())
            self.widget._report_exception()

tk.CallWrapper = Catcher

root = Tk()

root.mainloop()

これでmainloop内でアプリ終了例外が発生すると、ChatWorkに起動時のコマンドと例外のスタックトレースを送信します。

pythonについて調べようとすると、python 2の情報ばかり出てくるのは困りものですね。