PyPI使用指南

PyPI使用指南

fetch150zy

本文简单介绍了如何打包上传一个属于你自己的Python库,以及Python虚拟环境管理相关的一些问题

在Python包管理与分发的发展历程中,涌现了众多开发工具,主流的大致如下

  1. setuptools: 允许开发者定义如何构建、打包和安装他们的项目,广泛使用的老牌工具
  2. flit: 适合于不需要复杂构建过程的简单或单文件项目,简单易用
  3. Poetry: 提供了依赖管理和打包的集成解决方案

flit和Poetry都使用pyproject.toml进行配置,setuptools使用setup.py/pyproject.toml进行配置

关于一个优秀项目的像CI/CD,文档等等工作,本文不做详细介绍

xxxxxxxxxx11 1$ vim ~/.zshrc2ZSH_THEME=”powerlevel10k/powerlevel10k”3plugins=( git zsh-syntax-highlighting zsh-autosuggestions )4source ~/.zshrc5vim ~/.bashrc6alias ls=’lsd’7alias ll=’lsd -l’8alias la=’lsd -a’9alias lla=’lsd -la’10alias lt=’lsd –tree’11$ source ~/.bashrcbash

一些虚拟环境管理工具

  1. venv & virtualenv: 较古老的工具,功能也不够强大
  2. pipenv: 结合了pip和virtualenv,简化了依赖管理和虚拟环境的创建过程
  3. conda: 一个跨平台的包和环境管理器,还可以管理非Python库和依赖

包管理与分发

一般来说,一个Python包的项目结构应该看起来像下面这样

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

├── src/
│ └── example_package/
│ ├── __init__.py
│ └── your_module.py

├── docs/
│ └── ...

├── examples/
│ └── ...

├── tests/
│ ├── __init__.py
│ └── test_your_module.py

├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README.md
└── requirements.txt

如果你想要文档或CI/CD的配置,你可以学习相关知识并使用在你的项目中

  • .travis.yml: 如果您使用Travis CI或其他持续集成服务,您可能需要这样一个配置文件来定义如何运行测试和其他自动化任务
  • .readthedocs.yml: 如果您使用Read the Docs来托管文档,这个文件将帮助配置文档的构建过程

setuptools

尽管pyproject.toml正在逐渐成为替代setup.pysetup.cfg的新标准,还是建议了解一下后者的写法

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
from setuptools import setup, find_packages

setup(
name='library_name',
version='0.1.0',
author='Your Name',
author_email='your.email@example.com',
description='A short description of your project',
long_description=open('README.md').read(),
long_description_content_type='text/markdown',
url='https://github.com/yourusername/yourlibraryname',
packages=find_packages(where="src"),
package_dir={"": "src"},
classifiers=[
'Programming Language :: Python :: 3',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
],
python_requires='>=3.6',
install_requires=[
# 'numpy>=1.15',
],
include_package_data=True,
)
  • name: 包的名字
  • version: 包的版本(应遵循语义化版本控制)
  • author: 包的作者/包的团队
  • author_email: 作者/团队的邮箱
  • description: 该包的一个简短描述
  • long_description: 从README文件读取的长描述
  • long_description_content_type: 长描述的格式
  • url: 项目仓库的URL
  • packages: 自动查找包含__init__.py的文件夹
  • package_dir: 安装包时告诉setuptools应该从哪个目录下查找源代码
  • classifiers: 分类信息,方便帮助用户找到您的包
  • python_requires: Python版本要求
  • install_requires: 项目依赖列表
  • include_package_data: 包含在MANIFEST.in中的文件

上面最后提到了MANIFEST.in文件,下面简要介绍一下这个

  1. include: 指定需要包含在分发包中的文件
  2. exclude: 指定需要排除的文件
  3. recursive-include: 递归地包含某个目录中的文件
  4. recursive-exclude: 递归地排除某个目录中的文件
  5. global-include: 全局包含匹配模式的文件
  6. global-exclude: 全局排除匹配模式的文件
  7. prune: 排除某个目录的所有内容

e.g.

1
2
3
4
5
include LICENSE
include README.md
recursive-include docs *
recursive-include examples *
recursive-include data *

当您希望您的包在安装后提供一种(或多种)命令行工具供用户直接在终端中执行时,你可能需要配置entry_points

e.g.

1
2
3
4
5
6
7
8
9
setup(
# ...
entry_points={
'console_scripts': [
'cli_cmd=script_name.module:function',
],
},
# ...
)

如果你的项目只是一个或多个单文件模块(不包含子目录作为包),可以使用py_modules来配置

e.g.

1
2
3
4
5
6
setup(
name='library_name',
version='0.1.0',
py_modules=['module1', 'module2'],
# ...
)

当然,如此简单的结构通常只是一个脚本项目,你可以搭配上面entry_points来使用

setup.cfg

setup.cfg是一个配置文件,用于静态定义项目设置和选项,与setup.py结合使用,使得setup.py更简洁

e.g.

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
[metadata]
name = library_name
version = 0.1.0
author = Your Name
author_email = your.email@example.com
description = A short description of the project
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/yourusername/yourlibraryname
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: OS Independent

[options]
package_dir =
= src
packages = find:
include_package_data = True
python_requires = >=3.6
install_requires =
# numpy>=1.15

[options.packages.find]
where = src

我个人更习惯写setup.py,最佳实践而言,使用setup.cfg更现代化,setup.py可以确保向前兼容

对于上面在setup.py中提到的entry_points,在setup.cfg中可做如下配置

e.g.

1
2
3
4
5
6
[options]
# ...

[options.entry_points]
console_scripts =
cli_cmd = script_name.module:function

针对py_modules,在setup.cfg中做如下配置

e.g.

1
2
3
4
5
[options]
py_modules =
module1
module2
# ...

pyproject.toml

使用pyproject.toml配置setuptools是一个更现代的方法,提供了一种声明式的项目配置方式

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
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "library_name"
version = "0.1.0"
description = "A short description of the project"
readme = "README.md"
authors = [{name = "Your Name", email = "your.email@example.com"}]
license = {file = "LICENSE"}
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
requires-python = ">=3.6"

[tool.setuptools]
packages = find_namespace:
where = "src"

[options]
include_package_data = True
install_requires = [
# numpy>=1.15
]

[options.packages.find]
where = "src"

[options.entry_points]
console_scripts = [
"cli_cmd = script_name.module:function",
]

[project.urls]
homepage = "https://github.com/yourusername/yourlibraryname"
  • [build-system] : 指定构建这个项目所需的工具和版本

  • [project] : 项目的基本元数据

  • [tool.setuptools]

    1. find_namespace表示使用命名空间包,如果您的项目不使用命名空间包,改成find:

      命名控件包允许你将一个单独的Python包分散到多个目录中,这主要用于大型项目或在多个库之间共享代码的情况,命名空间包的特点是它们没有__init__.py文件

      在传统Python包中,一个包是一个目录,并且包含一个__init__.py文件,然而,在命名空间包中,包由多个目录组成,这些目录可以分布在文件系统的不同位置,而不是必须位于同一个父目录下

      命名空间包通常用于以下情况

      • 大型项目或框架:将包分为多个子包,这些子包分布在不同的项目或目录中
      • 代码共享:多个项目需要共享同一代码库的一部分
    2. where指定包的源代码位置

  • [options]

  • [options.packages.find]

  • [options.entry_points] : 定义一个或多个入口点,通常用于创建命令行工具

  • [project.urls]

上面setup.py中提到的py_modulespyproject.toml中没有直接的支持,如果你需要的话,可以使用如下方法

使用setup.py

1
2
3
4
5
from setuptools import setup

setup(
py_modules=["module1", "module2"],
)

这个setup.py可以与pyproject.toml一起存在,pyproject.toml负责定义构建系统和依赖,而setup.py负责py_modules

使用setup.cfg

1
2
3
4
[options]
py_modules =
module1
module2

build

配置完毕后,在setup.py/setup.cfg同级目录使用下面这行命令即可完成构建

1
python setup.py sdist bdist_wheel

构建完成后在dist/下有两个比较重要的文件

  • sdist: 源码分发(.tar.gz文件),包含您项目的源代码以及所有必要的文件,如 MANIFEST.in 中指定的文件
  • bdist_wheel: 轮式分发(.whl文件),是一种二进制分发格式,更易于安装且不需要运行构建脚本。这通常是推荐的分发方式,因为它更加现代化且具有更好的兼容性

tips: 在你构建前请确保你安装了setuptoolswheel

从Python 3.10开始,直接使用setup.py的方式不再是推荐的打包方法,推荐的方法是使用build工具

1
2
pip install build
python -m build

这个也是pyproject.toml的最佳搭配

upload

构建完成后,推荐使用twine工具来上传Python包到PyPI,下面是几个注意点

  • 账号准备,检测包名是否冲突

  • 安装twine,没安装的话请使用pip install twine安装

  • 上传命令

    1
    2
    twine upload --repository testpypi dist/*	# 上传到TestPyPI先进行测试
    twine upload dist/* # 正式上传
  • 检查

    1. 元数据检查,确保setup.pypyproject.tomlsetup.cfg中的元数据是最新且准确的
    2. 依赖项和兼容性检查
    3. 检查本地安装,上传后从PyPI安装上传的包以验证是否正确工作

flit

flit使用pyproject.toml文件进行配置,专注于纯Python包,并且不需要复杂的构建系统

这也就意味着flit非常适合于小型或中等规模的纯Python项目,特别是那些不包含编译扩展的项目

这里解释几个概念

  1. 纯Python包

    一个纯Python包仅仅包含Python代码和其他非编译文件,这些文件在安装时不需要编译,可以直接由Python解释器运行

  2. 编译拓展

    一些包包含编译拓展,编译拓展通常用C、C++或其他语言编写,然后编译成适合特定平台的二进制格式

编译扩展需要在目标机器上或为目标机器进行编译,这使得打包和分发过程更加复杂,setuptools提供了工具和功能来处理这种类型的包,但flit目前不支持这种用法

pyproject.toml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"

[tool.flit.metadata]
module = "library_name"
author = "Your Name"
author-email = "your.email@example.com"
home-page = "https://github.com/yourusername/yourlibraryname"
description-file = "README.md"
requires-python = ">=3.6"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
license = {file = "LICENSE"}

requires = [
# "numpy>=1.15"
]

[tool.flit.metadata.requires-extra]
test = ["pytest", "hypothesis"]
  • [build-system] : 定义了构建系统所需的flit版本和构建后端
  • [tool.flit.metadata] : 包含了包的元数据,如包名、作者、描述文件、所需 Python 版本、分类等
  • module : 指定主模块或包的名称。这应该与您的项目结构中的顶级包或模块名称匹配
  • requires : 列出了项目的运行时依赖
  • [tool.flit.metadata.requires-extra] : 定义了可选的额外依赖,比如用于开发或测试的依赖

tips: flit默认情况下会将所有文件视为包的一部分,除非在.gitignore中指定排除,不需要 include_package_data

确保您的项目结构中有一个与module指定的名称匹配的顶级模块或包

build & upload

pyproject.toml配置好后,可以使用flit来构建和发布包

  • build

    1
    flit build

    将在dist/目录下生成 .tar.gz.whl 文件

  • upload

    1
    flit publish

Poetry

Poetry是一个现代的Python包管理和打包工具,它旨在简化包的依赖管理和发布过程;它集成了依赖管理、打包、发布等功能,通过提供一致的命令和工作流来改善 Python 包的开发体验

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
[tool.poetry]
name = "library_name"
version = "0.1.0"
description = "A short description of the project"
authors = ["Your Name <your.email@example.com>"]
license = "MIT"
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.6"
numpy = "^1.15"

[tool.poetry.dev-dependencies]
# pytest = "^5.0"

[tool.poetry.packages]
include = [{ from = "src", include = ["**/*"] }]

[tool.poetry.scripts]
cli_cmd = "script_name.module:function"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.poetry.urls]
homepage = "https://github.com/yourusername/yourlibraryname"

pyproject.toml写法和之前的类似,只是稍作更改即可

可通过下面两种方式安装Poetry

1
2
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -

1
2
pip install poetry

重点介绍使用poetry用到的几个命令

初始化新项目

1
poetry new my_proj

添加依赖

1
2
3
4
5
6
7
8
poetry add package_name					# 添加常规依赖
poetry add package_name@^1.2.3 # 指定依赖版本
poetry add --dev package_name # 添加仅用于开发的依赖,不需要在生产环境安装
poetry add "package_name>=1.2,<2.0" # 版本约束
poetry add package_name --optional # 可选依赖,在其他依赖或特定配置中显式要求
poetry add `cat requirements.txt` # 从requirements.txt添加依赖
poetry update package_name # 更新依赖
poetry remove package_name # 删除依赖

tips: 在运行poetry add之前,确保您位于项目的根目录中,这样Poetry才能找到正确的pyproject.toml文件

poetry.lock文件是在您使用poetry add添加第一个依赖后由Poetry自动创建的,并且确实会随着项目依赖的变化自动更新,这个文件的主要目的是确保项目依赖的一致性

安装所有依赖

1
poetry install

执行poetry install命令时,Poetry会根据poetry.lock文件中锁定的版本来安装依赖,而不是依赖于 pyproject.toml 中的声明

定期运行poetry update以更新依赖,并在进行充分测试后提交新的poetry.lock文件,以保持依赖的更新

构建和发布包

1
poetry build			# 将在项目的 dist/ 目录下创建包文件
1
poetry publish

在将包发布到PyPI之前,您可能想先将其发布到TestPyPI进行测试,这有助于确保包的构建和发布过程按预期工作,使用 poetry publish --repository testpypi 来做到这一点

运行脚本

1
poetry run python my_script.py

poetry run命令是Poetry工具提供的一种方便的方式,用于在项目的虚拟环境中运行指定的命令;当您使用poetry run执行命令时,Poetry会确保该命令在项目的虚拟环境中执行,而不是在您的全局 Python 环境中,这意味着该命令将在一个包含您项目依赖的隔离环境中运行

poetry run不仅限于运行Python脚本,它可以用来运行项目环境中的任何命令

e.g.

1
2
poetry run pytest				# 运行测试
poetry run flask run # 启动一个开发服务器

PyPI

在你上传到PyPI时,需要验证登录,推荐使用token登录

在你注册完账号后,需要双重验证才允许你生成token(好像是因为太多恶意包上传到PyPI上了,为了安全考虑),在手机上下载一个google authenticator 即可,具体操作可以自行谷歌

生成token后,在上传时就可以输入__token__作为用户名,使用token作为密码就可以了

为了省事,你也可以使用配置文件,命名为.pypirc

1
2
3
[pypi]
username = __token__
password = pypi-AgEIcHlwaS5vcmcCJD...

可以为不同的项目创建不同的 token,并限制它们的权限,以提高安全性

版本号

软件版本号是一种标识软件特定版本的系统,它用于追踪和区分软件的不同更新或改进。版本号通常包含数字和点(.),有时也包括字母或其他字符。不同的项目或组织可能采用不同的版本命名规则,但以下是一些常见的元素和原则

常见版本号

  1. 主版本号:通常是第一个数字,表示大的版本更新,可能包含重大更改或新功能。例如,在版本 2.3.5 中,2 是主版本号
  2. 次版本号:紧跟在主版本号后面,通常表示较小的功能更新或改进。在 2.3.5 中,3 是次版本号
  3. 修补号/补丁号:通常是第三个数字,用于小的错误修复或安全补丁。在 2.3.5 中,5 是修订号
  4. 预发布标识:有时版本号后会跟有额外的标识,如 alphabetaRC(候选发布版本)等,表示这是一个尚未完全稳定的版本。例如,3.0.0-alpha
  5. 构建号:在一些情况下,可能还会包括构建号,通常表示编译次数或日期。例如,2.3.5.20210321 可能表示 2021 年 3 月 21 日的构建

版本号的作用

  • 追踪变化:帮助用户和开发者了解软件的更新历史和当前状态
  • 兼容性:指出不同版本之间的兼容性,特别是在软件库或API中,版本号可以帮助开发者判断不同版本的兼容性和依赖性
  • 发布跟踪:在软件开发的生命周期管理中,版本号是区分不同阶段的关键,如开发阶段、测试阶段和发布阶段
  • 错误跟踪:当用户报告问题时,了解他们使用的具体软件版本对于快速定位和解决问题至关重要

常见版本号命名规则

  1. 语义化版本控制:这是一种流行的版本命名规则,强调版本号应该传达关于底层代码更改的含义。例如,在SemVer中,主版本号的变更表示可能存在破坏向后兼容性的更改
  2. 日期-时间版本命名:一些项目使用日期或时间作为版本号,这在持续集成/持续部署(CI/CD)的环境中较为常见
  3. 阶段性版本命名:如 alphabetaRC 等,表示软件开发的不同阶段

虚拟环境

Python虚拟环境是一种工具,用于在隔离的环境中安装和管理Python包。它们允许您为每个项目创建独立的环境,这样不同项目的依赖就不会相互冲突,这对于管理项目依赖、保持一致的开发环境以及避免全局安装包引起的问题至关重要

为什么使用虚拟环境

  1. 依赖隔离:每个项目可以有自己的依赖,不同项目间的依赖不会相互干扰
  2. 版本控制:不同的项目可能需要相同包的不同版本,虚拟环境允许每个项目使用所需的特定版本
  3. 避免污染全局环境:在没有虚拟环境的情况下,所有包都会被安装到全局Python环境中,可能导致依赖混乱和版本冲突
  4. 实验和测试:为新工具或库创建一个独立的虚拟环境,而不会影响其他项目

pip

pip配置文件

全局配置
  • Unix & macOS:/etc/pip.conf
  • Windows:在 %PROGRAMDATA%\pip\pip.ini or C:\ProgramData\pip\pip.ini
用户级配置
  • Unix & macOS:$HOME/.config/pip/pip.conf or ~/.pip/pip.conf
  • Windows:%APPDATA%\pip\pip.ini

在虚拟环境下的配置,一般是在<venv>/pip.conf or <venv>/pip.in

也可以使用 pip config set xxxx 来配置

配置文件格式

e.g.

1
2
3
4
5
6
7
8
9
10
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
trusted-host = pypi.tuna.tsinghua.edu.cn
timeout = 60

[install]
require-hashes = true

[list]
format = columns

具体遇到什么配置,可以去互联网上搜索

常用命令

  1. 安装包

    1
    2
    3
    pip install package_name
    pip install package_name==version # 安装特定版本
    pip install package_name.whl # 直接从.whl文件安装包
  2. 卸载包

    1
    pip uninstall package_name
  3. 列出已安装包

    1
    pip list
  4. 显示包信息

    1
    pip show package_name
  5. 安装requirements.txt中的包

    1
    pip install -r requirements.txt
  6. 生成requirements.txt文件

    1
    pip freeze > requirements.txt
  7. 更新包

    1
    pip install --upgrade package_name
  8. 配置源(推荐写在配置文件中)

    1
    pip install package_name -i https://pypi.tuna.tsinghua.edu.cn/simple
  9. 查看配置

    1
    pip config list

venv & virtualenv

venv

venv是Python 3.3及以上版本的标准库的一部分,因此无需额外安装即可使用,且轻量级

创建虚拟环境

1
python3 -m venv /path/to/new/virtual/environment

激活虚拟环境

  • Unix & MacOS

    1
    source /path/to/new/virtual/environment/bin/activate
  • Windows

    1
    \path\to\new\virtual\environment\Scripts\activate.bat

退出虚拟环境

1
deactivate

删除虚拟环境

1
rm -rf /path/to/new/virtual/environment

virtualenv

virtualenvvenv更加强大且灵活,并且支持Python 2

安装

1
pip install virtualenv

创建虚拟环境

1
virtualenv /path/to/new/virtual/environment

也可指定Python解释器的版本

1
virtualenv -p python3.8 env

激活虚拟环境

  • Unix & MacOs

    1
    source /path/to/new/virtual/environment/bin/activate
  • Windows

    cmd

    1
    \path\to\new\virtual\environment\Scripts\activate.bat

    PowerShell

    1
    \path\to\new\virtual\environment\Scripts\Activate.ps1

退出虚拟环境

1
deactivate

删除虚拟环境

1
rm -rf /path/to/new/virtual/environment

Pipenv

Pipenv是一个Python开发工作流的工具,旨在将pip的包管理和venv的虚拟环境管理结合起来;它自动创建并管理一个虚拟环境,同时在项目的Pipfile中添加/删除项目依赖,从而使开发过程更加简化

安装

1
pip install pipenv

创建新项目

1
2
3
mkdir myproject
cd myproject
pipenv --python 3.8

安装和管理依赖

1
2
pipenv install requests		# 安装依赖
pipenv uninstall requests # 卸载依赖

如果你想添加一个仅在开发中需要的包

1
pipenv install pytest --dev

安装所有依赖,根据 Pipfile

1
pipenv install

激活 & 退出虚拟环境

1
2
pipenv shell
exit

Pipfile

查看依赖图
1
pipenv graph

查看项目的依赖图,这有助于理解依赖之间的关系

清理不需要的依赖
1
pipenv clean

卸载不在Pipfile中声明的包

锁定依赖
1
pipenv lock

确保您的生产环境中使用的依赖与您的开发环境完全一致

Pipfile的基本结构

e.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
requests = "*"
flask = ">=1.1,<2.0"

[dev-packages]
pytest = "*"

[requires]
python_version = "3.8"

通常,不需要手动创建或编辑Pipfile,当您使用Pipenv安装或卸载包时,Pipenv会自动为您创建和更新PipfilePipfile.lock

tips: Pipenv本身并不支持直接从.whl文件来安装包,可以使用pip安装后同步到Pipfile

  1. 使用pip安装.whl文件
  2. 使用pipenv lock来更新Pipfile.lock
  3. 使用pipenv sync来同步

反正就是不推荐在使用Pipenv时安装不受信赖的包

Conda/Miniconda

在linux中,一般都会有一个系统层面的python版本,如果随意使用pip安装各种奇怪的库,可能会导致在滚动更新时的各种Python包问题,在日常开发中使用虚拟环境就很有必要,本文推荐使用miniconda来管理各个版本的Python以及包

Miniconda

安装
  • arch linux

    1
    yay -S miniconda3
  • 从脚本安装(从官网下载Linux安装脚本后)

    1
    bash Miniconda3-latest-Linux-x86_64.sh
使用

创建新的Conda环境

1
conda create --name py39 python=3.9

激活Conda环境

1
conda activate py39

管理Conda环境

  • 列出所有Conda环境

    1
    conda env list
  • 删除环境

    1
    conda env remove --name py39

更新Conda及Python环境

  • 更新Conda

    1
    conda update conda
  • 更新某环境包

    1
    conda update --all --name py39

一个使用pipenv + 三种包构建分发工具的小例子 PyPI-demo