python setuptools之pkg_resources模块

[email protected] 's Blog

[email protected] 's Blog

python setuptools之pkg_resources模块python 2016-07-10 23:03:18 227 0 0 gimefi python

pkg_resources模块的功能

如果我们希望写一个可以供其他程序动态调用的package,那么我们只需要在这个package的setup.py文件中添加entry_points项,作为其他程序调用包内函数的接口,在编写完entry_points,并通过python setup.py install指令安装好该包之后,我们就能够在其他程序中通过import pkg_resources模块去动态调用其他包的函数。通过pkg_resources.iter_entry_points(group_name)函数可以找到当前环境中组名为group_name的所有entry_points,然后即可通过entry_points去调用对应的函数。

entry_points

Entry points可以理解为一种接口,定义在自己发布的包内,便于来让其他的程序动态地调用自己的包内的功能。entry_points定义的格式为: entry_points ={ ‘group_name’ : [ ‘interface_name:package.subpackage:function ’ ] } 也就是说,如果我们发布一个新的py包,然后我们可以在发布的时候在setup.py的中定义一些entry_points,然后这些entry_points会被setuptools模块保存到本地(这里应该是这样?),并且可以通过pkg_resources包获取这些entry_points,并加载entry_points接口所对应的功能(函数)。这样我们就能在新的程序中,通过import pkg_resources,来动态调用其他包的功能。

其中,通过pkg_resources包查找entry_points的函数就是iter_entry_points()。 虽然也可以直接import需要使用模块,但是,如果要使用很多package的同一类接口,那么一个个去import每个package就没有直接调用该group的所有entry_points方便。

pkg_resources模块的初始化

import pkg_resources后,模块会初始化一个WorkingSet的类,用来保存当前python环境中用户可以使用的模块的sys.path地址,模块名称,以及模块版本。 其中,entries[]列表,保存了sys.path返回的所有地址;entry_keys{}字典,保存了以前面的enties项为key,模块名称为value的数据;by_key{}字典则保存的是,以前面entry_key的value(模块的名称)为key,模块的发布版本(数据类型为模块中的Distribution类)为value的数据。 例如: entries=[‘c:\python27\lib\site-packages\abc-1-py2.7.egg’,…] entry_keys={‘c:\python27\lib\site-packages\abc-1-py2.7.egg’:[‘abc’],…} by_key={‘abc’: abc 1 (c:\python27\lib\site-packages\abc-1-py2.7.egg), …}

分析代码

1.  `import pkg_resources`
2.  `import time`
3.  `def main():`
4.  `whileTrue:`
5.  `for entry_point in pkg_resources.iter_entry_points('my_group'):`
6.  ` entry_point.load()`

以这段代码为示例代码进行分析

entrypoint的读取

iter_entry_points()函数

1.  `def iter_entry_points(self,group, name=None):`
2.  `"""Yield entry point objects from `group` matching `name``
3.  
4.  ` If ``name` is None, yields all entry points in `group` from all`
5.  ` distributions in the working set, otherwise only ones matching`
6.  ` both `group` and `name` are yielded (in distribution order).`
7.  ` """`
8.  `for dist inself:`
9.  ` entries = dist.get_entry_map(group)`
10.  `if name isNone:`
11.  `for ep in entries.values():`
12.  `yield ep`
13.  `elif name in entries:`
14.  `yield entries[name]`

for dist in self

1.  `def __iter__(self):`
2.  `"""Yield distributions for non-duplicate projects in the working set`
3.  
4.  ` The yield order is the order in which the items' path entries were`
5.  ` added to the working set.`
6.  ` """`
7.  ` seen ={}`
8.  `for item inself.entries:`
9.  `if item notinself.entry_keys:`
10.  `# workaround a cache issue`
11.  `continue`
12.  
13.  `for key inself.entry_keys[item]:`
14.  `if key notin seen:`
15.  ` seen[key]=1`
16.  `yieldself.by_key[key]`

我们可以看到,迭代器返回的是working_set的by_key的值,也就是一个个的模块发布版本类实例(Distribution类实例)

dist.get_entry_map(group)

1.  `def get_entry_map(self,group=None):`
2.  `"""Return the entry point map for `group`, or the full entry map"""`
3.  `try:`
4.  ` ep_map =self._ep_map`
5.  `exceptAttributeError:`
6.  ` ep_map =self._ep_map =EntryPoint.parse_map(`
7.  `self._get_metadata('entry_points.txt'),self`
8.  `)`
9.  `ifgroupisnotNone:`
10.  `return ep_map.get(group,{})`
11.  `return ep_map`

这里我们可以看到是通过_get_metadata(‘entry_points.txt’)去读取entry_point的。

_get_metadata()函数

1.  `def _get_metadata(self, name):`
2.  `ifself.has_metadata(name):`
3.  `for line inself.get_metadata_lines(name):`
4.  `yield line`

这里有个问题是函数中的hasmetadata()和后面的get_metadata_lines都不是Distribution类的成员函数,而是NullProvider的成员函数。(通过在原函数这个位置设置pdb断点,发现在self.has_metadata(name)调用的时候,先执行的是getattr(self, attr)函数,通过查找资料(参考链接enter link description here)知道当一般位置找不到attribute的时候,会调用getattr,返回一个值或AttributeError异常。 **_getattr()**

1.  `def __getattr__(self, attr):`
2.  `"""Delegate all unrecognized public attributes to .metadata provider"""`
3.  `if attr.startswith('_'):`
4.  `raiseAttributeError(attr)`
5.  `return getattr(self._provider, attr)`

可以看出通过getattr()函数后,_get_metadata()函数中的self.has_metadata(name)实际应该写作,self._provider.has_metadata(name)。后面的self.get_metadata_lines(name)也是同理。 所以说,从这里开始之后的函数都是self._provider去调用,而self._provider是一个基类为NullProvider的类实例。而这个_provider有一个module_path变量,被初始化为sys.path,这个_provider还有一个egg_info变量。

get_metadata_lines()

1.  `def get_metadata_lines(self, name):`
2.  `return yield_lines(self.get_metadata(name))`

这里继续调用get_metadata()。 yield_lines()

1.  `def yield_lines(strs):`
2.  `"""Yield non-empty/non-comment lines of a string or sequence"""`
3.  `if isinstance(strs, six.string_types):`
4.  `for s in strs.splitlines():`
5.  ` s = s.strip()`
6.  `# skip blank lines/comments`
7.  `if s andnot s.startswith('#'):`
8.  `yield s`
9.  `else:`
10.  `for ss in strs:`
11.  `for s in yield_lines(ss):`
12.  `yield s`

get_metadata()

1.  `def get_metadata(self, name):`
2.  `ifnotself.egg_info:`
3.  `return""`
4.  `returnself._get(self._fn(self.egg_info, name))`

这里的egg_info是由module_path和‘EGG-INFO’字符串合并形成的, path = self.module_path, self.egg_info = os.path.join(path,’EGG-INFO’) 这里的name也就是之前传递的’entry_points.txt’字符串。 _fn()

1.  `def _fn(self,base, resource_name):`
2.  `if resource_name:`
3.  `return os.path.join(base,*resource_name.split('/'))`
4.  `returnbase`

_fn()函数将egg_info的地址和’entry_points.txt’合并成新的地址,也就是我们后面读取entry_points的地址。(sys.path + ‘EGG-INFO’ + ‘entry_points’ )

_get() 基类NullProvider定义:

1.  `def _get(self, path):`
2.  `if hasattr(self.loader,'get_data'):`
3.  `returnself.loader.get_data(path)`
4.  `raiseNotImplementedError(`
5.  `"Can't perform this operation for loaders without 'get_data()'"`
6.  `)`

子类DefaultProvider定义:

1.  `def _get(self, path):`
2.  `with open(path,'rb')as stream:`
3.  `return stream.read()`

这时候的地址path就是_fn()返回的地址, 例如: C:\Python27\lib\site-packages\abc-1-py2.7.egg\EGG-INFO\entry_points.txt 两个定义的返回都是str类,格式例如: [my_group] foobar = foobar.client:main foobard = foobar.server:main 读取的数据在返回到EntryPoint.parse_map(self._get_metadata(‘entry_points.txt’) , self)处时,会对EntryPoint类的类变量进行更新。 EntryPoint.parse_map()

1.  `@classmethod`
2.  `def parse_map(cls, data, dist=None):`
3.  `"""Parse a map of entry point groups"""`
4.  `if isinstance(data, dict):`
5.  ` data = data.items()`
6.  `else:`
7.  ` data = split_sections(data)`
8.  ` maps ={}`
9.  `forgroup, lines in data:`
10.  `ifgroupisNone:`
11.  `ifnot lines:`
12.  `continue`
13.  `raiseValueError("Entry points must be listed in groups")`
14.  `group=group.strip()`
15.  `ifgroupin maps:`
16.  `raiseValueError("Duplicate group name",group)`
17.  ` maps[group]= cls.parse_group(group, lines, dist)`
18.  `return maps`

parse_group()

1.  `@classmethod`
2.  `def parse_group(cls,group, lines, dist=None):`
3.  `"""Parse an entry point group"""`
4.  `ifnot MODULE(group):`
5.  `raiseValueError("Invalid group name",group)`
6.  `this={}`
7.  `for line in yield_lines(lines):`
8.  ` ep = cls.parse(line, dist)`
9.  `if ep.name inthis:`
10.  `raiseValueError("Duplicate entry point",group, ep.name)`
11.  `this[ep.name]=ep`
12.  `returnthis`

parse()

1.  `@classmethod`
2.  `def parse(cls, src, dist=None):`
3.  `"""Parse a single entry point from string `src``
4.  
5.  ` Entry point syntax follows the form::`
6.  
7.  ` name = some.module:some.attr [extra1, extra2]`
8.  
9.  ` The entry name and module name are required, but the ``:attrs`` and`
10.  ` ``[extras]`` parts are optional`
11.  ` """`
12.  ` m = cls.pattern.match(src)`
13.  `ifnot m:`
14.  ` msg ="EntryPoint must be in 'name=module:attrs [extras]' format"`
15.  `raiseValueError(msg, src)`
16.  ` res = m.groupdict()`
17.  ` extras = cls._parse_extras(res['extras'])`
18.  ` attrs = res['attr'].split('.')if res['attr']else()`
19.  `return cls(res['name'], res['module'], attrs, extras, dist)`

也就是说EntryPoint.parse_map(self._get_metadata(‘entry_points.txt’),self)返回的是一系列以entrypoint名称为key,EntryPoint类的实例为value的字典。这同时也是最开始的iter_entry_points()函数中dist.get_entry_map(group)的返回值,最终,iter_entry_points(self, group,name=None)函数返回的是当前环境中所有组名group_name为group的entrypoint(EntryPoint类的实例) 其中EntryPoint有5个变量:

entrypoint的使用

load()函数

1.  `def load(self,require=True,*args,**kwargs):`
2.  `"""`
3.  ` Require packages for this EntryPoint, then resolve it.`
4.  ` """`
5.  `ifnotrequireor args or kwargs:`
6.  ` warnings.warn(`
7.  `"Parameters to load are deprecated.  Call .resolve and "`
8.  `".require separately.",`
9.  `DeprecationWarning,`
10.  ` stacklevel=2,`
11.  `)`
12.  `ifrequire:`
13.  `self.require(*args,**kwargs)`
14.  `returnself.resolve()`

resolve()函数

1.  `def resolve(self):`
2.  `"""`
3.  ` Resolve the entry point from its module and attrs.`
4.  ` """`
5.  `module= __import__(self.module_name, fromlist=['__name__'], level=0)`
6.  `try:`
7.  `return functools.reduce(getattr,self.attrs,module)`
8.  `exceptAttributeErroras exc:`
9.  `raiseImportError(str(exc))`

可以看到利用import返回了该entrypoint对应的模块,然后getattr()函数调用了该模块中的对应属性。除此之外,调用functools.reduce(参考链接enter link description here)是因为entrypoint的格式造成的,因为entrypoint对应的属性,可能在该模块的子模块中,所以需要借助functool.reduce去寻找。通过这里,可以看到最终load()函数返回了对应entrypoint的对应属性。

新建模块的测试

完成以上的内容,我们可以通过在本地新建一个带entrypoint的Python包,来测试如何通过pkg_resources去调用。 setup.py

1.  `from setuptools import setup`
2.  `setup(`
3.  ` name="test_for_pkg",`
4.  ` version="1.1",`
5.  ` description="Foo!",`
6.  ` author="zhx",`
7.  ` author_email="[email protected]",`
8.  ` packages=["pkg"],`
9.  ` entry_points={`
10.  `"test_group_name":[`
11.  `"ep_name1 = pkg:main",`
12.  `"ep_name2 = pkg:test",`
13.  `"ep_name3 = pkg.func:main",`
14.  `"ep_name4 = pkg.func:test",`
15.  `]`
16.  `},`
17.  `)`

这里要注意的参数,packages=[‘pkg’],代表你的需要处理的包,我们这里设置包名为‘pkg’,那么我们需要在setup.py文件的同级目录下新建pkg文件夹,并在文件夹中添加pkg包的_init.py文件,该py直接对应’ep_name1=pkg:main’和’ep_name2=pkg:test’中的pkg,main

和 test是init.py中的两个函数。 为了进一步了解entrypoint在setup.py文件中的格式,除了’epname1’和’epname2’我还添加了另外两个entrypoint,”ep_name3 = pkg.func:main”,”ep_name4 = pkg.func:test”, 他们两个的是pkg.func格式,也就是说,在pkg文件夹目录下,还有新的py文件func.py。 **__init.py**

1.  `def main():`
2.  `print'this is a main func in __init__.py'`
3.  `def test():`
4.  `print'this is a test func in __init__.py'`

func.py

1.  `def test():`
2.  `print'this is a test func in func.py'`
3.  `def main():`
4.  `print'this is a main func in func.py'`

他们的结构如下: setup.py和pkg文件夹在同一目录,init.pyfunc.py在pkg目录下。 python setup.py install 建好源代码后,通过CMD窗口,在setup.py目录下运行python setup.py install指令。成功后,在python的site-packages文件夹中会生成新的egg文件:

test_for_pkg-1.1-py2.7.egg

然后运行测试代码: test.py import pkg_resources def main():

1.  `import pkg_resources`
2.  `import pdb`
3.  `def main():`
4.  `#pdb.set_trace()`
5.  `#for i in pkg_resources.iter_entry_points('test_group_name'):`
6.  `#    print i`
7.  ` ep1 = pkg_resources.load_entry_point('test_for_pkg','test_group_name','ep_name1')`
8.  ` ep2 = pkg_resources.load_entry_point('test_for_pkg','test_group_name','ep_name2')`
9.  ` ep3 = pkg_resources.load_entry_point('test_for_pkg','test_group_name','ep_name3')`
10.  ` ep4 = pkg_resources.load_entry_point('test_for_pkg','test_group_name','ep_name4')`
11.  ` ep1()`
12.  ` ep2()`
13.  ` ep3()`
14.  ` ep4()`
15.  `main()`

可以看到输出结果为: this is a main func in init.py this is a test func in init.py this is a main func in func.py this is a test func in func.py 如果我们删除掉site-packages目录中的该.egg文件在运行test.py 则会报错找不到对应的entry_point。

最近发表

results matching ""

    No results matching ""