目录


摘要[返回到目录]

项目管理的主要工作是:

下面的介绍,也是以这几方面为主的。


Setuptools简介[返回到目录]

Setuptoolsdisutils(python2.6+)的增强版,它使得开发者可以更容易地构建和分发python包,尤其是依赖其他包的包。
对用户来说,使用Setuptools构建和分发的包看起来就像普通的基于distuils的python包。为了使用Setuptools,用户不必安装或了解它们,开发者也不必在发行版中包含全部的Setuptools工具。在用户没有安装Setuptools的情况下,只要源码包中包含bootstrap模块(一个12K的.py文件),那么构建源码包的时候,就会自动下载和安装Setuptools
功能亮点


安装setuptools[TOC]

请按照EasyInstall安装说明来安装setuptools。需要特别强调的是,使用easy_install的时候,如果想要把包安装到Python的site-packages目录之外的地方,那么可以参考下面的自定义安装位置区域。

自定义安装位置:
默认情况下,EasyInstall会把包安装到Python的主site-packages目录,然后通过在该目录下自定义一个.pth文件,来管理包。
但是经常有这样的需求:用户或开发者想使用easy_install将包安装到site-packages之外的地方。
有许多方式来实现自定义安装,下面列出了最简单和最有意义的几种方式:

补充说明:

python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()"

获取site-packages的路径


基本使用[TOC]

下面是一个最小化的setup脚本:

from setuptools import setup, find_packages
setup(
    name = "HelloWorld", #包名
    version = "0.1", # 版本
    packages = find_packages(), # 要包含的子包列表
)

可以看到,通过使用setuptools,不需要做太多的事情,项目就能够生成egg,上传到PYPI,自动地包含setup.py所在的目录中的所有的包。
当然,在把项目发布到PYPI之前,可以给setup脚本添加一些metadata,来帮助使用者发现和了解项目。并且项目可能包含一些依赖,一些数据文件脚本

from setuptools import setup, find_packages
setup(
    name = "HelloWorld", # 项目名
    version = "0.1", # 版本号
    packages = find_packages(),
    scripts = ['say_hello.py'],

    # Project uses reStructuredText, so ensure that the docutils get
    # installed or upgraded on the target machine
    install_requires = ['docutils>=0.3'],

    package_data = {
        # If any package contains *.txt or *.rst files, include them:
        '': ['*.txt', '*.rst'],
        # And include any *.msg files found in the 'hello' package, too:
        'hello': ['*.msg'],
    },

    # metadata for upload to PyPI
    author = "Me",
    author_email = "me@example.com",
    description = "This is an Example Package",
    license = "PSF",
    keywords = "hello world example examples",
    url = "http://example.com/HelloWorld/",   # project home page, if any

    # could also include long_description, download_url, classifiers, etc.
)

常用命令[TOC]

1,常规命令

2,上传到PYPI
Setuptools和Distutils提供了registerupload命令,向PyPI推送元数据(<project_name>.egg-info/目录下的文件)和发行版文件(dist/目录下的文件)。为了将包发布到PyPI,需要执行下面的操作:

[distutils]
index-servers =
    pypi
    other

[pypi]
repository: https://upload.pypi.org/legacy/
username: <username>
password: <password>

[other]
repository: <repository-url>
username: <username>
password: <password>
python setup.py register -r pypi # -r 用来指定使用哪一个PyPI源
python setup.py sdist bdist upload -r pypi # -r 用来指定使用哪一个PyPI源

列出全部的包[返回到目录]

packages参数用于指定一个包名的列表,Setuptools会处理(构建、发布、安装等)该列表列出的每一个包中的所有纯Python模块(如果想要处理某个包的子包,应该在packages中列出该子包,比如packages=["foo", "foo.bar"])。
package_dir参数用于将包名映射到文件系统的一个目录上。其值是一个字典,字典的key是包名(Setuptools会自动地将父包的映射应用到子包上,因此无需在package_dir参数中指定子包的映射关系),value是相对于项目根目录(setup.py所在的目录)的目录。当包名为空时,value所指定的目录代表“包的根目录”,Setuptools会去“包的根目录”下寻找所有未指定映射关系的包。比如:

setup(
    ...
    packages=["foo", "foo.bar", "baz"],
    package_dir={
        "foo": "lib",
        "": "src",
    },
    ...
)

在上面的例子中,Setuptools会将lib/目录当作包foo,lib/bar/目录当作包foo.bar,因此会寻找lib/__init__.pylib/bar/__init__.py
包的根目录是src/,Setuptools会将src/baz/目录当作包baz,因此会寻找src/baz/__init__.py
(之所以寻找__init__.py文件,是因为只有包含__init__.py文件的目录才是一个合法的Python包。)

对于简单的项目,人工地向setup()packages参数添加包名就足够了。然而对于大型项目,保持包名列表的更新是一个非常大的负担。为了解决这个麻烦,可以通过setuptools.find_packages()函数来生成包名列表。
find_packages()的第一个参数是源代码目录。如果省略了该参数,那么它的缺省值是setup.py所在的目录。对于使用src/或lib/目录作为源代码树的根目录的项目,应该使用"src"或"lib"作为find_packages()的第一个参数(这类项目仍然需要指定setup()package_dir参数,比如package_dir={"": "src"})。
find_packages()的另外两个参数是两个 包名模式 列表:

find_packages()会递归地遍历源代码目录,寻找Python包,并使用 包含模式列表 过滤。最终,再移除掉被 排除模式列表 匹配的包。
在包含模式和排除模式中,可以包含通配符。比如find_packages(exclude=["*.tests"])会排除掉所有名称的最后一部分是tests的包;find_packages(exclude=["*.tests", "*.tests.*"])会排除掉任何名称包含tests的子包。
之所以,在大多数情形下,使用find_packages()来设置setup()package参数,是因为:即使项目增加了顶级包或子包,也不必去修改setup脚本。


列出单独的模块[返回到目录]

可以通过py_modules参数列出要处理的所有纯Python模块。这些模块既可以在“包的根目录”下,也可以在某个包中。比如:

from setuptools import setup

setup(
    ...
    py_modules=["mod1", "pkg.subpkg.mod2"],
    package_dir={"": "src"}, # 包的根目录是src/
    ...
)

在上面的例子中,Setuptools会去src/目录下寻找mod1.py,src/pkg/subpkg/目录下寻找mod2.py。


描述扩展模块[返回到目录]

TODO

安装“包数据”[返回到目录]

通常情况下,需要将一些文件安装到包中,这些文件通常是跟包的实现紧密相关的数据 或者 是包含说明文档的文本文件。这些文件被称为“包数据”。
setup()中,跟“包数据”相关的参数有:

include_package_data:
如果将include_package_data设置为True,那么Setuptools会 自动地安装 包目录下的所有数据文件,这些数据文件必须在CVS或Subversion的控制之下,或者必须通过distutils的MANIFEST.in文件指定它们。
关于MANIFEST.in文件的详细介绍,可以参考官网。下面做一个简要的说明。
项目中的MANIFEST.in文件主要用来定义在发行版中包含哪些文件,MANIFEST.in中的每行是一个命令,这个命令用来指定包含或排除哪些文件。比如:

include *.txt
recursive-include examples *.txt *.py
prune examples/sample?/build

能够出现在清单文件MANIFEST.in中的命令包括:

命令描述
include pat1 pat2 ...包含名称匹配列表中任意一个模式的所有文件
exlude pat1 pat2 ...排除名称匹配列表中任意一个模式的所有文件
recursive-include dir pat1 pat2 ...递归地包含dir目录下,名称匹配列表中的任意一个模式的所有文件
recursive-exclude dir pat1 pat2 ...递归地排除dir目录下,名称匹配列表中的任意一个模式的所有文件
prune dir排除dir下的所有文件
graft dir包含dir下的所有文件

package_data:
如果数据文件不在VCS控制之下,或者在一个不被支持的VCS控制之下,或者想要细粒度地控制包含哪些文件,那么需要使用package_data关键字参数,比如:

from setuptools import setup, find_packages
setup(
    ...
    package_data = {
        # 包含任意包里的扩展名为.txt和.rst的所有文件:
        '': ['*.txt', '*.rst'],
        # 包含hello包中所有扩展名为.msg的文件:
        'hello': ['*.msg'],
    }
)

package_data参数是 一个 映射 包名称 到 glob模式列表 的字典。如果数据文件在包的子目录中,glob模式可以包含子目录名称。比如,包目录树如下:

setup.py
src/
    mypkg/
        __init__.py
        mypkg.txt
        data/
            somefile.dat
            otherdata.dat

那么setuptools setup文件,可能是这样的:

from setuptools import setup, find_packages
setup(
    ...
    packages = find_packages('src'),  # 包含src下的所有包及其子包
    package_dir = {'':'src'},   # 设置“包的根目录”为src/

    package_data = {
        # 包含任意包中,所有扩展名为.txt的文件:
        '': ['*.txt'],
        # 包含mypkg包的data/子目录中的、所有扩展名为.dat的文件:
        'mypkg': ['data/*.dat'],
    }
)

注意:在package_data参数中,当包名称为空时,那么对应的模式列表会被应用到所有的包(即使这些包列出了自己的模式),因此在上面的例子中,mypkg.txt虽然没在mypkg的模式中列出,仍然会被包含。
也要注意:如果使用路径,那么必须使用/作为路径分隔符(即使是在windows系统上),Setuptools在build期间会自动的把斜线转换成平台相关的分隔符。
有时,单独使用include_package_datapackage_data参数,并不能精确地定义包含哪些文件。比如:想要在版本控制系统和源代码中包含README.txt文件,但是在安装的时候不包含它们。对此,Setuptools也提供了exclude_package_data参数,其用法如下:

from setuptools import setup, find_packages
setup(
    ...
    packages = find_packages('src'),  # include all packages under src
    package_dir = {'':'src'},   # tell distutils packages are under src

    include_package_data = True,    # include everything in source control

    # !!!排除所有包中的README.txt!!!
    exclude_package_data = { '': ['README.txt'] },
)

package_data参数一样,exclude_package_data也是一个映射包名称到glob模式列表的字典,在使用这个选项的时候,""的键会把给定的模式列表应用到所有的包,匹配这些模式的任何文件在安装的时候都会被排除掉,即使这些文件在package_data中被列了出来 或者 被include_package_data包含了进来。
总的来说,可以通过下面三个选项来包含“包数据”:

注意:


zip_safe标记以及pkg_resources的资源访问API[返回到目录]

setuptools既支持将python项目安装为一个zip文件,也支持将其安装为一个目录。如果有访问包中的数据文件或源代码的需求,那么可以将Python项目安装为一个目录。比如:

from setuptools import setup, find_packages

setup(
    name="test_setuptools_1",
    version="0.0.1a",
    py_modules=["a"],
    zip_safe = False # 当zip_safe标记为False的时候,项目会被安装为一个目录
)

在这种情形下,可以通过操作包或模块的__file__属性,来寻找数据文件。比如,某个模块需要访问它所在目录下的foo.config文件:

foo_config = open(os.path.join(os.path.dirname(__file__), "foo.config")).read()

但是在基于PEP 302的导入钩子中,支持从zip文件和egg文件,导入包或模块。在这种情况下,就无法使用__file__属性来定位数据文件了。此时应该使用PEP 302的get_data()扩展。但是,使用导入协议非常复杂,因此egg运行时系统提供了pkg_resources资源管理API,用于访问数据文件。pkg_resources模块,也是Setuptools的一部分。
在使用pkg_resources的时候,只需要把包名或模块名 和 资源文件名 一起传递给resource_stringresource_streamresource_filename函数。并且应该尽量使用resource_stringresource_stream,不是使用resource_filename,因为它需要将资源文件抽取到临时目录中,这比返回文件内容或文件句柄代价更大。
当资源文件被包含在包的子目录中的时候,那么无论在哪个操作系统平台,在资源名称中都必须使用"/"作为分隔符。


安装非包数据文件[返回到目录]

我们把不在包中的数据文件称为非包数据文件。在distutils中,可以通过data_files参数,将不在包中的普通数据文件安装到特定平台的某个位置(比如/usr/share)。这些数据文件,通常是配置文件模版、文档等。
比如:

from distutils.core import setup

setup(...,
      data_files=[('bitmaps', ['bm/b1.gif', 'bm/b2.gif']),
                  ('config', ['cfg/data.cfg']),
                  ('/etc/init.d', ['init-script'])]
     )

data_files的值是一个列表,列表中的每个元素都是一个二元组,二元组的第一元是将要安装到的位置,第二元是数据文件列表。当要安装到的位置是一个相对位置时,那么它是相对sys.prefix的。
然而,Setuptools是把数据文件绑定到egg文件或目录中的。注意:这是一个特性,而不是bug。
在Setuptools中,当data_files的某个元素的第一元是相对路径的时候,那么它是相对发行版的根目录的pkg_resources模块的资源管理API也支持在某个包中,访问其他包中的数据文件,只需要使用Requirement代替包名,比如:

from pkg_resources import Requirement, resource_filename
filename = resource_filename(Requirement.parse("MyProject"),"sample.conf")

在上面的这个例子中,会返回“MyProject”发行版的根目录下的“simple.conf”的文件名。


定义依赖[返回到目录]

在安装包的时候,Setuptools会自动地安装依赖。跟依赖有关的信息,会包含在PythonEgg的元数据中。
对其他模块或包的依赖,可以通过setup()函数的install_requires关键字参数来指定。其值必须是一个字符串列表,每个字符用于指定一个被依赖的包,同时也可以指定一个版本。
当只使用名称,而不指定版本的时候,表示发行版可以依赖任意版本的模块或包。当依赖特定版本的包或模块的时候,可以在包名后面指定若干个修饰符,每个修饰符中包含一个比较操作符和一个版本号,可用的修饰符包括:

<    >    ==
<=   >=   !=

多个修饰符之间使用逗号进行分隔。这些修饰符之间是“AND”的关系,也就是所有的修饰符都应该被满足。比如:

修饰符含义
==1.0只兼容1.0版本
>1.0, !=1.5.1, <2.0兼容 1.0到2.0之间,除去1.5.1 的任意版本

项目也可以依赖不在PyPI上的包和模块,此时可以通过dependency_links参数指定一个URL列表,Setuptools会搜索这些页面,来下载egg包源发行版。比如:

setup(
    ...
    dependency_links=[
        "http://peak.telecommunity.com/snapshots/"
    ],
)

创建脚本[返回到目录]

在distutils中,可以通过setup()函数的scripts关键字参数,来指定要安装的脚本。比如:

[root@iZj6chejzrsqpclb7miryaZ install_scripts]# tree .
.
├── install_scripts
│   └── __init__.py
├── scripts
│   └── a
└── setup.py

2 directories, 3 files
[root@iZj6chejzrsqpclb7miryaZ install_scripts]# cat setup.py 
from setuptools import setup, find_packages

setup(
    name="install_scripts",
    version="0.1a1",
    packages=find_packages(),
    scripts=["scripts/a"],

    author="TimChow",
    author_email="744475502@qq.com",
    url="http://timd.cn/setuptools/"
)

但是这样做有点“笨拙”。比如,当真正运行的“主函数”是某个模块中的某个函数的时候,也必须为它创建一个单独的脚本文件。
使用Setuptools可以自动地生成脚本;在Windows上,还会创建一个.exe文件,因此用户不必改变PATHEXT设置。为了使用Setuptools的这个特性,需要在setup脚本中,定义入口点(entry points),比如下面的例子,会创建两个控制台脚本:foo、bar 和 一个GUI脚本:baz:

setup(
    # other arguments here...
    entry_points={
        'console_scripts': [
            'foo = my_package.some_module:main_func',
            'bar = other_module:some_func',
        ],
        'gui_scripts': [
            'baz = my_package_gui:start_func',
        ]
    }
)

当项目被安装到 非Windows平台 上的时候,Setuptools会创建foo、bar、baz脚本。执行foo脚本时,会调用my_package.some_modulemain_func;执行bar脚本时,会调用other_modulesome_func;执行baz脚本时,会调用my_package_guistart_func。并且这些函数是以无参的形式被调用的(在函数中可以通过命令选项或环境变量的方式接受参数),它们的返回值会被传递给sys.exit()
在Windows平台上,会创建foo.exebar.exebaz.exe启动器。.exe包装会找到合适的Python解释器,并使用它执行.py.pyw文件。

有时,某些项目有一些非必须的依赖,比如一个项目在ReportLab库被安装的时候,才提供PDF输出;或者在docutils库被安装的时候,才支持reStructuredText。这种特性被称为“extra”。在Setuptools中,可以通过setup()函数的extras_require关键字参数,来指定可选的依赖。同时,也可以通过install_requires关键字参数,强制 被依赖的项目 安装它的某些extra。比如:

setup(
    name="Project-A",
    ...
    extras_require={
        'PDF':  ["ReportLab>=1.2", "RXP"],
        'reST': ["docutils>=0.3"],
    }
)

extras_require关键字参数的值是一个映射 extra特性名称 到 字符串列表 的字典。列表中的每个字符串描述了一个可选的依赖。这些可选依赖不会被自动安装,除非直接或间接依赖该项目的其他项目强制要求安装(指定依赖时,在项目名后面的方括号中列出要强制安装的extra);使用EasyInstall安装包的时候,也可以用这种方式指定要强制安装的extra。比如:

setup(
    name="Project-B",
    install_requires=["Project-A[PDF]"],
    ...
)

假设,有朝一日,Project-A对PDF的支持,不再需要ReportLab了,那么可以将PDF的值设置为空列表,这样就无需更改Project-B的setup信息了:

setup(
    name="Project-A",
    ...
    extras_require={
        'PDF':  [],
        'reST': ["docutils>=0.3"],
    }
)

entry points中,可以使用extra特性,比如:

setup(
    name="Project-A",
    ...
    entry_points={
        'console_scripts': [
            'rst2pdf = project_a.tools.pdfgen [PDF]',
            'rst2html = project_a.tools.htmlgen',
            # more script entry points ...
        ],
    }
)

在上面的例子中,Project-A包含一个“rst2pdf”脚本,PDF依赖只有在“rst2pdf”脚本运行时,才会被解析。


动态发现服务和插件[返回到目录]

Setuptools支持通过注册入口点(entry point)的方式,动态地发现服务和插件。比如一个博客工具支持多种输出插件,每个输出插件可以将博客内容输出成一种格式。此时,可以定义一个叫blogtool.parsers入口点组,然后把插件注册为一个入口点。在运行时,博客工具可以通过查找入口点,找到对应的输出插件,比如,下面的例子是将.rst输出插件注册到blogtool.parsers组:

setup(
    # ...
    entry_points={'blogtool.parsers': '.rst = some_module:SomeClass'}
)

setup(
    # ...
    entry_points={'blogtool.parsers': ['.rst = some_module:a_func']}
)

setup(
    # ...
    entry_points="""
        [blogtool.parsers]
        .rst = some.nested.module:SomeClass.some_classmethod [reST]
    """,
    extras_require=dict(reST="Docutils>=0.3.5")
)

setup()函数的entry_points参数的值要么是.ini风格的字符串,要么是映射 入口点组名 到 字符串或字符串列表 的字典,其中,每个字符串都是一个入口点说明符,入口点说明符包含名称和值,名称和值之间用等号分隔。值包含点分隔的模块名;模块名后面是一个冒号,冒号后面是点分隔的、出现在模块中的一个对象名;接下来是放在方括号中的extra列表,(这部分是可选的)。在程序加载入口点的时候,任何通过extra指定的依赖,都会被传递给pkg_resources.require()函数,因此当被依赖的包缺失时,会打印相应的错误信息。
下面看一个例子:

[root@iZj6chejzrsqpclb7miryaZ discovery_plugins]# tree .
.
├── discovery_plugins
│   └── __init__.py
└── setup.py

1 directory, 2 files
[root@iZj6chejzrsqpclb7miryaZ discovery_plugins]# cat setup.py 
from setuptools import setup, find_packages

setup(
    name="discovery_plugins",
    version="0.1a1",
    packages=find_packages(),
    zip_safe=False,

    entry_points={
        "discovery_plugins.test_out": [".test_out1=test_plugins:test_print"],
    },

    author="TimChow",
    author_email="744475502@qq.com",
    url="http://timd.cn/setuptools/",
)
[root@iZj6chejzrsqpclb7miryaZ discovery_plugins]# cat discovery_plugins/__init__.py 
# coding: utf8

from pkg_resources import iter_entry_points

for entry_point in iter_entry_points(group='discovery_plugins.test_out', name=None):
    print(entry_point)

    # 加载入口点
    fun = entry_point.load()

    fun("a nice day")

假设test_plugins.py,已经在sys.path下:

def test_print(*a, **kw):
    print("you say:", a, kw)

那么,在导入discovery_plugins包的时候,会打印如下信息:

[root@iZj6chejzrsqpclb7miryaZ ~]# python
Python 2.7.5 (default, Aug  4 2017, 00:39:18) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import discovery_plugins
.test_out1 = test_plugins:test_print
('you say:', ('a nice day',), {})
>>> 

可执行的egg文件[返回到目录]

偶尔,有想要使一个.egg文件可以直接执行的情况。可以通过包含如下所示的 入口点(entry point) 来完成这件事:

setup(
    # other arguments here...
    entry_points = {
        'setuptools.installation': [
            'eggsecutable = my_package.some_module:main_func',
        ]
    }
)

任何通过上面的setup脚本构建而来的egg文件,都会包含一个简短的头:从my_package.some_module导入并调用main_func(),在unix-like平台上可以通过/bin/sh或赋予这个.egg文件可执行权限的方式,来执行这个egg文件。
这个特性的主要意图是支持在非Windows平台上安装Setuptools自己。值得注意的是:带有eggsecutable头的egg文件不能重命名,也不能通过符号链接来执行,否则会以错误退出。


集成单元测试[返回到目录]

在进行测试驱动开发时,或者运行自动化构建之前都需要执行单元测试。可以使用test命令在部署之前,运行项目的单元测试。为了使用这个命令,项目的单元测试必须被封装进一个unittest的TestSuite中。比如:

[root@iZj6chejzrsqpclb7miryaZ install_scripts]# tree .
.
├── install_scripts
│   ├── __init__.py
│   ├── __init__.pyc
│   ├── test.py
│   └── tests
│       ├── __init__.py
│       ├── __init__.pyc
│       ├── test_1.py
│       └── test_1.pyc
├── scripts
│   └── a
└── setup.py

3 directories, 9 files
[root@iZj6chejzrsqpclb7miryaZ install_scripts]# cat setup.py 
from setuptools import setup, find_packages

setup(
    name="install_scripts",
    version="0.1a1",
    packages=find_packages(),
    scripts=["scripts/a"],
    zip_safe=False,

    entry_points={
        'setuptools.installation': [
            'eggsecutable = install_scripts.test:main',
        ]
    },

    test_suite="install_scripts.tests",

    author="TimChow",
    author_email="744475502@qq.com",
    url="http://timd.cn/setuptools/"
)
[root@iZj6chejzrsqpclb7miryaZ install_scripts]# cat install_scripts/tests/test_1.py
from unittest import TestCase

class TestCase1(TestCase):
    def testTest1(self):
        print "testTest1 in test_1:TestCase1"
[root@iZj6chejzrsqpclb7miryaZ install_scripts]# python setup.py test
running test
running egg_info
creating install_scripts.egg-info
writing install_scripts.egg-info/PKG-INFO
writing top-level names to install_scripts.egg-info/top_level.txt
writing dependency_links to install_scripts.egg-info/dependency_links.txt
writing entry points to install_scripts.egg-info/entry_points.txt
writing manifest file 'install_scripts.egg-info/SOURCES.txt'
reading manifest file 'install_scripts.egg-info/SOURCES.txt'
writing manifest file 'install_scripts.egg-info/SOURCES.txt'
running build_ext
testTest1 (install_scripts.tests.test_1.TestCase1) ... testTest1 in test_1:TestCase1
ok

----------------------------------------------------------------------
Ran 1 test in 0.000s
OK

如上所示,当test_suite是一个包的时候,Setuptools会递归地把该包下所有的模块和子包中的测试用例加载到整个TestSuite中(如果通过test_loader关键字指定了TestLoader,那么这个处理规则可能会发生改变)。


参考资料[返回到目录]