Sphinx 拡張のエラー出力方法まとめ

Sphinx 拡張ではエラーを表示したくなる場面はいくつかある。
設定値が誤っている場合や、環境の設定不足、記述の誤りや通信エラーなど。

そんなときに Sphinx/docutils フレームワークを使ってエラーを出力していくのだが、
場面や用途によってエラーの出力方法が異なる。

今回はそんな Sphinx 拡張のエラー出力方法をまとめたいと思う。
というか、最近 sphinxcontrib-blockdiag を書き直していて、何種類も使い分けている理由が分からずに混乱していたので、
自分のためにまとめ直すことにした。

ディレクティブ内でエラーを出力する

DirectiveError を投げる

docutils.parsers.rst に定義されている DirectiveError 例外を投げることでエラーが出力される。
また、ショートカットとして Directive クラスには debug(), info(), warning(), error(), severe() というメソッドが用意されていて、
エラーメッセージを引数にこれを呼び出すと DirectiveError のインスタンスを得ることができる。

これを使ってエラーを出力する場合は次のようになる。

class MyDirective(Directive):
    def run(self):
        if not self.arguments:
            raise self.error('no arguments')

        # 通常の処理...

メソッドインスタンスを生成するだけなので、忘れず raise する必要がある。

Reporter を呼び出す

DirectiveError は例外処理を用いるので、ディレクティブの処理を続けることはできない。
これは致命的な問題があった場合はやむを得ないが、軽微な問題を報告しつつ処理を先に進めたいということもある。

その場合は Reporter を使う方法がある。
Reporter には debug(), info(), warning(), error(), severe() のメソッドがあり(ちなみに実装は docutils/utils/__init__.py にある)、
メソッドは system_message ノードを返してくるので、
ディレクティブの実行結果にこれを含めることでエラーを通知する。

Reporter クラスのインスタンスは、ディレクティブからは self.state.document.reporter から得ることができる。

これを使ってエラーを出力する場合は次のようになる。

class MyDirective(Directive):
    def run(self):
        paragraph = nodes.paragraph()
        if not os.path.exists(self.argument[0]):
            msg = "file not found: %s" % self.argument[0]
            node += self.state.document.reporter.warning(msg, line=self.lineno)
        else:
             # ...

        return [node]

reporter が生成した system_message ノードを単に return する実装もよく見かけるが、
うまく利用するとエラーを報告しつつ処理を進めることができるのでうまく利用したい。

もちろん、system_message ノードは単純な docutils のノードであるため、
自分で nodes.system_message() として生成することも可能である。

ロール内でエラーを出力する

Reporter を呼び出す

ロール内でも Reporter を利用することができる。
Reporter オブジェクトには引数 `inliner` の reporter 属性からアクセスできる。

また、ロール内でエラーが発生した場合は problematic ノードを返却する必要があるため、
Reporter とあわせて Inliner#problematic() を呼び出す必要がある。

def my_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
    try:
        # 通常の処理
    except Exception as exc:
        msg = inliner.reporter.error('critical error: %r' % exc)
        prb = inliner.problematic(rawtext, rawtext, msg)
        return [prb], [msg]

軽微なエラーの場合は problematic ノードを返すのではなく、返り値のノードに system_message ノードを混ぜ込むとよい。

Writer 内でエラーを出力する

docutils の場合

system_message ノードを作って self.visit_system_message() メソッドに投げ込むとよい(みたい)。

HTML writer では self.document.reporter から Reporter にアクセスできるが、
LaTeX writer では self.document そのものがなく、代わりに self.warn() や self.error() に reporter のメソッドが登録されているなど、
統一的な方法は用意されていない模様*1

sphinx の場合 (vistor 関数)

Builder クラスの warn(), info() を使う。
Builder オブジェクトには引数 `self` の builder 属性からアクセスできる。

def html_visit_my_node(self, node):
    try:
        # 通常の処理
    except Exception as exc:
        self.builder.warn('Error happen: %r" % exc)
        raise nodes.SkipNode

warn() メソッドはエラーを画面に表示するだけなので、処理を終了する場合は SkipNode を投げる必要がある。
軽微なエラーなどで処理を継続する場合はそのまま進めて良い。

Sphinx の場合 (その他のイベントハンドラ)

Sphinx クラスの warn(), info() を使う。
Sphinx オブジェクトはハンドラの最初の引数として渡される。

def on_doctree_resolved(self, doctree, docname):
    try:
        # 通常の処理
    except Exception as exc:
        self.warn('Error happen: %r' % exc)

Builder#warn() メソッドと同じく*2 Sphinx#warn() メソッドはエラーを画面に表示するだけなので、
処理を終了する場合は自分で return などを呼び関数から抜け出す必要がある。

まとめ

理由はなんとなくわかるんだけども、やり方がバラバラでつらいですね。諦めましょう。

*1:実装を眺めると system_message ノードを作っているが、何も処理していないケースが散見されるので、まじめにエラー処理をしていない予感…

*2:Builder#warn() メソッドの実体は Sphinx#warn() メソッドなので当然のことだが