一只普通的计算机爱好者
本文以一个最简单的hello world包为例子,一步步操作,作为构建Python包的入门指引。首先创建一个hello.py:
def say(to):
print('hello', to)
此时在另一个同级的test.py中,已经可以使用该函数了:
import hello
hello.say('world') # hello world
但在别的地方运行test.py会报ModuleNotFoundError
,这是当然的,本文就是为了解决此问题。但此问题先放一放,先来添加另一个特性。
如果直接用python命令行运行此文件:python hello.py
或python -m hello
,它什么也不会输出,因为它只定义了函数,而没有调用。 一般来说对于库函数,无输出是正常的,它们本来就没设计成能被直接调用。不过这里我们选择给它添加这个功能,把hello.py改成以下内容:
import sys
def say(to):
print('hello', to)
if __name__ == '__main__':
say(sys.argv[1])
原理是python命令行运行目标时会把__name__
设为字符串__main__
,就能进入if中;而import时不会,就不会产生不想要的输出。
调用:python hello.py world
或python -m hello world
,能输出hello world。
接下来就是创建包的关键,创建一个setup.py:
import setuptools
setuptools.setup(
name='hellopkg', # 包的名字,可随意取
py_modules=['hello'] # 对应hello.py,也是安装了包之后实际import的名字
)
运行命令,注意那个点不能省,它代表当前目录:
pip install .
完成,你已经可以在任何地方import hello; hello.say('world')
了,也可以命令行调用python -m hello world
。pip list可以看到包的名字是hellopkg且不存在名叫hello的包,版本号没指定默认是0.0.0,python -m hellopkg world
会失败,这说明包的名字与安装好了包后能获得的模块名是无关的。
接下来我们实现在命令行中直接用hello world
输出hello world,为此需要单独把入口函数拿出来,修改hello.py:
import sys
def say(to):
print('hello', to)
def main():
say(sys.argv[1])
if __name__ == '__main__':
main()
再在setup.py中添加一行:
import setuptools
setuptools.setup(
name='hellopkg',
py_modules=['hello'],
entry_points={'console_scripts': ['pyhello = hello:main']}
# 定义终端入口点,将产生pyhello.exe,会执行hello模块的main函数
)
再pip install .
安装一遍。完成,你已经可以在命令行中用pyhello world
输出hello world了,把pyhello改成hello再装一遍就是hello world
输出hello world。
我们还希望把源代码打包成一个单独的whl文件,但是wheel这个包不自带。创建一个虚拟环境模拟最初的情况:
pip uninstall hellopkg # 卸载系统级别的包
python -m venv .venv
.venv\Scripts\activate
pip install -U pip setuptools wheel
然后使用pip wheel .
就能在当前目录下生成hellopkg-0.0.0-py3-none-any.whl
,这就是打包好的包,用pip install hellopkg-0.0.0-py3-none-any.whl
就能装上,也可以发给别人或者上传到PyPI。
传统方法是用python setup.py bdist_wheel
,会生成三个文件夹:build和xxx.egg-info文件夹是构建时的临时文件及元数据,方便你检查哪些文件被怎样处理了;dist文件夹中有whl文件,与上条命令一致,其实上调命令就是调用了本条命令。bdist_wheel是一条必须装了wheel包才能使用的verb,用--help-commands可查看所有verb,其中sdist生成源代码压缩包hellopkg-0.0.0.tar.gz
,也能被pip install安装。
以上就是setuptools和setup.py的最简陋的用法,下面介绍简单但且正常的用法。正常情况下源代码应该这样组织:
.
├── hello
│ ├── hello_impl.py
│ ├── __init__.py
│ └── __main__.py
├── LICENSE
├── Readme.md
├── setup.cfg
└── setup.py
各个文件的内容:
# setup.py;会自动读取setup.cfg中的设置
import setuptools
setuptools.setup() # 也可有参调用,则会覆盖.cfg的对应条目
# setup.cfg;所有条目见 https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html
[metadata]
name = hellokpg
version = 1.0
author = xxx
long_description = file: Readme.md # 从文件中读取
license = MIT
url = https://github.com/user/repo
classifiers = # PyPI的分类,类似于标签,所有条目见 https://pypi.org/pypi?%3Aaction=list_classifiers
Development Status :: 3 - Alpha
Programming Language :: Python :: 3
[options]
packages = find: # 自动搜索存在__init__.py的文件夹作为包
install_requires = # 依赖,pip安装时靠的就是这个而不是requirements.txt
requests
[options.entry_points]
console_scripts =
pyhello = hello.__main__:main
# hello_impl.py
def say(to):
print('hello', to)
# __init__.py
from .hello_impl import say
# __main__.py
from . import say
import sys
def main():
say(sys.argv[1])
if __name__ == '__main__':
main()
使用setup.cfg而不是setup.py的理由是,前者是声明式的配置文件,后者是实际的python代码,可能不安全。setuptools的文档中推荐从setup.py迁移到setup.cfg。 其实未来应该会使用pyproject.toml,但是现在setuptools不支持将它代替setup.cfg,只能代替setup.py,所以就没什么用,有个issue讨论但是没有ETA。
上传到PyPI我就不写了,用的是twine,看其它人的教程吧。我暂时没有该需求,因为pip支持install git+https://github.com/...
进行安装,只要那个repo中存在setup.py就行,实际上是下下来在本机构建。wheel的好处是不用在本机上编译C扩展,我这样做没有用到wheel的优势,但是我不会C扩展所以也无所谓。
一些提示:
-
推荐用setup.cfg,除非想结构最精简;未来用pyproject.toml
-
.egg已经deprecated了,用.whl;dependency_links也弃用了
-
不需要用distutils,有讨论将它从标准库中移除
-
打错字很可能没有任何提示,比如entry_points写成entry_point打包时不生效也没有任何报错
-
whl概念上的“包”和python概念上的“包”是两个概念,前者更类似于“软件包”,后者是存在
__init__.py
的文件夹;本文中“包的名字,可随意取”等句说的就是前者 -
有一些工具能从setup的依赖中生成requirements.txt,但是对于不锁定依赖的简单项目就无所谓了
-
pip install -e
在开发时很有用 -
setup.py install
用的是easy_install,是pip的前身,没必要用 -
最好在一开始就建立好虚拟环境,方便测试。本文为了降低最初的学习曲线没在一开始启用
-
我看不出有什么理由用pbr
进一步学习的资源:
这些并不是本文的参考资料,而是我推荐阅读的文章。因为在学setuptools的过程中我对搜出来的垃圾文章深感痛心,setup.cfg可是在python2的时期就存在了,居然少有文章提到。
-
-
-
setuptools官方文档:
-
绝对引用和相对引用:
-
处理命令行参数:
-
关于wheel的一些特性:
-
PyPI打包文档:
文章评论