前言

本文讨论的内容有如果要编写自己的python模块,并上传到pypi.org则需要了解的知识点。主要有setuptool模块的使用,pip命令行工具,如何在pypi上上传自己的模块和其他相关知识。

setuptools

本章知识是我们理解前人编写的各个有用的模块包的基础,也是编写自己的模块包的基础。

请结合Github上的 pyskeleton项目 来阅读本章。

虽然官方内置distutils模块也能实现类似的功能,不过现在人们更常用的是第三方模块setuptools,其相当于distutils模块的加强版,初学者推荐就使用setuptools模块。更多内容请参看setuptools模块的 官方文档

现在setuptools推荐使用setup.cfg来进行相关配置管理,而不是之前的 setup.py 里的 setup 函数。pypi生态圈和相关PEP规范在不断完善中,现在推荐使用 build 模块,运行 python -m build 来进行你项目的打包工作。

你首先需要新建一个 pyproject.toml 文件,指定本项目的安装环境,setuptools相关如下:

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

小型模块项目

即使是非常小型的单python文件的模块,也推荐组织成为python的模块结构而不是通过py_modules来指定。

现在假定这里我们讨论的python模块名字是pyskeleton,如果该python模块很小型,则推荐采用如下结构:

----pyskeleton
  --__init__.py
  --another_file.py

setup.cfg
pyproject.toml

对应的setup.cfg文件内容大体如下:

[metadata]
name = pyskeleton
version = attr: pyskeleton.__version__
description = quickly create a python module, have some other good concern.
url=https://github.com/a358003542/pyskeleton
long_description = file: README.md
long_description_content_type=text/markdown

[options]
include_package_data = True
packages = pyskeleton

也就是直接将packages写上去即可。

NOTICE: packages这个关键词是setuptools模块继承自distutil模块的,其不会进一步递归扫描该模块可能有的子模块。也就是上面这个写法只能添加pyskeleton文件夹下的 __init__.py 文件和与其平行的一些python文件。如果你还希望在下面添加几个子模块,则参见下面的讨论。

中型模块项目

----pyskeleton
  -- submodule1
    -- __init__.py
  -- submodule2
    -- __init__.py
  --__init__.py
  --another_file.py

setup.cfg
pyproject.toml

类似上面这样中型大小的python模块项目,如果子模块不是很多的话,那么你可以继续如下这样编写:

packages = 
  pyskeleton
  pyskeleton.submodule1
  pyskeleton.submodule2

显然这样子模块多于两个以上就显得很麻烦了,那么这时就推荐使用setuptools模块提供的find函数。

packages = find:

其会自动从根目录开始进行扫描,然后根据 __init__.py 是否存在来自动生成packages列表。这个时候就有一个注意事项:你的测试文件夹 tests 最好不要有 __init__.py 文件了,实际上利用pytest这样的测试辅助模块也是不需要 __init__.py 文件的。

然后有的时候你可能希望某个子模块不要添加进去了,反正有的时候就有这个需求,那么你可以加上 exclude 参数:

[options.packages.find]
exclude = my_python_module.backup

注意写法是你预期要添加进入packages列表的字段,然后排除掉。

多python模块项目

绝大部分情况基本上就属于上面描述的哪两种了,但对于某些公司内部大型的复杂的python项目,可能会有多个python模块放在一个文件夹下,而且这里谈论的多个python模块的意思,就是多个python模块,比如requests和scapy这样完全不相干,不属于一个父模块名的独立的python模块,大体类似下面这样的文件夹结构:

----src
  ----pyskeleton
    --__init__.py
  ----other_module
    --__init__.py

setup.cfg
pyproject.toml

这种结构的 setup.cfg 大体内容如下所示:

[metadata]
name = pyskeleton
version = attr: pyskeleton.__version__
description = quickly create a python module, have some other good concern.
url=https://github.com/a358003542/pyskeleton
long_description = file: README.md
long_description_content_type=text/markdown

[options]
include_package_data = True
packages = find:
package_dir =
    = src

[options.packages.find]
where = src
include = pyskeleton

[options.entry_points]
console_scripts =
    pyskeleton = pyskeleton.__main__:main

首先来集中解释一下 package_dir 这个参数,这个参数实际上是从distuitl模块那边来的,上面这种写法对应的传统写法是:

package_dir = {'': 'src'}

这个字典值key是模块名,value是对应的文件夹,空字符串意思是根模块。这个说起来有点难懂,我下面再将整个过程理一遍好让读者更容易懂一点。

find函数的where是说要从哪里寻找python模块,所以现在find开始在src文件夹下面寻找了,include和exclude参数用来进一步控制find的查找行为,其中如果有include参数则只添加include参数指定的那些模块,比如上面只会添加pyskeleton模块,但这里还有一个点没讲。如果package_dir那个参数没配置的话,按照道理讲其对应的package name是 src.pyskeleton,因为包名是根据文件夹的结构来的,虽然我指定find从src文件夹开始查找,但是具体某个模块的文件夹结构是没变的,继而对应的packages那边的名字也是没变的,因为有了上面 package_dir 那个配置,现在模块的根位置对应的src文件夹,于是pyskeleton那个文件夹对应的模块名就是pyskeleton了。说得再具体一点pyskeleton现在将作为一个独立模块拷贝到 site-packages 哪里的根目录下面。

为了加深理解我们可以将上面的include参数改成 pyskeleton* ,然后再随便新建一个pyskeleton2模块,经过测试就会发现又会多了一个pyskeleton2的模块。

一般子模块会放在总模块的下面方便管理,但项目合作的时候可能各个子模块会分开开发,那么这个时候你可以使用find_namespace 来实现多个子模块在一个父模块名字之下,这块讨论这里就略过了。

metadata

一些metadata的填写还是很简单的,不过需要注意上面的 attr:file: 写法。attr可以提取本模块的某些属性信息,而可用于提取某文件的内容。

name

本软件的名字

version

本软件的版本号

author

本软件的作者

author_email

本软件作者的邮箱

maintainer

本软件的维护者

maintainer_email

本软件维护者的邮箱

contact

本软件的联系人。可以不写,则是维护者的名字,如果没有则是作者的名字。

contact_email

本软件的联系人的邮箱,可以不写,则是维护者的邮箱,如果没有则是作者的邮箱。

license

本软件的license

url

本软件项目主页地址

description

本软件的简要描述 long_description

本软件的完整描述

platforms

本软件经过测试可运行的平台

classifiers

本软件的分类,请参考 这个网页 给出一些值。是字符串的列表。

keywords

本软件在pypi上搜索的关键词,字符串的列表。

options

options这里除了上面已经提到的一些,其他的都略过讨论了,一般只在某些特殊情况下才会使用到。

entry_point
entry_points = {
'console_scripts' :[ 'zwc=zwc.zwc:main',],
}

其中zwc是你的shell调用的名字,然后zwc是你的模块,另外一个zwc是你的主模块的子模块,然后main是其中的main函数。这就是你的shell调用程序的接口了。类似的还有gui_script可以控制你调用GUI图形的命令入口。

include_package_data

一般推荐设置为 True,然后通过 MANIFAST.in 文件来管理各个数据文件。

install_requires
接受字符串的列表值,将你依赖的可以通过pip安装的模块名放入进去,然后你的软件安装会自动检测并安装这些依赖模块。
package_data

你的软件的模块额外附加的(除了py文件的)其他文件,具体设置类似这样 {"skeleton":['*.txt'],} 其中skeleton这里就是具体的你的软件的模块(对应的文件夹名),然后后面跟着的就是一系列的文件名列表,可以接受glob语法。注意这里只能包含你的模块文件夹也就是前面通过packages控制的文件夹下面的内容。

不推荐使用的选项

  • scripts 不推荐使用,推荐通过entry_point来生成脚本。
  • setup_requires 不推荐使用,基于PEP-518 。
  • py_modules 不推荐使用,推荐使用packages来管理模块。
  • data_files 前面的package_data是只能在你的模块文件夹里面的其他数据文件等,然后可能还有一些数据文件你需要包含的,用data_files来控制,具体后面跟着的参数格式如下面例子所示:
data_files = [('icos',['icos/wise.ico'])],
#这是添加的icos文件夹下面的wise.ico文件
data_files = [('',['skeleton.tar.gz'])],
#这是添加的主目录下的skeleton.tar.gz文件

值得一提的是data_files不能接受glob语法。

data_files已经不推荐使用了,推荐用MANIFAST.in来管理,可以方便用pkg_resources里面的方法来引用其中的资源文件。

读取资源文件

如下所示:

from pkg_resources import resource_filename
resource_stream('wise','icos/Folder-Documents.ico')

第一个参数是模块名字,第二个参数是模块中的文件的相对路径表达。

上面的例子是resource_filename,返回的是引用的文件名。此外还有命令:resource_string,参数和resource_filename一样,除了它返回的是字节流。这个字节流可以赋值给某个变量从而直接使用,或者存储在某个文件里面。

pip的develop模式

本小节参考了 这个问题

对于其他第三方包你不需要修改的,就直接 python setup.py install 就是了,而对于你自己写的包,可能需要频繁变动,最好是加载引用于本地某个文件夹,那么推荐是采用 python setup.py develop 命令来安装。develop模式下你修改了你的模块源码是直接生效的,因为安装过程只是提供了一个引用链接,实际还是用的你的源码这边的代码。

python setup.py install 对应的是 pip install . 命令,如果你没有setup.py这个文件了那么可以使用这个命令来从本地源码安装。develop模式对应的命令是: pip install -e .

在pypi上传你的模块

正确处理README文档

现在pypi已经支持markdow文档格式了。推荐按照官方文档 这里 来处理:

long_description = file: README.md
long_description_content_type=text/markdown

注意上面配置的 long_description_content_type ,如果你喜欢 reStructuredText 格式,那么设置为 text/x-rst 即可。

打包模块

首先推荐升级最新的setuptools,wheel和twine模块。

然后直接用下面这句:

python setup.py sdist bdist_wheel

这样将直接dist文件夹下面生成源码tar包和wheel包。

没有setup.py 的项目【也就是采用setup.cfg新式管理方式的项目需要安装 build 模块,然后运行 python -m build

然后推荐运行下:

twine check dist/*

来确保你的文档格式没问题。

使用twine上传

使用twine上传到pypi很简单:

twine upload dist/*

你每次都需要输入用户名和密码,你可以安装 keyring 模块,然后运行:

keyring set https://upload.pypi.org/legacy/ your-username

来本地安全保存你的用户名和密码。

pypi下载使用国内源

豆瓣的pypi源 https://pypi.douban.com/simple 或者 清华的pypi源 https://pypi.tuna.tsinghua.edu.cn/simple 都可以吧。

临时使用用 -i 或者 --index 选项:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple some-package

永久更改本地配置:

pip install pip -U
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

pypi只下载软件源文件

下载pypi上的目标软件源文件而不是安装。参考了 这个网页

pip install --download="/pth/to/downloaded/files" package_name

其他注意事项

注意那个什么egg-info这个缓存文件夹,如果你更改了你的项目的配置,最好将这个缓存文件夹删掉,有时会有干扰。