专注于做有价值的技术原创

0%

如何发布自己的 python 包?

前言

本文以笔者实际发布的 python 包 imgkernel 为例。因此,在本文所有出现 imgkernel 的地方,都替换成读者自己项目或包的名称。 同时,imgkernel 也托管在 github 上,后续 master 分支会更新,但是项目单独检出的 pkg 分支将保持与本文内容一致,不再改动。因此,可以将此分支 clone 下来作为新项目启动工程。clone pkg 分支的方法如下:

1
git clone -b pkg https://github.com/kenblikylee/imgkernel.git

下面分步骤讲解 imgkernel 项目的创建,生成,发布测试,正式发布到 PyPi 和安装使用。

1. 创建github仓库

2. 克隆仓库到本地

1
2
git clone https://github.com/kenblikylee/imgkernel.git
cd imgkernel

3. 创建 setup.py

setup.py 是 setuptools 的构建脚本,告知 setuptools 包的名称和版本,以及哪些文件将被打包。

在项目根目录新建文件 setup.py ,复制黏贴如下代码到 setup.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import setuptools

with open("README.md", "r") as fh:
long_description = fh.read()

setuptools.setup(
name="imgkernel",
version="0.0.1",
author="ken",
author_email="kenbliky@gmail.com",
description="Image kernel.",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/kenblikylee/imgkernel",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires='>=3.6',
)

4. 创建项目包 imgkernel

setup.py 会自动查找项目根目录下,包含文件 __init__.py 的子目录,作为项目的包,并以目录名称作为包名。这里只创建了一个与项目同名的包 imgkernel,当然不是必须创建一个同名的包,事实上,可以创建任意名称的多个包。

创建后,整个项目目录结构如下:

1
2
3
4
5
6
7
8
.
├── imgkernel
│   ├── __init__.py
│   ├── imgconv.py
│   └── kernels.py
├── LICENSE
├── README.md
└── setup.py

imgkernel 包有三个文件: __init__.py, imgconv.py, kernels.py,下面分别是三个文件的代码实现:

4.1 __init__.py

__init__.py 是包内部模块对外的导出接口,或者说,当使用 import 时的导入对象。 我们把需要提供外部应用使用的函数,数据,类等都放在这个文件里。当然,并非一定要此文件里实现,import 进来的模块也会被导出,例如下面的 kernels 下的所有 kernel 以及 imgconv 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from .kernels import *
from .imgconv import imgconv

def identity(imgpath, gray=True, **argkw):
return imgconv(imgpath, identity_kernel(**argkw), strides=1, mean=False, gray=gray)

def sharpen(imgpath, gray=True, **argkw):
return imgconv(imgpath, sharpen_kernel(**argkw), strides=1, mean=False, gray=gray)

def blur(imgpath, gray=True, **argkw):
return imgconv(imgpath, blur_kernel(**argkw), strides=1, mean=False, gray=gray)

def emboss(imgpath, gray=True, **argkw):
return imgconv(imgpath, emboss_kernel(**argkw), strides=1, mean=False, gray=gray)

def outline(imgpath, gray=True, **argkw):
return imgconv(imgpath, outline_kernel(**argkw), strides=1, mean=True, gray=gray)

def sobel(imgpath, gray=True, **argkw):
return imgconv(imgpath, sobel_kernel(**argkw), strides=1, mean=False, gray=gray)

4.2 imgconv.py

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
from PIL import Image
import numpy as np

def read_img(imgpath, gray=True):
img = Image.open(imgpath)
if gray:
img = img.convert("L")
return img

def crop_center(img):
w, h = img.size
c_x, c_y = w/2, h/2
offset = min(w, h) / 2
crop_box = (c_x-offset, c_y-offset, c_x+offset, c_y+offset)
return img.crop(crop_box)

def resize_width(img, width):
w, h = img.size
height = int(width * h / w)
return img.resize((width, height))

def apply_img_kernel(img, kernel, strides=1, mean=False):
img = np.asarray(img)

h, w = img.shape[:2]
k_h, k_w = kernel.shape[:2]
x_range = range(0, w - k_w + 1, strides)
y_range = range(0, h - k_h + 1, strides)

if mean:
prosum = lambda a,b: min((a*b).mean(), 255)
else:
prosum = lambda a,b: min((a*b).sum(), 255)

cal = lambda img: np.array([[prosum(img[i:i+k_h, j:j+k_w], kernel)
for j in x_range]
for i in y_range]).astype(np.uint8)

if len(img.shape) == 2:
data = cal(img)
return Image.fromarray(data)
elif len(img.shape) == 3:
r, g, b = np.transpose(img, (2, 0, 1))
_r, _g, _b = cal(r), cal(g), cal(b)
return Image.merge('RGB', [Image.fromarray(d) for d in [_r, _g, _b]])

imgs = dict()

def imgconv(imgpath, kernel, strides=1, mean=False, gray=True):
if imgpath in imgs:
img = imgs[imgpath]
else:
img = read_img(imgpath, gray)
img = crop_center(resize_width(img, 320))
return img, apply_img_kernel(img, kernel, mean=mean)

4.3 kernels.py

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
import numpy as np

# 鲜明
def identity_kernel(iden=1.0):
return np.array([[0, 0, 0],
[0, iden, 0],
[0, 0, 0]])

# 锐化
def sharpen_kernel(inner=5.0, edge=-1.0):
return np.array([[0, edge, 0],
[edge, inner, edge],
[0, edge, 0]])

# 模糊
def blur_kernel(inner=0.25, edge=0.125, corner=0.0625):
return np.array([[corner, edge, corner],
[edge, inner, edge],
[corner, edge, corner]])

# 浮雕
def emboss_kernel(diag=2.0, iden=1.0):
return np.array([[-diag, -iden, 0],
[-iden, iden, iden],
[0, iden, diag]])

# 轮廓线
def outline_kernel(inner=8.0, outer=-1.0):
return np.array([[outer, outer, outer],
[outer, inner, outer],
[outer, outer, outer]])

# 边缘检测
def sobel_kernel(direction, base=None, edge=2.0, corner=1.0):
if base is not None:
edge = base
corner = base / 2
if direction == 'top':
return np.array([[corner, edge, corner], [0, 0, 0], [-corner, -edge, -corner]])
elif direction == 'bottom':
return np.array([[-corner, -edge, -corner], [0, 0, 0], [corner, edge, corner]])
elif direction == 'left':
return np.array([[corner, 0, -corner], [edge, 0, -edge], [corner, 0, -corner]])
elif direction == 'right':
return np.array([[-corner, 0, corner], [-edge, 0, edge], [-corner, 0, corner]])

return identity_kernel()

5. 生成包

先确保已经安装了最新版本的 setuptools, wheel, twine

1
pip install --user --upgrade setuptools wheel twine

生成项目包:

1
python setup.py sdist bdist_wheel

运行后,项目目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.
├── build
│   ├── bdist.macosx-10.7-x86_64
│   └── lib
│   └── imgkernel
│   ├── __init__.py
│   ├── imgconv.py
│   └── kernels.py
├── dist
│   ├── imgkernel-0.0.1-py3-none-any.whl
│   └── imgkernel-0.0.1.tar.gz
├── imgkernel
│   ├── __init__.py
│   ├── imgconv.py
│   └── kernels.py
├── imgkernel.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   └── top_level.txt
├── LICENSE
├── README.md
└── setup.py

6. 上传包到 PyPI

6.1 注册 PyPI 测试账号

注册地址:https://test.pypi.org/account/register/

6.2 使用 twine 上传 dist 目录下的存档

1
python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*

打开: https://test.pypi.org/project/imgkernel/0.0.1/ 可看到,包已经成功发布到测试 PyPI。

7. 安装

1
pip install --index-url https://test.pypi.org/simple/ --no-deps imgkernel

使用 python 解释器测试包是否安装成功:

1
2
3
4
5
6
$ python
>>> import imgkernel
>>> imgkernel.blur
<function blur at 0x108c00d90>
>>> imgkernel.outline
<function outline at 0x108c00ea0>

8. 正试发布!

如果在第6小节,是发布到 PyPI 的测试环境,这一步就是发布到 PyPI 生产环境。如果顺利完成上面的步骤的,发布正试 PyPI 也基本一样。需要注意的是,测试 PyPI 不是永久存储的,后台会定期清楚,而正试 PyPI 是永久存储的,且一旦发布,就不能修改,只能迭代新的版本,此外,还需要确保包名不能和已经发布的包重名。

正试和测试是独立的系统,因此,还需要注册一个账号, 注册地址:https://pypi.org

使用 twine 发布:

1
python -m twine upload dist/*

安装:

1
pip install imgkernel

结语

本文示例项目 github 地址:https://github.com/kenblikylee/imgkernel

使用方法:

1
git clone -b pkg https://github.com/kenblikylee/imgkernel.git

参考

  • [1] Python Packaging User Guide
  • [2] Packaging Python Projects
青笔 wechat
我是一条小青蛇 我有很多小秘密