使用console_scripts打包python代码

使用console_scripts打包python代码

我们写了一个Python包,比如包的名字叫cat,现在想提供一个命令行工具,用户只要在命令行中输入cat+各种参数就可以运行我们的包,比如输入cat call返回miaow! ,进一步地,如果在后面再输入一个数字,则可控制cat叫的次数,比如cat call 2. 输入cat draw画出一只小猫的漫画. 我们可以用argparse库实现对参数的解析,并将其绑定到不同函数上,用setup.py中的console_scripts参数来实现命令行工具的打包. 帝国理工的PyFR项目是用这一套方案来进行开发的,安装好PyFR包以后,只要在命令行中输入

1
pyfr run mesh.pyfrm configuration.ini

就可以展开模拟了,其中mesh.pyfrm是网格数据,configuration.ini是模拟的输入文件. 像是这种配置文件.. hexo采用的是YAML格式,Vscode是JSON格式,julia的项目配置文件是TOML,python的pdm也是TOML,看起来TOML好像更方兴未艾一点..

argparse

argparse这个库使用的基本想法就是维护一个ArgumentParser对象,通过这个对象的各种方法来实现对命令行参数的解析,并将最终解析的结果保存到一个Namespace对象中. 像是Lammps其实也是维护了一个lammps对象,并通过这个对象定义的方法开展模拟的. 我们可以用.运算符来获取Namespace对象中的参数.

1
2
3
auto lammps = new LAMMPS(argc, argv, lammps_comm);
lammps->input->file();
delete lammps;

所以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# cat.py

import argparse

def main():

ap = argparse.ArgumentParser(prog='cat')
ap.add_argument('-n', '--name', type=str, default='spike', help='name of the cat') # 这里实际上会用type对输入的参数做一个强制类型转换
ap.add_argument('-l', '--lovefood', type=str, default='fish', help='favorite food of the cat') # -或--为可选的关键字参数,-为简写
args = ap.parse_args()

print(args)
print(args.name)
print(args.lovefood)

if __name__ == '__main__':
main()

在命令行中输入

1
python cat.py -n jet --lovefood meat

输出为

Namespace(lovefood='meat', name='jet')

我们也可以通过subparser实现对函数的调用,subparser可以继续定义该函数的参数,通过subparser的set_defaults方法绑定对应函数,比如下面的程序,我们可以执行

1
python cat.py call 3

输出为

miaow! miaow! miaow!

这里的逻辑是当给定参数call后,call对应的sub_parser -> ap_call会继续对后面的参数按照add_argument方法的指定进行检索.

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# cat.py

import argparse
import turtle as t

def main():

ap = argparse.ArgumentParser(prog='cat')
ap.add_argument('-n', '--name', type=str, default='spike', help='name of the cat') # 这里实际上会用type对输入的参数做一个强制类型转换
ap.add_argument('-l', '--lovefood', type=str, default='fish', help='favorite food of the cat') # -或--为可选的关键字参数

# 这里定义子parser
sp = ap.add_subparsers(dest='cmd', help='sub-command') # dest设置它的别名,可以用args.cmd找到它
ap_call = sp.add_parser('call', help='bark')
ap_call.add_argument('times', type=int, help='times of barking') # 没有-代表必须指定的参数
ap_call.set_defaults(func=call)

ap_draw = sp.add_parser('draw', help='draw a cat')
ap_draw.set_defaults(func=draw)

args = ap.parse_args()

if hasattr(args, 'func'): # 这里必须加一个判断,否则不调用函数时程序会报错
args.func(args)

def call(args):
for i in range(args.times):
print('miaow!')

def draw(args):
t.screensize(500, 500)
# 【头部轮廓】
t.pensize(5)
t.home()
t.seth(0)
t.pd()
t.color('black')
t.circle(20, 80) # 0
t.circle(200, 30) # 1
t.circle(30, 60) # 2
t.circle(200, 29.5) # 3
t.color('black')
t.circle(20, 60) # 4
t.circle(-150, 22) # 5
t.circle(-50, 10) # 6
t.circle(50, 70) # 7
# 确定鼻头大概位置
x_nose = t.xcor()
y_nose = t.ycor()
t.circle(30, 62) # 8
t.circle(200, 15) # 9
# 【鼻子】
t.pu()
t.goto(x_nose, y_nose + 25)
t.seth(90)
t.pd()
t.begin_fill()
t.circle(8)
t.end_fill()
# 【眼睛】
t.pu()
t.goto(x_nose + 48, y_nose + 55)
t.seth(90)
t.pd()
t.begin_fill()
t.circle(8)
t.end_fill()
# 【耳朵】
t.pu()
t.color('#444444')
t.goto(x_nose + 100, y_nose + 110)
t.seth(182)
t.pd()
t.circle(15, 45) # 1
t.color('black')
t.circle(10, 15) # 2
t.circle(90, 70) # 3
t.circle(25, 110) # 4
t.rt(4)
t.circle(90, 70) # 5
t.circle(10, 15) # 6
t.color('#444444')
t.circle(15, 45) # 7
# 【身体】
t.pu()
t.color('black')
t.goto(x_nose + 90, y_nose - 30)
t.seth(-130)
t.pd()
t.circle(250, 28) # 1
t.circle(10, 140) # 2
t.circle(-250, 25) # 3
t.circle(-200, 25) # 4
t.circle(-50, 85) # 5
t.circle(8, 145) # 6
t.circle(90, 45) # 7
t.circle(550, 5) # 8
# 【尾巴】
t.seth(0)
t.circle(60, 85) # 1
t.circle(40, 65) # 2
t.circle(40, 60) # 3
t.lt(150)
t.circle(-40, 90) # 4
t.circle(-25, 100) # 5
t.lt(5)
t.fd(20)
t.circle(10, 60) # 6
# 【背部】
t.rt(80)
t.circle(200, 35)
# 【项圈】
t.pensize(20)
t.color('#F03C3F')
t.lt(10)
t.circle(-200, 25) # 5
# 【爱心铃铛】
t.pu()
t.fd(18)
t.lt(90)
t.fd(18)
t.pensize(6)
t.seth(35)
t.color('#FDAF17')
t.begin_fill()
t.lt(135)
t.fd(6)
t.right(180) # 画笔掉头
t.circle(6, -180)
t.backward(8)
t.right(90)
t.forward(6)
t.circle(-6, 180)
t.fd(15)
t.end_fill()
# 【前小腿】
t.pensize(5)
t.pu()
t.color('black')
t.goto(x_nose + 100, y_nose - 125)
t.pd()
t.seth(-50)
t.fd(25)
t.circle(10, 150)
t.fd(25)
# 【后小腿】
t.pensize(4)
t.pu()
t.goto(x_nose + 314, y_nose - 125)
t.pd()
t.seth(-95)
t.fd(25)
t.circle(-5, 150)
t.fd(2)
t.hideturtle()
t.done()

if __name__ == '__main__':
main()

console_scripts

现在我们想把它打包成一个包,并提供相应的命令行工具,希望安装好cat包以后,直接在命令行里执行cat call 3就可以实现之前的功能了,我们可以借助setuptools中提供的console_scripts入口实现这一想法. 按照下述的方式组织cat包.

其中各文件内容为,这个console_scripts就相当于打包了__main__.py中的main函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# setup.py

from setuptools import setup

setup(name="cat",
version="1.0",
description="A lovely cat!",
author="Yang Shen",
author_email="sy980829@163.com",
url="https://github.com/santashen",
install_requires=["turtle"],
pcakages=["cat"],
entry_points={
"console_scripts": ['cat=cat.__main__:main']
}
)
1
2
# __init__.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# __main__.py

import argparse

from cat.action import call
from cat.operate import draw

def main():

ap = argparse.ArgumentParser(prog='cat')

sp = ap.add_subparsers(dest='cmd', help='sub-command')
ap_call = sp.add_parser('call', help='bark')
ap_call.add_argument('times', type=int, help='times of barking')
ap_call.set_defaults(func=call)

ap_draw = sp.add_parser('draw', help='draw a cat')
ap_draw.set_defaults(func=draw)

args = ap.parse_args()

if hasattr(args, 'func'):
args.func(args)
1
2
3
4
5
# action.py

def call(args):
for i in range(args.times):
print('miaow!')
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# operate.py

import turtle as t

def draw(args):
t.screensize(500, 500)
# 【头部轮廓】
t.pensize(5)
t.home()
t.seth(0)
t.pd()
t.color('black')
t.circle(20, 80) # 0
t.circle(200, 30) # 1
t.circle(30, 60) # 2
t.circle(200, 29.5) # 3
t.color('black')
t.circle(20, 60) # 4
t.circle(-150, 22) # 5
t.circle(-50, 10) # 6
t.circle(50, 70) # 7
# 确定鼻头大概位置
x_nose = t.xcor()
y_nose = t.ycor()
t.circle(30, 62) # 8
t.circle(200, 15) # 9
# 【鼻子】
t.pu()
t.goto(x_nose, y_nose + 25)
t.seth(90)
t.pd()
t.begin_fill()
t.circle(8)
t.end_fill()
# 【眼睛】
t.pu()
t.goto(x_nose + 48, y_nose + 55)
t.seth(90)
t.pd()
t.begin_fill()
t.circle(8)
t.end_fill()
# 【耳朵】
t.pu()
t.color('#444444')
t.goto(x_nose + 100, y_nose + 110)
t.seth(182)
t.pd()
t.circle(15, 45) # 1
t.color('black')
t.circle(10, 15) # 2
t.circle(90, 70) # 3
t.circle(25, 110) # 4
t.rt(4)
t.circle(90, 70) # 5
t.circle(10, 15) # 6
t.color('#444444')
t.circle(15, 45) # 7
# 【身体】
t.pu()
t.color('black')
t.goto(x_nose + 90, y_nose - 30)
t.seth(-130)
t.pd()
t.circle(250, 28) # 1
t.circle(10, 140) # 2
t.circle(-250, 25) # 3
t.circle(-200, 25) # 4
t.circle(-50, 85) # 5
t.circle(8, 145) # 6
t.circle(90, 45) # 7
t.circle(550, 5) # 8
# 【尾巴】
t.seth(0)
t.circle(60, 85) # 1
t.circle(40, 65) # 2
t.circle(40, 60) # 3
t.lt(150)
t.circle(-40, 90) # 4
t.circle(-25, 100) # 5
t.lt(5)
t.fd(20)
t.circle(10, 60) # 6
# 【背部】
t.rt(80)
t.circle(200, 35)
# 【项圈】
t.pensize(20)
t.color('#F03C3F')
t.lt(10)
t.circle(-200, 25) # 5
# 【爱心铃铛】
t.pu()
t.fd(18)
t.lt(90)
t.fd(18)
t.pensize(6)
t.seth(35)
t.color('#FDAF17')
t.begin_fill()
t.lt(135)
t.fd(6)
t.right(180) # 画笔掉头
t.circle(6, -180)
t.backward(8)
t.right(90)
t.forward(6)
t.circle(-6, 180)
t.fd(15)
t.end_fill()
# 【前小腿】
t.pensize(5)
t.pu()
t.color('black')
t.goto(x_nose + 100, y_nose - 125)
t.pd()
t.seth(-50)
t.fd(25)
t.circle(10, 150)
t.fd(25)
# 【后小腿】
t.pensize(4)
t.pu()
t.goto(x_nose + 314, y_nose - 125)
t.pd()
t.seth(-95)
t.fd(25)
t.circle(-5, 150)
t.fd(2)
t.hideturtle()
t.done()

使用conda新建一个虚拟环境,在setup.py目录下执行pip install .,即可安装cat包,在命令行里测试一下