TL;DR
rich(https://rich.readthedocs.io/en/stable/index.html)で出力されたstack traceが見やすいので、雑にdebugログをlogging.FileHandler等で吐いたときにログでも見れるようにしたい。このため、prettyなTracebackをstrで取得したい。
調べてはみたが、Traceback含めprettyな出力をstrで返してくれる関数がみあたらなかったので試行錯誤してみたがいい方法が見つからず、結局io.StringIOに頼ったよ。
モチベーション
ほぼ上述の通りだが、端的にいうと、loggingのRotatingFileHandlerでファイルにdebug logをダダ流しできると不具合調査が楽だよな、と。
richはコンソールアプリでの出力をかなりきれいにしてくれる。ただ、サクッと書いたツール等を人に使ってもらうとき、「不具合が出たらこのログ送って」で済ませたく、色はつかなくていいからなんとかloggingのFileHandlerで出力できないかなと思った。
コード
結局いい方法が見つからず、io.StringIO()をrich.print()のfile引数に渡して出力をstrで取得することで期待した動作にはなった。
以下サンプルコード。
- Python 3.11.4
 - rich==13.7.0
 
import traceback
import rich
import rich.traceback
def get_logger(name="main"):
    import logging
    logger = logging.getLogger(name)
    logger.setLevel(logging.DEBUG)
    handler = logging.StreamHandler()
    formatter = logging.Formatter(fmt='%(asctime)-15s [%(name)s] %(message)s')
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    return logger
def main():
    logger = get_logger()
    try:
        hoge=1/0
    except Exception as e:
        e_args={"exc_type":type(e), "exc_value":e, "traceback":e.__traceback__, "show_locals":True}
        tb = rich.traceback.Traceback.from_exception(**e_args)
        import io
        with io.StringIO() as s:
            rich.print(tb, file=s)
            c = "\n" + s.getvalue()
            logger.debug(c)
if __name__ == "__main__":
    main()
結果は以下。
これができるならFileHandler系に出力できるでしょう。
2024-01-05 04:11:58,060 [main]
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /home/bounoki/work/20231229_rich/test1.py:19 in main                                             │
│                                                                                                  │
│   16 def main():                                                                                 │
│   17 │   logger = get_logger()                                                                   │
│   18 │   try:                                                                                    │
│ ❱ 19 │   │   hoge=1/0                                                                            │
│   20 │   except Exception as e:                                                                  │
│   21 │   │   e_args={"exc_type":type(e), "exc_value":e, "traceback":e.__traceback__, "show_lo    │
│   22 │   │   tb = rich.traceback.Traceback.from_exception(**e_args)                              │
│                                                                                                  │
│ ╭───────────────────────────── locals ─────────────────────────────╮                             │
│ │      e = ZeroDivisionError('division by zero')                   │                             │
│ │ e_args = {                                                       │                             │
│ │          │   'exc_type': <class 'ZeroDivisionError'>,            │                             │
│ │          │   'exc_value': ZeroDivisionError('division by zero'), │                             │
│ │          │   'traceback': <traceback object at 0x7f0c7d28b040>,  │                             │
│ │          │   'show_locals': True                                 │                             │
│ │          }                                                       │                             │
│ │ logger = <Logger main (DEBUG)>                                   │                             │
│ ╰──────────────────────────────────────────────────────────────────╯                             │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ZeroDivisionError: division by zero
サンプルはStreamHandlerでやってるけど、StreamHandlerで処理したいならrichに積まれてるrich.logging.RichHandlerを使えばいいと思う。