加载中...

Python打包


Python打包

最近感兴趣想将开发的项目转成Package,研究了一下相关文章,并且自己跑通了,走了一下弯路,这里记录一下如何打包一个简单的Python项目,展示如何添加必要的文件和结构来创建包,如何构建包,以及如何将其上传到Python包索引(PyPI)。

pyproject简介

之前python因为规范太宽松,导致每个项目的结构五花八门,且打包方式非常繁琐;就比如 setuptools 打包方式要写各种配置,实在难学。但是!Python从PEP 518开始引入使用pyproject.toml管理项目元数据的方案,且该规范目前已在很多开源项目中得以支持!

pyproject.toml 是在 PEP 518 中提出并在 PEP 621 中扩展的新配置文件 。目的是管理构建依赖,同时也可以存储 Python 项目的任何工具配置。

使用pyproject的目的:

  • 在一个 Python 项目中,我们需要管理 requirements.txtflake8 等等的配置文件,当一个项目中使用的工具越多,根目录就越杂乱,管理成本越高,对新人也就越不友好
  • 将诸多工具的配置集中到 pyproject.toml 统一管理,将小而零散的开发工具配置提取并放到同一个地方,便于了解项目构建、开发流程等信息

项目结构

假设我们软件包的名字是myutils,那么整个项目的目录结构在推荐的风格下看起来应该像这样:

.
├── LICENSE
├── pyproject.toml
├── README.md
├── src
│   └── hatch_demo   # src 下面是包名,包下面是业务代码 
│       ├── core.py
│       └── __init__.py
└── tests
    └── __init__.py

3 directories, 6 files

选择构建后端

像pip和build这样的工具实际上不会将源代码转换为分发包(如轮子);该工作由构建后端执行。构建后端决定您的项目将如何指定其配置,包括元数据(有关项目的信息,例如,PyPI上显示的名称和标签)和输入文件。构建后端具有不同级别的功能,例如它们是否支持构建扩展模块,应该选择适合需求和偏好的一个。

这里可以从许多后端中进行选择;本教程默认使用Hatchling,但它将与支持元数据的setuptools、Flight、PDM和其他支持[project]表的方法相同。

pyproject.toml告诉构建前端工具,如pip和build,为项目使用哪个后端。以下是一些常见构建后端的示例,但请查看后端自己的留档以获取更多详细信息。

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[build-system]
requires = ["flit_core>=3.4"]
build-backend = "flit_core.buildapi"

[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"

最小化示例

后面使用 hatch 后端进行构建,详细配置请参阅 hatch 官方文档:https://hatch.pypa.io/latest/config/metadata/

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "demo"    # 软件包名
version = "0.0.1"   # 软件版本

简单示例

下面小实践一下,目录结构如下:

.
├── myutils
│   ├── example.py
│   └── __init__.py
└── pyproject.toml

1 directory, 3 files

pyproject.toml 文件内容:

[build-system]
build-backend = "hatchling.build"
requires = [
  "hatchling>=1.21",
  "importlib_metadata>=4.8.3",
  "versioningit>=2.3",
]

[project]
name = "myutils"
license = "Apache-2.0"
requires-python = ">=3.9"
classifiers = [
  "Private :: Do Not Upload",
  "Programming Language :: Python :: 3 :: Only",
  "Programming Language :: Python :: 3.9",
  "Programming Language :: Python :: 3.10",
  "Programming Language :: Python :: 3.11",
  "Programming Language :: Python :: 3.12",
]
version = "0.1.0"

[tool.hatch.build.targets.wheel]
packages = ["myutils"]

# 如果需要把.pyc编译的文件打包,需要使用此配置(注意:此方式打包安装后要到python包目录查看确认一下)
# force-include 是强制包含;这里就是强制把myutils目录打入whl包的根目录内 
#[tool.hatch.build.targets.wheel.force-include]
#"./myutils" = "/myutils"

example.py文件内容,简单写一个add函数:

def add(x: int, y: int) -> int:
    return x + y

文件准备完成后,即可打包使用。

  1. 安装 build 依赖并用 build 来打包

    # 安装依赖
    pip install build
    
    # 执行打包
    python -m build
    # 或者
    pyproject-build .    # 默认生成 .tar.gz 和 .whl 文件
    pyproject-build -w . # -w参数是只保留whl包
    pyproject-build -s . # -s参数是只保留.tar.gz源码包
  2. 打包成功后会生成一个dist目录,里面存放有whl包

    pip install dist/myutils-0.1.0-py3-none-any.whl
  3. 测试使用

    >>> from myutils import example
    >>> example.add(1,3)
    4
  4. 也可以上传到pypi

    将打好的包上传到Python包索引,可供其它人安装。需要做的第一件事是在TestPyPI上注册一个帐户,这是一个用于测试和实验的包索引的单独实例。对于像本教程这样不一定想上传到真实索引的东西,这样的测试环境是非常好的。 注意:这不是永久存储。Test系统偶尔会删除包和帐户。

    如何使用TestPyPI,请参阅使用TestPyPI

    上传之前需要先下载 twine

    pip install --upgrade twine

    上传软件包:

    # 上传dist下的所有存档到 testpypi 源:
    python3 -m twine upload --repository testpypi dist/*
    
    # 上传单个软件包到 pypi
    twine upload dist/myutils-0.1.0-py3-none-any.whl

    下载上传的软件包:

    python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps myutils

    当准备好将真实包上传到Python包索引时,可以像本教程中一样执行相同的操作,但有以下重要区别:

复杂示例

接下是打包一个django app:

目录结构如下:

Two
├── app01
│   └── ......
├── app02          # 要打包的app
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── models.py
│   ├── README.md
│   ├── ser.py
│   ├── templates
│   │   └── test.html
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── db.sqlite3
├── dist
│   ├── django_app02-0.1.0-py3-none-any.whl
│   └── django_app02-0.1.0.tar.gz
├── manage.py
├── pyproject.toml
├── templates
├── Two
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── utils
    ├── orm_test.py
    └── schema.py

根据上面目录结构可知这是一个名为Two的django程序,里面有app01, app02 两个子app,这也是我们常用的目录结构。接下来打包app02

  1. 编写 pyproject.toml 文件(环境准备看上面):

    [build-system]
    build-backend = "hatchling.build"
    requires = [
      "hatchling>=1.21",
      "importlib_metadata>=4.8.3",
      "versioningit>=2.3",
    ]
    
    [project]
    name = "django-app02"
    license = "Apache-2.0"
    requires-python = ">=3.6"
    classifiers = [
      "Private :: Do Not Upload",
      "Programming Language :: Python :: 3 :: Only",
      "Programming Language :: Python :: 3.6",
      "Programming Language :: Python :: 3.7",
      "Programming Language :: Python :: 3.8",
      "Programming Language :: Python :: 3.9",
      "Programming Language :: Python :: 3.10",
      "Programming Language :: Python :: 3.11",
      "Programming Language :: Python :: 3.12",
    ]
    version = "0.1.0"
    dependencies = [
      "djangorestframework~=3.12.4"		# 用到的依赖,在安装该软件包时会自动关联安装restframework
    ]
    
    # 如果需要压缩包,则需要取消此注释
    #[tool.hatch.build.targets.sdist]
    #only-include = ["app02"]			# 对app02目录下内容打包
    
    [tool.hatch.build.targets.wheel]
    packages = ["app02"]				# 对app02目录下内容打包
    
    # 使用ruff检查规范
    [tool.ruff]
    include = [
      "app02/*.py",
    ]
    exclude = [
      "app02/*/migrations/*.py",
    ]
    line-length = 120
    target-version = "py39"
    output-format = "full"
    show-fixes = true
    
    [tool.ruff.format]
    preview = true
    
    [tool.ruff.lint]
    preview = true
    select = [
      "ALL",
    ]
    ignore = [
      "D",
      "ANN",
      "COM812",
      "ISC001",
      "FA100",
      "FA102",
      "N818",
      "TID252",
      "RUF012",
      "DJ001",
      "DJ008",
    ]
  2. 执行打包命令:

    pyproject-build -w .
  3. 安装:

    pip install dist/django_app02-0.1.0
  4. 测试使用:

    • 创建新的django项目

      django-admin startproject myproject
    • 修改 myproject/settings.py 文件,添加 restframeworkapp02 两个app

      INSTALLED_APPS = [
          ......
          'rest_framework',
          'app02.apps.App02Config'
      ]
    • 把app02路由添加到 myproject/urls.py 中:

      urlpatterns = [
          ......
          path("app02/", include('app02.urls')),
      ]
    • 启动测试:

      python manage.py runserver 0.0.0.0:8080

      启动后访问app02即可。

    额外补充:有没有发现在settings配置中,rest_framework配置只需要写个名字就行了,但是我们的app02还需要写那么多,那如何做到呢?这需要我们修改 app02/__init__.py 文件(可以参考rest源码),修改如下即可:

    import django
    
    __title__ = 'app02'
    __version__ = '0.0.1'
    __author__ = 'wuye'
    
    VERSION = __version__
    
    django.VERSION >=(2, 2) and django.VERSION <= (2, 3) 	# 限定了django版本为2.2.*
        default_app_config = 'app02.apps.App02Config'		# app的配置类

[参考附录]

  1. build 包文档说明:https://build.pypa.io/en/stable/mission.html
  2. hatch 官方文档:https://hatch.pypa.io/latest/config/metadata/
  3. pep-0517:https://peps.python.org/pep-0517/
  4. pep-0518:https://peps.python.org/pep-0518/
  5. toml 格式说明:https://toml.io/en/

文章作者: 无夜
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 无夜 !
评论
  目录