pypi 官网给出了4个打包 whl 格式的工具:Hatchling、setuptools、Flit、PDM。
初步了解了一下,还是用 setuptools 。
本文主要内容包括:pyproject.toml 的简单配置、使用 python -m build 打包、使用 cython 编译模块、生成命令等内容。
本文仅为学习经验,深入学习请参考官方文档。
安装 setuptools 和 build
pip install --upgrade setuptools # 65.5.0
# 安装 build 以可以运行命令: python -m build
pip install --upgrade build # 0.9.0
以名为 pypackage 的项目为例(src 结构)
pypackage/
├── LICENSE # LICENSE
├── pyproject.toml # 项目配置信息
├── README.md # 自述文件
├── src # 源码
│ └── pypackage
│ ├── __init__.py
│ └── __main.py
└── tests # 相关的测试
└── test_main.py
还有另一种结构(flat 结构)
pypackage/
├── LICENSE # LICENSE
├── pyproject.toml # 项目配置信息
├── README.md # 自述文件
│── pypackage # 名字为项目名
│ ├── __init__.py
│ └── __main.py
└── tests # 相关的测试
└── test_main.py
setuptools 具有自动检测项目代码目录的功能。另外需要说明的是,使用 src-layout 的形式时,会添加 src 下的所有目录,建议 src 下只保留一个与项目名同名的目录
src/pypackage/__init__.py
from .__main import say_hello
src/pypackage/__main.py
def say_hello(name: str):
__private_say(name)
def __private_say(name: str):
print(f"private say: Hello, {name}!")
tests/test_main.py
TEST_RELEASE = True
if not TEST_RELEASE:
import sys
import pathlib
root_dir = pathlib.Path(__file__).absolute().parent.parent
sys.path.insert(0, str(root_dir.joinpath("src")))
# using pytest
def test_hello():
import pypackage
pypackage.say_hello("yluuu")
pyproject.toml
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "pypackage"
version = "0.0.1"
dependencies = [
"requests",
"rtoml; python_version<'3.8'",
]
默认情况会根据依赖包创建一个隔离的虚拟环境来打包,但我是离线环境,所以不能用这种方式,需要添加 --no-isolation
选项
python -m build --no-isolation
打包如果成功会提示:Successfully built pypackage-0.0.1.tar.gz and pypackage-0.0.1-py3-none-any.whl
,共生成了两个文件
使用 pip 安装 whl 文件,查看安装目录 ~/miniconda3/lib/python3.7/site-packages/pypackage
-rw-rw-r-- 1 yl yl 30 10月 31 08:19 __init__.py
-rw-rw-r-- 1 yl yl 124 10月 31 08:19 __main.py
drwxrwxr-x 2 yl yl 4.0K 10月 31 08:19 __pycache__
在项目根目录下直接运行 pytest
命令,测试没有问题
~/test/pypackage ----------------- base py | yl@jisuan01 | 08:30:28
> pytest
======================== test session starts =========================
platform linux -- Python 3.7.13, pytest-7.2.0, pluggy-1.0.0
rootdir: /home/yl/test/pypackage
collected 1 item
tests/test_main.py . [100%]
========================= 1 passed in 0.01s ==========================
setuptools 有自动发现的功能:对于常用的两种项目形式(src-layout、flat-layout),setuptools 可以自动扫描并发现代码目录。
添加文件 src/pypackage/lib1/mod1.py
def func1():
return 1
添加文件 src/pypackage/lib2/mod2.py
def func2():
return 2
src 及 项目下的模块均可以自动被添加
adding 'pypackage/__init__.py'
adding 'pypackage/__main.py'
adding 'pypackage/lib1/mod1.py' # 自动添加了
adding 'pypackage/lib2/mod2.py' # 自动添加了
adding 'pypackage-0.0.1.dist-info/LICENSE'
adding 'pypackage-0.0.1.dist-info/METADATA'
adding 'pypackage-0.0.1.dist-info/WHEEL'
adding 'pypackage-0.0.1.dist-info/top_level.txt'
adding 'pypackage-0.0.1.dist-info/RECORD'
更多请参考: https://setuptools.pypa.io/en/latest/userguide/package_discovery.html
添加选项 --wheel
即可
python -m build --no-isolation --wheel
如 添加 keep.parquet 排除 exclude.txt
pypackage/
├── ...
│── src
│ └── pypackage
│ ├── __init__.py
│ ├── __main.py
│ └── data
│ ├── keep.parquet
│ └── exclude.txt
└── ...
在 pyproject.toml
添加
[tool.setuptools]
include-package-data = true
[tool.setuptools.package-data]
pypackage = ["data/*.parquet"]
[tool.setuptools.exclude-package-data]
pypackage = ["data/*.txt"]
其实 include-package-data
默认就是 true 的
打包记录
adding 'pypackage/__init__.py'
adding 'pypackage/__main.py'
adding 'pypackage/data/keep.parquet' # 这里
adding 'pypackage/lib1/mod1.py'
adding 'pypackage/lib2/mod2.py'
adding 'pypackage-0.0.1.dist-info/LICENSE'
adding 'pypackage-0.0.1.dist-info/METADATA'
adding 'pypackage-0.0.1.dist-info/WHEEL'
adding 'pypackage-0.0.1.dist-info/top_level.txt'
adding 'pypackage-0.0.1.dist-info/RECORD'
更多请参考: https://setuptools.pypa.io/en/latest/userguide/datafiles.html
使用 cython
pip install cython --upgrade
创建 setup.py
# 方式一:按 setuptools 文档,使用 Extension 类,使用 cython 编译时无法设置 language_level TODO 待解决
# from setuptools import setup, Extension
# setup(
# ext_modules=[
# Extension(
# name="pypackage.lib1.mod1",
# sources=["src/pypackage/lib1/mod1.pyx"],
# )
# ]
# )
# 方式二:直接调用 cython,可以设置 language_level
# 备注: 子模块尽量添加 __init__.py 以确保编译后的库文件可以正常打包到子模块中。
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize(module_list=[
"src/pypackage/lib1/mod1.pyx"
], language_level=3)
)
另外在 pyproject.toml
中排除 c 文件打包
[tool.setuptools.exclude-package-data]
pypackage = ["info/*.txt"]
"pypackage.lib1" = ["*.c"]
将 src/pypackage/lib1/mod1.py
重命名为 src/pypackage/lib1/mod1.pyx
,并在 src/pypackage/lib1
下添加 __init__.py
文件
然后使用 python -m build
时会自动调用 setup.py
生成库文件并打包
单独编译模块时运行 python setup.py build_ext
备注:测试过程中的坑,都在上面的注释里了
adding 'pypackage/__init__.py'
adding 'pypackage/__main.py'
adding 'pypackage/info/keep.parquet'
adding 'pypackage/lib1/__init__.py'
adding 'pypackage/lib1/mod1.cpython-37m-x86_64-linux-gnu.so'
adding 'pypackage/lib2/mod2.py'
adding 'pypackage-0.0.1.dist-info/LICENSE'
adding 'pypackage-0.0.1.dist-info/METADATA'
adding 'pypackage-0.0.1.dist-info/WHEEL'
adding 'pypackage-0.0.1.dist-info/top_level.txt'
adding 'pypackage-0.0.1.dist-info/RECORD'
在 __main.py
中添加以下内容作为可执行命令的入口
def hello():
__private_say("pypackage")
然后在 pypackage.toml
中添加名为 pypackage-hello
的命令(在命令行中可直接运行 pypackage-hello
)
[project.scripts]
pypackage-hello = "pypackage.__main:hello"
完成。重新打包安装后效果如下:
> pypackage-hello
private say: Hello, pypackage!