如何在Python setup.py中递归添加包数据?

问题描述 投票:0回答:11

我有一个新库,它必须包含许多小数据文件的子文件夹,我正试图将它们添加为包数据。想象一下我的图书馆是这样的:

 library
    - foo.py
    - bar.py
 data
   subfolderA
      subfolderA1
      subfolderA2
   subfolderB
      subfolderB1 
      ...

我想通过

setup.py
添加所有子文件夹中的所有数据,但似乎我必须手动进入每个子文件夹(大约有100个)并添加一个
__init__.py
文件。此外,
setup.py
会递归地找到这些文件,还是我需要在
setup.py
中手动添加所有这些文件,例如:

package_data={
  'mypackage.data.folderA': ['*'],
  'mypackage.data.folderA.subfolderA1': ['*'],
  'mypackage.data.folderA.subfolderA2': ['*']
   },

我可以用脚本做到这一点,但似乎超级痛苦。我怎样才能在

setup.py
中实现这一目标?

PS,这些文件夹的层次结构很重要,因为这是一个材料文件数据库,我们希望在将它们以 GUI 形式呈现给用户时保留文件树,因此保留此文件结构对我们有利完好无损。

python distutils setup.py
11个回答
57
投票

glob
答案的问题在于它只能做这么多。 IE。它不是完全递归的。
copy_tree
答案的问题是复制的文件将在卸载时留下。

正确的解决方案是递归解决方案,它可以让您在设置调用中设置

package_data
参数。

我写了这个小方法来做到这一点:

import os

def package_files(directory):
    paths = []
    for (path, directories, filenames) in os.walk(directory):
        for filename in filenames:
            paths.append(os.path.join('..', path, filename))
    return paths

extra_files = package_files('path_to/extra_files_dir')

setup(
    ...
    packages = ['package_name'],
    package_data={'': extra_files},
    ....
)

您会注意到,当您执行

pip uninstall package_name
时,您会看到列出了您的其他文件(与包裹一起跟踪)。


31
投票
  1. 使用 Setuptools 而不是 distutils.
  2. 使用数据文件代替包数据。这些不需要
    __init__.py
    .
  3. 使用标准 Python 代码生成文件和目录列表,而不是按字面意思编写:

    data_files = []
    directories = glob.glob('data/subfolder?/subfolder??/')
    for directory in directories:
        files = glob.glob(directory+'*')
        data_files.append((directory, files))
    # then pass data_files to setup()
    

15
投票

package_data
中使用
setup.py
添加所有子文件夹: 根据您的子目录结构添加
*
条目的数量

package_data={
  'mypackage.data.folderA': ['*','*/*','*/*/*'],
}

10
投票

使用 glob 选择你的所有子文件夹

setup.py

...
packages=['your_package'],
package_data={'your_package': ['data/**/*']},
...

6
投票

更新

根据更改日志

setuptools
现在支持递归glob,使用
**
,在
package_data
(截至
v62.3.0
,2022年5月发布)。

原答案

@gbonetti 的 answer,使用递归 glob 模式,即

**
,将是完美的。

但是,正如@daniel-himmelstein 所评论的那样,在设置工具中还不起作用

package_data
.

所以,暂时,我喜欢使用以下解决方法,基于

pathlib
Path.glob()

def glob_fix(package_name, glob):
    # this assumes setup.py lives in the folder that contains the package
    package_path = Path(f'./{package_name}').resolve()
    return [str(path.relative_to(package_path)) 
            for path in package_path.glob(glob)]

这将返回相对于包路径的路径字符串列表,因为需要

这是使用它的一种方法:

setuptools.setup(
    ...
    package_data={'my_package': [*glob_fix('my_package', 'my_data_dir/**/*'), 
                                 'my_other_dir/some.file', ...], ...},
    ...
)

glob_fix()
可以在 setuptools 支持
**
package_data
后立即删除。


4
投票

如果你的 setup.py 代码没有任何问题,请使用

distutils.dir_util.copy_tree
.
整个问题是如何从中排除文件。
这是一些代码:

import os.path
from distutils import dir_util
from distutils import sysconfig
from distutils.core import setup

__packagename__ = 'x' 
setup(
    name = __packagename__,
    packages = [__packagename__],
)

destination_path = sysconfig.get_python_lib()
package_path = os.path.join(destination_path, __packagename__)

dir_util.copy_tree(__packagename__, package_path, update=1, preserve_mode=0)

一些注意事项

  • 此代码递归地将源代码复制到目标路径中。
  • 您可以只使用相同的
    setup(...)
    ,但使用
    copy_tree()
    将您想要的目录扩展到安装路径中。
  • distutil安装的默认路径可以在它的API中找到。
  • 有关 distutils 的 copy_tree() 模块的更多信息,请参见here.


  • 2
    投票

    我可以建议一些代码来在 setup() 中添加 data_files:

    data_files = []
    
    start_point = os.path.join(__pkgname__, 'static')
    for root, dirs, files in os.walk(start_point):
        root_files = [os.path.join(root, i) for i in files]
        data_files.append((root, root_files))
    
    start_point = os.path.join(__pkgname__, 'templates')
    for root, dirs, files in os.walk(start_point):
        root_files = [os.path.join(root, i) for i in files]
        data_files.append((root, root_files))
    
    setup(
        name = __pkgname__,
        description = __description__,
        version = __version__,
        long_description = README,
        ...
        data_files = data_files,
    )
    

    0
    投票

    我可以用脚本做到这一点,但似乎超级痛苦。我怎样才能在 setup.py 中实现这个?

    这里有一个可重复使用的简单方法:

    在你的

    setup.py
    中添加以下函数,并按照使用说明调用它。这本质上是已接受答案的通用版本。

    def find_package_data(specs):
        """recursively find package data as per the folders given
    
        Usage:
            # in setup.py
            setup(...
                  include_package_data=True,
                  package_data=find_package_data({
                     'package': ('resources', 'static')
                  }))
    
        Args:
            specs (dict): package => list of folder names to include files from
    
        Returns:
            dict of list of file names
        """
        return {
            package: list(''.join(n.split('/', 1)[1:]) for n in
                          flatten(glob('{}/{}/**/*'.format(package, f), recursive=True) for f in folders))
            for package, folders in specs.items()}
    
    

    0
    投票

    我会把我的解决方案放在这里,以防有人正在寻找一种干净的方法来将他们编译的狮身人面像文档作为

    data_files
    .

    setup.py

    from setuptools import setup
    import pathlib
    import os
    
    here = pathlib.Path(__file__).parent.resolve()
    
    # Get documentation files from the docs/build/html directory
    documentation = [doc.relative_to(here) for doc in here.glob("docs/build/html/**/*") if pathlib.Path.is_file(doc)]
    data_docs = {}
    for doc in documentation:
        doc_path = os.path.join("your_top_data_dir", "docs")
        path_parts = doc.parts[3:-1]  # remove "docs/build/html", ignore filename
        if path_parts:
            doc_path = os.path.join(doc_path, *path_parts)
        # create all appropriate subfolders and append relative doc path
        data_docs.setdefault(doc_path, []).append(str(doc))
    
    setup(
        ...
        include_package_data=True,
        # <sys.prefix>/your_top_data_dir
        data_files=[("your_top_data_dir", ["data/test-credentials.json"]), *list(data_docs.items())]
    )
    

    使用上述解决方案,安装包后,您将在

    os.path.join(sys.prefix, "your_top_data_dir", "docs")
    获得所有已编译的文档。所以,如果你想使用 nginx 提供现在静态的文档,你可以将以下内容添加到你的 nginx 文件中:

    location /docs {
        # handle static files directly, without forwarding to the application
        alias /www/your_app_name/venv/your_top_data_dir/docs;
        expires 30d;
    }
    

    完成后,您应该能够访问

    {your-domain.com}/docs
    并查看您的 Sphinx 文档。


    0
    投票

    如果您不想添加自定义代码来遍历目录内容,您可以使用

    pbr
    库,它扩展了
    setuptools
    。有关如何使用它复制整个目录并保留目录结构的文档,请参见此处:

    https://docs.openstack.org/pbr/latest/user/using.html#files


    0
    投票

    需要写一个函数返回所有文件及其路径,可以使用如下

    def sherinfind():
        # Add all folders contain files or other sub directories 
        pathlist=['templates/','scripts/']
        data={}        
        for path in pathlist:
            for root,d_names,f_names in os.walk(path,topdown=True, onerror=None, followlinks=False):
                data[root]=list()
                for f in f_names:
                    data[root].append(os.path.join(root, f))                
        
        fn=[(k,v) for k,v in data.items()]    
        return fn
    

    现在更改 setup() 中的 data_files 如下,

    data_files=sherinfind()
    
    © www.soinside.com 2019 - 2024. All rights reserved.