python setuptools之pkg_resources模块
[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.py和func.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。