前言

这是一次从创建项目到测试打包上传Pypi的过程,记录一下。

创建项目

这里我是用的是pdm这个包管理工具,创建项目的命令是pdm init,跟着引导一步步走,之后会生成一个pyproject.toml文件,里面有项目的一些基本信息,比如作者、版本号等。下面是一个示例:

mkdir project
cd project
pdm init

之后生成的目录大概如下:

.
├── pyproject.toml
├── pdm.lock
└── src
    ├── project
    |   └── __init__.py
    └── tests
        └── __init__.py

安装依赖这些就不赘述了。

注意将源码放在src/project目录下,这样打包的时候会自动将src/project目录下的文件打包,而不是将整个项目打包。而且要加上build信息

[tool.pdm.build]
includes = ["src/project"]

[tool.pdm.version]
source = "file"
path = "src/project/__init__.py"

测试

用例测试

这里我是用的是pytest框架,用pdm add -d pytest来将pytest安装在dev依赖上,这样不会影响到用户pdm install project的安装。

测试代码放在tests目录下,测试用例文件名以test_开头,用例方法也以test_开头,比如

# tests/test_add.py

def test_add():
    assert add(1, 2) == 3

使用pytest命令来进行测试

mock测试

当测试数据依赖于某些API,且不方便测试时获得,那么可以使用mock来模拟数据。pytest的mock框架是pytest-mock,用pdm add -d pytest-mock来安装。

# tests/test_api.py
import requests

def api():
    return requests.get("https://example.com").json()

def test_request(mocker):
    mock_response = mocker.Mock()
    mock_response.json.return_value = {"code": 0, "data": "success"}
    mocker.patch("requests.get", return_value=mock_response)
    assert api() == {"code": 0, "data": "success"} 

Coverage覆盖率测试

pytest的coverage测试框架是pytest-cov,用pdm add -d pytest-cov来安装。测试用例不需要改变,但需要加上--cov=src指定覆盖率测试的目录;需要分支覆盖率,需要加上--cov-branch;如果还要测试报告,命令就变成了pytest --cov=src --cov-branch --cov-report=html:tests/report/coverage。注意这里要加上pytest-html依赖才能生成html报告。

Tox多Py版本兼容性测试

如果你要测试你的项目在不同Python版本下是否能正常运行,那么在PDM的环境下,正好可以使用tox-pdm来进行测试,pdm add -d tox-pdm,然后创建tox.ini文件,内容如下:

[tox]
env_list = py{38, 39, 310, 311}

[testenv]
groups = dev
commands =
    your test commands here

env_list是你要测试的Python版本,groups是测试组,commands是测试命令,比如pytest。

持续集成测试

其实就是运用Github Actions,下面有案例。

测试报告

不同版本不同的测试报告生成起来会挤在一堆,我自己的做法是写一个测试脚本:

# tests/__init__.py
import sys
from pathlib import Path

import pytest
import toml

py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
version = toml.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"][
    "version"
]


def test():
    path = f"v{version}/{py_version}"
    args = [
        "--cov=src",
        "--cov-branch",
        f"--cov-report=html:tests/report/{path}/coverage",
        f"--html=tests/report/{path}/report.html",
    ]
    args.extend(sys.argv[1:])  # Pass additional arguments to pytest if any
    pytest.main(args)

然后在pyproject.toml上添加

[tool.pdm.scripts]
test = {call = "tests:test"}

这样就可以使用pdm test来运行测试了。

最后在tox.ini上把commands改成pdm test,报告就分别存放了。

打包上传Pypi

pdm build是打包,pdm publish是上传。但这里我用一个更复杂的例子来说明,做到持续集成。

name: Upload Python Package

on:
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      version:
        description: Version Number
        required: true
        
permissions:
  contents: write

jobs:
  deploy:

    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v4
      with:
        fetch-depth: 0
        ref: ${{ env.GITHUB_REF }}
        token: ${{ secrets.GH_TOKEN }}

    - name: Config Git Username
      run: |
        git config user.name github-actions[bot]
        git config user.email github-actions[bot]@users.noreply.github.com
    
    - name: Setup Python
      uses: actions/setup-python@v4
      with:
        python-version: "3.9.x"
        
    - name: Install dependencies
      run: |
        pip install pdm

    - name: Change version
      run: |
        python ./bump-version.py ${{ github.event.inputs.version }}
        git add .
        git diff-index --quiet HEAD || git commit -m ":sparkles: ${{ github.event.inputs.version }}"
        git push        
    
    - name: Build and Publish
      run: |
        pdm publish --username __token__ --password ${{ secrets.PYPI_API_TOKEN }}

    - name: Release
      env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          tag: v${{ github.event.inputs.version }}
      run: |
        gh release create "$tag" \
            --repo="$GITHUB_REPOSITORY" \
            --title="${GITHUB_REPOSITORY#*/} ${tag#v}" \
            --generate-notes

流程是,输入version值后,他会提取项目设置里设置的secrets.GH_TOKEN和secrets.PYPI_API_TOKEN来进行打包、发布和创建release。其中还使用bump-version.py处理了__init__.py上的__version__信息。其他更多的命令,请自行搜索。