python 打包

前言

其实对于 setup.pysetup.cfg 的关注是从 OpenStack 的源码包中开始的,OpenStack 每个组件的发布时都是一个 tar.gz 包,同样,我们直接从 github 上 clone 代码后也会发现两个文件的存在。当阅读 Nova 或 Ceilometer(其他组件可能也会涉及)的代码时,发现 setup.cfg 中内容对于代码的理解有很大的影响。那么,到底 setup.pysetup.cfg 是干什么的?

setup.py

我们从例子开始。假设你要分发一个叫 foo 的模块,文件名 foo.py,那么 setup.py 内容如下:

from distutils.core import setup
setup(name='foo',
      version='1.0',
      py_modules=['foo'],
      )   

然后,运行python setup.py sdist为模块创建一个源码包

root@network:/kong/setup# python setup.py sdist
running sdist
running check
warning: check: missing required meta-data: url
warning: check: missing meta-data: either (author and author_email) or (maintainer and maintainer_email) must be supplied
warning: sdist: manifest template 'MANIFEST.in' does not exist (using default file list)
warning: sdist: standard file not found: should have one of README, README.txt
writing manifest file 'MANIFEST'
creating foo-1.0
making hard links in foo-1.0...
hard linking foo.py -> foo-1.0
hard linking setup.py -> foo-1.0
creating dist
Creating tar archive
removing 'foo-1.0' (and everything under it)

在当前目录下,会创建 dist 目录,里面有个文件名为 foo-1.0.tar.gz ,这个就是可以分发的包(如果使用命令 python setup.py bdist_egg ,那么会在 dist 目录中生成 foo-1.0-py2.7.egg 包,setup.py 中第一句引入需要改为 from setuptools import setup )。使用者拿到这个包后,解压,到 foo-1.0 目录下执行: python setup.py install ,那么,foo.py 就会被拷贝到 python 类路径下,可以被导入使用(如果安装是 egg 文件,会把 egg 文件拷贝到 dist-packages 目录下)。

对于 Windows,可以执行python setup.py bdist_wininst生成一个 exe 文件;若要生成 RPM 包,执行python setup.py bdist_rpm,但系统必须有 rpm 命令的支持。可以运行下面的命令查看所有格式的支持:

root@network:/kong/setup# python setup.py bdist --help-formats
List of available distribution formats:
  --formats=rpm      RPM distribution
  --formats=gztar    gzip'ed tar file
  --formats=bztar    bzip2'ed tar file
  --formats=ztar     compressed tar file
  --formats=tar      tar file
  --formats=wininst  Windows executable installer
  --formats=zip      ZIP file
  --formats=msi      Microsoft Installer

执行 sdist 命令时,默认会打包哪些东西呢?

还有一种方式是写一个 manifest template,名为MANIFEST.in,定义如何生成 MANIFEST 文件,内容就是需要包含在分发包中的文件。一个 MANIFEST.in 文件如下:

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

setup.cfg

setup.cfg 提供一种方式,可以让包的开发者提供命令的默认选项,同时为用户提供修改的机会。对 setup.cfg 的解析,是在 setup.py 之后,在命令行执行前。

setup.cfg 文件的形式类似于

[command]
option=value
...

其中,command是 Distutils 的命令参数,option是参数选项,可以通过python setup.py –help build_ext方式获取。

需要注意的是,比如一个选项是–foo-bar,在 setup.cfg 中必须改成 foo_bar 的格式

符合 Distutils2 的 setup.cfg 有些不同。包含一些 sections: 1、global 定义 Distutils2 的全局选项,可能包含 commands,compilers,setup_hook(定义脚本,在 setup.cfg 被读取后执行,可以修改 setup.cfg 的配置,pbr 就用到了这个) 2、metadata 3、files

Setuptools

上面的 setup.py 和 setup.cfg 都是遵循 python 标准库中的 Distutils,而 setuptools 工具针对 Python 官方的 distutils 做了很多针对性的功能增强,比如依赖检查,动态扩展等。很多高级功能我就不详述了,自己也没有用过,等用的时候再作补充。详情可参见这里。

一个典型的遵循 setuptools 的脚本:

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'],
        # 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.
)

管理依赖

我们写依赖声明的时候需要在 setup.py 中写好抽象依赖(install_requires),在 requirements.txt 中写好具体的依赖,但是我们并不想维护两份依赖文件,这样会让我们很难做好同步。 requirements.txt 可以更好地处理这种情况,我们可以在有 setup.py 的目录里写下一个这样的 requirements.txt

--index https://pypi.python.org/simple/
-e .

这样 pip install -r requirements.txt 可以照常工作,它会先安装该文件路径下的包,然后继续开始解析抽象依赖,结合 –index 选项后转换为具体依赖然后再安装他们。

这个办法可以让我们解决一种类似这样的情形:比如你有两个或两个以上的包在一起开发但是是分开发行的,或者说你有一个尚未发布的包并把它分成了几个部分。如果你的顶层的包 依然仅仅按照 “名字” 来依赖的话,我们依然可以使用 requirements.txt 来安装开发版本的依赖包:

--index https://pypi.python.org/simple/
-e https://github.com/foo/bar.git#egg=bar
-e .

这会首先从 https://github.com/foo/bar.git 来安装包 bar , 然后进行到第二行 -e . ,开始安装 setup 中的抽象依赖,但是包 bar 已经安装过了, 所以 pip 会跳过安装。

-->