Как добавить данные пакета в Python setup.py?

У меня есть новая библиотека, которая должна включать в себя множество подпапок небольших файлов данных, и я пытаюсь добавить их в качестве данных пакета. Представьте, что у меня есть библиотека так:

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

Я хочу добавить все данные во все подпапки через setup.py, но мне кажется, что мне нужно вручную войти в каждую подпапку (их около 100) и добавить файл инициализации.py. Кроме того, setup.py найдет эти файлы рекурсивно или мне нужно вручную добавить все эти файлы в setup.py, например:

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

Я могу сделать это с помощью сценария, но, похоже, супер боль. Как я могу добиться этого в setup.py?

PS, иерархия этих папок важна, потому что это база данных файлов материалов, и мы хотим, чтобы дерево файлов сохранялось, когда мы представляем их в графическом интерфейсе пользователю, поэтому было бы в наших интересах сохранить целостность этой файловой структуры.,

12 ответов

Решение
  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()
    

Проблема с glob Ответ в том, что он так много делает. Т.е. это не полностью рекурсивно. Проблема с 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, что вы увидите ваши дополнительные файлы в списке (как отслеживается с пакетом).

Чтобы добавить все подпапки с помощью package_data в setup.py: добавьте количество * записей в зависимости от структуры подкаталогов

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

Используйте glob, чтобы выбрать все подпапки в вашем setup.py

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

@gbonetti игрового ответ, используя рекурсивный шаблон Глобы, т.е. **, было бы идеально.

Однако, как прокомментировал @daniel-himmelstein, это еще не работает в инструментах настройкиpackage_data.

Итак, на данный момент мне нравится использовать следующий обходной путь, основанный на pathlib"S 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.

Если у вас нет проблем с грязным использованием кода 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.
  • Более подробную информацию о copy_tree() модуле distutils можно найти здесь.

  • Я могу предложить немного кода для добавления data_files в setup():

    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,
    )
    

    Я могу сделать это с помощью сценария, но это кажется супер болью. Как я могу добиться этого в 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()}
    
    

    find_packagesобнаруживает пакеты рекурсивно:

          setup(
        # [...]
        packages=find_packages(),
        # [...]
    )
    

    Но для этого требуется__init__.py.

    Я собираюсь бросить здесь свое решение на случай, если кто-то ищет чистый способ включить свои скомпилированные документы sphinx как 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.

    Вам нужно написать функцию для возврата всех файлов и их путей, вы можете использовать следующее

          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
    

    Теперь измените data_files в setup() следующим образом:

          data_files=sherinfind()
    

    Если вы не хотите добавлять собственный код для перебора содержимого каталога, вы можете использовать pbrбиблиотека, расширяющая setuptools. См. здесь документацию о том, как использовать его для копирования всего каталога с сохранением структуры каталогов:

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

    Другие вопросы по тегам