Как добавить данные пакета в 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 ответов
- Используйте Setuptools вместо distutils.
- Используйте файлы данных вместо данных пакета. Эти не требуют
__init__.py
, Создайте списки файлов и каталогов, используя стандартный код 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()
расширить каталог, который вы хотите в путь установки. Я могу предложить немного кода для добавления 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
. См. здесь документацию о том, как использовать его для копирования всего каталога с сохранением структуры каталогов: