日拱一卒之:Python发布工具setuptools的用法

作为Python标准的打包及分发工具,setuptools可以说相当地简单易用。它会随着Python一起安装在你的机器上。你只需写一个简短的setup.py安装文件,就可以将你的Python应用打包。本文就会介绍下如何编写安装文件及如何打包分发

首先,我们安装setuptools可以使用

1
2
3
4
#找到setuptools的下载页面:https://pypi.python.org/pypi/setuptools 并下载
#解压
#cd 到解压后的文件夹内
#执行 python setup.py install 即可

tips:如何在Mac中使用目录树

Mac下我们有时需要用到Linux下的tree命令去显示文件树,但是Mac又没有这个命令,于是我们需要安装一个tree包。

首先安装tree包

1
brew install tree

使用方法

1
2
3
>tree #显示当前目录的目录树
>tree -L n #显示当前目录下的n级目录树
>tree -L n >README.md #将目录树输出为文件

最简单的安装文件

新建一个setuptools的文件夹,他的目录形式如下

1
2
3
4
setuptools
├── MyApp
│   └── __init__.py
└── setup.py

一个最基本的setup.py的文件内容如下

1
2
3
4
5
6
7
8
9
#encoding:--utf8--
from setuptools import setup
setup(
name = 'MyApp',#应用名字
version = '1.0', #版本号
packages = ['MyApp'] #包括在安装包内的Python包(这里就是我们要装的MyApp这个包)
)

执行安装

  1. 创建egg包

现在最简单的安装文件已经有了,现在要执行安装,需要如下的命令

1
$ python setup.py bdist_egg

这个命令会在当前的主目录setuptools_test中建立一个文件夹dist,里面包含创建的egg文件,名字是MyApp-1.0-py2.7.egg,格式是应用的名字(你要发布的应用的名字,不一定和你的内部包名一致)-版本号-对应的Python版本.egg。同时,会在文件夹内,建立两个其他文件夹,分别是buildMyApp.egg-info,这两个文件夹存放打包的中间结果。

  1. 创建tar.gz包
1
$ python setup.py sdist --formats=gztar

同上例类似,只不过创建的文件类型是tar.gz,文件名为”MyApp-1.0.tar.gz”。

  1. 安装应用
  • 1
    2
    $ python setup.py install
    #该命令会将当前的Python应用安装到当前Python环境的”site-packages”目录下,这样其他程序就可以像导入标准库一样导入该应用的代码了。
  • 1
    2
    $ python setup.py develop
    #如果应用在开发过程中会频繁变更,每次安装还需要先将原来的版本卸掉,很麻烦。使用”develop”开发方式安装的话,应用代码不会真的被拷贝到本地Python环境的”site-packages”目录下,而是在”site-packages”目录里创建一个指向当前应用位置的链接。这样如果当前位置的源码被改动,就会马上反映到”site-packages”里。

安装之后要卸载的话,可以使用pip进行卸载

1
$ pip uninstall 应用名字 #此时的应用名字就是我们setup()函数中的name

此时的目录树如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
setuptools_test
├── MyApp
│   ├── __init__.py
│   ├── __init__.pyc
│  
├── MyApp.egg-info #打包生成的
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   └── top_level.txt
├── build #打包生成的
│   ├── bdist.macosx-10.13-x86_64
│   └── lib
│   └── MyApp
│   └── __init__.py
├── dist
│   └── MyApp-1.0-py2.7.egg
└── setup.py

引入非Python文件(不是.py的文件)

上例中,我们只会将”MyApp”包下的源码打包,如果我们还想将其他非Python文件也打包,比如静态文件(JS,CSS,图片),应该怎么做呢?这时我们要在项目目录下添加一个”MANIFEST.in”文件夹。假设我们把所有静态文件都放在”static”子目录下,现在的项目结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setuptools_test
├── MANIFEST.in #清单文件
├── MyApp #源代码
│   ├── __init__.py
│   ├── __init__.pyc
│   └── static #静态目录
├── MyApp.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   └── top_level.txt
├── build
│   ├── bdist.macosx-10.13-x86_64
│   └── lib
│   └── MyApp
│   └── __init__.py
├── dist
│   └── MyApp-1.0-py2.7.egg
└── setup.py #安装文件

此时,我们可以在MANIFEST.in中李处想要在包内引入的目录的路径

1
2
recursive-include MyApp/static *
#其他的文件夹的引入形式一样

然后在”setup.py”中将” include_package_data”参数设为True:

1
2
3
4
5
6
7
8
9
10
#encoding:--utf8--
from setuptools import setup
setup(
name = 'MyApp',#应用名字
version = '1.0', #版本号
packages = ['MyApp'], #包括在安装包内的Python包(这里就是我们要装的MyApp这个包)
include_package_data = True
)

之后再次打包或者安装,”myapp/static”目录下的所有文件都会被包含在内。如果你想排除一部分文件,可以在setup.py中使用”exclude_package_date”参数,比如:

1
2
3
4
5
setup(
...
include_package_data=True, # 启用清单文件MANIFEST.in
exclude_package_date={'':['.gitignore']}
)

上面的代码会将所有”.gitignore”文件排除在包外。如果上述”exclude_package_date”对象属性不为空,比如”{‘MyApp’:[‘.gitignore’]}”,就表明只排除”MyApp”包下的所有”.gitignore”文件。

自动安装依赖包

我们的应用会依赖于第三方的Python包,虽然可以在说明文件中要求用户提前安装依赖包,但毕竟很麻烦,用户还有可能装错版本。其实我们可以在setup.py文件中指定依赖包,然后在使用setuptools安装应用时,依赖包的相应版本就会被自动安装。让我们来修改上例中的setup.py文件,加入”install_requires”参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#coding:utf8
from setuptools import setup
setup(
name='MyApp', # 应用名
version='1.0', # 版本号
packages=['myapp'], # 包括在安装包内的Python包
include_package_data=True, # 启用清单文件MANIFEST.in
exclude_package_date={'':['.gitignore']},
install_requires=[ # 依赖列表
'Flask>=0.10',
'Flask-SQLAlchemy>=1.5,<=2.1'
]
)

上面的代码中,我们声明了应用依赖Flask 0.10及以上版本,和Flask-SQLAlchemy 1.5及以上、2.1及以下版本。setuptools会先检查本地有没有符合要求的依赖包,如果没有的话,就会从PyPI中获得一个符合条件的最新的包安装到本地。

大家可以执行下试试,你会发现不但Flask 0.10.1(当前最新版本)被自动安装了,连Flask的依赖包Jinja2和Werkzeug也被自动安装了,很方便吧。

自动搜索Python包进行安装

之前我们在setup.py中指定了”packages=[‘MyApp’]”,说明将Python包”MyApp”下的源码打包。如果我们的应用很大,Python包很多怎么办。大家看到这个参数是一个列表,我们当然可以将所有的源码包都列在里面,但肯定很多人觉得这样做很傻。的确,setuptools提供了”find_packages()”方法来自动搜索可以引入的Python包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#coding:utf8
from setuptools import setup, find_packages
setup(
name='MyApp', # 应用名
version='1.0', # 版本号
packages=find_packages(), # 包括在安装包内的Python包
include_package_data=True, # 启用清单文件MANIFEST.in
exclude_package_date={'':['.gitignore']},
install_requires=[ # 依赖列表
'Flask>=0.10',
'Flask-SQLAlchemy>=1.5,<=2.1'
]
)

这样当前项目内所有的Python包都会自动被搜索到并引入到打好的包内。”find_packages()”方法可以限定你要搜索的路径,比如使用”find_packages(‘src’)”就表明只在”src”子目录下搜索所有的Python包。

Setuptools 的setup()函数的所有参数的介绍

1
2
3
4
5
6
7
8
9
10
11
name: 即项目名称,本例为 MyApp
version: 即版本号,关于版本号的取法请见 Choosing a versioning scheme
keywords: 描述项目的关键字
description: 项目简介
long_description: 项目详细介绍
author: 作者名称
author_email: 作者邮箱
url: 项目的 homepage
packages: 项目包括的 python package,setuptools.find_packages() 可自动找出包含的 package
license: 如 MIT, APACHE, GNU 等
classifiers: 它包含 package 的成熟度,类型,以及支持的平台等信息
1
2
3
4
install_requires=[ # 依赖列表,用于添加我们要建立的包的依赖库
'Flask>=0.10',
'Flask-SQLAlchemy>=1.5,<=2.1'
]
1
packages=find_packages(), # 包括在安装包内的Python包,可以使用参数exclude=["tests.*", "tests"]的方式来除去一些不安装的包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#配置CLI,即提供命令行的入口
entry_points={
'console_scripts': [
'redyna-sync = dock.redynadb.sync:run',
'redyna-stats = dock.redynadb.stats:run',
'reds3-sync = dock.reds3.sync:run',
'dock-server = dock.common.run:run_server',
'dock-shell = dock.common.run:run_shell',
#setup时令dock-tasklet指向dock.common.tasklet:run
'dock-tasklet = dock.common.tasklet:run']}
#这个参数提供我们一个动态发现服务和插件的方法
#The most commonly used entry point is “console_scripts”
#'console_scripts'指明了命令行工具的名称,
#Use “console_script” entry points to register your script interfaces. You can then let #the toolchain handle the work of turning these interfaces into actual scripts [2]. The #scripts will be generated during the install of your distribution.
------
#例子:dock-server就是在命令行可以输入的命令,dock.common.run:run_server中dock.common.run是对应的程序入口地址,run_server是执行的函数。
1
2
zip_safe=False
#决定应用是否作为一个zip压缩后的egg文件安装在当前Python环境中,还是作为一个以.egg结尾的目录安装在当前环境中。因为有些工具不支持zip压缩文件,而且压缩后的包也不方便调试,所以建议将其设为False:”zip_safe=False”。