Разбираемся с egg's entrypoints ∞
В setuptools есть механизм плагинов - так называемых "точек входа". Сегодня поговорим о том, как использовать egg's entrypoints в своих программах.
Egg's entrypoints - вводная
setuptools позволяют определять точки входа для плагинов. Эти точки определяются у плагинов, а программа, которая хочет использовать плагины "спрашивает" у setuptools, кто реализует данную EP (entry point, точка входа). Прежде чем начать, советую познакомиться с Python eggs и с тем, как их правильно устанавливать.
Чтобы было легче с пониманием, будем разбираться на простом примере. Напишем программу, которая будет печатать списки. "Что" будет определяться плагином ввода, "как" - плагином вывода.
Без плагинов
Для начала, напишем программу, без плагинов. Программа будет выводить содержимое текущей директории
# lister.input
import os
def dir_list():
"""
Lists current dir
"""
return os.listdir('.')
в виде обычного списка
# lister.output
import pprint
def raw_list(ilist):
"""
Prints list 'AS IS'
"""
pprint.PrettyPrinter().pprint(list(ilist))
Соединяя вместе, получаем желаемую программу, без использования плагинов:
from lister.input import dir_list
from lister.output import raw_list
def listit_wo_plugins():
raw_list(dir_list())
С плагинами
Указание точек входа
Как выше сказано, нужно указать у плагинов точки входа. У нас будут плагины ввода (точка входаlister.input) и плагины вывода (точка входа lister.output). Точки входа указываются в метаданных пакета - в определении setup.py:
from setuptools import setup
setup(
# ...
entry_points="""
[lister.output]
raw = lister.output:raw_list
[lister.input]
dir = lister.input:dir_list
"""
)
Точки входа указываются либо в формате ini-файла, многострочником; либо в виде словаря:
from setuptools import setup
setup(
# ...
entry_points={
'lister.output': ['raw = lister.output:raw_list'],
'lister.input': ['dir = lister.input:dir_list'],
}
)
У нас получилось так, что имя EP совпало с именем модуля, но это случайность, в общем случае это не так. Что касается определения плагина - то запись выглядит как
имя_плагина = путь.к.модулю:объект
Конечным объектом может служить любой Python-объект (класс, функция, представитель класса и т.д.).
Использование точек входа
После определения точек входа мы можем воспользоваться инструментами setuptools для доступа к плагинам (поправка: egg с определенными EP должен быть установлен, так что я рекомендую поставить уже готовый lister).
>>> import pkg_resources
>>> pkg_resources.get_entry_map('lister')
{'lister.input': {'dir': EntryPoint.parse('dir = lister.input:dir_list')},
'lister.output': {'raw': EntryPoint.parse('raw = lister.output:raw_list')}}
>>> pkg_resources.load_entry_point('lister', 'lister.output', 'raw')
<function raw_list at 0xb788c95c>
>>> list(pkg_resources.iter_entry_points('lister.input'))
[EntryPoint.parse('dir = lister.input:dir_list')]
Указанных функций хватит для большинства случаев. Документация по setuptools весьма бедна, зато практически у всех функций/методов есть как их правильно устанавливать.
Я выделил работу с плагинами в отдельный модуль - plug.py. Основная функция показаны ниже:
def get_plugins_by_entrypoint(ep_name, plug_name=None):
"""
Returns iterator over names, descriptions and plugin-actions
for current entrypoint
"""
for entrypoint in pkg_resources.iter_entry_points(ep_name, plug_name):
plugin_func = entrypoint.load()
plugin_description = plugin_func.__doc__
yield (entrypoint.name, plugin_description, plugin_func)
Если имя плагина не передается, то строится список по всем плагинам для данной точки входа. В случае какой-либо ошибки (не найдена точка входа, не найден плагин) - возвращается пустой итератор.
Всё вместе
Отдельные элементы уже работают как надо. так что нужно собрать всё в месте.
Стоит начать с управление параметрами командной строки (lister.command):
# lister.command
import sys
from lister import plug
def runner(argv):
"""
Parses command-line options
"""
# defaults
iplugin = 'dir'
oplugin = 'raw'
# ... Здесь идет разбор переданных параметров.
# ... В зависимости от них, либо выводится список
# ... доступных плагинов, либо переопределяются
# ... переменные iplugin и/или oplugin
iplugins_data = list(plug.get_input_plugins(iplugin))
if not iplugins_data:
print "No such input plugin: %s" % iplugin
sys.exit(1)
else:
input_plugin = iplugins_data[0][-1]
oplugins_data = list(plug.get_output_plugins(oplugin))
if not oplugins_data:
print "No such output plugin: %s" % oplugin
sys.exit(1)
else:
output_plugin = oplugins_data[0][-1]
return input_plugin, output_plugin
По результатам разбора параметров командной строки, либо программа завершает свою работу (например, в случае ошибки), либо возвращает загруженные плагины. Так что в главном модуле остается просто воспользоваться результатами:
from lister.command import runner
def listit(input_plugin, output_plugin):
"""
Lists data using specified input and output plugins
"""
output_plugin(input_plugin())
def main():
"""
Runs lister from command line
"""
iplugin, oplugin = runner(sys.argv)
listit(iplugin, oplugin)
if __name__ = '__main__':
main()
Теперь вроде всё. Можно попробовать запустить программу:
$ listit
['lister', 'setup.py', 'scripts', 'lister.egg-info']
Дописываем плагины
Надеюсь, у вас получилось с lister и теперь мы попробуем написать плагин, скажем, для вывода в html.
Для этого создаем пакет, скажем listerhtml. В нем пишем функцию вывода списка в html:
def html_list(ilist):
"""
Prints list as HTML unordered list
"""
print "<ul>"
for element in ilist:
print "<li>%s</li>" % element
print "</ul>"
И в метаданных пакета записываем, что это - плагин к lister.output:
from setuptools import setup
setup(
# ...
entry_points={
'lister.output': ['html=listerhtml:html_list'],
}
)
Теперь устанавливаем его (python setup.py install) и смотрим список плагинов к listit:
$ listit --list
Input plugins:
dir
Lists current dir
Output plugins:
raw
Prints list 'AS IS'
html
Prints list as HTML unordered list
$ listit -o html
<ul>
<li>lister</li>
<li>setup.py</li>
<li>scripts</li>
<li>lister.egg-info</li>
</ul>
Заключение
setuptools дает возможность достаточно просто реализовать плагины к своим программам. Единственный недостаток - это необходимость, чтобы нужный egg был установлен. Рабочий код lister и плагина к нему listerhtml, как обычно, - на code.google.com.
