Setup.py & Setup.cfg 学习

本文主要内容:setup.py 和 setup.cfg的功能和作用
本文主要内容:setuptools的用法

setuptools是python自带的用来构建包的工具,构建出来的wheel(.whl)可供其他人pip install和import。

1 从一个小例子开始

1.1 初始文件(夹)结构

(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ tree -L 2
.
├── main.py
└── venv
    ├── bin
    ├── include
    ├── lib
    ├── lib64 -> lib
    └── pyvenv.cfg

5 directories, 2 files

解说:

  • 当前系统Linux-ubuntu,当前终端所在文件夹为IDE-Pycharm构建的新项目文件夹SetupLearning

  • 由于文件内容较多,所以使用命令tree -L 2查看当前文件夹下,指定最多查看2级目录下的文件夹内容

  • 初始情况下,文件夹内部包括.py文件一个main.py和一个文件夹venv

1.2 小例子

① 文件(夹)结构
(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ tree -L 2
.
├── hello.py
├── main.py
└── venv
    ├── bin
    ├── include
    ├── lib
    ├── lib64 -> lib
    └── pyvenv.cfg

5 directories, 3 files
② .py 文件内容
# hello.py
# version 1.0
def say(str):
print("hello", str)
# main.py
# version 1.0
import hello
hello.say('guys') # hello guys
③ 执行命令&结果

在文件夹~/PycharmProjects/SetupLearning下:

(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ python main.py
hello guys

在文件夹~/PycharmProjects下:

(base) t@t-virtual-machine:~/PycharmProjects$ python main.py
python: can't open file '/home/t/PycharmProjects/main.py': [Errno 2] No such file or directory
④ 结论

在main.py文件所在目录下,执行命令,有效输出
在其他文件夹下,执行命令,提示无法找到文件main.py

此时,我们可以认为hello.py相当于一个库函数,简单理解为:通过用户调用(例如用户代码中输入:import hello)才能进行相应的操作


1.3 改进的小例子

接下来,我们将继续修改文件内容,使其可以直接被执行,实现函数功能

① .py 文件内容
# hello.py
# version 2.0
import sys
def say(str):
print('hello', str)
if __name__ == '__main__':
say(sys.argv[1])
② 执行命令&结果
(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ python hello.py guys
hello guys
③ 解释(原理参见blog)
  • 当python命令直接运行目标时会把__name__设为字符串__main__,就能进入if所在的条件语句中;

  • 而通过导入模块包的形式,即import时不会执行if所在的条件语句。

  • 注意python hello.py guys的语法中,guys是提供的入口参数(即,say(sys.argv[1]))


2 setuptools 创建包

2.1 基础版

① 创建setup.py文件,键入如下代码块
# setup.py
# version 1.0
import setuptools
setuptools.setup(
name='hellopkg', # 包的名字,可随意取
py_modules=['hello'] # 对应hello.py,也是安装了包之后实际import的模块名字
)
② 运行命令(注意命令最后的点不能遗漏,它代表当前目录)
(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ pip install .
③ 命令执行结果

Processing /home/t/PycharmProjects/SetupLearning 
Building wheels for collected packages: hellopkg
Building wheel for hellopkg (setup.py) ... done
Created wheel for hellopkg: filename=hellopkg-0.0.0-py3-none-any.whl size=1242 sha256=df4a65d6ebbd11b9b86d2f20ec4b250d6bb3d56872bae79ea61e553140320c6f
Stored in directory: /tmp/pip-ephem-wheel-cache-fxboa8qy/wheels/41/dd/d9/e8cc80e75a9ddfbcd2934da1b5fa23e9c83fc077cdbd96483a
Successfully built hellopkg
Installing collected packages: hellopkg
Successfully installed hellopkg-0.0.0

④ 效果

名为hellopkg的包已经安装完成

  • 现在可以在任何地方执行包含代码import hello; hello.say('guys')的.py文件了;

  • 也可以命令行调用python -m hello guys。

使用命令pip list可以看到包的名字是hellopkg,且不存在名叫hello的包;

  • 版本号没指定默认是0.0.0;

  • python -m hellopkg guys会失败,这说明包的名字(hellopkg,setuptools.setup的name参数值)与安装好了包后能获得的模块名(hello,setuptools.setup的py_modules参数值)是无关的。

⑤ 验证

在SetupLearning文件夹下:

(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ python 
Python 3.9.2 (default, Mar  3 2021, 20:02:32) 
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import hello
>>> hello.say('guys')
hello guys
>>> exit()
(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ python -m hello guys
hello guys

在PycharmProjects文件夹下:

(base) t@t-virtual-machine:~/PycharmProjects$ python -m hello guys
hello guys
(base) t@t-virtual-machine:~/PycharmProjects$ python
Python 3.9.2 (default, Mar  3 2021, 20:02:32) 
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import hello
>>> hello.say('guys')
hello guys
⑥ 解释
  • 注意python -m hello guys的语法中,guys是提供的入口参数(即,say(sys.argv[1]))

  • 此时的hello是包(hellopkg)内的模块,相当于(numpy,pandas等),已经被安装在当前项目所在的环境中,故而可以在任何位置使用。


2.2 改进版

① 在命令行中直接用hello xxx输出hello world(重点)

接下来我们实现在命令行中直接用hi xxx输出hi xxx,为此需要单独把入口函数拿出来,为方便区分,将文件hello.py重命名为hi.py:

# hi.py
# update from hello.py version 2.0
import sys
def say(str):
print('hi', str)
def main():
say(sys.argv[1])
if __name__ == '__main__':
main()
print("Enter into if. i.e., running this file directly")

再修改文件setup.py,即,添加一行:

# setup.py
# version 2.0
import setuptools
setuptools.setup(
name='hellopkg2', # 注意此时修改了包名为hellopkg2
py_modules=['hi'], # 对应hi.py,也是安装了包之后实际import的模块名字
entry_points={'console_scripts': ['pyhi = hi:main']}
# 定义终端入口点,将产生pyhi.exe,会执行hi模块的main函数
# 注意此时执行的是hi模块的main函数,而没有进入if语句
)

通过命令pip install .再安装一遍。完成即可在命令行中用pyhi guys输出hi guys了,把pyhi改成hi再装一遍就是hi guys输出hi guys。

② 运行命令,注意那个点不能省,它代表当前目录
(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ pip install .
③ 命令执行结果

Processing /home/t/PycharmProjects/SetupLearning 
Building wheels for collected packages: hellopkg2
Building wheel for hellopkg2 (setup.py) ... done
Created wheel for hellopkg2: filename=hellopkg2-0.0.0-py3-none-any.whl size=1543 sha256=df95afb1d89ea748be107fcea4b0559993f47f741690ca9c61874eff96b3809a
Stored in directory: /tmp/pip-ephem-wheel-cache-g8sgqc81/wheels/41/dd/d9/e8cc80e75a9ddfbcd2934da1b5fa23e9c83fc077cdbd96483a
Successfully built hellopkg2
Installing collected packages: hellopkg2
Successfully installed hellopkg2-0.0.0

④ 效果

名为hellopkg2的包已经安装完成

  • 现在可以在任何地方执行命令pyhi guys

⑤ 验证

在SetupLearning文件夹下:

(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ pyhi guys
hi guys

在PycharmProjects文件夹下:

(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ cd ..
(base) t@t-virtual-machine:~/PycharmProjects$ pyhi guys
hi guys

查看当前所有的package,命令:pip list

(base) t@t-virtual-machine:~/PycharmProjects$ pip list
Package                Version
---------------------- -------------------
brotlipy               0.7.0
certifi                2020.12.5
cffi                   1.14.5
chardet                4.0.0
conda                  4.9.2
conda-package-handling 1.7.2
cryptography           3.3.1
hellopkg               0.0.0
hellopkg2               0.0.0
idna                   2.10
pip                    21.0.1
pycosat                0.6.3
pycparser              2.20
pyOpenSSL              20.0.1
PySocks                1.7.1
requests               2.25.1
ruamel-yaml-conda      0.15.80
setuptools             52.0.0.post20210125
six                    1.15.0
tqdm                   4.56.0
urllib3                1.26.3
wheel                  0.36.2

卸载名为hellopkg2的包

(base) t@t-virtual-machine:~/PycharmProjects$ pip uninstall hellopkg2
Found existing installation: hellopkg2 0.0.0
Uninstalling hellopkg2-0.0.0:
  Would remove:
    /home/t/anaconda3/bin/pyhi
    /home/t/anaconda3/lib/python3.9/site-packages/hellopkg2-0.0.0.dist-info/*
    /home/t/anaconda3/lib/python3.9/site-packages/hi.py
Proceed (y/n)? y
  Successfully uninstalled hellopkg2-0.0.0

2.3 setuptools功能实例

① 查看当前的文件夹结构
(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ tree -L 2
.
├── hello.py
├── hi.py
├── main.py
├── __pycache__
│   ├── hello.cpython-39.pyc
│   └── hi.cpython-39.pyc
├── setup.py
└── venv
    ├── bin
    ├── include
    ├── lib
    ├── lib64 -> lib
    └── pyvenv.cfg

6 directories, 7 files
② 执行命令以生成.whl文件

由于linux系统下,存在文件权限,大家也可在Pycharm的Terminal中进行操作

python -m venv .venv 创建虚拟环境

cd .venv
cd bin
activate 激活当前的虚拟环境

pip install -U pip setuptools wheel
cd ../../ 返回含有setup.py文件的目录

pip wheel . 生成.whl文件

(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ cd .venv
(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning/.venv$ cd bin
(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning/.venv/bin$ activate
(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning/.venv/bin$ pip install -U pip setuptools wheel
Requirement already satisfied: pip in /home/t/anaconda3/lib/python3.9/site-packages (21.0.1)
Requirement already satisfied: setuptools in /home/t/anaconda3/lib/python3.9/site-packages (52.0.0.post20210125)
Collecting setuptools
  Using cached setuptools-54.1.1-py3-none-any.whl (784 kB)
Requirement already satisfied: wheel in /home/t/anaconda3/lib/python3.9/site-packages (0.36.2)
Installing collected packages: setuptools
  Attempting uninstall: setuptools
    Found existing installation: setuptools 52.0.0.post20210125
    Uninstalling setuptools-52.0.0.post20210125:
      Successfully uninstalled setuptools-52.0.0.post20210125
Successfully installed setuptools-54.1.1
(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning/.venv/bin$ cd ../../
(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ pip wheel .
③ 运行结果
(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ pip wheel .
Processing /home/t/PycharmProjects/SetupLearning
Building wheels for collected packages: hellopkg2
  Building wheel for hellopkg2 (setup.py) ... done
  Created wheel for hellopkg2: filename=hellopkg2-0.0.0-py3-none-any.whl size=1543 sha256=5c3561e041d26fa0702649b8b7b0080105414a693132cd9c303a504aad3c91fb
  Stored in directory: /tmp/pip-ephem-wheel-cache-z5emqes3/wheels/41/dd/d9/e8cc80e75a9ddfbcd2934da1b5fa23e9c83fc077cdbd96483a
Successfully built hellopkg2

此时,SetupLearning文件夹下,生成文件hellopkg2-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生成源代码压缩包hellopkg2-0.0.0.tar.gz,也能被pip install安装。

3 setup.py & setup.cfg & setuptools

以上就是setuptools和setup.py的最简陋的用法。下面介绍简单但且正常的用法。

3.1 正常情况下源代码应该这样组织:

(base) t@t-virtual-machine:~/PycharmProjects/testSetupTools$ tree -L 2
.
├── hello
│   ├── hello_imply.py
│   ├── __init__.py
│   └── __main__.py
├── main.py
├── Readme.md
├── setup.cfg
└── setup.py

3.2 各个文件的内容 实现 setup.py & setup.cfg & __init__.py

# hello_imply.py
def say(str):
print('hi', str)
# __init__.py
from .hello_imply import say
# __main__.py
from . import say
import sys
def main():
print("Enter into [main()]...")
say(sys.argv[1])
if __name__ == '__main__':
print("Enter into [if]...")
main()
# setup.cfg;
# 所有条目见 https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html
[metadata]
name = helloPkgCompleteMain
version = 1.0
[options]
author = xxx
long_description = file: Readme.md # 从文件中读取
license = MIT
url = https://github.com/user/repo
# PyPI的分类,类似于标签,所有条目见 https://pypi.org/pypi?%3Aaction=list_classifiers
classifiers =
Development Status :: 3 - Alpha
Programming Language :: Python :: 3
# 自动搜索存在__init__.py的文件夹作为包
packages = find:
# 依赖,pip安装时靠的就是这个而不是requirements.txt
# install_requires =
# numpy ~= 1.16.1
[options.entry_points]
console_scripts =
# pyhelloOKMain = hello.__main__ # invalid
pyhelloOKMain = hello.__main__:main
# setup.py;
# 会自动读取setup.cfg中的设置
import setuptools
setuptools.setup() # 也可有参调用,则会覆盖.cfg的对应条目
print("Enter into [setup.py]...")

3.3 执行命令&效果

进入setup.py文件所在目录下,执行命令pip install . 或者 python setup.py install即可安装包名为helloPkgCompleteMain的包
执行命令pip list可以查看当前所有已安装的包
执行命令pyhelloOKMain string即可运行程序

(base) t@t-virtual-machine:~/PycharmProjects/SetupLearning$ pyhelloOKMain t
Enter into [main()]...
hi t

3.4 相关提示

Tips

  • 使用setup.cfg而不是setup.py的理由是,前者是声明式的配置文件,后者是实际的python代码,可能不安全。

  • 推荐用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的前身,没必要用

  • 最好在一开始就建立好虚拟环境,方便测试。

4 总结

  • 本文首先利用普通的小例子逐渐引出setup.py的基础配置;

  • 然后本文通过对小例子进行不断改进,梳理了setup.py 与 setuptools的基础用法;

  • 最后通过一套较为完备的文件系统,展示了setup.py, setup.cfg和setuptools的实际效用。


参考资料

目录