如何动态添加和加载入口点?

时间:2016-11-09 19:05:06

标签: python plugins setuptools entry-point

我正在使用入口点开发一个带插件的松散机器人。我想在运行时动态添加一个插件。

我有一个具有这种结构的项目:

+ ~/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会执行以下操作:

  1. 将所需文件复制到/usr/share/my_project_dir/

  2. /usr/share/my_project_dir/venv/bin/activate

  3. 激活virtualenv
  4. 运行:python setup.py develop

  5. 这可以正常工作并正确设置我的入口点,以便我可以通过bot使用它们。

    但是我希望能够在setup.py添加一个插件,并且能够在机器人运行时使用它。所以我想添加一行:pluginN+1 = plugins.pluginN+1:pluginN+1_class并且可以使用pluginN + 1.

    我尝试/学过的东西:

    • /usr/share/my_project_dir/venv/bin/activate之后我打开一个Python交互式shell并遍历pkg_resources.iter_entry_points(),它列出了从setup.py初始状态加载的所有内容(即plugin1到pluginN)

    • 如果我向setup.py添加一行并运行sudo python setup.py develop并使用相同的Python shell再次迭代,它就不会选择新的插件但是如果我退出shell并重新打开它,新的插件被拿起。

    • 我注意到当我安装机器人时,部分输出显示:

      • 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,然后从shell中运行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

3 个答案:

答案 0 :(得分:4)

我需要做类似的事情来加载一个虚拟插件用于测试目的。这与您的用例略有不同,因为我特意试图避免需要在包中定义入口点(因为它只是测试代码)。

我发现我可以动态地将条目插入到pkg_resources数据结构中,如下所示:

import pkg_resources
# Create the fake entry point definition
ep = pkg_resources.EntryPoint.parse('dummy = dummy_module:DummyPlugin')

# Create a fake distribution to insert into the global working_set
d = pkg_resources.Distribution()

# Add the mapping to the fake EntryPoint
d._ep_map = {'namespace': {'dummy': ep}}

# Add the fake distribution to the global working_set
pkg_resources.working_set.add(d, 'dummy')

这在运行时添加了一个名为“虚拟”的入口点。到'命名空间',这将是班级' DummyPlugin'在' dummy_module.py'。

这是通过使用setuptools文档和对象上的dir()来确定的,以便根据需要获取更多信息。

文档在这里:http://setuptools.readthedocs.io/en/latest/pkg_resources.html

如果您需要做的只是加载刚刚存储到本地文件系统的插件,您可能会特别关注http://setuptools.readthedocs.io/en/latest/pkg_resources.html#locating-plugins

答案 1 :(得分:2)

自从我第一次问自己几乎同样的问题以来,已经超过至少5年了,现在你的问题是最终找到它的冲动。

对我而言,如果可以在不安装软件包的情况下从脚本添加与同一目录的入口点,那么它也很有趣。虽然我总是知道包的唯一内容可能是某些元素,其中包含查看其他包的入口点。

无论如何,这是我的目录的一些设置:

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))
   ....:

但是,如果新版本的插件包基于其他已使用模块的重大更改,则可能需要重新加载和重新加载这些已更改模块的特定顺序。这可能会成为一项繁琐的任务,因此重新启动机器人将是唯一的方法。

答案 2 :(得分:1)

我必须通过@ j3p0uk改变一点解决方案才能为我工作。我想在单元测试(unittest框架)中使用它。我做的是:

def test_entry_point(self):
    distribution = pkg_resources.Distribution(__file__)
    entry_point = pkg_resources.EntryPoint.parse('plugin1 = plugins.plugin1:plugin1_class', dist=distribution)
    distribution._ep_map = {'my_project.plugins': {'plugin1': entry_point}}
    pkg_resources.working_set.add(distribution)

这也使得entry_point.load()在我正在测试的代码中工作,以真正加载入口点引用的符号。在我的测试中,我还有my_project.plugins具有测试文件的名称,然后要加载的符号位于该文件的全局范围内。

相关问题