「入門TortoiseHg+Mercurial」発売中(詳細は「執筆情報」参照)


コードウォーキングのための準備運動

Mercurial のソースコードを参照する際に、 必要となる基本的な知識に関して説明します。

ここでは、 『じっくり読み込む』レベルの一歩手前の、 『ウォークスルー』(walk through) で概要を把握する程度の『参照』を想定しています。

extension に関する説明も併せて参照してください。

  1. コード中での使用頻度が高い Mercurial 固有の慣用句
  2. コード参照時の確認頻度が高い Mercurial 固有のクラス
  3. コード中での利用頻度が高い Mercurial 固有のファイル
  4. Mercurial での使用頻度が高い Python 固有の慣用句/基本文法

コード中での使用頻度が高い Mercurial 固有の慣用句

メッセージ国際化 "_()"

Mercurial では、 メッセージの国際化に対応するため、 mercurial/i18n.py において、 gettext() 関数を定義しています。

gettext() を使用することで、 環境設定に応じたメッセージ文字列を表示することができます。

しかし、メッセージ表示が必要な全ての箇所において gettext() 呼び出しをすると、 コードの可読性が低下してしまうため、 別名として定義されたアンダースコア("_") を使用した呼び出しが多様されています。

    if not pats:
       raise util.Abort(_('at least one filename or pattern is required'))
メッセージの国際化

@command アノテーション

mercurial/command.py 等でのコマンド定義の際には、 以下のような @command アノテーションが使用されます (エクステンション類での利用には、ばらつきがあります)。

@command('^commit|ci',
    [('A', 'addremove', None,
     _('mark new/missing files as added/removed before committing')),
    ('', 'close-branch', None,
     _('mark a branch as closed, hiding it from the branch list')),
    ] + walkopts + commitopts + commitopts2 + subrepoopts,
    _('[OPTION]... [FILE]...'))
def commit(ui, repo, *pats, **opts):
    """..... ※ 以下コマンドのヘルプドキュメント
@command アノテーションの利用例

@command アノテーションの実体は、 mercurial/cmdutil.py で定義された command から返却された関数です。

※ in mercurial/commands.py:

command = cmdutil.command(table)
@command アノテーションの生成

上記の処理を実行することで、 @command アノテーションは、 アノテーション対象の関数を、 指定された辞書オブジェクトの table に追加します。

@command アノテーションは、 以下の引数を取ります。

コマンド名定義:
コマンド名/別名に関する文字列ないし正規表現。 コマンド実行時に、当該コマンドの処理引き当てに使用されます。
オプションリスト:

コマンド実行時に指定可能な、固有オプションのリスト。 全コマンドで指定可能なグローバルオプションは、除外されています。 オプションリストの各要素は、以下のものから構成されます。

短縮名称:
単一ハイフン('-')で始まる一文字でのオプション指定の際のオプション名。 短縮名を持たない場合は空文字列('')。
完全名称:
2連ハイフン('--')で始まるオプション指定の際のオプション名。 処理実装関数側で、 オプション指定値の引き当てに使用する名前なので、 基本的には指定必須。
デフォルト値:
コマンドラインで指定が無かった場合のオプションの値。 真偽値の場合は None ないし False、 文字列の場合は空文字列、 複数指定可能な引数の場合は空リストが一般的 (Python 上では、これらは全て『偽値』扱いとなる)。
オプション説明
オンラインヘルプで表示される、 オプションの説明文字列。
オプション引数ラベル(省略可能):
オンラインヘルプで表示される、 オプション引数値の説明文字列。
実行時の引数指定概要(省略可能):
Unix 系 OS での、man コマンドによるオンラインマニュアルにおける、 SYNOPSIS 相当の文字列。

完全名称オプションの格納形式

Mercurial のコマンド実装関数における、 コマンドラインオプション指定の受け取りは、 以下の様な形式で行われるのが一般的です。

def commit(ui, repo, *pats, **opts):
オプション引数の受け取り

オプション指定値の取得は、 上記の例であれば、 対象オプションの完全名称を使って opts から値の引き当てを行います。

例えば "--addremove" オプションの指定値は、 "opts['addremove']" で取得します ("opts[name]" と "opts.get(name)" は混在している模様)。

その一方で、 "--close-branch" オプションの指定値は、 "opts['close-branch']" ではなく、 "opts['close_branch']" で取得します。

これは、 コマンドラインの引数解析後に、 完全名称中のハイフンがアンダースコアに変換されるためです (指定値を格納する局所変数名と統一するためでは無いかと推測)。

@propertycache アノテーション

Mercurial では、 履歴情報を初めとして、 以下のような性質を持つ情報が多々存在します。

このような性質を持つ情報を返す関数に対して、 Mercurial では @propertycache アノテーションを使用します。

※ in mercurial/dirstate.py:

    @propertycache
    def _dirs(self):
        dirs = {}
        for f, s in self._map.iteritems():
            if s[0] != 'r':
                _incdirs(dirs, f)
        return dirs

    def dirs(self):
        return self._dirs
@propertycache アノテーションの使用例

上記例で _dirs() が返却する値は、 作業領域の現親リビジョンのマニフェスト(= 管理対象ファイル一覧) を格納している self._map を全走査する必要があるため、 必要が無ければ処理を実施したくありません。

@propertycache アノテーションが指定された場合、 self._dirs の値は、 以下の要領で『キャッシュ』されます。

  1. dirstate.dirs() が呼ばれる
  2. 初回の self._dirs 参照であれば、 dirstate._dirs() が実行
  3. 実行結果が属性として self._dirs に設定される
  4. 以後の self._dirs 参照は、この設定値が参照

@propertycache アノテーションが指定された関数は、 他のコードからは属性として参照されている点に注意してください。

なお、確定した値をファイルに書き出してキャッシュする機能を持つ @filecache アノテーション (mercurial/scmutil.py で定義) もありますが、適用箇所は少数です (2.1.2 版では bookmarks 関連情報や dirstate 等を保存する3箇所程)。

コード参照時の確認頻度が高い Mercurial 固有のクラス

changectx クラス

changectx は、 履歴記録における各リビジョンの情報 (コンテキスト:context) にアクセスするためのクラスです (mercurial/context.py で定義)。

changectx の派生クラス workingctx は、 仮想的にリビジョン化した作業領域の情報を扱うためのクラスです。

このクラスのオブジェクトから、 以下の様な情報を得ることができます (詳細は "The Mercurial API" の "6. Change contexts" 参照)。

localrepository クラス

localrepository は、 mercurial/repo.py で定義された repository クラスの派生クラスで、 『ローカルリポジトリ』の機能を実現します (mercurial/localrepo.py で定義)。

以下のような機能を始めてとして、 リポジトリに対する参照/改変操作に関して、 このクラスを参照する機会は非常に多いです。

dirstate クラス

dirstate は、 作業領域に対する操作/状態参照を行うためのクラスです (mercurial/dirstate.py で定義)。

一番の肝は、 現在の作業領域中の各ファイルの状態を参照するための status() (および対象ファイルを絞り込むための walk()) と言えます。

localrepositorystatus() の比較対象リビジョンのいずれかに 「作業領域」が指定された場合、 workingctx を経由しつつ、 最終的にこのメソッドが呼び出されます。

実は dirstate の管理情報では、 clean と modified の区別を付けておらず、 状態を確定させるためには status() を呼び出す必要があります。

match クラス

match は、 処理対象ファイルを特定するための、 指定パターンを管理するためのクラスです (mercurial/match.py で定義)。

ファイル名、ディレクトリ名、 --include/-I--exclude/-X オプションによる取捨選択など、 対象ファイルの特定に使用する情報全てが格納されます。

match オブジェクトを参照する変数 "m" がある場合、 変数 "filename" が参照するファイル名が、 ユーザによる指定パターンに合致するか否かは、 "m(filename)" や "m.matchfn(filename)" で判定可能です。

files() が返す値は、 厳密には『合致対象ファイルの一覧』ではなく、 『指定パターンから、特殊文字部分を除去したパスの一覧』です。

例えば、 パターンとして "path/to/files" が指定された場合、 files() が返す値は、 "path/to/files" ですが、 "glob:path/to/files/**.py" が指定された場合は、 "path/to/files" になります。

ui クラス

ui は、 主に以下の2種類の用途に使用されます (mercurial/ui.py で定義)。

『入出力の橋渡し』に関しては、 以下の様な特徴を挙げることができます。

出力のバッファリング:

pushbuffer() 実施から、 popbuffer() 実施までの間の ui 経由の出力を、 全てバッファリング可能。

現状は専ら、 内部処理で呼び出した機能からの出力を、 破棄する用途に使用されることが多い。

バッファ設定は多段化できるので、 バッファ実施処理が入れ子になっても大丈夫。

進捗情報提示機能:

進捗情報表示に対応している機能は、 progress() で進捗状況を都度更新。

progress エクステンションは、 ui の派生クラスで既存の ui を置き換えることで、 進捗状況表示を行っている。

出力種別識別用のラベル機能:

各種出力機能は、 オプションとして label 指定を受け付けており、 この値が該当出力の種別を識別するのに使用されている。

color エクステンションや GUI アプリは、 ui 経由での出力を監視し、 label 種別を判定することで、 出力種別に応じた色付けや、 ダイアログ表示等を行っている。

コード中での利用頻度が高い Mercurial 固有のファイル

コマンド実装を参照する際に、 コード中での利用頻度が高い Mercurial 固有のファイルは以下のものです:

mercurial/util.py:
Mercurial utility functions and platform specfic implementations
mercurial/cmdutil.py:
help for command processing in mercurial
mercurial/scmutil.py:
Mercurial core utility functions
mercurial/hg.py:
repository classes for mercurial

各ファイル毎に説明文はあるものの、 私自身は正直これらの区別に関して、 イマイチピンと来ていません(笑) (独立性の高さで util.py が際立っているのは感じますが…)

但し、各ファイルの import を通じた依存関係に、 相互依存による巡回状態が生じないように注意が払われていますので、 その観点から見ると、 切り分けがスッキリと感じるかもしれません。

Mercurial での使用頻度が高い Python 固有の慣用句/基本文法

以下は、 あくまで『Mercurial のソースを読む上で頻出するもの』 に絞った、Python の簡単な説明です。

また、他のプログラミング言語における、 ある程度の経験を前提としています。

『Mercurial のソースを読むために Python を修得』という、 偏った視点に基いた説明のため、 一般的な Python 界隈で用いられるものと、 用語等が異なる可能性もあります。

詳細に関しては、 別途 Python に関する書籍/公開ドキュメント等を参照してください。

辞書の利用

キーと値の対応関係を管理する場合、 組み込みクラスの『辞書』の機能を使用します。

辞書オブジェクトは、 "key: value" 形式の列挙を波括弧 ("{ }") で囲んだもので初期化します。

    btypes = {'none': 'HG10UN', 'bzip2': 'HG10BZ', 'gzip': 'HG10GZ'}
辞書の初期化

辞書から値を引き当てる場合、 以下の2つの形式が使用されます。 辞書に指定のキーが含まれている場合に、 引き当てた値が返却される点は共通ですが、 キーが含まれていない場合の挙動が異なります。

"dict[key]" 形式:
キーが含まれていない場合、例外が発生します。
"dict.get(key, default=None)" 形式:
キーが含まれていない場合、 default に指定された値が返却されます。 default 指定を省略した場合は、 None が返却されます。

また、組み込みの辞書クラス(or その派生クラス)のオブジェクトではなくても、 当該クラスで "__getitem__(self, key) が定義されている場合、 "dict[key]" 形式での値引き当てが可能です。

    def __getitem__(self, key):
        '''Return the current state of key (a filename) in the dirstate.

        States are:
          n  normal
          m  needs merging
          r  marked for removal
          a  marked for addition
          ?  not tracked
        '''
        return self._map.get(key, ("?",))[0]
dirstate クラスでの __getitem__() 定義

上記の例では、 self._map に含まれないファイルが指定された場合でも、 不正キー指定の例外を発生させずに、 『未知 (unknown) のファイル』に相当する '?' を返却します。

メンバ判定

リストや辞書に、 指定したオブジェクトが含まれているか否かを判定する場合、 以下のような記述を行います (辞書の場合は「キー」として保持されているか否かを判定します)。

    key in object
メンバ判定の記述例

また、リストや辞書オブジェクトではなくても、 当該クラスで "__contains__(self, key) が定義されている場合、 "key in object" 形式でのメンバ判定が可能です。

    def __contains__(self, key):
        return key in self._map
dirstate クラスでの __contains__() 定義

上記の例では、 self._map 中の有無をもって、 dirstate におけるメンバ判定を行っています。

処理の繰り返し

指定された一覧を元に繰り返し処理を行う場合、 for ステートメントを使用します。

    for x in object:
        ※ x を使った処理
object が保持する一覧に対する繰り返しの実施

繰り返し対象に辞書オブジェクトが指定された場合、 辞書のキーが繰り返し対象になります。

Python の for ステートメントでは、 ループが回り切った時に限り、 else 節が実行されます。

    for x in object:
        if x == key:
            break
    else:
        ※ "x == key" が成立しない場合の処理
for ステートメントにおける else 節記述

また、 Python では『リスト内包表記』と呼ばれる以下のような形式で、 配列の初期化が記述できます。

    converted = [convert(x) for x in object]
リスト内包表記

"map(convert, object)" でも同等の事が実現できますが、 リスト内包表記の場合は、 "x * 2" の様な単純な式も記述できる点が優れています。

リスト内包表記では、 if 節を付加することで、 特定の条件に合致するものだけを抽出することが可能です。

    converted = [convert(x) for x in object if x != key]
リスト内包表記での if 節記述

繰り返し対象

リストや辞書オブジェクトではなくても、 当該クラスで "__iter__(self)" が定義されている場合、 繰り返し対象として指定することが可能です。

    def __iter__(self):
        for x in sorted(self._map):
            yield x
dirstate クラスでの __iter__() 定義

上記の例では、 self._map のキーを、 辞書昇順で繰り返す事を保証しています。

上記の例で使用されている "yield" は、 Python によって特別な解釈が行われ、 以下の様な特殊な制御遷移を生じます (いわゆる『継続』 (continuation) 的な実行)。

  1. yield を実施する関数が呼び出されると、 反復実施オブジェクト(generator)が返却
  2. この時点では、関数自体は実行されない
  3. 反復実施オブジェクト自体に対して "next()" 呼び出しが実施
  4. 最初に yield された値を返却
  5. 返却された値で初回の反復処理を実施
  6. 反復実施オブジェクト自体に対して再度 "next()" 呼び出しが実施
  7. 最初に yield された位置から処理を再開
  8. 次に yield された値を返却
  9. 返却された値で反復処理を実施
  10. 以下、反復実施オブジェクトの "next()" 呼び出しが例外終了するまで繰り返し

単に "yield" する関数も、 繰り返し対象として指定可能です。

def pluszero(targets):
    for x in targets:
        yield x
    yield 0

for x in pluszero([1, 2, 3, 4]):
    ※ 1, 2, 3, 4, 0 に対して実施する処理

処理対象が膨大な場合、 あらかじめ反復対象をリスト化する場合と比較して、 yield を用いたジェネレータ形式にすることで、 以下の様な利点があります。

関数引数

def func(a, b, *rest, **kwargs):

上記のように定義された関数 func() での引数受理は、 以下のように行われます。

func(1, 2):
a, b はそれぞれ 1, 2、 rest は空のリストオブジェクト、 kwargs は空の辞書オブジェクトを参照します。
func(1, 2, 3, 4):
a, b はそれぞれ 1, 2、 rest はリストオブジェクト "[3, 4]"、 kwargs は空の辞書オブジェクトを参照します。
func(1, 2, foo=3, bar=4):
a, b はそれぞれ 1, 2、 rest は空のリストオブジェクト、 kwargs は辞書オブジェクト "{'foo': 3, 'bar': 4}" を参照します。
func(1, 2, 3, 4, foo=5, bar=6):
a, b はそれぞれ 1, 2、 rest はリストオブジェクト "[3, 4]"、 kwargs は辞書オブジェクト "{'foo': 5, 'bar': 6}" を参照します。
func(1):
引数不正で例外が発生します。