前言

提了一个新需求,要把开发的 Python 项目发布到 PyPI 上,但因为需要保密,所以得自己搭建一个内部的 PyPI 服务器😓

Python 项目打包

项目结构

首先创建一个待发布的文件夹 packaging,整理一下项目结构并添加几个必要的文件(LICENSEREADME.mdsetup.py

1
2
3
4
5
6
7
8
9
10
11
12
13
packaging
├── my_project
│   ├── module1
│   │   ├── __init__.py
│   │   └── hello.py
│   ├── module2
│   │   ├── __init__.py
│   │   └── bye.py
│   ├── __init__.py
│   └── utils.py
├── LICENSE
├── README.md
└── setup.py # 或 setup.cfg

示例工程很简单,所有 __init__.py 都是空文件,其他文件内容如下:

  1. hello.py

    1
    2
    def hello_world():
    print("hello world!")
  2. bye.py

    1
    2
    def goodbye():
    print("goodbye!")
  3. utils.py

    1
    2
    def test():
    print("A demo project for packaging.")

选择许可证

PyPI 要求所有上传的包都必须包含一个许可证,利用 https://choosealicense.com/ 帮助选择许可证,然后将许可证内容复制到 LICENSE 文件中。

注意有些许可证需要填入年份([year])和所有者([fullname][name of copyright owner]

例如,MIT 许可证:

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

Copyright (c) [2021] [my_project]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

编写说明

根据 markdown 语法编写项目的详细说明,之后可以作为 setup.py 中的 long_description

1
2
3
4
5
# Example Package

This is a simple example package. You can use
[Github-flavored Markdown](https://guides.github.com/features/mastering-markdown/)
to write your content.

配置元数据

有两种元数据类型:静态元数据(setup.cfg)和动态元数据(setup.py),官方推荐首选静态元数据。

静态

下面是官方示例的 setup.cfg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[metadata]
name = example-pkg-YOUR-USERNAME-HERE
version = 0.0.1
author = Example Author
author_email = author@example.com
description = A small example package
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/pypa/sampleproject
project_urls =
Bug Tracker = https://github.com/pypa/sampleproject/issues
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: OS Independent

[options]
package_dir =
= src
packages = find:
python_requires = >=3.6

[options.packages.find]
where = src
字段名称 说明
name 如果要发布在 pypi.org 上,名称必须是唯一的,只能由英文字母、_- 组成
authorauthor_email 标识作者
description 包的简短介绍
long_description 包的详细介绍,可以指定说明文件
url 项目主页
project_urls 和项目相关的额外链接
classifiers 附加元数据,例如许可证、兼容。完整列表见 https://pypi.org/classifiers/

动态

官方示例的 setup.py,可以看出字段基本是相同的,也可以利用现成的模板进行编写:kennethreitz/setup.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
import setuptools

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

setuptools.setup(
name="example-pkg-YOUR-USERNAME-HERE",
version="0.0.1",
author="Example Author",
author_email="author@example.com",
description="A small example package",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/pypa/sampleproject",
project_urls={
"Bug Tracker": "https://github.com/pypa/sampleproject/issues",
},
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
package_dir={"": "src"},
packages=setuptools.find_packages(where="src"),
python_requires=">=3.6",
)

实际上,可以把 setup.cfg 理解为包含 setup.py 命令默认选项的 ini 文件。

实验环境

Ubuntu 20.04 Server(Python 3.8.10)安装 venv,创建并使用虚拟环境

1
2
3
4
5
6
7
8
9
# 安装 venv
sudo apt install python3-venv -y

# 创建虚拟环境
mkdir code
python -m venv /home/jck/code

# 激活虚拟环境
source /home/jck/code/bin/activate

生成包

使用以下 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", encoding="utf-8") as fh:
long_description = fh.read()

setuptools.setup(
name="my_project",
version="0.0.1",
author="jckling",
author_email="jckling@163.com",
description="A small example package",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/jckling",
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
packages=setuptools.find_packages(),
python_requires=">=3.6",
)

安装 setuptoolswheel,支持从源码构建包

1
2
pip install setuptools
pip install wheel

进入 packaging 文件夹,检查 setup.py,如果有错误会打印提示信息

1
2
python setup.py check
# running check

打包,自动创建 dist 目录,以及相应的 .tar.gz 文件

1
python setup.py sdist build

本地 PyPI 服务器搭建

搭建服务器

安装 pypiserver,创建文件夹 packages 用于放置发布的包

1
2
pip install pypiserver
mkdir ~/packages

my_project-0.0.1.tar.gz 上传到 ~/packages 目录下,在同一台虚拟机上操作直接使用 mv

1
mv dist/my_project-0.0.1.tar.gz ~/packages

运行服务器,端口指定为 8080, 默认监听所有 IP 地址

1
pypi-server -p 8080 ~/packages &

测试

搜索本地服务器上是否有 my_project

1
pip search --index http://localhost:8080 my_project

安装和使用 my_project

1
pip install --extra-index-url http://localhost:8080 my_project

参阅