ひよっこ。

I want to…

tracのソースを読む(その1) – Standalone.py –

Posted by hikaruworld : 2009 9月 26

前回のエントリにある通りとりあえず、デバッグ環境は構築出来たので、
どこから読むか迷ったんですが、起動スクリプトからソースを読んでいきたいと思います。
# 全体的なtracのアーキテクチャをまず理解するか、テストソースの方がいいのか迷って…

メモ代わりに書いていますので、間違っている部分など突っ込んでいただけると感謝です。

という訳で、まずはtracの内蔵サーバを利用して起動させる際に利用されるStandalone.pyから見ていこうと思います。
今回読んだソースはL296-L298のたった3行のみです(涙)
# のわりには、めちゃ長文なんですが(汗

ここで起動スクリプトがstandalone.pyであると知っているのは、公式のドキュメントにそう書いてあるからです。
あ、言い忘れましたがTracのバージョンは0.11.5になります。

さて、起動部分からです。
standalone.pyは基本的にはクラス(class)と関数(def)のみで、実際の実行は最後尾の以下のみになります。

if __name__ == '__main__':
	pkg_resources.require('Trac==%s' % VERSION)
	main()

速攻まずここで躓きました。
よくわからなかったのは以下の通り(ほとんどわかっていないんです)。

  1. __name__とは何か
  2. pkg_resourcesとは何か
  3. pkg_resources.requireとは何か

1. __name__とは何か

pythonはスクリプトとして実行した場合(コンソール上からpython standalone.pyとかやる)は__name____main__という文字列が
割り当てられるということは知っていましたが、実際に細かい挙動がわからなかったので復習しておくことにしました。
へたれプログラマの日々というサイトがわかりやすくて参考になりました。感謝。

といわけで上記サイトを参考に早速実験してみます。
カレントディレクトリにHoge.pyというファイルを作成します。中身は以下の通りです。

#!/usr/bin/python

print "hoge";
print __name__;

if __name__ == '__main__':
	print 'oooo....'

これをコンソール上から実行してみます。

python Hoge.py

hoge
__main__
oooooo…..

書いてある通り、__name__には__main__という文字列が格納されているようです。
__name__に設定されている文字列も正しかったためif文内部の実行も行われています。

では今度は、インタプリタ上で実行してみます

import Hoge

hoge
Hoge

ふむふむ。importした際のクラス名が__name__に格納されていますね。
そして当然if文内部は実行されていません。

では、今度はエイリアスをつけてみます。

import Hoge as piyo

こちらでもエイリアスが設定される訳ではなく、importの実体が設定されるため変わらないようです。

hoge
Hoge

やはり参考にしたサイトでも示されていた通りの挙動を示すようですね。

今回参照しているStandalone.pyはスクリプトとして実行された際に特別な挙動で動くように設定されていましたが、
その他の方法としては、これらの処理はdoctestを実行する際に便利に利用できるようです。

2. pkg_resourcesとは何か

次に気になったのは、以下です。

pkg_resources.require('Trac==%s' % VERSION)

2-1. ‘Trac==%s’ % VERSIONとは何か

とりあえず細かく分解して確認していきたいと思います。
%sをVERSIONで置換している事はわかりますが、VERSIONの取得がどうなっているか気になりました。
VERSION自体はimport時にエイリアスとして設定されており、実際はtracパッケージのパッケージ変数を参照しています(import文を参照)
Pythonはas演算子を使ってimportした関数やクラスを任意の文字列に置き換える事が出来るそうです。

from trac import __version__ as VERSION

from tracとfromにパッケージを指定することで、__init__.py上に設定されている変数が参照可能になります。

2-1-1. __init__.pyとは

__init__.pyはpythonのサブディレクトリを示す際に必要となるファイルです。
pythonランタイムによって対象のディレクトリがサブパッケージと認識させるためにはこのファイルを必ず置いておく必要があるそうです。
この名前のファイルが置いてあること」が重要で、ファイルの中身は空でもかまいません。

ただ、今回のように__init__.pyに変数が設定されているような場合、__init__.pyで定義されているファイルを読み込みたい場合は、
fromでパッケージを指定して、「package名.initに存在する変数」で参照することが可能になります。
というわけで、__init__.pyの中身を参照してみます。

__version__ = __import__('pkg_resources').get_distribution('Trac').version
2-1-1-1. __import__とは

__import__はPythonによって提供されている組み込み関数です。
余談ですが、Python上で「__」(アンダースコア2つ)によって定義される関数は特別な意味を持つそうです。

__import__はimportによって任意のオブジェクトをimportした際に呼び出される組み込み関数になります。
そのため、以下お処理は同じ物を表していいます。

# __import__とimportは同じ処理をしている
__import__('pkg_resources')
import pkg_resources
2-1-1-2. pkg_resources.get_distributionとは

get_distributionはsetuptoolsによって提供される関数で、get_distributionの引数で指定したモジュール名をキーとしてDistributionを返します。
これはTracがPythonモジュールの配布の方式で配布されているため、Distributionからバージョン情報が取得できるようになるためのようです。

ここでは単純にversion情報の取得のみ行っていますが、以下のような情報も参照可能なようです。
詳しくはDistributionのインスタンスを引数にhelp()を実行してみると色々と出てくると思います。

>>> __import__('pkg_resources').get_distribution('Trac').key
>>> __import__('pkg_resources').get_distribution('Trac').extras

なお、これらのDistributionとしての設定はインストール時のsetup.pyのsetup()に記述されています。
setup()で引数に取れるデフォルトのプロパティは、distutils.core — Distutils のコア機能を、setuptoolsによって拡張されるプロパティはNew and Changed setup() Keywords(邦訳)を参照してください。
どうせなのでpythonのsetup.pyを確認してみます(maintainerなどが書いてあるためtrac0.11.1-ja1を表示しています)。

from setuptools import setup, find_packages

setup(
	# パッケージの名前
	name = 'Trac',
	# パッケージのバージョン番号
	version = '0.11.1.ja1',
	# 1行で書いたパッケージ解説
	description = 'Integrated SCM, wiki, issue tracker and project environment',
	# パッケージの長い解説
	long_description = """
Trac is a minimalistic web-based software project management and bug/issue
tracking system. It provides an interface to the Subversion revision control
systems, an integrated wiki, flexible issue tracking and convenient report
facilities.

Japanese translated edition.
""",
	# パッケージ作者の名前
	author = 'Edgewall Software',
	# パッケージ作者のemailアドレス
	author_email = 'info@edgewall.com',
	# 現在のメンテナの名前(パッケージ作者と異なる場合)
	maintainer = 'InterAct',
	# 現在のメンテナのemailアドレス(パッケージ作者と異なる場合)
	maintainer_email = 'trac-ja@i-act.co.jp',
	# パッケージのライセンス
	license = 'BSD',
	# パッケージのURL(ホームページ)
	url = 'http://trac.edgewall.org/',
	# パッケージダウンロード用URL
	download_url = 'http://trac.edgewall.org/wiki/TracDownload',
	# パッケージのカテゴリのリスト
	# 詳細は右記 http://pypi.python.org/pypi?:action=list_classifiers
	classifiers = [
		'Environment :: Web Environment',
		'Framework :: Trac',
		'Intended Audience :: Developers',
		'License :: OSI Approved :: BSD License',
		'Operating System :: OS Independent',
		'Programming Language :: Python',
		'Topic :: Software Development :: Bug Tracking',
		'Topic :: Software Development :: Version Control',
	],
	# distutilsが操作するPythonパッケージのリスト
	packages = find_packages(exclude=['*.tests']),
	# パッケージ名と、ファイル名のパターンのリストの対応付け
	package_data = {
		'': ['templates/*'],
		'trac': ['htdocs/*.*', 'htdocs/README', 'htdocs/js/*', 'htdocs/css/*',
				 'htdocs/guide/*'],
		'trac.wiki': ['default-pages/*'],
		'trac.ticket': ['workflows/*.ini'],
	},
	# この引数を指定すると、例えばsetup.py testを使って、指定されたテストスイートをtestコマンドで実行することが可能
	test_suite = 'trac.test.suite',
	# プロジェクトがzipファイルから安全にインストールされ、実行されるか指定します???
	zip_safe = False,

	# この配布物がインストールされるときに、インストールされていなければならない他の配布物を指定
	install_requires = [
		'setuptools>=0.6b1',
		'Genshi>=0.5'
	],
	# 拡張機能を使うためにインストールされていなければならない他の配布物を示す文字列か文字列のリストを対応付ける辞書
	extras_require = {
		'Pygments': ['Pygments>=0.6'],
		'reST': ['docutils>=0.3'],
		'SilverCity': ['SilverCity>=0.9.4'],
		'Textile': ['textile>=2.0'],
	},
	# エントリーポイントのグループ名と、エントリーポイントを定義する文字列または文字列のリストを対応付ける辞書です。
	# エントリーポイントは、プロジェクトが提供するサービスまたはプラグインを動的に探す手助けをします。
	entry_points = """
		[console_scripts]
		trac-admin = trac.admin.console:run
		tracd = trac.web.standalone:main

		[trac.plugins]
		trac.about = trac.about
		trac.admin.console = trac.admin.console
		trac.admin.web_ui = trac.admin.web_ui
		trac.attachment = trac.attachment
		trac.db.mysql = trac.db.mysql_backend
		trac.db.postgres = trac.db.postgres_backend
		trac.db.sqlite = trac.db.sqlite_backend
		trac.mimeview.enscript = trac.mimeview.enscript
		trac.mimeview.patch = trac.mimeview.patch
		trac.mimeview.php = trac.mimeview.php
		trac.mimeview.pygments = trac.mimeview.pygments[Pygments]
		trac.mimeview.rst = trac.mimeview.rst[reST]
		trac.mimeview.silvercity = trac.mimeview.silvercity[SilverCity]
		trac.mimeview.txtl = trac.mimeview.txtl[Textile]
		trac.prefs = trac.prefs.web_ui
		trac.search = trac.search.web_ui
		trac.ticket.admin = trac.ticket.admin
		trac.ticket.query = trac.ticket.query
		trac.ticket.report = trac.ticket.report
		trac.ticket.roadmap = trac.ticket.roadmap
		trac.ticket.web_ui = trac.ticket.web_ui
		trac.timeline = trac.timeline.web_ui
		trac.versioncontrol.svn_fs = trac.versioncontrol.svn_fs
		trac.versioncontrol.web_ui = trac.versioncontrol.web_ui
		trac.web.auth = trac.web.auth
		trac.wiki.macros = trac.wiki.macros
		trac.wiki.web_ui = trac.wiki.web_ui
	""",
)

これらの設定はEgg化した際に、EGG-INFO内に格納されるようですが、
今はTracのソースを読んでいるのでsetuptoolsについては深追いしません。
出来れば、この辺りは一度きちんとドキュメントをしっかり読んでおきたいところです。

今回はget_distributionのメソッドの内部までは踏み入りませんので、ソースのコメントだけ見てみます。
引数で渡された情報を元に合致するDistributionオブジェクトを返しているようですね(ソースは変数は省略)。
DistributionObjectを探

"""Return a current distribution object for a Requirement or string"""

3. pkg_resources.requireとは何か

pkg_resourcesはファイルの冒頭で以下の様にimportされています。

import pkg_resources

pkg_resourcesはsetuptoolsで定義されたモジュールで、setuptoolsはPythonのdistutilsの多くの機能が拡張されたものだそうです。
distutilsとはPythonモジュールの配布の形式で、俗に言う

python setup.py insatll 

でのインストールが可能になるための方法になります。

pkg_resourcesに関してはなんとなくわかったような気がするので(というか深追いするときりがない)
その関数であるrequireの実装がどうなっているかだけ覗いてみました。

	def require(self, *requirements):
		"""Ensure that distributions matching `requirements` are activated

		`requirements` must be a string or a (possibly-nested) sequence
		thereof, specifying the distributions and versions required.  The
		return value is a sequence of the distributions that needed to be
		activated to fulfill the requirements; all relevant distributions are
		included, even if they were already activated in this working set.
		"""

		needed = self.resolve(parse_requirements(requirements))

		for dist in needed:
			self.add(dist)

		return needed

コメントで丁寧に説明されていますが、引数で指定したrequirementsに関連するモジュールを取得しているようですね。
self.resolveで必要なDistributionの配列を返しています。

ちょっと面白かったのは、requireの引数にはバージョン番号の指定が必須になっているところです。
バージョン管理が意識されているなぁと感じます。

あと、ソースの内部を覗いているとresolve内部でリストに対して以下のような処理を行っている部分がありました。

hoge[::-1]

???だったんですが、つぶやいてみると以下のように教えてくれた方がいて納得しました(@miau_jp, @nishio に感謝!)

@hikaruworld pythonでlist[::1]とかlist[::-1]っていう書き方を見るんだけど、これはどう解釈するんだろう .
@miaumiau_jp @hikaruworld スライスの step 指定だとは思いますけど、1 なら省略可能なような・・・。
@nishio @hikaruworld 3番目はstepなので’abc'[::-1]==’cba’で’abcdefg'[::2]==’aceg’

つまり…

# [::x]のxは実行するステップ単位を表す
# [::]と[::1]は同じ。
>>> hoge = 'abcdefgh'
>>> hoge[::1]
'abcdefgh'
# 逆順
>>> hoge[::1]
'hgfedcab'
# 2つ単位で
>>> hoge[::2]
'aceg'

ということらしいです。この書き方は新鮮で面白いです。
テキストスライスに関してはこの辺りが面白かったです。

今回ははじめの一歩という事で。
これから起動部分のmain()関数に入っていきたいと思います。
その際にtracdからどうやってstandalone.pyが実行されるのかも確認していきたいと思います。

残件

以下は、今回調べきれなかった残件になります。

  • setuptoolsによる挙動の詳細(インストール周りなど)
  • doctestとdocstringの使い方

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

 
%d人のブロガーが「いいね」をつけました。