Как динамически добавлять и загружать точки входа?

Я разрабатываю слабый бот с плагинами, использующими точки входа. Я хочу динамически добавлять плагин во время выполнения.

У меня есть проект с этой структурой:

+ ~/my_project_dir/
    + my_projects_python_code/
    + plugins/
        - plugin1.py
        - plugin2.py
        - ...
        - pluginN.py
    - setup.py
    - venv/
    - install.sh

Мой файл setup.py выглядит следующим образом:

from setuptools import setup, find_packages

setup(
    name="My_Project_plugins",
    version="1.0",
    packages=['plugins'],
    entry_points="""
        [my_project.plugins]
        plugin1 = plugins.plugin1:plugin1_class
        plugin2 = plugins.plugin2:plugin2_class
        ...
        pluginN = plugins.pluginN:pluginN_class
    """
        )

Запуск sudo install.sh выполняет следующие действия:

  • Копирует необходимые файлы в /usr/share/my_project_dir/

  • Активируйте virtualenv на /usr/share/my_project_dir/venv/bin/activate

  • Запуск: python setup.py develop

Это работает так, как ожидалось, и правильно устанавливает мои точки входа, чтобы я мог использовать их через бота.

Но я хочу иметь возможность добавить плагин к setup.py и иметь возможность использовать его во время работы бота. Поэтому я хочу добавить строку: pluginN+1 = plugins.pluginN+1:pluginN+1_class и использовать pluginN + 1.

Что я пробовал/узнал:

  • После /usr/share/my_project_dir/venv/bin/activate я открываю интерактивную оболочку Python и перебираю через pkg_resources.iter_entry_points(), в котором перечисляются все, что было загружено из начального состояния setup.py(т.е. плагин1 через плагинN)

  • Если я добавлю строку в setup.py и запустим sudo python setup.py develop и снова перейду с той же оболочкой Python, она не заберет новый плагин, но если я выйду из оболочки и снова ее открою, плагин получает.

  • Я заметил, что когда я устанавливаю бота, часть вывода говорит:

    • Copying My_Project_plugins-1.0-py2.7.egg to /usr/share/my_project-dir/venv/lib/python2.7/site-packages
  • Когда я cd /usr/share/my_project_dir/, активируйте мой virtualenv и запустите setup.py из оболочки, в которой говорится:

    • Creating /usr/local/lib/python2.7/dist-packages/My_Project-plugins.egg-link (link to .) My_Project-plugins 1.0 is already the active version in easy-install.pth

Ответы

Ответ 1

Это больше, чем, по крайней мере, 5 лет, с тех пор, как я впервые задал себе почти тот же вопрос, и ваш вопрос теперь является импульсом, чтобы наконец найти его.

Для меня было также интересно, если можно добавить точки входа из того же каталога, что и script, без установки пакета. Хотя я всегда знал, что единственным содержимым пакета может быть какая-то мета с точками входа, смотрящими на некоторые другие пакеты.

В любом случае, вот некоторые настройки моего каталога:

ep_test newtover$ tree
.
├── foo-0.1.0.dist-info
│   ├── METADATA
│   └── entry_points.txt
└── foo.py

1 directory, 3 files

Вот содержимое foo.py:

ep_test newtover$ cat foo.py
def foo1():
    print 'foo1'

def foo2():
    print 'foo2'

Теперь откройте ipython:

In [1]: def write_ep(lines):  # a helper to update entry points file
   ...:     with open('foo-0.1.0.dist-info/entry_points.txt', 'w') as f1:
   ...:         print >> f1, '\n'.join(lines)
   ...:        

In [2]: write_ep([  # only one entry point under foo.test
   ...: "[foo.test]",
   ...: "foo_1 = foo:foo1",
   ...: ])

In [3]: !cat foo-0.1.0.dist-info/entry_points.txt
[foo.test]
foo1 = foo:foo1

In [4]: import pkg_resources

In [5]: ws = pkg_resources.WorkingSet()  # here is the answer on the question

In [6]: list(ws.iter_entry_points('foo.test'))
Out[6]: [EntryPoint.parse('foo_1 = foo:foo1')]

In [7]: write_ep([  # two entry points
   ...: "[foo.test]",
   ...: "foo_1 = foo:foo1",
   ...: "foo_2 = foo:foo2"
   ...: ])

In [8]: ws = pkg_resources.WorkingSet()  # a new instance of WorkingSet

С параметрами по умолчанию WorkingSet просто пересматривает каждую запись в sys.path, но вы можете сузить список. pkg_resources.iter_entry_points привязан к глобальному экземпляру WorkingSet.

In [9]: list(ws.iter_entry_points('foo.test'))  # both are visible
Out[9]: [EntryPoint.parse('foo_1 = foo:foo1'), EntryPoint.parse('foo_2 = foo:foo2')]

In [10]: foos = [ep.load() for ep in ws.iter_entry_points('foo.test')]

In [11]: for func in foos: print 'name is {}'.format(func.__name__); func()
name is foo1
foo1
name is foo2
foo2

И содержимое METADATA:

ep_test newtover$ cat foo-0.1.0.dist-info/METADATA
Metadata-Version: 1.2
Name: foo
Version: 0.1.0
Summary: entry point test

UPD1. Я снова подумал об этом и теперь понимаю, что вам нужен дополнительный шаг перед использованием новых плагинов: вам нужно перезагрузить модули.

Это может быть просто:

In [33]: modules_to_reload = {ep1.module_name for ep1 in ws.iter_entry_points('foo.test')}

In [34]: for module_name in modules_to_reload:
   ....:     reload(__import__(module_name))
   ....:

Но если новая версия вашего пакета плагинов основана на значительных изменениях в других используемых модулях, вам может потребоваться определенный порядок перезагрузки и перезагрузки этих измененных модулей. Это может стать громоздкой задачей, так что перезапуск бота будет единственным способом.