如何向wheel添加额外文件?

106 投票
8 回答
65003 浏览
提问于 2025-04-18 10:39

如何控制在一个wheel文件中包含哪些文件?看起来MANIFEST.inpython setup.py bdist_wheel中并没有被使用。

更新:

我之前对从源代码压缩包安装和从wheel安装的区别理解错了。源代码分发会包含MANIFEST.in中指定的文件,但安装后的包只包含Python文件。无论是通过源代码分发、egg还是wheel安装,都需要明确哪些额外的文件应该被安装。具体来说,package_data用于额外的包文件,而data_files则用于包外的文件,比如命令行脚本或系统配置文件。

原始问题

我有一个项目,我一直在使用python setup.py sdist来构建我的包,使用MANIFEST.in来控制包含和排除的文件,并且用pyromacheck-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 个回答

5

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 跟踪(或其他支持的工具)
  • 数据文件在一个包的目录下

这样,数据文件才会被包含进来。

12

你可以通过 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')),
    ],
)
28

你可以在 setup.py 文件中使用 package_datadata_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_datapackages

如果你的 Python 模块在一个名为 "pymod" 的文件夹里,下面是合适的设置:

setup( ...
    include_package_data = True,
    packages = ['pymod'],
)

如果你的 Python 脚本在根目录下,使用:

setup( ...
    include_package_data = True,
    packages = ['.'],
)

然后你可以用像 7-zip 这样的压缩软件打开你的 .whl 文件,检查你想要的所有文件是否都在里面。

56

在你对 MANIFEST.insetup.py 进行任何修改之前,一定要先删除旧的输出目录。因为 Setuptools 会缓存一些数据,这可能会导致意想不到的结果。

rm -rf build *.egg-info

如果不这样做,可能会导致一切都无法正常工作。

现在这件事说完了。

  1. 如果你在构建一个 源分发包sdist),那么你可以使用下面的任何方法。

  2. 如果你在构建一个 轮子包bdist_wheel),那么 include_package_dataMANIFEST.in 会被忽略,你必须使用 package_datadata_files

INCLUDE_PACKAGE_DATA

这是一个不错的选择,但 bdist_wheel 不会支持它。

setup(
    ...
    include_package_data=True
)

# MANIFEST.in
include package/data.json

DATA_FILES 用于非包数据

这是最灵活的选项,因为你可以将任何文件从你的代码库添加到 sdistbdist_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
)
55

你有没有试过在你的 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 版本,那样的话我只能祝你好运了。

撰写回答