【其他】打包一个 python 项目

打包一个 python 项目

1. 项目的分发打包

对源代码进行封装,使使用者的安装部署更为简洁。

1
2
pip install pwntools
python setup.py install

distutils

distutils 是 Python 的一个标准库,从命名上很容易看出它是一个分发(distribute)工具(utlis),它是 Python 官方开发的一个分发打包工具,所有后续的打包工具,全部都是基于它进行开发的。

通过一个 setup.py 来实现模块的封装与安装(参考链接:https://docs.python.org/3.6/distutils/setupscript.html):

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python3

from distutils.core import setup

setup(name='Distutils',
version='1.0',
description='Python Distribution Utilities',
author='Greg Ward',
author_email='gward@python.net',
url='https://www.python.org/sigs/distutils-sig/',
packages=['distutils', 'distutils.command'],
)

setuptools

setuptools 是 distutils 增强版,不包括在标准库中。setuptools 有一系列分支版本,包括 distribute 等。

在安装了 setuptools 之后,就可以使用 easy_install 来安装一个包了:

通过包名,从PyPI寻找最新版本,自动下载、编译、安装

1
easy_install pkg_name

通过包名从指定下载页寻找链接来安装或升级包

1
2
$ easy_install -f http://pythonpaste.org/package_index.html 
$ easy_install -f http://pythonpaste.org/package_index.html

指定线上的包地址安装

1
$ easy_install http://example.com/path/to/MyPackage-1.2.3.tgz

从本地的 .egg 文件安装

1
$ easy_install xxx.egg

在安装时你可以添加额外的参数

1
2
指定安装目录:--install-dir=DIR, -d DIR
指定用户安装:--user

但是比起 easy_install,我更喜欢 pip。

2. setup.py 的编写

最简单的 setup.py

一个最简单的例子:如果我们想要创建一个名为 foo 的包,只包含一个文件 foo.py ,那么 setup.py 的写法如下:

1
2
3
4
5
from distutils.core import setup
setup(name='foo',
version='1.0',
py_modules=['foo'],
)

setup() 函数的参数就是需要提供的信息。主要包含两种类型:

  • 包的元数据:包名称,版本号等
  • 包内文件:这个包中包含哪些文件内容,需要哪些依赖等等

当你编写好 setup.py 后,运行:

1
python setup.py sdist

这将会生成一个 foo-1.0.tar.gz (或者 .zip) 文件。

运行:

1
python setup.py install

将会执行安装。这会将 foo.py 复制到当前系统中 Python 的第三方模块目录下。

接下来看一个更复杂的例子:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python

from distutils.core import setup

setup(name='Distutils',
version='1.0',
description='Python Distribution Utilities',
author='Greg Ward',
author_email='gward@python.net',
url='https://www.python.org/sigs/distutils-sig/',
packages=['distutils', 'distutils.command'],
)

和前面的简单例子的区别在于,这个 setup.py 的元数据比较多。其他的元数据信息可以看官方文档

下面会反复提到 package 和 module 的概念,这里作一个解释:

  • module:每一个 python 文件都是一个 module
  • package:本质上是一个目录,装有 module 的目录就是一个 package。原则上,一个 package(目录)中必须有一个 __init__.py 文件来表示该目录是一个 package。

下面提到的 模块 都是指 module 和 package,我也尽量用英文单词。

打包哪些 python 文件?

在打包时,必须告诉 distutils 哪些源文件(.py)是需要被打包的。这个时候就有两种方式:

  • 以 package 为单位打包:适合更大的项目,有多个 package 管理的情况下使用;
  • 以 module 为单位打包:适合小项目,只有若干个源文件的情况下使用。

packages 参数: 列出所有需要的 package

packages 是一个 list,每一个元素都是一个 package 名,distutils 会分别查找这些 package,将其中的 python 文件(也就是 module)进行打包。

为了实现这一点,需要有一个 package 名与其目录的对应关系。当你输入了类似 package=["foo"] 这样的语句时,表示在 setup.py 同目录下存在一个名为 foo 的目录,且存在 foo/__init__.py 文件来表示 foo 是一个package。

当然,如果你使用的 package 的位置不在当前目录下,那么可以使用 package_dir 参数来指定,类似于 package_dir = {'': 'lib'},它是一个字典,键表示的是包名,值表示的是当前目录距离包所在目录的相对位置。这个例子的意思是,包 foo 位于 lib/foo/__init__.py

py_modules 参数: 列出独立的模块

也可以直接引用单独的 module。如果只有一个 module,尤其是只有单个源文件的时候,使用 py_modules 更好。

1
py_modules = ['mod1', 'pkg.mod2']

上面这句话描述了两个 module:一个位于包的根目录下,另一个位于名为 pkg 的包中,模块名为 mod2。

如何注册一个命令行脚本?

一般情况下,setup.py 安装一个包后,只能通过 import xxx 的形式在 python 源代码里使用。但是也可以把某个脚本注册到命令行下。

被注册的脚本需要是一个 python 脚本,最好在脚本开头写上 #!python

1
2
3
setup(...,
scripts=['scripts/xmlproc_parse', 'scripts/xmlproc_val']
)

这样,将会把 scripts/xmlproc_parsescripts/xmlproc_val 添加到环境变量,可以直接输入 xmlproc_parsexmlproc_val 来执行这两个脚本。

还有一种方法是使用 setuptools,这样就可以使用 entry_points

1
2
3
4
5
6
7
setup(...,
entry_points={
'console_scripts': [
'foo = foo.main:main'
]
},
)

这样就会在环境变量中添加一个 foo 命令,它指向的是 foo/main.py 中的 main 函数。

3. argparse 的使用

官方文档 (终于有中文文档了)

创建一个解析器:

1
parser = argparse.ArgumentParser(description='Process some integers.')

添加参数:

给一个 ArgumentParser 添加程序参数信息是通过调用 add_argument() 方法完成的。通常,这些调用指定 ArgumentParser 如何获取命令行字符串并将其转换为对象。这些信息在 parse_args() 调用时被存储和使用。

参数有必选参数可选参数两种:

  • 必选参数:类似于前面例子中的 integers ,即参数名不加 - 的参数。
  • 可选参数:类似于前面例子中的 --sum,即参数名添加了 - 的参数。

必选参数必须被指定。否则会报错。(可选参数也可以使用 required=True 来指定,使其成为一个必须的参数。)

普通参数是通过顺序来区分的,而可选参数则是通过类似 --name="abc" 这样的形式传递,增加了命令行中的可读性。

下面是一个例子

1
2
3
4
5
6
7
8
9
import argparse

parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='an integer for the accumulator')
parser.add_argument('--name', type=str, default='三', help='名')

args = parser.parse_args()
print(args.accumulate(args.integers))
  • nargs='+' 表示传入的参数个数,其中+表示至少传入一个参数。

  • default='三' 表示默认值是 '三'

    前面的例子中,必选参数 integers 和 可选参数 --name 都是为了传递一个(或多个)变量,但有时候,我们希望可选参数来表示一个不同的操作,例如:

1
2
3
4
5
6
7
8
9
10
11
import argparse

parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max,
help='sum the integers (default: find the max)')

args = parser.parse_args()
print(args.accumulate(args.integers))

这个例子将接收若干个整数,输出他们的最大值。如果设置了 --sum 参数,则会输出它们的和。这个依靠 action 参数。(参考链接:https://blog.csdn.net/Drievn/article/details/70821188)

解释 action 参数:在解析到这个参数时,触发某种动作。argparse 内置了六种动作:

  • "store" :将参数的传递的值原本地保存下来

  • "store_const" :将保存一个固定值,这个固定值由 const=xxx 来指定

  • "store_true" / "store_false" :保存 true / false

  • "append" :将参数的值 append 到一个 list 中

  • append_const : 将固定的值 append 到一个 list 中

上述六个操作,都会将值保存到 dest=xxx 指定的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-s', action='store', dest='simple_value',
help='Store a simple value')

parser.add_argument('-c', action='store_const', dest='constant_value',
const='value-to-store',
help='Store a constant value')

parser.add_argument('-t', action='store_true', default=False,
dest='boolean_switch',
help='Set a switch to true')
parser.add_argument('-f', action='store_false', default=False,
dest='boolean_switch',
help='Set a switch to false')

parser.add_argument('-a', action='append', dest='collection',
default=[],
help='Add repeated values to a list')

parser.add_argument('-A', action='append_const', dest='const_collection',
const='value-1-to-append',
default=[],
help='Add different values to list')
parser.add_argument('-B', action='append_const', dest='const_collection',
const='value-2-to-append',
help='Add different values to list')

results = parser.parse_args()
print 'simple_value =', results.simple_value
print 'constant_value =', results.constant_value
print 'boolean_switch =', results.boolean_switch
print 'collection =', results.collection
print 'const_collection =', results.const_collection