"""
print / logging を切り替えられる簡易デバッグモジュール(print風API)。
このモジュールは、開発者がデバッグメッセージを柔軟に出力するためのシンプルな機能を提供します。
`DEBUG_LEVEL` 設定により、標準出力(print)またはロギングファイルへの出力、
あるいはデバッグ出力の完全に無効化を切り替えることができます。
主な機能は `dbg()` 関数であり、Pythonの組み込み `print()` 関数に似たAPIを提供しつつ、
自動的に呼び出し元のファイル名や行番号、タイムスタンプを付加します。
関連リンク: :doc:`tkdebug_usage`
"""
from __future__ import annotations
import datetime as _dt
import inspect
import logging
import os
import sys
from typing import Any
# ------------------------------------------------------------
# 設定(教材向けの最小構成)
# 0: 無効
# 1: print出力
# 2: logging出力
# ------------------------------------------------------------
DEBUG_LEVEL = 1
LOG_FILE = "debug.log"
# logging は import 時に設定だけしておく(出力はしない)
logging.basicConfig(
filename=LOG_FILE,
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(message)s",
encoding="utf-8",
)
[ドキュメント]
def set_debug(level: int | None = None, log_file: str | None = None) -> None:
"""
デバッグ出力設定を変更します。
`DEBUG_LEVEL` と `LOG_FILE` のグローバル設定値を動的に変更し、
デバッグ出力の挙動を切り替えることができます。
`LOG_FILE` が変更された場合、loggingモジュールのハンドラを再設定し、
新しいファイルにログが出力されるようにします。
:param level: デバッグレベル。0: 無効, 1: print出力, 2: logging出力。Noneの場合、現在の設定を維持します。
:type level: int | None
:param log_file: ログファイルのパス。Noneの場合、現在の設定を維持します。
既存の`LOG_FILE`と異なる場合、logging設定が再初期化されます。
:type log_file: str | None
:returns: なし
:rtype: None
"""
global DEBUG_LEVEL, LOG_FILE
if level is not None:
DEBUG_LEVEL = level
if log_file is not None and log_file != LOG_FILE:
LOG_FILE = log_file
# basicConfig は2回目以降効かないため、handlersを差し替える
root = logging.getLogger()
for h in list(root.handlers):
root.removeHandler(h)
h.close()
logging.basicConfig(
filename=LOG_FILE,
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(message)s",
encoding="utf-8",
)
[ドキュメント]
def dbg(
*msgs: Any,
sep: str = " ",
end: str = "\n",
flush: bool = False,
) -> None:
"""
print() 関数に似たAPIでデバッグメッセージを出力します。
`DEBUG_LEVEL` の設定に応じて、標準出力にメッセージを出力するか、
設定されたログファイルにメッセージを記録します。
呼び出し元のファイル名、行番号、現在の時刻が自動的にメッセージに付加されます。
`DEBUG_LEVEL` が0の場合、何も出力されません。
`DEBUG_LEVEL` が1の場合、組み込みの `print()` 関数と同様に標準出力に出力され、
`sep`, `end`, `flush` 引数が尊重されます。
`DEBUG_LEVEL` が2以上の場合、loggingモジュールを使用してログファイルに出力されます。
この場合、`logging`は通常1レコード1行で扱われるため、`end`引数は内部で調整され、
`flush`引数は無視されます。
:param msgs: 出力する任意の数のメッセージ。これらは文字列に変換され、`sep`で結合されます。
:type msgs: Any
:param sep: メッセージ間の区切り文字。`print()`と同様の動作です。
:type sep: str
:param end: メッセージの末尾に追加される文字列。`print()`と同様の動作ですが、logging出力時は調整されます。
:type end: str
:param flush: 出力バッファを強制的にフラッシュするかどうか。`print()`と同様の動作ですが、logging出力時は無視されます。
:type flush: bool
:returns: なし
:rtype: None
"""
if DEBUG_LEVEL <= 0:
return
frame = inspect.currentframe()
try:
# dbg() 呼び出し元
caller = frame.f_back if frame is not None else None
if caller is not None:
filename = os.path.basename(caller.f_code.co_filename)
lineno = caller.f_lineno
else:
filename = "?"
lineno = 0
now = _dt.datetime.now().strftime("%H:%M:%S")
body = sep.join(str(m) for m in msgs)
prefix = f"[{now} {filename}:{lineno}] "
text = prefix + body
if DEBUG_LEVEL == 1:
# print風に end / flush を受ける
print(text, end=end, flush=flush)
elif DEBUG_LEVEL >= 2:
# logging は通常1レコード=1行なので end は吸収しておく
# (末尾改行だけ除去しておく)
if end and text.endswith("\n"):
text = text.rstrip("\n")
logging.debug(text)
finally:
# currentframe 参照の明示解放(循環参照対策)
del frame
# ------------------------------------------------------------
# 動作確認用(import時には何も出力しない)
# ------------------------------------------------------------
if __name__ == "__main__":
# 0: off / 1: print / 2: logging
set_debug(level=1)
x = 10
y = 3.14
dbg("start")
dbg("x =", x, "y =", y)
dbg("i=", 5, " j=", 9, sep="") # print風に sep 指定
dbg("done", flush=True)
# logging側の確認
set_debug(level=2, log_file="debug.log")
dbg("これは logging に出ます")