[Python] setuptoolsを使用すると、スクリプト本体はパッケージのディレクトリ/EGG-INFO/scriptsにインストールされる。
はじめに
この記事の内容は、Ubuntu Linux 6.10, Python 2.4.4c1で確認しました。
結論
以下のように、setup.pyでsetuptoolsを用い、フロントエンドとなるスクリプトpydumpfsをsetup関数のscripts引数に指定したとします。
#! python # -*- coding: utf-8 -*- import ez_setup ez_setup.use_setuptools() from setuptools import setup, find_packages setup(name="pydumpfs", version="0.1", packages=find_packages("src"), package_dir={"": "src"}, test_suite="pydumpfs.tests", scripts=["pydumpfs"])
このとき、
$ sudo python setup.py install
を実行すると、/usr/bin/にpydumpfsというスクリプトがインストールされますが、これはsetuptoolsが作成したラッパーで、実際のスクリプトは/usr/lib/python2.4/site-packages/pydumpfs-0.1-py2.4.egg/EGG-INFO/scriptsにインストールされます。
詳細
/usr/bin/pydumpfsは、以下のようになっています。
#!/usr/bin/python # EASY-INSTALL-SCRIPT: 'pydumpfs==0.1','pydumpfs' __requires__ = 'pydumpfs==0.1' import pkg_resources pkg_resources.run_script('pydumpfs==0.1', 'pydumpfs')
/usr/lib/python2.4/site-packages/setuptools-0.6c6-py2.4.egg/pkg_resources.pyを読むと、run_scriptは以下のようになってます。
def run_script(dist_spec, script_name): """Locate distribution `dist_spec` and run its `script_name` script""" ns = sys._getframe(1).f_globals name = ns['__name__'] ns.clear() ns['__name__'] = name require(dist_spec)[0].run_script(script_name, ns)
require関数は、パッケージの名前とバージョン番号から、「何か」(このコードではそれが何かわかりません)をリストとして返す関数のようです。同じファイルで、以下のように定義されています。
working_set = WorkingSet() (中略) require = working_set.require
WorkingSetクラスの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
この関数の戻り値となるneededが何か、まだ分かりません。次にresolveメソッド(以下)を読んでみますが...。
def resolve(self, requirements, env=None, installer=None): """List all distributions needed to (recursively) meet `requirements` `requirements` must be a sequence of ``Requirement`` objects. `env`, if supplied, should be an ``Environment`` instance. If not supplied, it defaults to all distributions available within any entry or distribution in the working set. `installer`, if supplied, will be invoked with each requirement that cannot be met by an already-installed distribution; it should return a ``Distribution`` or ``None``. """ requirements = list(requirements)[::-1] # set up the stack processed = {} # set of processed requirements best = {} # key -> dist to_activate = [] while requirements: req = requirements.pop(0) # process dependencies breadth-first if req in processed: # Ignore cyclic or redundant dependencies continue dist = best.get(req.key) if dist is None: # Find the best distribution and add it to the map dist = self.by_key.get(req.key) if dist is None: if env is None: env = Environment(self.entries) dist = best[req.key] = env.best_match(req, self, installer) if dist is None: raise DistributionNotFound(req) # XXX put more info here to_activate.append(dist) if dist not in req: # Oops, the "best" so far conflicts with a dependency raise VersionConflict(dist,req) # XXX put more info here requirements.extend(dist.requires(req.extras)[::-1]) processed[req] = True return to_activate # return list of distros to activate
これを読んでみても、何をしているのか分かりません。どうやら、さらにEnvironmentクラスのbest_matchメソッドを読まないといけないようですが、これ以上は深追いしないことにします。
さて、require関数が返した「何か」のrun_scriptメソッドを呼び出すことで、実際のスクリプトを実行しているようですが、run_scriptメソッドを持っているクラスは、pkg_resources.pyの中に3つあります。
ひとつ目はIMetadataProviderクラスです。このクラスのrun_scriptメソッド(以下)は、実装がないので、これが呼び出されることはないと思われます。
class IMetadataProvider: (中略) def run_script(script_name, namespace): """Execute the named script in the supplied namespace dictionary"""
ふたつ目はWorkingSetクラスです(以下)。
class WorkingSet(object): (中略) def run_script(self, requires, script_name): """Locate distribution for `requires` and run `script_name` script""" ns = sys._getframe(1).f_globals name = ns['__name__'] ns.clear() ns['__name__'] = name self.require(requires)[0].run_script(script_name, ns)
三つめはNullProviderクラスです。
class NullProvider: (中略) def run_script(self,script_name,namespace): script = 'scripts/'+script_name if not self.has_metadata(script): raise ResolutionError("No script named %r" % script_name) script_text = self.get_metadata(script).replace('\r\n','\n') script_text = script_text.replace('\r','\n') script_filename = self._fn(self.egg_info,script) namespace['__file__'] = script_filename if os.path.exists(script_filename): execfile(script_filename, namespace, namespace) else: from linecache import cache cache[script_filename] = ( len(script_text), 0, script_text.split('\n'), script_filename ) script_code = compile(script_text,script_filename,'exec') exec script_code in namespace, namespace
どうもコードの内容からすると、最終的に呼び出されるのはNullProviderクラスのrun_scriptメソッドではないかという気がします。
どう動いているのかはっきりさせることはできませんでしたが、setuptoolsが/usr/binにインストールするスクリプトは「プロキシ」であって、実際のスクリプトはパッケージの中にインストールされるようです。