Hatena::Groupcoders

ラシウラ出張所 このページをアンテナに追加 RSSフィード

2007/04/20

[][] tracのPluginの書き方  tracのPluginの書き方 - ラシウラ出張所 を含むブックマーク はてなブックマーク -  tracのPluginの書き方 - ラシウラ出張所  tracのPluginの書き方 - ラシウラ出張所 のブックマークコメント

tracは0.9以降、pluginシステムを持つようになりました。pluginをつくり組み込むことで、機能拡張だけでなく、システムの振る舞いを変更するようなことまでできるようになります。

tracの外部リンクを変えるようにしたい機会があったので、それができるような簡単なpluginを作りました。tracサイトを作るとき適用したpluginがあるコミュニティTrac Hacksに登録しておきました:

このExtLLinkRewriterPluginを例にします。

setuptoolsによるeggパッケージ形式

tracではsetuptoolsを使ったeggパッケージでpluginを管理します。

setuptoolsは、.NETのアセンブリ、もしくはJavajarファイルバージョン管理をつけたもの、らと同じ位置づけにあるツールライブラリです。パッケージ作成は、Python標準のdistutilsと同様、setup.pyを使うようになっています。実際distutils用のsetup.pyのほとんどは、importするモジュールをsetuptoolsに変えるだけで使えるようになります。

setuptools用のsetup.pyはbdist_eggというコマンドを提供しeggパッケージを作成できます。作成されるeggパッケージは、PKZIP形式で、中にはメタデータ情報ファイルと対象のPythonコードが入っています(このあたりはjarに似ています)。これはunzipで確認できます。

trac pluginとextension point

tracはextension pointで機能のフックを管理できるようになっており、pluginは主にextension pointに対して機能を提供していくことになります。

たとえば、ExtLinkRewriterPluginでは、trac.wiki.api.IWikiSyntaxProviderを提供しています。

ExtLinkRewriterPluginの構成

READMEやサンプルredirectorを除くと、以下の構成です:

  • setup.py
  • ExtLinkRewriter/__init__.py
  • ExtLinkRewriter/provider.py

このうち__init__.pyはExtLinkRewriterモジュール用で、中は空です。

ExtLinkRewriter/provider.pyにはExtLinkRewriter.provider.ExtLinkRewriterProviderクラスがあり、それが前述のIWikiSyntaxProvider extension pointにむけた機能を実装しています。

setup.pyにはtrac pluginのためのメタデータを設定しています。

setup.py

from setuptools import setup

setup(
    name="ExtLinkRewriter",
    version="0.4",
    packages=['ExtLinkRewriter'],
    entry_points = {'trac.plugins':
                    ['ExtLinkRewriter.provider = ExtLinkRewriter.provider',],},
    license = "BSD")

ほぼdistutilsのsetup.pyと同じです

[trac.plugins]
ExtLinkRewriter.provider = ExtLinkRewriter.provider

trac.pluginカテゴリに、左辺はプラグインID、右辺は後述するComponentのサブクラスを取り出せるモジュール名を書くようです。

ExtLinkRewriter/provider.py

このモジュールでは、Extension point機能を提供するクラス記述します。このクラスtrac.core.Componentのサブクラスである必要があります。


from trac.core import *
from trac.wiki import IWikiSyntaxProvider
from trac.util.html import html


class ExtLinkRewriterProvider(Component):
    """Rewrite External Link URL
    """
    implements(IWikiSyntaxProvider)

    _rewrite_format = "http://del.icio.us/url?url=%s"
    _rewrite_namespaces = "http,https,ftp"
    _rewrite_target = ""

    def get_wiki_syntax(self):
        """IWikiSyntaxProvider#get_wiki_syntax
        """
        return []

    def get_link_resolvers(self):
        """IWikiSyntaxProvider#get_link_resolvers
        """
        self._load_config()
        return [(ns.strip(), self._link_formatter)
                for ns in self._rewrite_namespaces.split(",")]

    def _link_formatter(self, formatter, ns, target, label):
        try:
            newtarget = self._rewrite_format % (ns + ":" + target,)
        except:
            newtarget = ns + ":" + target
            msg = "ExtLinkRewriter Plugin format error: %s"
            msg %= (self._rewrite_format,)
            self.log.error(msg)
            pass
        return self._make_ext_link(formatter, newtarget, label,
                                   self._rewrite_target)

    def _make_ext_link(self, formatter, url, text, target=""):
        """Formatter._make_ext_link with target attr
        """
        if not url.startswith(formatter._local):
            return html.A(html.SPAN(text, class_="icon"),
                          class_="ext-link", href=url, target=target or None)
        else:
            return html.A(text, href=url, target=target or None)
        pass

    def _load_config(self):
        self._update_config("format")
        self._update_config("namespaces")
        self._update_config("target")
        pass

    def _update_config(self, key):
        attrname = "_rewrite_" + key
        oldval = getattr(self, attrname)
        newval = self.config.get("extlinkrewriter", key, oldval)
        setattr(self, attrname, newval)
        pass
    pass
implements(IWikiSyntaxProvider)

implements()関数引数はExtension pointのクラスを列挙します。それによって、システムが対応するextension pointを使うときに、このコンポーネントを使ってくれるようになります。

IWikiSyntaxProviderは以下のメソッドを提供しなくてはいけません

  • get_wiki_syntax(): 今回は何もしない
  • get_link_resolvers(): 今回のメイン機能

以下のソースにはそれらメソッドの説明が書いてあります(IWikiSyntaxProviderは96行目くらい)

get_link_resolvers

このメソッドの仕様は、

    def get_link_resolvers():
         """Return an iterable over (namespace, formatter) tuples.
 
         Each formatter should be a function of the form
         fmt(formatter, ns, target, label), and should
         return some HTML fragment.
         The `label` is already HTML escaped, whereas the `target` is not.
         """

返すのは[(namespace,formatter),...](もしくはgenerator)であり、formatterは、formatter(formatter, namespace, target, label)という引数関数になります。

    def get_link_resolvers(self):
        """IWikiSyntaxProvider#get_link_resolvers
        """
        self._load_config()
        return [(ns.strip(), self._link_formatter)
                for ns in self._rewrite_namespaces.split(",")]

で、最初のself._load_config()は、trac.iniのデータを取り込む。うしろは、pluginで処理するnamespace(http,https,ftpなど)とフォーマッターself._rewrite_namespaceのペアのタプルを返しています。

_link_formatter, _make_ext_link

これはプライベートメソッドです。

このフォーマッタはリンクに関する情報を受け取り、処理した結果であるHTML文字列を返すメソッドです。

    def _link_formatter(self, formatter, ns, target, label):
        try:
            newtarget = self._rewrite_format % (ns + ":" + target,)
        except:
            newtarget = ns + ":" + target
            msg = "ExtLinkRewriter Plugin format error: %s"
            msg %= (self._rewrite_format,)
            self.log.error(msg)
            pass
        return self._make_ext_link(formatter, newtarget, label,
                                   self._rewrite_target)

    def _make_ext_link(self, formatter, url, text, target=""):
        """Formatter._make_ext_link with target attr
        """
        if not url.startswith(formatter._local):
            return html.A(html.SPAN(text, class_="icon"),
                          class_="ext-link", href=url, target=target or None)
        else:
            return html.A(text, href=url, target=target or None)
        pass

この中でURL書き換えと、リンク部分だけのHTML生成を行っています。

HTMLレンダリング部分は、Trac標準のFormatterを参考にしています:

_load_config, _update_config

これもプライベートメソッドです。

trac.iniの情報を読み込んで、メンバーフィールドの上書きしていっています。

self.configはComponentのフィールドで、getメソッド等で、trac.iniから文字列やその他形式でデータを取り出すことができます。

たとえば、

self.config.get("extlinkrewriter", "format", "")

trac.iniの

[extlinkrewriter]
format = ...

の右辺を文字列として(strip()された状態で)取り出します(項目がない場合は第三引数の値が渡されます)。

ちなみにiniの右辺をダブルクオートでくくったりすると、ダブルクオート入りのstringが入りますので注意します。

パッケージ化とインストール

ソースが出来上がったらsetup.pyを使ってeggパッケージを作成します。

python setup.py bdist_egg

すると、dist/ExtLinkRewriter-0.4-py2.5.eggのような形式でeggパッケージが作られます。

tracで使うにはこのeggファイルをtrachomeのpluginsディレクトリコピーします。

プラグイン有効化

実際にプラグインを使うにはtrac.iniのcomponentsカテゴリモジュールをenableにするような記述をする必要があります

[components]
ExtLinkRewriter.* = enabled

つぎにtracアクセスしたら、pluginが有効になっているはずです(mod_pythonだと再起動が必要かも)。

まとめ

ExtLinkRewriterは単純なプラグインですが、plugin開発で何をすればいいかを一通りたどっています。

あとExtLinkRewriterの詳細な仕様は、以下のページに書いてあります。

リソース