whl包构建

Posted 城南花已开

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了whl包构建相关的知识,希望对你有一定的参考价值。

安装依赖

pip install whell
pip install twine

参数对应

标注*号的为重要参数

描述性参数 —— 提供包信息,供PiPy识别管理

描述性参数,只是作为包的信息用的,没有特殊作用,可有可无。

参数 类型 说明
*name str 包名称
*version str 包版本
*author str 程序的作者,这个包从头到尾都是你先开发的,那么你就是原作者。
*author_email str 程序的作者的邮箱地址
maintainer[^maintainer] str 维护者,如果你不是原作者,这个包是你修改原作者的包,那么你就是维护者。
maintainer_email str 维护者的邮箱地址
*url str 程序的官网地址
license str 程序的授权信息
description str 程序的简单描述
long_description str 程序的详细描述,详细描述在包上传PyPi后会在包页面下显示,支持RST和MD两种格式。
platforms str 程序适用的软件平台列表
classifiers str 程序的所属分类列表。影响上传PyPi会被分类到哪个类别。
keywords str 程序的关键字列表。同上。
download_url str 程序的下载地址

包文件搜集 —— 设置包的哪些文件需要打包,怎么找这些文件

参数 说明
*package_dir 用来给setuptools指示packages里的包名要从哪些目录下找,默认是setup.py所在的根目录下找packages。
*packages 打包的包目录(通常为包含 init.py 的文件夹)。可以手动一个个包名添加,也可以直接用find_package自动找指定目录下所有带 init.py的文件夹。默认只将文件夹内所有的.py文件打包。如果文件夹内有其他类型文件,并且包依赖这些文件,需要通过设置package_data来声明要打包的文件/类型。搜集的文件夹会被安装到site-packages目录下。
*package_data 指定包内需要包含的数据文件类型。默认packages只会把.py文件打包进去,如果包依赖其他类型文件就需要在package_data里声明要打包进去的文件类型。
include_package_data 如果设置为True,这将告诉setuptools自动将它找到的所有数据文件包含在MANIFEST.in文件指定的软件包目录中。MANIFEST.in可通过setuptool插件自动跟踪版本控制工具生成。
exclude_package_data 将包名称映射到应该从包目录中排除的全局模式列表的字典。您可以使用它来修剪include_package_data包含的所有多余文件。有关完整的描述和示例,请参见“包括数据文件”部分。这个参数服务于include_package_data。
*py_modules 需要打包的 Python 单文件列表。如果模块只是一个py文件,那么添加到这里打包进去。注意只需要写文件名,不要带.py后缀。
*data_files 打包时需要打包的数据文件,如图片,配置文件等。格式("路径","文件"),路径是PYTHON_HOME目录下的相对路径。
scripts 指定可执行脚本,安装时脚本会被安装到系统PATH路径下(PYTHON_HOME\\Scripts)。注意,一般是指命令行脚本,只有这种脚本安装到系统PATH路径下可以直接在命令行里调用。

依赖

参数 说明
ext_modules 指定c扩展模块
requires 指定依赖的其他包。你的包依赖了其他外部包就添加到这里,安装你的包的时候会一并安装。(已过时,被install_requires代替了)
install_requires 安装时需要安装的依赖包,同上。
setup_requires 指定运行 setup.py 文件本身所依赖的包。如果你在setup.py里面引用了依赖的包,就需要添加到这里。
extras_require 当前包的高级/额外特性需要依赖的分发包。extras你可以理解为可选外部依赖,比如你的包有导出数据功能,默认支持csv格式,如果要导出excel格式则需要安装openxlrd,那么openxlrd就是可选外部依赖。
provides 指定可以为哪些模块提供依赖。pip已经忽略这个参数了。
dependency_links 指定依赖包的下载地址。pip已经不支持了。

打包项目

1、目录结构

2、创建setup.py

# -*- coding: utf-8 -*-
from setuptools import setup, find_packages

try:
    long_description = open("README.md").read()
except IOError:
    long_description = ""
    
kwargs = {\'author\': \'Sucheon Algoritm Department\',
 \'author_email\': \'haobin.zhang@sucheon.com\',
 # 此处是把该文件打包到python的安装目录下的share目录中,如果share替换为Doc目录,并且文件替换为package.txt,则使用python -m package命令,会自动打印package.txt中的信息。
 \'data_files\': [(\'share\',
                 [\'scdap_algorithm/function/other/sim_zs167/zsdl_model.pkl\'])],
 \'description\': \'Sucheon Distributed Algorithm Processing System - Algorithm \'
                \'Module.\',
 # 此处搭配package_data参数,设置为True的时候,会把package_data中指定的包名下的文件进行上传
 \'include_package_data\': True,
 \'install_requires\': [\'numpy~=1.19.1\',
                      \'pandas==1.1.5\',
                      \'cython\',
                      \'statsmodels==0.12.2\',
                      \'scikit-learn~=0.21.3\',
                      \'scipy~=1.6.1\',
                      \'tensorflow==2.4.1\'],
 \'license\': \'\',
 \'long_description\': long_description,
 \'long_description_content_type\': \'text/markdown\',
 \'name\': \'scdap_algorithm\',
 \'package_data\': {\'scdap_algorithm\': [\'*.pkl\']},
 \'packages\': find_packages(),
 \'python_requires\': \'>=3.7\',
 \'version\': \'1.20211209.2009\'}

setup(**kwargs)

3、执行打包命令

 python setup.py

上传whl包

python -m twine upload dist/*

示例代码

1、参数文件.whl.json

{
  "desc": "算法包",
  "module_name": "scdap_algorithm",
  "lib_name": "scdap_algorithm",
  "packages_parameter": {
    "exclude": [
      "function.decision",
      "function.evaluation",
      "function.other",
      "function.decision.*",
      "function.evaluation.*",
      "function.other.*",
      "lib",
      "lib.*"
    ]
  },
  "package_data": {
    "scdap_algorithm": [
      "function/decision/__init__.py",
      "function/evaluation/__init__.py",
      "function/other/__init__.py",
      "lib/__init__.py",
      "function/decision/motor61.py",
      "function/evaluation/hgear64.py",
      "function/evaluation/hgear75.py",
      "function/evaluation/hreducer88.py",
      "*.pkl"
    ]
  },
  "install_requires": "reqirements.txt"
}

2、setup.py

"""
@author: 开花的马铃薯
@create on: 2021.03.22

package to wheels
.whl_parameters.json -> wheel包信息配置
{
    "lib_name": "目标库目录路径名称",
    "module_name": "需要打包成的库名称",
    "package_data": {}                  // setup.package_data参数
    "packages_parameter": {             // setuptools.find_namespace_packages参数
        "include": ("*",),
        "exclude": ()
    }
    "version": "1.0.0",                 // 版本号, 在develop(测试环境下上传至pypi-develop)master环境下才上传至正式的pypi
    "author": "xxx",                  // 作者
    "author_email": "xxxx@xxx.com",    // email
    "install_requires": "requirements.txt"  // 依赖文件路径
    "description": "str",                   // 库的简要描述
    "long_description": "README.md",        // 完整的库说明文件路径
    "long_description_content_type": "text/markdown",   // 库说明文件类型
    "python_requires": ">=3.7",     // python版本配置
    "license": "LICENSE"            // LICENSE
    ...
}

"""
import os
import sys
import json
from pprint import pprint
from typing import Optional
from importlib import import_module

import requests
from twine import cli
from setuptools.extension import Extension
from setuptools import setup, find_namespace_packages


def get_requirements(path: str) -> list:
    """
    解析依赖库信息
    """
    requires = list()

    if not os.path.exists(path):
        return requires

    with open(path, \'r\') as file:
        for line in file.readlines():
            if len(line) <= 1:
                continue
            if line[-1] == \'\\n\':
                line = line[:-1]
            requires.append(line)
    return requires


def from_file(path: str):
    if not os.path.exists(path):
        return \'\'
    with open(path, \'r\', encoding=\'utf-8\') as f:
        return f.read()


def get_extensions(lib_dir: str, lib_name: str, exclude: list = ()):
    packages = find_namespace_packages(lib_name)
    packages = [f"{lib_name}.{path}" for path in packages]
    packages.append(lib_name)
    lib_dir = os.path.normpath(lib_dir)
    extensions = []
    for package in packages:
        path = os.path.join(lib_dir, package.replace(\'.\', os.path.sep))
        for fname in os.listdir(path):
            simple_path = os.path.join(path, fname)
            if fname.endswith(\'.py\') and fname not in exclude:
                simple_package = f\'{package}.{os.path.splitext(fname)[0]}\'
                # print(f\'{simple_package} -> {simple_path}\')
                extensions.append(Extension(simple_package, [simple_path]))
    return extensions


def get_parameter(setup_parameter: dict, setup_parameter_key: str,
                  module=None, module_key: str = None, default=None):
    p = setup_parameter.get(setup_parameter_key)
    if p is not None:
        return p

    if module is None or module_key is None:
        if default is None:
            print(f\'can not find {setup_parameter_key}.\')
            raise SystemExit(1)
        return default

    p = getattr(module, module_key, default)
    if p is None:
        print(f\'can not find {setup_parameter_key} {module}.\')
        raise SystemExit(1)
    return p


def do_request(method: str, url: str, data: dict = None, token: str = \'\') -> Optional[dict]:
    """

    :param method:
    :param url:
    :param data:
    :param token:
    :return:
    """
    response = getattr(requests, method)(url, json=data, timeout=5, headers={\'Authorization\': token})
    response.close()

    if response.status_code != 200:
        raise Exception(f\'sqlapi接口: {url}调用失败, http返回码为: {response.status_code}\')

    response = response.json()
    # 无法找到数据
    if response[\'code\'] == \'B0100\':
        print(f\'sqlapi接口: {url}调用失败, 返回码: {response["code"]}, 错误信息: {response.get("message")}\')
        return None

    if response[\'code\'] != \'00000\':
        raise Exception(f\'sqlapi接口: {url}调用失败, 返回码: {response["code"]}, 错误信息: {response.get("message")}\')

    return response[\'result\']


def package_wheel(setup_parameter: dict):
    lib_name = setup_parameter[\'lib_name\']
    module_name = setup_parameter.get(\'module_name\', lib_name)

    try:
        module = import_module(lib_name)
    except Exception as e:
        print(f\'can not import module: {lib_name}, error: {e}\')
        raise SystemExit(1)

    packages_parameter = setup_parameter.get(\'packages_parameter\', dict())
    packages = find_namespace_packages(lib_name,
                                       include=packages_parameter.get(\'include\', (\'*\',)),
                                       exclude=packages_parameter.get(\'exclude\', ()))
    packages = [f"{lib_name}.{path}" for path in packages]
    packages.insert(0, lib_name)

    sys.argv.extend([\'bdist_wheel\', \'-q\'])
    kwargs = {
        "name": module_name,
        "packages": packages,
        "include_package_data": True,
        "data_files": [("share", ["scdap_algorithm/function/other/sim_zs167/zsdl_model.pkl"])],
        "package_data": setup_parameter.get(\'package_data\', dict()),
        "version": get_parameter(setup_parameter, \'version\', module, \'__version__\'),
        "long_description": from_file(get_parameter(
            setup_parameter, \'long_description\', default=\'README.md\')),
        "long_description_content_type": get_parameter(
            setup_parameter, \'long_description_content_type\', default="text/markdown"),
        "license": from_file(
            get_parameter(setup_parameter, \'license\', default=\'LICENSE\')),
        "author": get_parameter(
            setup_parameter, \'author\', module, "__author__", default=\'Sucheon Algoritm Department\'),
        "author_email": get_parameter(
            setup_parameter, \'author_email\', module, "__email__", default=\'haobin.zhang@sucheon.com\'),
        "description": get_parameter(
            setup_parameter, \'description\', module, "__description__", default=f\'Sucheon Algoritm Lib - {lib_name}\'),
        "install_requires": get_requirements(
            get_parameter(
                setup_parameter, \'install_requires\', default=\'requirements.txt\')),
        "python_requires": get_parameter(
            setup_parameter, \'python_requires\', default=\'>=3.7\'),
    }

    pprint(kwargs)
    setup(**kwargs)
    return kwargs


def main(whl_parameter_path):
    # 读取gitlab中的项目名称, 以解析成whl库名称
    lib_name = os.environ.get(\'CI_PROJECT_NAME\')
    if not lib_name:
        lib_name = os.path.split(os.path.split(__file__)[0])[1]
    lib_name = lib_name.lower().replace(\' \', \'_\').replace(\'-\', \'_\')

    # 获取分支名称
    # 用于区分多套环境
    # develop -> 测试环境
    # master -> 正式环境
    env = os.environ.get(\'CI_COMMIT_REF_NAME\')
    env_upper = env.upper()

    # 根据环境获取对应的pypi服务器
    twine_url = os.environ.get(f\'{env_upper}_TWINE_SERVER_URL\')
    if twine_url is None:
        print(F\'can not find env value: {env_upper}_TWINE_SERVER_URL\')
        raise SystemExit(1)
    print(\'twine server:\', twine_url)
    user = os.environ.get(\'TWINE_SERVER_USER\')
    if user is None:
        print(\'can not find env value: TWINE_SERVER_USER\')
        raise SystemExit(1)
    user, password = user.split(\'/\')

    # 根据环境获取对应的sqlapi服务
    # 服务用于保存版本号至数据库
    server_url = os.environ.get(f\'{env_upper}_SQLAPI_SERVER_URL\')
    if server_url is None:
        print(f\'can not find env value: {env_upper}_SQLAPI_SERVER_URL\')
        raise SystemExit(1)
    # sqlapi接口权限token
    token = os.environ.get(\'SQLAPI_SERVER_TOKEN\', \'\')

    # 读取模块打包的参数配置文件
    print(f\'load file: {whl_parameter_path}\')
    if os.path.exists(whl_parameter_path):
        with open(whl_parameter_path, \'r\', encoding=\'utf-8\') as f:
            whl_parameters = json.load(f)
            # 如果加载的whl的json文件中没有该参数,就使用默认的值
            if not whl_parameters.get("module_name"):
                whl_parameters["lib_name"] = lib_name
    else:
        print(f\'can`t find file: {whl_parameter_path}\')
        whl_parameters = {\'lib_name\': lib_name}

    module_name = whl_parameters.get(\'module_name\', lib_name)
    # 打包成whl
    print(\'whl parameter:\')
    pprint(whl_parameters)
    whl_result = package_wheel(whl_parameters)
    print(\'package module success.\')
    print(\'module:\', list(os.listdir(\'dist/\')))
    print(\'result module info:\')
    pprint(whl_result)

    # 上传whl至pypi
    print(\'upload module wheel to twine.\')
    cli.dispatch([\'upload\', \'--repository-url\', twine_url, \'-u\', user, \'-p\', password, \'dist/*\'])
    print(\'upload module wheel success.\')

    version = whl_result[\'version\']
    url = f\'{server_url}/module-version/{module_name}/\'
    result = do_request(\'get\', url, token=token)
    print(\'old module version data:\')
    pprint(result)

    extra = dict()
    old_version = \'null\'
    if result:
        extra = result[\'data\'][\'extra\']
        old_version = result[\'data\'][\'version\']

    data = {
        \'module_name\': module_name,
        \'version\': version,
        \'description\': whl_result[\'description\'],
        \'extra\': extra
    }

    extra[\'CI_COMMIT_SHA\'] = os.environ.get(\'CI_COMMIT_SHA\')
    extra[\'CI_COMMIT_SHORT_SHA\'] = os.environ.get(\'CI_COMMIT_SHORT_SHA\')
    extra[\'CI_COMMIT_REF_NAME\'] = os.environ.get(\'CI_COMMIT_REF_NAME\')
    print(\'update module version data:\')
    pprint(data)

    # 更新版本号信息至数据库
    if old_version == \'null\':
        do_request(\'post\', url, data, token=token)
    else:
        do_request(\'put\', url, data, token=token)


if __name__ == \'__main__\':
    # whl_parameters = dict()
    # if os.path.exists(\'.tool_whl.json\'):
    #     with open(\'.tool_whl.json\', \'r\', encoding=\'utf-8\') as f:
    #         whl_parameters = json.load(f)
    # package_wheel({"lib_name": "scdap_algorithm"})
    if sys.argv[1:]:
        pjson = sys.argv[-1]
        sys.argv.pop(-1)
    else:
        pjson = \'.whl.json\'
    main(pjson)

3、执行打包命令

python setup.py .whl.json

以上是关于whl包构建的主要内容,如果未能解决你的问题,请参考以下文章

Python安装安装.whl包(安装pylint)

Anaconda安装第三方包(whl文件)

Mac下如何安装whl包

python下如何安装.whl包?

Pycharm安装了第三方包后不显示代码提示

python用whl文件安装包