Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13737] ファイルの概要

このコミットは、Mercurial のコードレビュー拡張機能 (codereview.py) におけるエラーハンドリングの仕組みを修正するものです。特に、Mercurial 2.1 で導入されたコマンドのエラー処理に関する変更に対応し、デコレータの使用方法を見直すことで、Mercurial がコマンド引数の誤りについて適切なエラーメッセージを出力できるように改善しています。

コミット

commit 9b8c94a46f3ad978ed3e0fa9037bf18dec6c30b0
Author: Uriel Mangado <uriel@berlinblue.org>
Date:   Sat Sep 1 19:55:29 2012 -0400

    codereview.py: correct error handling without decorator
    
    The decorator hides the number of function arguments from Mercurial,
    so Mercurial cannot give proper error messages about commands
    invoked with the wrong number of arguments.
    
    Left a 'dummy' hgcommand decorator in place as a way to document
    what functions are hg commands, and just in case we need some other
    kind of hack in the future.
    
    R=adg, rsc
    CC=golang-dev
    https://golang.org/cl/6488059

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/9b8c94a46f3ad978ed3e0fa9037bf18dec6c30b0

元コミット内容

codereview.py: correct error handling without decorator

このコミットは、codereview.py におけるエラーハンドリングをデコレータなしで正しく処理するように修正します。

デコレータがMercurialから関数の引数の数を隠してしまうため、Mercurialが誤った引数で呼び出されたコマンドに対して適切なエラーメッセージを提供できませんでした。

将来的に他の種類のハックが必要になる場合に備え、またどの関数がhgコマンドであるかを文書化する方法として、'dummy' の hgcommand デコレータはそのまま残されています。

変更の背景

この変更の背景には、Mercurial 2.1 で導入されたコマンドのエラーハンドリングに関する重要な変更があります。Mercurial 2.1 以降、カスタムコマンドとして登録された関数(@hgcommand デコレータで装飾された関数)は、エラーを示すために任意の文字列を返すことが許されなくなり、整数型の終了コードを返すことが必須となりました。以前のバージョンでは文字列を返すことが可能でしたが、Mercurial 2.1 では TypeError トレースバックが発生するようになりました。

このコミット以前の codereview.py では、hgcommand デコレータがコマンド関数のラッパーとして機能し、関数が返したエラーメッセージ(文字列)を捕捉し、それをMercurialが期待する整数型の終了コードに変換していました。しかし、このラッパーデコレータの存在が、Mercurialがコマンド関数のシグネチャ(引数の数など)を正しく認識するのを妨げていました。その結果、ユーザーがコマンドを誤った引数で実行した場合に、Mercurialが提供すべき具体的なエラーメッセージではなく、一般的なエラーしか表示されないという問題が発生していました。

このコミットは、この問題を解決するために、デコレータによるエラーコード変換のロジックを削除し、代わりに各コマンド関数内で直接 hg_util.Abort 例外を発生させるように変更することで、Mercurialが引数の検証を適切に行えるようにしています。

前提知識の解説

  • Mercurial (Hg): 分散型バージョン管理システム(DVCS)の一つ。Gitと同様に、コードの変更履歴を管理するために使用されます。
  • Mercurial Extensions: Mercurialは拡張機能を通じてその機能をカスタマイズ・拡張できます。codereview.py は、Mercurialのコードレビュープロセスを支援するためのカスタムコマンドを提供する拡張機能の一部です。
  • Python Decorators: Pythonのデコレータは、関数やメソッドの定義を変更せずに、その振る舞いを変更するための構文です。@decorator_name の形式で関数定義の直前に記述されます。このコミットでは、@hgcommand デコレータがその対象です。
  • エラーハンドリングと終了コード: プログラムが正常に終了したか、またはどのような種類のエラーが発生したかを示すために、プログラムは終了コード(または終了ステータス)を返します。慣例として、0は成功を、非ゼロはエラーを示します。Mercurial 2.1 では、コマンドの終了コードがより厳密に整数型である必要がありました。
  • hg_util.Abort: Mercurialのユーティリティライブラリ hg_util に含まれる例外クラス。この例外を発生させることで、Mercurialのコマンド実行を中断し、指定されたエラーメッセージを表示させることができます。これは、Mercurialのコマンドラインインターフェースにおいて、ユーザーにエラーを通知する標準的な方法です。

技術的詳細

このコミットの主要な技術的変更点は以下の通りです。

  1. hgcommand デコレータの簡素化:

    • 変更前: hgcommand デコレータは、ラップされた関数が返すエラー(文字列または整数)を捕捉し、Mercurialが期待する整数型の終了コードに変換するロジックを含んでいました。特に、文字列エラーメッセージを hg_util.Abort 例外に変換していました。
    • 変更後: hgcommand デコレータは、単に引数として渡された関数をそのまま返すだけの「ダミー」デコレータになりました (return f)。これにより、デコレータがMercurialからコマンド関数のシグネチャを隠すことがなくなりました。コミットメッセージにあるように、これは「どの関数がhgコマンドであるかを文書化する方法」として、また将来的な拡張の可能性のために残されています。
  2. エラーハンドリングの変更:

    • 変更前: 多くのコマンド関数内で、エラーが発生した場合にエラーメッセージ文字列を return していました(例: return "cannot specify CL name and file patterns")。これらの文字列は hgcommand デコレータによって捕捉され、hg_util.Abort 例外に変換されていました。
    • 変更後: 各コマンド関数内でエラーが発生した場合、直接 raise hg_util.Abort("エラーメッセージ") を呼び出すように変更されました。これにより、エラー処理の責任がデコレータから個々のコマンド関数に移り、Mercurialの内部エラー処理メカニズムとより直接的に連携できるようになりました。

この変更により、Mercurialはコマンドが呼び出された際に、その引数の数を直接検証できるようになり、引数の不一致があった場合に、より具体的で役立つエラーメッセージをユーザーに提供できるようになります。例えば、必要な引数が不足している場合や、予期しない引数が渡された場合に、Mercurial自体がその問題を検出し、適切なエラーを報告できるようになります。

コアとなるコードの変更箇所

変更は lib/codereview/codereview.py ファイルに集中しています。

  1. hgcommand デコレータの定義変更:

    --- a/lib/codereview/codereview.py
    +++ b/lib/codereview/codereview.py
    @@ -1247,24 +1247,8 @@ def MatchAt(ctx, pats=None, opts=None, globbed=False, default='relpath'):
     #######################################################################
     # Commands added by code review extension.
     
    -# As of Mercurial 2.1 the commands are all required to return integer
    -# exit codes, whereas earlier versions allowed returning arbitrary strings
    -# to be printed as errors.  We wrap the old functions to make sure we
    -# always return integer exit codes now.  Otherwise Mercurial dies
    -# with a TypeError traceback (unsupported operand type(s) for &: 'str' and 'int').
    -# Introduce a Python decorator to convert old functions to the new
    -# stricter convention.
    -\n def hgcommand(f):\n-\tdef wrapped(ui, repo, *pats, **opts):\n-\t\terr = f(ui, repo, *pats, **opts)\n-\t\tif type(err) is int:\n-\t\t\treturn err\n-\t\tif not err:\n-\t\t\treturn 0\n-\t\traise hg_util.Abort(err)\n-\twrapped.__doc__ = f.__doc__\n-\treturn wrapped
    +\treturn f
    

    この変更により、hgcommand デコレータは、元の関数 f をそのまま返すだけのシンプルな関数になりました。以前のバージョンで存在した、エラーコードの変換や hg_util.Abort の呼び出しを行うラッパー関数 wrapped が削除されています。

  2. 各コマンド関数内のエラーハンドリングの変更: change, code_login, clpatch, undo, release_apply, download, file, gofmt, mail, pending, submit, sync, upload など、Mercurialコマンドとして登録されている多くの関数で、エラーメッセージ文字列を return していた箇所が、raise hg_util.Abort(...) に変更されています。

    例: change 関数の一部

    --- a/lib/codereview/codereview.py
    +++ b/lib/codereview/codereview.py
    @@ -1293,42 +1277,42 @@ def change(ui, repo, *pats, **opts):\n     """\n     \n      if codereview_disabled:\n-    		return codereview_disabled
    +    		raise hg_util.Abort(codereview_disabled)
     	\n      dirty = {}\n      if len(pats) > 0 and GoodCLName(pats[0]):\n     		name = pats[0]\n     		if len(pats) != 1:\n-    			return "cannot specify CL name and file patterns"
    +    			raise hg_util.Abort("cannot specify CL name and file patterns")
     		pats = pats[1:]\n     		cl, err = LoadCL(ui, repo, name, web=True)\n     		if err != '':\n-    			return err
    +    			raise hg_util.Abort(err)
     		if not cl.local and (opts["stdin"] or not opts["stdout"]):\n-    			return "cannot change non-local CL " + name
    +    			raise hg_util.Abort("cannot change non-local CL " + name)
     	else:\n     		name = "new"\n     		cl = CL("new")\n     		if repo[None].branch() != "default":\n-    			return "cannot create CL outside default branch; switch with 'hg update default'"
    +    			raise hg_util.Abort("cannot create CL outside default branch; switch with 'hg update default'")
     		dirty[cl] = True\n     		files = ChangedFiles(ui, repo, pats, taken=Taken(ui, repo))\n     \n      if opts["delete"] or opts["deletelocal"]:\n     		if opts["delete"] and opts["deletelocal"]:\n-    			return "cannot use -d and -D together"
    +    			raise hg_util.Abort("cannot use -d and -D together")
     		flag = "-d"\n     		if opts["deletelocal"]:\n     			flag = "-D"\n     		if name == "new":\n-    			return "cannot use "+flag+" with file patterns"
    +    			raise hg_util.Abort("cannot use "+flag+" with file patterns")
     		if opts["stdin"] or opts["stdout"]:\n-    			return "cannot use "+flag+" with -i or -o"
    +    			raise hg_util.Abort("cannot use "+flag+" with -i or -o")
     		if not cl.local:\n-    			return "cannot change non-local CL " + name
    +    			raise hg_util.Abort("cannot change non-local CL " + name)
     		if opts["delete"]:\n     			if cl.copied_from:\n-    				return "original author must delete CL; hg change -D will remove locally"
    +    				raise hg_util.Abort("original author must delete CL; hg change -D will remove locally")
     			PostMessage(ui, cl.name, "*** Abandoned ***", send_mail=cl.mailed)\n     			EditDesc(cl.name, closed=True, private=cl.private)\n     		cl.Delete(ui, repo)\n    @@ -1338,7 +1322,7 @@ def change(ui, repo, *pats, **opts):\n     		s = sys.stdin.read()\n     		clx, line, err = ParseCL(s, name)\n     		if err != '':\n-    			return "error parsing change list: line %d: %s" % (line, err)
    +    			raise hg_util.Abort("error parsing change list: line %d: %s" % (line, err))
     		if clx.desc is not None:\n     			cl.desc = clx.desc;\n     			dirty[cl] = True\n    @@ -1360,7 +1344,7 @@ def change(ui, repo, *pats, **opts):\n     			cl.files = files\n     		err = EditCL(ui, repo, cl)\n     		if err != "":\n-    			return err
    +    			raise hg_util.Abort(err)
     		dirty[cl] = True
    

コアとなるコードの解説

このコミットの核心は、Mercurialのコマンド処理におけるエラー伝達の責任を、デコレータから個々のコマンド関数へと移した点にあります。

変更前の hgcommand デコレータは、以下のような役割を担っていました。

  1. デコレータがラップする関数(実際のMercurialコマンド)を実行する。
  2. ラップされた関数がエラーを示す文字列を返した場合、それを捕捉する。
  3. 捕捉した文字列を hg_util.Abort 例外として再スローする。
  4. Mercurial 2.1 で導入された、コマンドが整数型の終了コードを返す必要があるという要件に対応するため、返り値が整数型でない場合に hg_util.Abort を発生させる。

しかし、このデコレータのラッパー機能が、Mercurialがコマンド関数の引数シグネチャを正しく検査するのを妨げていました。Mercurialは、コマンドの引数検証を行う際に、デコレータによって隠蔽された元の関数のシグネチャではなく、ラッパー関数のシグネチャを見てしまうため、引数の数が間違っている場合でも適切なエラーメッセージを出力できませんでした。

このコミットでは、この問題を解決するために、hgcommand デコレータを以下のように変更しました。

def hgcommand(f):
    return f

これにより、hgcommand はもはやラッパーではなく、単に元の関数をそのまま返すだけの「パススルー」デコレータとなりました。結果として、Mercurialはコマンド関数の実際の引数シグネチャを直接参照できるようになり、引数の検証を正確に行えるようになりました。

デコレータがエラー変換の役割を放棄したため、各コマンド関数は自身でエラーを適切に処理する必要があります。そこで、以前はエラーメッセージ文字列を return していた箇所が、Mercurialのエラー処理の標準的な方法である raise hg_util.Abort("エラーメッセージ") に変更されました。hg_util.Abort 例外がスローされると、Mercurialのフレームワークがそれを捕捉し、指定されたエラーメッセージをユーザーに表示し、適切な非ゼロの終了コードでコマンドを終了させます。

この変更は、Mercurialのバージョンアップに伴う互換性の問題に対処しつつ、より堅牢で情報量の多いエラーハンドリングを実現するための重要なリファクタリングと言えます。

関連リンク

参考にした情報源リンク