4.7 扩展点

你可能已经在并不了解setuptools的情况下使用过它的入口点。如果还没决定用setuptools为你的软件提供setup.py文件,这里有一些功能的介绍也许能帮你做决定。

使用setuptools分发软件包括重要的元数据描述,如需要的依赖以及与这个主题更相关的“入口点”的列表。这些入口点能够被其他Python程序用来动态发现包所提供的功能。

广告:个人专属 VPN,独立 IP,流量大,速度快,连接稳定,多机房切换,每月最低仅 10 美元

在下面几节中,我们将讨论如何利用入口点为软件添加扩展能力。

4.7.1 可视化的入口点

要看到一个包中可用的入口点的最简单的方法就是使用一个叫entry_point_inspectorhttps://pypi.python.org/pypi/entry_point_inspector)的包。

安装后,它提供了名为epi的命令,可以从终端运行并能交互式地发现某个安装包的入口点,如示例4.4所示。

示例4.4 epi group list的运行结果

 +--------------------------+
 | Name           |
 +--------------------------+
 | console_scripts      |
 | distutils.commands    |
 | distutils.setup_keywords  |
 | egg_info.writers      |
 | epi.commands        |
 | flake8.extension      |
 | setuptools.file_finders  |
 | setuptools.installation  |
 +--------------------------+

示例4.4显示系统有很多不同地包都提供了入口点。你可能注意到,这个列表包含console_scripts(将在节中讨论)。

示例 4.5epi group show console_scripts的运行结果

 +----------+----------+--------+--------------+-------+
 | Name   | Module   | Member | Distribution | Error |
 +----------+----------+--------+--------------+-------+
 | coverage | coverage | main   | coverage 3.4  |   |
 +----------+----------+--------+--------------+-------+

示例4.5显示了一个名为coverage的入口点,并引用了coverage模块的成员的main。这个入口点是由包coverage 3.4提供的。可以使用kepi ep show获得更多信息。

示例 4.6 ` kepi ep show console_scripts coverage `的运行结果

 +--------------+----------------------------------+
 | Field     | Value           |
 +--------------+----------------------------------+
 | Module    | coverage            |
 | Member     | main             |
 | Distribution  | coverage 3.4        |
 | Path      | /usr/lib/python2.7/dist-packages  |
 | Error     |              |
 +--------------+----------------------------------+

这里所用的工具只是很薄的一层,它建立在更复杂的能够发现任何Python库或程序的入口点的Python库之上。入口点有许多不同的用处,如对控制台脚本和动态代码发现都很有用,这些将在下面几节介绍。

4.7.2 使用控制台脚本

开发Python应用程序时,通常要提供一个可启动的程序,也就是最终用户实际可以运行的Python脚本。这个程序需要被安装在某个包含在系统路径中的目录里。

大多数项目都会有下面这样几行代码:

#!/usr/bin/python
import sys
import mysoftware
mysoftware.SomeClass(sys.argv).run()

这实际上是一个理想情况下的场景:许多项目在系统路径中会有一个非常长的脚本安装。但使用这样的脚本有一些主要的问题。

 
  • 没办法知道Python解释器的位置和版本。
  • 安装的二进制代码不能被其他软件或单元测试导入。
  • 很难确定安装在哪里。
  • 如何以可移植的方式进行安装并不明确(如是Unix还是Windows)。

setuptools有一个功能可以帮助我们解决这些问题,即console_scripts。console_scripts是一个入口点,能够用来帮助setuptools安装一个很小的程序到系统目录中,并通过它调用应用程序中某个模块的特定函数。

设想一个foobar程序,它由客户端和服务器端两部分组成。这两部分各自有自己独立的模块——foobar.clientfoobar.server

foobar/client.py

def main():
  print("Client started")

foobar/server.py

def main():
  print("Server started")

当然,这个程序做不了什么——客户端和服务器端甚至不能彼此通信。但对于我们这个例子的目的来说,只需要在它们成功启动之后能输出消息即可。

接下来可以在根目录中添加下面的setup.py文件。

setup.py

From setuptools import setup
setup(
  name="foobar",
  version="1",
  description="Foo!",
  author="Julien Danjou",
  author_email="julien@danjou.info",
  packages=["foobar"],
  entry_points={
    "console_scripts": [
      "foobard = foobar.server:main",
      "foobar = foobar.client:main",
    ],
   },
)

使用格式package.subpackage:function可以定义自己的入口点。

当运行python setup.py install时,setuptools会创建示例4.7所示的脚本。

示例 4.7 setuptools 生成的控制台脚本

#!/usr/bin/python
# EASY-INSTALL-ENTRY-SCRIPT: 'foobar==1','console_scripts','foobar'
__requires__ = 'foobar==1'
import sys
From pkg_resources import load_entry_point

if __name__ == '__main__':
  sys.exit(
    load_entry_point('foobar==1', 'console_scripts', 'foobar')()
  )

这段代码会扫描foobar包的入口点并从console_scripts目录中抽取foobar键,从而定位并运行相应的函数。

使用这一技术能够保证代码在Python包内,并能够被其他应用程序导入(或测试)。

 提示

如果在setuptools之上使用pbr,那么生成的脚本会比通过setuptools默认创建的要简单(因此也更快),因为它会调用写在入口点中的函数而无需在运行时动态扫描入口点列表。

4.7.3 使用插件和驱动程序

通过入口点可以很容易得发现和动态加载其他包部署的代码。可以使用pkg_resourceshttp://pythonhosted.org/distribute/pkg_resources.html)从自己的Python程序中发现和加载入口点文件。(你可能已经注意到,这与示例4.7中setuptools创建的控制台脚本所使用的是同一个包。)

在本节中,我们将创建一个cron风格的守护进程,它通过注册一个入口点到pytimed组中即可允许任何Python程序注册一个每隔几秒钟运行一次的命令。该入口点指向的属性应该是一个返回number_of_secondscallable的对象。

下面是一个使用pkg_resources发现入口点的pycrond实现。

pytimed.py

import pkg_resources
import time

def main():
  seconds_passed = 0
  while True:
    for entry_point in pkg_resources.iter_entry_points('pytimed'):
    try:
      seconds, callable = entry_point.load()()
    except:
      # Ignore failure
      pass
    else:
      if seconds_passed % seconds == 0:
        callable()
  time.sleep(1)
  seconds_passed += 1

这是一个非常简单而朴素的实现,但对我们的例子来说足够了。现在可以写另一个Python程序,需要周期性地调用它的一个函数。

hello.py

def print_hello():
  print("Hello, world!")
def say_hello():
  return 2, print_hello

使用合适的入口点注册这个函数。

setup.py

from setuptools import setup

setup(
  name="hello",
  version="1",
  packages=["hello"],
  entry_points={
    "pytimed": [
      "hello = hello:say_hello",
    ],
  },)

现在如果运行pytimed脚本,将会看到在屏幕上每两秒钟打印一次“Hello, world!”,如示例4.8示例。

示例 4.8  运行pytimed

% python3
Python + (default, Aug 4 2013, 15:50:24)
[GCC ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pytimed
>>> pytimed.main()
Hello, world!
Hello, world!
Hello, world!

这一机制提供了巨大的可能性:它可以用来构建驱动系统、钩子系统以及简单而通用的扩展。在每一个程序中手动实现这一机制是非常繁琐的,不过幸运的是,已经有Python库可以处理这部分无聊的工作。

stevedore(https://pypi.python.org/pypi/stevedore)基于我们在前面例子中展示的机制提供了对动态插件的支持。在这个例子中我们的用例并不复杂,但是仍然可以用stevedore稍做简化。

pytimed_stevedore.py

from stevedore.extension import ExtensionManager import time

def main():
  seconds_passed = 0
  while True:
    for extension in ExtensionManager('pytimed', invoke_on_load=True):
    try:
      seconds, callable = extension.obj
    except:
      # Ignore failure
      pass
    else:
      if seconds_passed % seconds == 0:
        callable()
  time.sleep(1)
  seconds_passed += 1

我们的例子仍然非常简单,但是如果看了stevedore文档就会发现,ExtensionManager有很多用来处理不同场景的子类,例如基于名字或者函数运行结果加载特定的扩展。


1参见PEP 453(http://www.python.org/dev/peps/pep-0453/)及ensureepip模块。
2“Benevolent Dictator For Life”是Python作者Guido van Rossum给的称号。