Python setup.py entry_points 详解

鲁老师大约 9 分钟Python

一个 Python 软件包中的 setup.py 文件包含这个 Python 包如何打包发布等重要信息。本文主要对这篇博客open in new window进行了翻译解读,以解释 Python setup.py 文件中 entry_point 用法。

第一个简单的 Python 包:snek

我作为技术合伙人兼任CTO与其他人合伙成立了一家软件公司。软件公司的第一个 Python 产品名为 snek,这个软件可以在屏幕上画出一条蛇。我写下了公司第一行代码,得到如下 Python 文件 snek.py

ascii_snek = """\
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`
"""

def main():
    print(ascii_snek)

if __name__ == '__main__':
    main()

代码编写好了,可以给投资人和客户们演示了!

不过,这个程序需要我们在命令行里,使用 python snek.py 的方式调用。打开命令行 ,使用 cd 命令进入到工程目录,也就是存放 snek.py 的文件夹,执行 python snek.py 看一下效果:程序能跑起来了!

$ python snek.py
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`

setup.pyopen in new window 打包安装

但是客户可能不太了解 Python、命令行、cd 这些东西,客户只想简单一些,比如不需要 cd,打开命令行,执行一个命令就能画出这条蛇。因此,作为技术合伙人,我需要把这个 snek.py 包装一下,打包成一个软件,这让客户可以安装这个软件,并且打开命令行,直接执行。在对 snek.py 这个 Python 文件打包时,需要一个重要的文件:setup.pysetup.py 文件含有 这个 Python 包的名称、开发者是谁、还依赖哪些软件等。setup.py 中还有一个重要的功能:它可以用于注册 entry_points 。后面会用代码来解释 entry_points 到底是干嘛的。

from setuptools import setup

setup(
    name='snek',
    entry_points={
        'console_scripts': [
            'snek = snek:main',
        ],
    }
)

在上面这段代码里, console_scripts 是一种特殊的 entry_points 。 setuptools 从中读取 "<console_script_name> = <python_package:object_name>" 。当把这个 Python 包安装好后,它会自动创建一个命令行工具。后文还会细讲 "<console_script_name> = <python_package:object_name>" 对应的内容。现在,我们先在本地用开发者模式安装这个包:

$ python setup.py develop

running develop
running egg_info
writing snek.egg-info\PKG-INFO
writing dependency_links to snek.egg-info\dependency_links.txt
writing entry points to snek.egg-info\entry_points.txt
writing top-level names to snek.egg-info\top_level.txt
reading manifest file 'snek.egg-info\SOURCES.txt'
writing manifest file 'snek.egg-info\SOURCES.txt'
running build_ext
Creating c:\program files (x86)\py36-32\lib\site-packages\snek.egg-link (link to .)
snek 0.0.0 is already the active version in easy-install.pth
Installing snek-script.py script to C:\Program Files (x86)\Py36-32\Scripts
Installing snek.exe script to C:\Program Files (x86)\Py36-32\Scripts
Installing snek.exe.manifest script to C:\Program Files (x86)\Py36-32\Scripts

Installed c:\users\rachum\notebooks
Processing dependencies for snek==0.0.0
Finished processing dependencies for snek==0.0.0

上面这段输出中,一个重要的信息是,这个名为 snek 的 Python 包被安装到了 C:\Program Files (x86)\Py36-32\Scripts 里面。

现在,我们可以打开命令行,不需要 cd 到这个存放代码的目录,在任何位置,执行这个 Python 包。好了,至少在公司产品路演上,我们可以给客户和投资人看,我们有一个可以打包安装的产品。

$ snek
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`

更多酷炫的蛇

snek 软件开始流行起来了,拿到了投资,更多人开始用这款软件,我们的开发团队也在扩招,更多程序员加入,于是我们推出功能更加强大的 snek :

"""Print an ASCII Snek.

Usage:
    snek [--type=TYPE]

"""
import docopt

normal_snek = """\
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`
"""

fancy_snek = """\
                          _,..,,,_
                     '``````^~"-,_`"-,_
       .-~c~-.                    `~:. ^-.
   `~~~-.c    ;                      `:.  `-,     _.-~~^^~:.
         `.   ;      _,--~~~~-._       `:.   ~. .~          `.
          .` ;'   .:`           `:       `:.   `    _.:-,.    `.
        .' .:   :'    _.-~^~-.    `.       `..'   .:      `.    '
       :  .' _:'   .-'        `.    :.     .:   .'`.        :    ;
       :  `-'   .:'             `.    `^~~^`   .:.  `.      ;    ;
        `-.__,-~                  ~-.        ,' ':    '.__.`    :'
                                     ~--..--'     ':.         .:'
                                                     ':..___.:'
"""

def get_sneks():
    return {
        'normal': normal_snek,
        'fancy': fancy_snek,
    }


def main():
    args = docopt.docopt(__doc__)
    snek_type = args['--type'] or 'normal'
    print(get_sneks()[snek_type])

if __name__ == '__main__':
    main()

新的程序中,增加了不同形状的蛇,而且给用户选项,一种普通版本的蛇叫 normal,另一种时尚版本的蛇叫 fancy 。给用户选项,让用户做选择。再次运行:

$ snek
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`

$ snek --type fancy
                          _,..,,,_
                     '``````^~"-,_`"-,_
       .-~c~-.                    `~:. ^-.
   `~~~-.c    ;                      `:.  `-,     _.-~~^^~:.
         `.   ;      _,--~~~~-._       `:.   ~. .~          `.
          .` ;'   .:`           `:       `:.   `    _.:-,.    `.
        .' .:   :'    _.-~^~-.    `.       `..'   .:      `.    '
       :  .' _:'   .-'        `.    :.     .:   .'`.        :    ;
       :  `-'   .:'             `.    `^~~^`   .:.  `.      ;    ;
        `-.__,-~                  ~-.        ,' ':    '.__.`    :'
                                     ~--..--'     ':.         .:'
                                                     ':..___.:'

更多开发者加入进来

越来越多的人都开始用这款软件,我们自己的软件团队已经无法满足客户的需求。第三方开发者希望能够在我们发行的软件基础上进行二次开发,我们在 snek.py 上改进,兼容其他开发者:

"""Print an ASCII Snek.

Usage:
    snek [--type=TYPE]

"""
import docopt
import pkg_resources

normal_snek = """\
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`
"""

fancy_snek = """\
                          _,..,,,_
                     '``````^~"-,_`"-,_
       .-~c~-.                    `~:. ^-.
   `~~~-.c    ;                      `:.  `-,     _.-~~^^~:.
         `.   ;      _,--~~~~-._       `:.   ~. .~          `.
          .` ;'   .:`           `:       `:.   `    _.:-,.    `.
        .' .:   :'    _.-~^~-.    `.       `..'   .:      `.    '
       :  .' _:'   .-'        `.    :.     .:   .'`.        :    ;
       :  `-'   .:'             `.    `^~~^`   .:.  `.      ;    ;
        `-.__,-~                  ~-.        ,' ':    '.__.`    :'
                                     ~--..--'     ':.         .:'
                                                     ':..___.:'
"""

def get_sneks():
    sneks = {
        'normal': normal_snek,
        'fancy': fancy_snek,
    }
    for entry_point in pkg_resources.iter_entry_points('snek_types'):
        sneks[entry_point.name] = entry_point.load()
    return sneks


def main():
    args = docopt.docopt(__doc__)
    snek_type = args['--type'] or 'normal'
    print(get_sneks()[snek_type])

if __name__ == '__main__':
    main()

在上面这段代码中,增加了:

    for entry_point in pkg_resources.iter_entry_points('snek_types'):
        sneks[entry_point.name] = entry_point.load()

pkg_resources.iter_entry_points('snek_types') 将遍历 当前系统 所安装的所有名为 snek_types 的 entry_points 。如果其他的包在 setup.py 中定义了叫 "snek_types" 的 entry_points ,将会在 snek 运行时动态加载进来。

比如说,第三方开发者能够画出更加酷炫的蛇,另外创建了一个工程,名为 cute_snek ,并且在里面编写了 cute_snek.py

cute_snek = r"""
                    /^\/^\
                  _|__|  O|
         \/     /~     \_/ \
          \____|__________/  \
                 \_______      \
                         `\     \                 \
                           |     |                  \
                          /      /                    \
                         /     /                       \
                       /      /                         \ \
                      /     /                            \  \
                    /     /             _----_            \   \
                   /     /           _-~      ~-_         |   |
                  (      (        _-~    _--_    ~-_     _/   |
                   \      ~-____-~    _-~    ~-_    ~-_-~    /
                     ~-_           _-~          ~-_       _-~ 
                        ~--______-~                ~-___-~
"""

第三方开发者将其打包成 cute_snek, 同时也需要让我们最原始的 snek 知道如何找到 cute snek 。

from setuptools import setup

setup(
    name='cute_snek',
    entry_points={
        'snek_types': [
            'cute = cute_snek:cute_snek',
        ],
    }
)

这时候,需要在 cute_snek 包的 setup.py 中的 entry_points 里注册 cute_snek 变量。

现在我们又安装了 cute_snek

$ cd cute_snek_folder 
$ python setup.py develop
running develop
running egg_info
writing cute_snek.egg-info\PKG-INFO
writing dependency_links to cute_snek.egg-info\dependency_links.txt
writing entry points to cute_snek.egg-info\entry_points.txt
writing top-level names to cute_snek.egg-info\top_level.txt
reading manifest file 'cute_snek.egg-info\SOURCES.txt'
writing manifest file 'cute_snek.egg-info\SOURCES.txt'
running build_ext
Creating c:\program files (x86)\py36-32\lib\site-packages\cute-snek.egg-link (link to .)
cute-snek 0.0.0 is already the active version in easy-install.pth

Installed c:\users\rachum\cute_snek
Processing dependencies for cute-snek==0.0.0
Finished processing dependencies for cute-snek==0.0.0

运行 snek 命令, cute snek 将从 cute_snek 包中动态加载进来。

$ snek --type cute
                    /^\/^\
                  _|__|  O|
         \/     /~     \_/ \
          \____|__________/  \
                 \_______      \
                         `\     \                 \
                           |     |                  \
                          /      /                    \
                         /     /                       \
                       /      /                         \ \
                      /     /                            \  \
                    /     /             _----_            \   \
                   /     /           _-~      ~-_         |   |
                  (      (        _-~    _--_    ~-_     _/   |
                   \      ~-____-~    _-~    ~-_    ~-_-~    /
                     ~-_           _-~          ~-_       _-~ 
                        ~--______-~                ~-___-~

重构snek包

既然第三方的 cute snek 可以动态加载进来,那么原始的 snek 呢?其实,所有的 snek 都是可以动态加载的。我们对原始的 snek.py 进行了重构,都使用动态加载的方式。

--- a/snek.py
+++ b/snek.py
@@ -31,10 +31,7 @@ fancy_snek = """\
 """

 def get_sneks():
-    sneks = {
-        'normal': normal_snek,
-        'fancy': fancy_snek,
-    }
+    sneks = {}
     for entry_point in pkg_resources.iter_entry_points('snek_types'):
         sneks[entry_point.name] = entry_point.load()
     return sneks

原始的 snek 也像 cute snek 一样,在 setup.py 中注册。

--- a/setup.py
+++ b/setup.py
@@ -6,5 +6,9 @@ setup(
         'console_scripts': [
             'snek = snek:main',
        ],
+       'snek_types': [
+           'normal = snek:normal_snek',
+           'fancy = snek:fancy_snek',
+       ],
     },
 )

改完后的 snek/snek.pysnek/setup.py 长这样:

"""Print an ASCII Snek.

Usage:
    snek [--type=TYPE]
    
"""
import docopt
import pkg_resources

normal_snek = """\
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`
"""

fancy_snek = """\
                          _,..,,,_
                     '``````^~"-,_`"-,_
       .-~c~-.                    `~:. ^-.
   `~~~-.c    ;                      `:.  `-,     _.-~~^^~:.
         `.   ;      _,--~~~~-._       `:.   ~. .~          `.
          .` ;'   .:`           `:       `:.   `    _.:-,.    `.
        .' .:   :'    _.-~^~-.    `.       `..'   .:      `.    '
       :  .' _:'   .-'        `.    :.     .:   .'`.        :    ;
       :  `-'   .:'             `.    `^~~^`   .:.  `.      ;    ;
        `-.__,-~                  ~-.        ,' ':    '.__.`    :'
                                     ~--..--'     ':.         .:'
                                                     ':..___.:'
"""

def get_sneks():
    sneks = {}
    for entry_point in pkg_resources.iter_entry_points('snek_types'):
        sneks[entry_point.name] = entry_point.load()
    return sneks


def main():
    args = docopt.docopt(__doc__)
    snek_type = args['--type'] or 'normal'
    print(get_sneks()[snek_type])
    
if __name__ == '__main__':
    main()
from setuptools import setup

setup(
    name='snek',
    entry_points={
        'console_scripts': [
            'snek = snek:main',
        ],
        'snek_types': [
            'normal = snek:normal_snek',
            'fancy = snek:fancy_snek',
       ],
    }
)

重新安装 snek:

$ cd snek_folder
$ python setup.py develop
running develop
running egg_info
writing snek.egg-info\PKG-INFO
writing dependency_links to snek.egg-info\dependency_links.txt
writing entry points to snek.egg-info\entry_points.txt
writing top-level names to snek.egg-info\top_level.txt
reading manifest file 'snek.egg-info\SOURCES.txt'
writing manifest file 'snek.egg-info\SOURCES.txt'
running build_ext
Creating c:\program files (x86)\py36-32\lib\site-packages\snek.egg-link (link to .)
snek 0.0.0 is already the active version in easy-install.pth
Installing snek-script.py script to C:\Program Files (x86)\Py36-32\Scripts
Installing snek.exe script to C:\Program Files (x86)\Py36-32\Scripts
Installing snek.exe.manifest script to C:\Program Files (x86)\Py36-32\Scripts

Installed c:\users\rachum\notebooks
Processing dependencies for snek==0.0.0
Finished processing dependencies for snek==0.0.0
$ snek
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`

$ snek --type fancy
                          _,..,,,_
                     '``````^~"-,_`"-,_
       .-~c~-.                    `~:. ^-.
   `~~~-.c    ;                      `:.  `-,     _.-~~^^~:.
         `.   ;      _,--~~~~-._       `:.   ~. .~          `.
          .` ;'   .:`           `:       `:.   `    _.:-,.    `.
        .' .:   :'    _.-~^~-.    `.       `..'   .:      `.    '
       :  .' _:'   .-'        `.    :.     .:   .'`.        :    ;
       :  `-'   .:'             `.    `^~~^`   .:.  `.      ;    ;
        `-.__,-~                  ~-.        ,' ':    '.__.`    :'
                                     ~--..--'     ':.         .:'
                                                     ':..___.:'

$ snek --type cute
                    /^\/^\
                  _|__|  O|
         \/     /~     \_/ \
          \____|__________/  \
                 \_______      \
                         `\     \                 \
                           |     |                  \
                          /      /                    \
                         /     /                       \
                       /      /                         \ \
                      /     /                            \  \
                    /     /             _----_            \   \
                   /     /           _-~      ~-_         |   |
                  (      (        _-~    _--_    ~-_     _/   |
                   \      ~-____-~    _-~    ~-_    ~-_-~    /
                     ~-_           _-~          ~-_       _-~ 
                        ~--______-~                ~-___-~

完整逻辑

总结一下,我们自己的团队开发了一款名为 snek 的软件,源代码包括如下:

snek
├── setup.py
└── snek.py

第三方开发者开发了一款名为 cute_snek 的软件,源代码包括如下:

cute_snek
├── cute_snek.py
└── setup.py

分别 cd 到两款软件源代码目录,用 python setup.py develop 方式,将源代码打包安装到当前系统内。由于两个软件的 setup.pyentry_points 都注册了名为 snek_types 的字段,Python 执行时会去系统中寻找所有注册过的值。虽然代码分散在不同的地方、不同的包,但是都可以被动态加载进来。

        'snek_types': [
            'normal = snek:normal_snek',
        ]

在上面这段 entry_points 注册代码中,共有两层,第一层:snek_types,第二层: cute / normal / fancy'normal = snek:normal_snek'snek:normal_snek 部分,冒号左侧是包名,冒号右侧为变量名或者方法名。

    for entry_point in pkg_resources.iter_entry_points('snek_types'):
        sneks[entry_point.name] = entry_point.load()

上面这段代码中的 for 循环会对所有 entry_points 进行注册和加载。 比如,for 循环中某个 entry_point 就是 'normal = snek:normal_snek'entry_point.load() 将包名为 snek 中的 normal_snek 变量加载进来。 'normal = snek:normal_snek' 符合如下规则:"<user_defined_key> = <python_package:object_name>"<user_defined_key> 是开发者定义的跟业务高度相关的 Key,<python_package:object_name> 为 Python 对象名(变量或者方法)。

有一种特殊的 entry_points ,名为 console_scripts。在这里注册的内容可以直接被集成为命令行工具。

        'console_scripts': [
            'snek = snek:main',
        ]

在上面这个例子中,'snek = snek:main' 等号左侧的 snek 是命令行工具的名字,等号右侧是“包名:包内对应的方法名”。 "<console_script_name> = <python_package:object_name>" 这样的模式,<console_script_name> 为生成的命令行工具的名字, <python_package:object_name> 为 Python 对象名(变量或者方法)。

应用场景

那到底实际有哪些应用场景?

著名科学数据分享平台 zenodoopen in new window 是在 Python Web 框架 Flask 下继续进行开发的,它由很多个包组成。在开发时,就大量使用了 entry_points 来注册具有相互继承关系的包之间需要同时注册的变量或者方法。

Loading...