如何向wheel添加额外文件?
如何控制在一个wheel文件中包含哪些文件?看起来MANIFEST.in
在python setup.py bdist_wheel
中并没有被使用。
更新:
我之前对从源代码压缩包安装和从wheel安装的区别理解错了。源代码分发会包含MANIFEST.in
中指定的文件,但安装后的包只包含Python文件。无论是通过源代码分发、egg还是wheel安装,都需要明确哪些额外的文件应该被安装。具体来说,package_data用于额外的包文件,而data_files则用于包外的文件,比如命令行脚本或系统配置文件。
原始问题
我有一个项目,我一直在使用python setup.py sdist
来构建我的包,使用MANIFEST.in
来控制包含和排除的文件,并且用pyroma和check-manifest来确认我的设置。
我最近把它转换成了支持Python 2和3的代码,并添加了一个setup.cfg文件,内容如下:
[bdist_wheel]
universal = 1
我可以用python setup.py bdist_wheel
来构建一个wheel,看起来是一个通用的wheel,正如我所希望的那样。然而,它并没有包含MANIFEST.in
中指定的所有文件。
安装了什么?
我深入研究了一下,现在对打包和wheel有了更多了解。以下是我学到的:
我向PyPi上的multigtfs项目上传了两个包文件:
multigtfs-0.4.2.tar.gz
- 源代码压缩包,包含MANIFEST.in
中的所有文件。multigtfs-0.4.2-py2.py3-none-any.whl
- 这是我们讨论的二进制分发文件。
我创建了两个新的虚拟环境,都是Python 2.7.5,并安装了每个包(pip install multigtfs-0.4.2.tar.gz
)。这两个环境几乎是一样的。它们有不同的.pyc
文件,这些是“编译过的”Python文件。还有一些日志文件记录了不同的磁盘路径。通过源代码压缩包安装时,会包含一个multigtfs-0.4.2-py27.egg-info
文件夹,详细记录了安装过程,而wheel安装则有一个multigtfs-0.4.2.dist-info
文件夹,里面是该过程的详细信息。然而,从使用multigtfs项目的代码来看,这两种安装方法没有区别。
明确来说,两者都没有我测试中使用的.zip文件,因此测试套件会失败:
$ django-admin startproject demo
$ cd demo
$ pip install psycopg2 # DB driver for PostGIS project
$ createdb demo # Create PostgreSQL database
$ psql -d demo -c "CREATE EXTENSION postgis" # Make it a PostGIS database
$ vi demo/settings.py # Add multigtfs to INSTALLED_APPS,
# Update DATABASE to set ENGINE to django.contrib.gis.db.backends.postgis
# Update DATABASE to set NAME to test
$ ./manage.py test multigtfs.tests # Run the tests
...
IOError: [Errno 2] No such file or directory: u'/Users/john/.virtualenvs/test/lib/python2.7/site-packages/multigtfs/tests/fixtures/test3.zip'
指定额外文件
根据回答中的建议,我在setup.py
中添加了一些额外的指令:
from __future__ import unicode_literals
# setup.py now requires some funky binary strings
...
setup(
name='multigtfs',
packages=find_packages(),
package_data={b'multigtfs': ['test/fixtures/*.zip']},
include_package_data=True,
...
)
这将把zip文件(以及README)安装到文件夹中,现在测试可以正确运行了。感谢大家的建议!
8 个回答
include_package_data
是一个很重要的选项,它可以让你在打包时包含数据文件,这对 sdist 和 wheels 都有效。
不过,你得正确使用这个选项,我花了几个月才搞明白,所以我把我的经验分享给你。
这个选项的名字 include_PACKAGE_data
就给了我们一个提示:数据文件必须放在一个包的子文件夹里。
只有在以下情况下,数据文件才会被包含:
include_package_data
设置为 True- 数据文件在
MANIFEST.in
文件中列出(*关于setuptools_scm
的说明请看最后) - 数据文件在一个包的目录下
这样,数据文件才会被包含进来。
工作示例:
假设你的项目结构和文件如下:
|- MANIFEST.in
|- setup.cfg
|- setup.py
|
\---foo
|- __init__.py
|
\---data
- example.png
还有以下配置:
Manifest.in:
recursive-include foo/data *
setup.py
import setuptools
setuptools.setup()
setup.cfg
[metadata]
name = wheel-data-files-example
url = www.example.com
maintainer = None
maintainer_email = none@example.com
[options]
packages =
foo
include_package_data = True
这样生成的 sdist 包 和你构建的 wheels 都会包含 example.png
这个数据文件。
(当然,你也可以直接在 setup.py 中指定配置,而不是使用 setup.cfg,但这在这个例子中不重要。)
更新:对于 src 布局的项目
对于使用 src 布局的项目,这种方法也适用,结构如下:
|- MANIFEST.in
|- setup.cfg
|- setup.py
|
\---src
|
\---foo
|- __init__.py
|
\---data
- example.png
要让它工作,需要通过 package_dir
告诉 setuptools src 目录的位置:
setup.cfg
[metadata]
name = wheel-data-files-example
url = www.example.com
maintainer = None
maintainer_email = none@example.com
[options]
packages =
foo
include_package_data = True
package_dir =
=src
在 manifest 中调整路径:
Manifest.in:
recursive-include src/foo/data *
注意:如果使用 setuptools_scm
,则不需要 Manifest.in
如果你使用 setuptools 并添加了 setuptools_scm
插件(在 pypi 上),那么你就不需要管理 Manifest.in 文件了。setuptools_scm 会自动处理所有被 git 跟踪的文件,并将它们添加到包中。
所以在这种情况下,判断文件是否被添加到 sdist/wheel 的规则是:
include_package_data
设置为 True- 文件被 git 跟踪(或其他支持的工具)
- 数据文件在一个包的目录下
这样,数据文件才会被包含进来。
你可以通过 data_files 这个指令来指定额外要安装的文件。你是在找这个吗?下面是一个简单的例子:
from setuptools import setup
from glob import glob
setup(
name='extra',
version='0.0.1',
py_modules=['extra'],
data_files=[
('images', glob('assets/*.png')),
],
)
你可以在 setup.py
文件中使用 package_data
和 data_files
来指定额外的文件,但这两个选项真的很难用,而且容易出错。
一个替代的方法是使用 MANIFEST.in
文件,并在你的 setup.py
中的 setup()
函数里添加 include_package_data=True
,就像这里提到的那样。
通过这个设置,MANIFEST.in
文件将用来指定要包含的文件,这些文件不仅会出现在源代码的压缩包(tarball/zip)中,还会出现在 wheel 和 Windows 32 安装程序中。这种方法适用于任何 Python 版本(我在从 Python 2.6 到 3.6 的项目中测试过)。
更新于2020年:似乎在 Python 3 中,wheel 不再遵循 MANIFEST.in
的设置,虽然在 tar.gz 中仍然有效,即使你设置了 include_package_data=True
。
解决这个问题的方法是,你需要同时指定 include_package_data
和 packages
。
如果你的 Python 模块在一个名为 "pymod" 的文件夹里,下面是合适的设置:
setup( ...
include_package_data = True,
packages = ['pymod'],
)
如果你的 Python 脚本在根目录下,使用:
setup( ...
include_package_data = True,
packages = ['.'],
)
然后你可以用像 7-zip 这样的压缩软件打开你的 .whl 文件,检查你想要的所有文件是否都在里面。
在你对 MANIFEST.in
或 setup.py
进行任何修改之前,一定要先删除旧的输出目录。因为 Setuptools 会缓存一些数据,这可能会导致意想不到的结果。
rm -rf build *.egg-info
如果不这样做,可能会导致一切都无法正常工作。
现在这件事说完了。
如果你在构建一个 源分发包(
sdist
),那么你可以使用下面的任何方法。如果你在构建一个 轮子包(
bdist_wheel
),那么include_package_data
和MANIFEST.in
会被忽略,你必须使用package_data
和data_files
。
INCLUDE_PACKAGE_DATA
这是一个不错的选择,但 bdist_wheel
不会支持它。
setup(
...
include_package_data=True
)
# MANIFEST.in
include package/data.json
DATA_FILES 用于非包数据
这是最灵活的选项,因为你可以将任何文件从你的代码库添加到 sdist
或 bdist_wheel
中。
setup(
....
data_files=[
('output_dir',['conf/data.json']),
]
# For sdist, output_dir is ignored!
#
# For bdist_wheel, data.json from conf dir in root of your repo
# and stored at `output_dir/` inside of the sdist package.
)
PACKAGE_DATA 用于包内的非Python文件
和上面类似,但对于 bdist_wheel
来说,可以把你的数据文件放在包内部。对于 sdist
来说是一样的,但相比 data_files
有更多限制,因为文件只能来自你的包的子目录。
setup(
...
package_data={'package':'data.json'},
# data.json must be inside of your actual package
)
你有没有试过在你的 setup.py
文件里使用 package_data
呢?MANIFEST.in
似乎是针对 Python 版本小于等于 2.6 的,我不确定更高版本的 Python 是否还会看这个文件。
在查看了 https://github.com/pypa/sampleproject 后,他们的 MANIFEST.in
文件里写着:
# If using Python 2.6 or less, then have to include package data, even though
# it's already declared in setup.py
include sample/*.dat
这似乎暗示这个方法已经过时了。同时,在 setup.py
文件里,他们声明了:
setup(
name='sample',
...
# If there are data files included in your packages that need to be
# installed, specify them here. If using Python 2.6 or less, then these
# have to be included in MANIFEST.in as well.
include_package_data=True,
package_data={
'sample': ['package_data.dat'],
},
...
)
(我不太明白他们为什么在 MANIFEST.in
里用通配符,而在 setup.py
里用文件名。它们指的是同一个文件。)
这说明,使用 package_data
的方法更简单,也似乎比 MANIFEST.in
的方法更好。除非你需要支持 2.6 版本,那样的话我只能祝你好运了。