如何使用setuptools/distutils包含软件包数据?

192
使用setuptools时,我无法让安装程序获取任何package_data文件。我读到的所有内容都说以下是正确的方法。请问有人能给予建议吗?
setup(
   name='myapp',
   packages=find_packages(),
   package_data={
      'myapp': ['data/*.txt'],
   },
   include_package_data=True,
   zip_safe=False,
   install_requires=['distribute'],
)

myapp/data/ 是数据文件的位置。


3
我有同样的问题... 手动指定“data_files”解决了问题。 但这很容易出错,而且对我来说不太“正确”。 有人可以验证是否真的必须在“package_data”和“data_files”中都复制配置吗? - exhuma
2
https://github.com/wimglenn/resources-example 展示了一个现代的setuptools项目结构,可以使用pyproject.toml正确地将数据文件打包到wheels和sdists中,无需setup.py文件。 - wim
2
我真的无法让下面任何一个答案起作用,而且上面的评论需要完全重写我的许多项目。 - Wolfgang Fahl
14个回答

377

我知道这是一个老问题,但对于通过谷歌找到这里的人们: package_data 是一个卑鄙的、肮脏的谎言。它仅在构建二进制包(python setup.py bdist ...)时使用,而不是在构建源码包(python setup.py sdist ...)时使用。这当然是荒谬的——人们期望构建源代码分发应该会产生一组可以发送给其他人构建二进制分发的文件集合。

无论如何,使用MANIFEST.in将适用于二进制和源代码分发。


128
我已经研究了这个问题一个小时,并尝试了许多方法。就像你说的一样, package_data 对于 bdist 有效而对于 sdist 无效。然而,MANIFEST.in 对于 sdist 有效,但是对于 bdist 无效! 因此,我能想到的最好解决办法就是同时包含 package_dataMANIFEST.in 来适应 bdistsdist - Wesley Baugh
9
我发现另一个支持@WesleyBaugh的人。在https://dev59.com/l3A85IYBdhLWcg3wCfBZ#2969087中,使用`MANIFEST.in`来处理不需要安装的文件,例如文档,而使用`package_data`处理非Python代码的文件(如图像或模板)。 - Drake Guan
15
我正在使用sdist,必须同时包括MANIFEST.inpackage_data。似乎MANIFEST.in控制了要包含在分发中的内容,并且package_data控制了安装期间随后被复制到site_packages目录中的内容。令人困惑的是,MANIFEST.in中的路径是相对于setup.py的位置,而package_data则是相对于各个包(例如模块)的根目录。 - Edward Newell
10
自2.7版本起更改:如果没有提供模板,则与package_data匹配的所有文件都将添加到MANIFEST文件中。请参见指定要分发的文件。因此,只有当您没有现有的MANIFEST.in文件,并且只使用2.7+时,才会自动将package_data中的文件包含在ZIP中。 - Johnus
82
说实话,我感觉这张票是给使用setuptools并发现自己陷入糟糕境地的人提供团体治疗机会。 - Matt Joyce
显示剩余10条评论

44

我遇到了同样的问题。解决方法就是简单地删除 include_package_data=True

这里阅读后,我意识到 include_package_data 旨在包含来自版本控制的文件,而不仅仅是像名称所示的“包含软件包数据”。根据文档:

[include_package_data的]数据文件必须受到CVS或Subversion控制

...

如果您想更精细地控制包含哪些文件(例如,如果您的软件包目录中有文档文件,并且想要将它们排除在安装之外),那么还可以使用 package_data 关键字。

删除该参数可以解决这个问题,这也是转换为distutils时为什么它也能正常工作的原因。


3
我的经验不同,我在没有包括include_package_data=True的情况下遇到了同样的问题。对我来说唯一的解决办法是像上面建议的那样,在清单中添加一个条目。请注意,我使用的是setuptools,也许你的版本适用于“distribute”? - TimStaley
5
删除include_package_data参数能解决问题的实际原因可以在原文链接中找到。如果使用setuptools特定的include_package_data参数,那么除非在MANIFEST.in文件中列出,否则package_data指定的文件将不会自动添加到清单(manifest)中。 - Piotr Dobrogost
1
package_data 设置为非空列表并指定 include_package_data=False 的用例是什么?为什么需要在 MANIFEST.inpackage_data 中指定文件两次? - Herbert

23

按照 @Joe 的建议,删除 include_package_data=True 这一行也对我有用。

再详细解释一下,我没有 MANIFEST.in 文件。我使用 Git 而不是 CVS。

代码库的结构如下:

/myrepo
    - .git/
    - setup.py
    - myproject
        - __init__.py
        - some_mod
            - __init__.py
            - animals.py
            - rocks.py
        - config
            - __init__.py
            - settings.py
            - other_settings.special
            - cool.huh
            - other_settings.xml
        - words
            - __init__.py
            word_set.txt

setup.py:

from setuptools import setup, find_packages
import os.path

setup (
    name='myproject',
    version = "4.19",
    packages = find_packages(),  
    # package_dir={'mypkg': 'src/mypkg'},  # didnt use this.
    package_data = {
        # If any package contains *.txt or *.rst files, include them:
        '': ['*.txt', '*.xml', '*.special', '*.huh'],
    },

#
    # Oddly enough, include_package_data=True prevented package_data from working.
    # include_package_data=True, # Commented out.
    data_files=[
#               ('bitmaps', ['bm/b1.gif', 'bm/b2.gif']),
        ('/opt/local/myproject/etc', ['myproject/config/settings.py', 'myproject/config/other_settings.special']),
        ('/opt/local/myproject/etc', [os.path.join('myproject/config', 'cool.huh')]),
#
        ('/opt/local/myproject/etc', [os.path.join('myproject/config', 'other_settings.xml')]),
        ('/opt/local/myproject/data', [os.path.join('myproject/words', 'word_set.txt')]),
    ],

    install_requires=[ 'jsonschema',
        'logging', ],

     entry_points = {
        'console_scripts': [
            # Blah...
        ], },
)

我运行了python setup.py sdist以创建源分发文件(未尝试二进制文件)。

当我在全新的虚拟环境中时,我有一个myproject-4.19.tar.gz文件,并且我使用:

(venv) pip install ~/myproject-4.19.tar.gz
...

除了所有内容被安装到我的虚拟环境的 site-packages,那些特殊的数据文件被安装到 /opt/local/myproject/data/opt/local/myproject/etc


18

include_package_data=True 对我有用。

如果你使用 git,请记得在 install_requires 中包含 setuptools-git。这样会比在 Manifest 中列出所有路径或在 package_data 中包含所有路径更加简单(在我的情况下,这是一个带有各种静态文件的 Django 应用程序)。

(我粘贴了我发表的评论,因为正如 k3-rnc 所说,它实际上很有帮助。)


9

使用setup.cfg(setuptools≥30.3.0)

从setuptools 30.3.0(发布于2016-12-08)开始,您可以将配置文件移到setup.cfg文件中,从而使setup.py非常小。通过这种方法,您可以将软件包数据放在[options.package_data]部分中:

[options.package_data]
* = *.txt, *.rst
hello = *.msg

在这种情况下,您的setup.py可以非常简短:
from setuptools import setup
setup()

了解更多信息,请参见使用setup.cfg文件配置设置

目前有一些关于废弃setup.cfg,转而采用pyproject.toml的讨论,这是根据PEP 518提出的,但截至2020年2月21日,这仍然是暂定的。


这个答案没有提到 MANIFEST 文件,所以我认为它实际上不能与 sdists 一起使用。只能与 wheels 一起使用。你应该提到这一点。 - wim
1
@wim 我对 MANIFEST、sdist 和 wheels 的理解不够,无法回答这个问题。我使用 pip install 安装成功了。 - gerrit
这是因为对于足够现代的pip版本,pip install将首先构建一个wheel,然后安装它。但对于许多用户来说,这种方法将在静默中无法包含软件包数据。有关详细信息,请参见被接受的答案和其下的评论。在调用setup时使用package_data关键字参数(通过传递package_data关键字参数来完成)在本质上只是以不同的方式编写问题中已经在setup.py中执行的操作,因此我认为这并不特别有助于回答这个问题。它根本没有解决潜在的问题。 - wim
1
@wim 说实话,我很高兴终于找到一个有 setup.cfg 示例的例子。不知怎么的,官方包教程建议使用 setup.cfg 而不是 setup.py,但大多数答案都适用于 setup.py。而且这些答案已经遍布各地,大多数都是半破不清楚的,所以我不需要尝试将选项翻译成 setup.cfg 的另一个未知量。不幸的是,这个特定的答案似乎对我的项目无效。但它希望能帮助其他人。 - Eric Duminil

6

更新: 此答案已过时,信息已不再有效。所有setup.py配置都应使用import setuptools。我在https://dev59.com/3GAf5IYBdhLWcg3wuklk#49501350添加了更完整的答案。


我通过切换到distutils来解决这个问题。看起来distribute已经被弃用和/或损坏。

from distutils.core import setup

setup(
   name='myapp',
   packages=['myapp'],
   package_data={
      'myapp': ['data/*.txt'],
   },
)

2
分发(distribute)并没有被弃用,它正在取代distutils。我不知道你为什么会遇到问题,但这不是原因。 - agf
1
那是我从IRC得到的回复,所以我应该相信谁?如果您有使用distribute的工作示例,我会很感激。 - cmcginty
6
澄清:distribute旨在替代setuptools,两者都建立在distutils之上。distutils本身最终将被一个名为“distutils2”的新包所取代,在Python2中叫做“packaging”。 - Kevin Horn
1
转换到distutils解决了我的问题,其中include_package_data=True没有被遵守。因此,只需要MANIFEST.in设置,无需在package_data设置中重复文件列表。 - Daniel Sokolowski

6

我有几天遇到了同样的问题,但即使这个帖子也不能帮助我因为一切都很混乱。所以我进行了研究并找到了以下解决方案:

Basically in this case, you should do:

from setuptools import setup

setup(
   name='myapp',
   packages=['myapp'],
   package_dir={'myapp':'myapp'}, # the one line where all the magic happens
   package_data={
      'myapp': ['data/*.txt'],
   },
)

这里是完整的其他stackoverflow答案


尝试过这个,但仍然没有复制任何内容。 - gerrit

5

我在遇到同样的问题时找到了这篇文章。

我的经验与其他答案中的经验相矛盾include_package_data=True确实包含了 bdist 中的数据!setuptools 文档 中的解释缺乏上下文和故障排除提示,但是 include_package_data按照广告所说的那样工作。

我的设置:

  • Windows / Cygwin
  • git 版本 2.21.0
  • Python 3.8.1 Windows 发行版
  • setuptools v47.3.1
  • check-manifest v0.42

以下是我的操作指南。

如何包含软件包数据

这是我在 PyPI 上发布的一个项目的文件结构。 (它将应用程序安装在 __main__.py 中)。

├── LICENSE.md
├── MANIFEST.in
├── my_package
│   ├── __init__.py
│   ├── __main__.py
│   └── _my_data          <---- folder with data
│       ├── consola.ttf   <---- data file
│       └── icon.png      <---- data file
├── README.md
└── setup.py

起始点

这是在setup.py中用于setuptools.setup()的通用起始点。

setuptools.setup(
    ...
    packages=setuptools.find_packages(),
    ...
)

setuptools.find_packages() 包含了我的所有包在分发中。我唯一的包是 my_package

Python 不认为我的数据子文件夹 _my_data 是一个包,因为它不包含 __init__.py 文件,所以 find_packages() 找不到它。

一个经常被引用但错误的解决方案是在 _my_data 文件夹中放置一个空的 __init__.py 文件。

这样可以使它成为一个包,因此它可以在分发中包括文件夹 _my_data。但是_my_data 内部的数据文件不会被包含

因此,将 _my_data 转换为包 没有帮助

解决方法如下:

  • sdist 已经包含了数据文件
  • 添加 include_package_data=True 来在 bdist 中也包含数据文件

实验(如何测试解决方案)

有三个步骤可以使这个实验具有可重复性:

$ rm -fr build/ dist/ my_package.egg-info/
$ check-manifest
$ python setup.py sdist bdist_wheel

我会逐步解释以下内容:
  1. 清除旧版本构建:
$ rm -fr build/ dist/ my_package.egg-info/
  1. 运行 check-manifest 确保 MANIFEST.in 与版本控制下的 Git 文件索引匹配
$ check-manifest

如果 MANIFEST.in 不存在,请从 Git 版本控制下的文件索引中创建它:
$ check-manifest --create

这里是创建的 MANIFEST.in 文件:

include *.md
recursive-include my_package *.png
recursive-include my_package *.ttf

这个文件没有手动编辑的必要。

只要所有应该在版本控制下的内容都在版本控制下(即是Git索引的一部分),check-manifest --create就会做正确的事情。

注意:如果文件符合以下任一条件,则属于Git索引:

  • .gitignore中被忽略
  • .git/info/exclude中被排除
  • 或者仅仅是还未被添加到索引中的新文件

如果有任何不应该受版本控制的文件却被纳入了版本控制,check-manifest会发出警告并指出建议从Git索引中删除哪些文件。

  1. 构建:
$ python setup.py sdist bdist_wheel

现在检查sdist(源分发)和bdist_wheel(构建分发),看它们是否包含数据文件。
查看sdist的内容(只显示相关行):
$ tar --list -f dist/my_package-0.0.1a6.tar.gz
my_package-0.0.1a6/
...
my_package-0.0.1a6/my_package/__init__.py
my_package-0.0.1a6/my_package/__main__.py
my_package-0.0.1a6/my_package/_my_data/
my_package-0.0.1a6/my_package/_my_data/consola.ttf <-- yay!
my_package-0.0.1a6/my_package/_my_data/icon.png    <-- yay!
...

因为在MANIFEST.in中列出了数据文件,所以sdist已经包含了这些文件。不需要额外操作来将数据文件包含在sdist中。

查看bdist的内容(它是一个.zip文件,使用zipfile.ZipFile进行解析):

$ python check-whl.py
my_package/__init__.py
my_package/__main__.py
my_package-0.0.1a6.dist-info/LICENSE.md
my_package-0.0.1a6.dist-info/METADATA
my_package-0.0.1a6.dist-info/WHEEL
my_package-0.0.1a6.dist-info/entry_points.txt
my_package-0.0.1a6.dist-info/top_level.txt
my_package-0.0.1a6.dist-info/RECORD

注意:您需要创建自己的check-whl.py脚本以生成上述输出。它只有三行代码:
from zipfile import ZipFile
path = "dist/my_package-0.0.1a6-py3-none-any.whl" # <-- CHANGE
print('\n'.join(ZipFile(path).namelist()))

正如预期的那样,bdist缺少数据文件。

_my_data文件夹完全丢失。

如果我创建一个_my_data/__init__.py会怎样?我重复实验,发现数据文件仍然不在那里!_my_data/文件夹已经被包含了,但它不包含数据文件!

解决方案

与其他人的经验相反,这个方法是可行的

setuptools.setup(
    ...
    packages=setuptools.find_packages(),
    include_package_data=True, # <-- adds data files to bdist
    ...
)

修复后,重新进行实验:

$ rm -fr build/ dist/ my_package.egg-info/
$ check-manifest
$ python.exe setup.py sdist bdist_wheel

确保 sdist 仍然包含数据文件:

$ tar --list -f dist/my_package-0.0.1a6.tar.gz
my_package-0.0.1a6/
...
my_package-0.0.1a6/my_package/__init__.py
my_package-0.0.1a6/my_package/__main__.py
my_package-0.0.1a6/my_package/_my_data/
my_package-0.0.1a6/my_package/_my_data/consola.ttf <-- yay!
my_package-0.0.1a6/my_package/_my_data/icon.png    <-- yay!
...

查看 bdist 的内容:

$ python check-whl.py
my_package/__init__.py
my_package/__main__.py
my_package/_my_data/consola.ttf        <--- yay!
my_package/_my_data/icon.png           <--- yay!
my_package-0.0.1a6.dist-info/LICENSE.md
my_package-0.0.1a6.dist-info/METADATA
my_package-0.0.1a6.dist-info/WHEEL
my_package-0.0.1a6.dist-info/entry_points.txt
my_package-0.0.1a6.dist-info/top_level.txt
my_package-0.0.1a6.dist-info/RECORD

如何测试数据文件是否已包含

我建议使用上述方法来检查sdistbdist以进行故障排除/测试。

可编辑模式下的pip安装不是有效的测试

注意:pip install -e . 不能 显示数据文件是否已包含在bdist中。

符号链接会导致安装行为就像数据文件已包含一样(因为它们已经存在于开发者的计算机上)。

pip install my_package之后,数据文件位于虚拟环境的lib/site-packages/my_package/文件夹中,使用与whl内容列表中所示的完全相同的文件结构。

发布到TestPyPI是缓慢的测试方式

发布到TestPyPI,然后安装并查看lib/site-packages/my_packages是一种有效的测试方法,但时间成本太高。


4

像这个帖子中的其他人一样,我对长寿和仍然缺乏清晰度的组合感到有些惊讶,但对我最好的答案是使用 check-manifest,正如来自 @mike-gazes 的答案所推荐的那样。

因此,只使用 setup.cfg 而没有需要包含在软件包中的额外文本和 Python 文件,我的做法是将以下内容保留在 setup.cfg 中:

[options]
packages = find:
include_package_data = true

并根据 check-manifest 的输出更新 MANIFEST.in

include *.in
include *.txt
include *.yml
include LICENSE
include tox.ini
recursive-include mypkg *.py
recursive-include mypkg *.txt

4

虽然这是一个古老的问题,但是Python软件包管理确实有很多不足之处。所以我有个使用情况,需要使用pip本地安装到指定目录,但是package_data和data_files路径都没有起作用。我不想在代码库中再添加另一个文件,所以最终我利用了data_files和setup.py选项--install-data,类似于:

pip install . --install-option="--install-data=$PWD/package" -t package  

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接