Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)
Обзор
Python - замечательный язык программирования. Одной из самых слабых его сторон является пакеты. Это общеизвестный факт в сообществе. Установка, импорт, использование и создание пакетов улучшилась с годами, но она по-прежнему не соответствует новым языкам, таким как Go и Rust, которые могут многому научиться из-за проблем Python и других более зрелых языков.
В этом уроке вы узнаете все, что вам нужно знать, чтобы создавать и делиться своими собственными пакетами. Для общей информации о пакетах Python ознакомьтесь с разделом «Как использовать пакеты Python».
Упаковка проекта
Упаковка проекта - это процесс, с помощью которого вы берете надежно согласованный набор модулей Python и, возможно, других файлов и помещаете их в структуру, которую можно легко использовать. Существуют различные вещи, которые вы должны учитывать, такие как зависимости от других пакетов, внутренняя структура (подпакеты), управление версиями, целевая аудитория и форма пакета (исходный код и/или двоичный файл).
Пример
Начнем с быстрого примера. Пакет conman представляет собой пакет для управления конфигурацией. Он поддерживает несколько форматов файлов, а также распределенную конфигурацию с помощью etcd.
Содержимое пакета обычно хранится в одной директории (хотя обычно для разделения подпакетов в нескольких каталогах), а иногда, как и в этом случае, в собственном репозитории git.
Корневой каталог содержит различные файлы конфигурации (setup.py является обязательным и наиболее важным), а сам код пакета обычно находится в подкаталоге, имя которого является именем пакета и в идеале является каталогом тестов. Вот как это выглядит для «conman»:
1 |
> tree
|
2 |
|
3 |
.
|
4 |
|
5 |
├── LICENSE |
6 |
|
7 |
├── MANIFEST.in |
8 |
|
9 |
├── README.md |
10 |
|
11 |
├── conman |
12 |
|
13 |
│ ├── __init__.py |
14 |
|
15 |
│ ├── __pycache__ |
16 |
|
17 |
│ ├── conman_base.py |
18 |
|
19 |
│ ├── conman_etcd.py |
20 |
|
21 |
│ └── conman_file.py |
22 |
|
23 |
├── requirements.txt |
24 |
|
25 |
├── setup.cfg |
26 |
|
27 |
├── setup.py |
28 |
|
29 |
├── test-requirements.txt |
30 |
|
31 |
├── tests |
32 |
|
33 |
│ ├── __pycache__ |
34 |
|
35 |
│ ├── conman_etcd_test.py |
36 |
|
37 |
│ ├── conman_file_test.py |
38 |
|
39 |
│ └── etcd_test_util.py |
40 |
|
41 |
└── tox.ini |
Давайте быстро заглянем в файл setup.py. Он импортирует две функции из пакета setuptools: setup() и find_packages(). Затем он вызывает функцию setup() и использует find_packages() для одного из параметров.
1 |
from setuptools import setup, find_packages |
2 |
|
3 |
|
4 |
|
5 |
setup(name='conman', |
6 |
|
7 |
version='0.3', |
8 |
|
9 |
url='https://github.com/the-gigi/conman', |
10 |
|
11 |
license='MIT', |
12 |
|
13 |
author='Gigi Sayfan', |
14 |
|
15 |
author_email='the.gigi@gmail.com', |
16 |
|
17 |
description='Manage configuration files', |
18 |
|
19 |
packages=find_packages(exclude=['tests']), |
20 |
|
21 |
long_description=open('README.md').read(), |
22 |
|
23 |
zip_safe=False, |
24 |
|
25 |
setup_requires=['nose>=1.0'], |
26 |
|
27 |
test_suite='nose.collector') |
28 |
Это довольно нормально. Хотя файл setup.py является обычным файлом Python, и вы можете делать все, что хотите, его основное задание - вызвать функцию setup() с соответствующими параметрами, поскольку он будет запускаться различными инструментами стандартным способом при установке вашего пакета. Я расскажу подробности в следующем разделе.
Файлы конфигурации
В дополнение к setup.py есть еще несколько дополнительных файлов конфигурации, которые могут отображаться здесь и обслуживать различные цели.
Setup.py
Функция setup() принимает большое количество именованных аргументов для управления многими аспектами установки пакета, а также для запуска различных команд. Многие аргументы указывают метаданные, используемые для поиска и фильтрации при загрузке вашего пакета в репозиторий.
- name: имя вашего пакета (и как оно будет указано в PYPI)
- version: это важно для поддержания правильного управления зависимостями
- url: URL вашего пакета, обычно GitHub или URL-адрес readthedocs
- packages: список подпакетов, которые необходимо включить; здесь помогает
find_packages() - setup_requires: здесь вы указываете зависимости
- test_suite: какой инструмент запускается во время тестирования
long_description устанавливается здесь на содержимое файла README.md, что является наилучшей практикой для использования одного источника правды.
setup.cfg
Файл setup.py также поддерживает интерфейс командной строки для запуска различных команд. Например, чтобы запустить модульные тесты, вы можете ввести: python setup.py test
1 |
running test
|
2 |
|
3 |
running egg_info |
4 |
|
5 |
writing conman.egg-info/PKG-INFO |
6 |
|
7 |
writing top-level names to conman.egg-info/top_level.txt |
8 |
|
9 |
writing dependency_links to conman.egg-info/dependency_links.txt |
10 |
|
11 |
reading manifest file 'conman.egg-info/SOURCES.txt'
|
12 |
|
13 |
reading manifest template 'MANIFEST.in'
|
14 |
|
15 |
writing manifest file 'conman.egg-info/SOURCES.txt'
|
16 |
|
17 |
running build_ext |
18 |
|
19 |
test_add_bad_key (conman_etcd_test.ConManEtcdTest) ... ok |
20 |
|
21 |
test_add_good_key (conman_etcd_test.ConManEtcdTest) ... ok |
22 |
|
23 |
test_dictionary_access (conman_etcd_test.ConManEtcdTest) ... ok |
24 |
|
25 |
test_initialization (conman_etcd_test.ConManEtcdTest) ... ok |
26 |
|
27 |
test_refresh (conman_etcd_test.ConManEtcdTest) ... ok |
28 |
|
29 |
test_add_config_file_from_env_var (conman_file_test.ConmanFileTest) ... ok |
30 |
|
31 |
test_add_config_file_simple_guess_file_type (conman_file_test.ConmanFileTest) ... ok |
32 |
|
33 |
test_add_config_file_simple_unknown_wrong_file_type (conman_file_test.ConmanFileTest) ... ok |
34 |
|
35 |
test_add_config_file_simple_with_file_type (conman_file_test.ConmanFileTest) ... ok |
36 |
|
37 |
test_add_config_file_simple_wrong_file_type (conman_file_test.ConmanFileTest) ... ok |
38 |
|
39 |
test_add_config_file_with_base_dir (conman_file_test.ConmanFileTest) ... ok |
40 |
|
41 |
test_dictionary_access (conman_file_test.ConmanFileTest) ... ok |
42 |
|
43 |
test_guess_file_type (conman_file_test.ConmanFileTest) ... ok |
44 |
|
45 |
test_init_no_files (conman_file_test.ConmanFileTest) ... ok |
46 |
|
47 |
test_init_some_bad_files (conman_file_test.ConmanFileTest) ... ok |
48 |
|
49 |
test_init_some_good_files (conman_file_test.ConmanFileTest) ... ok |
50 |
|
51 |
|
52 |
|
53 |
----------------------------------------------------------------------
|
54 |
|
55 |
Ran 16 tests in 0.160s
|
56 |
|
57 |
|
58 |
|
59 |
OK |
60 |
Файл setup.cfg является файлом формата ini, который может содержать параметры по умолчанию для команд, которые вы передаете setup.py. Здесь setup.cfg содержит некоторые опции для nosetests (наш тестовый раннер):
1 |
[nosetests] |
2 |
|
3 |
verbose=1 |
4 |
|
5 |
nocapture=1 |
MANIFEST.in
Этот файл содержит файлы, которые не являются частью внутреннего каталога пакетов, но вы все еще хотите включить его. Обычно это файл readme, файл лицензи Важным файлом является файл requirements.txt. Этот файл используется pip для установки других необходимых пакетов.
Вот файл MANIFEST.in в conman:
1 |
include LICENSE |
2 |
|
3 |
include README.md |
4 |
|
5 |
include requirements.txt |
Зависимости
Вы можете указать зависимости как в разделе install_requires файла setup.py, так и в файле requirements.txt. Pip установит автоматически зависимости от install_requires, но не из файла requirements.txt. Чтобы установить эти требования, вам нужно будет указать его явно при запуске pip install -r requirements.txt.
Параметр install_requires предназначен для указания минимальных и более абстрактных требований на основном уровне версии. Файл requirements.txt предназначен для более конкретных требований, часто с привязкой к второстепенным версиям.
Вот файл требований conman. Вы можете видеть, что все версии зафиксированы, что означает, что это может негативно повлиять, если один из этих пакетов обновится и внесет изменения, которые ломают conman.
1 |
PyYAML==3.11 |
2 |
|
3 |
python-etcd==0.4.3 |
4 |
|
5 |
urllib3==1.7 |
6 |
|
7 |
pyOpenSSL==0.15.1 |
8 |
|
9 |
psutil==4.0.0 |
10 |
|
11 |
six==1.7.3 |
Пиннинг дает вам предсказуемость и спокойствие. Это особенно важно, если многие люди устанавливают ваш пакет в разное время. Без закрепления каждый человек получит различное сочетание версий зависимостей на основе того, когда они его установили. Недостатком пиннинга является то, что если вы не справляетесь с развитием зависимостей, вы можете застрять на старой, плохо исполняющейся и даже уязвимой версии некоторой зависимости.
Я изначально написал conman в 2014 году и не обращал на него особого внимания. Теперь для этого урока я обновил все, и для почти каждой зависимости были некоторые существенные улучшения.
Распределения
Вы можете создать дистрибутив источника или бинарный дистрибутив. Я расскажу об обоих.
Распределение источников
Вы создаете исходный дистрибутив с помощью команды: python setup.py sdist. Вот вывод для conman:
1 |
> python setup.py sdist
|
2 |
|
3 |
running sdist |
4 |
|
5 |
running egg_info |
6 |
|
7 |
writing conman.egg-info/PKG-INFO |
8 |
|
9 |
writing top-level names to conman.egg-info/top_level.txt |
10 |
|
11 |
writing dependency_links to conman.egg-info/dependency_links.txt |
12 |
|
13 |
reading manifest file 'conman.egg-info/SOURCES.txt'
|
14 |
|
15 |
reading manifest template 'MANIFEST.in'
|
16 |
|
17 |
writing manifest file 'conman.egg-info/SOURCES.txt'
|
18 |
|
19 |
warning: sdist: standard file not found: should have one of README, README.rst, README.txt |
20 |
|
21 |
|
22 |
|
23 |
running check |
24 |
|
25 |
creating conman-0.3 |
26 |
|
27 |
creating conman-0.3/conman |
28 |
|
29 |
creating conman-0.3/conman.egg-info |
30 |
|
31 |
making hard links in conman-0.3...
|
32 |
|
33 |
hard linking LICENSE -> conman-0.3 |
34 |
|
35 |
hard linking MANIFEST.in -> conman-0.3 |
36 |
|
37 |
hard linking README.md -> conman-0.3 |
38 |
|
39 |
hard linking requirements.txt -> conman-0.3 |
40 |
|
41 |
hard linking setup.cfg -> conman-0.3 |
42 |
|
43 |
hard linking setup.py -> conman-0.3 |
44 |
|
45 |
hard linking conman/__init__.py -> conman-0.3/conman |
46 |
|
47 |
hard linking conman/conman_base.py -> conman-0.3/conman |
48 |
|
49 |
hard linking conman/conman_etcd.py -> conman-0.3/conman |
50 |
|
51 |
hard linking conman/conman_file.py -> conman-0.3/conman |
52 |
|
53 |
hard linking conman.egg-info/PKG-INFO -> conman-0.3/conman.egg-info |
54 |
|
55 |
hard linking conman.egg-info/SOURCES.txt -> conman-0.3/conman.egg-info |
56 |
|
57 |
hard linking conman.egg-info/dependency_links.txt -> conman-0.3/conman.egg-info |
58 |
|
59 |
hard linking conman.egg-info/not-zip-safe -> conman-0.3/conman.egg-info |
60 |
|
61 |
hard linking conman.egg-info/top_level.txt -> conman-0.3/conman.egg-info |
62 |
|
63 |
copying setup.cfg -> conman-0.3 |
64 |
|
65 |
Writing conman-0.3/setup.cfg |
66 |
|
67 |
creating dist |
68 |
|
69 |
Creating tar archive
|
70 |
|
71 |
removing 'conman-0.3' (and everything under it) |
72 |
Как вы можете видеть, я получил одно предупреждение о том, что отсутствует файл README с одним из стандартных префиксов, потому что мне нравится Markdown, поэтому вместо этого у меня есть «README.md». Кроме этого, все исходные файлы пакета были включены и дополнительные файлы. Затем в каталоге conman.egg-info была создана куча метаданных. Наконец, создается сжатый tar-архив под названием conman-0.3.tar.gz и помещается в подкаталог dist.
Для установки этого пакета потребуется шаг сборки (хотя это чистый Python). Вы можете установить его с помощью pip, просто передав путь к пакету. Например:
1 |
pip install dist/conman-0.3.tar.gz
|
2 |
|
3 |
Processing ./dist/conman-0.3.tar.gz |
4 |
|
5 |
Installing collected packages: conman |
6 |
|
7 |
Running setup.py install for conman ... done |
8 |
|
9 |
Successfully installed conman-0.3 |
Conman был установлен в site-packages и может быть импортирован как любой другой пакет:
1 |
import conman |
2 |
|
3 |
conman.__file__ |
4 |
|
5 |
'/Users/gigi/.virtualenvs/conman/lib/python2.7/site-packages/conman/__init__.pyc'
|
Wheels
Wheels - относительно новый способ упаковки кода Python и, возможно, C-расширений. Они заменяют формат egg. Существует несколько типов колес: чистые колеса Python, платформы и универсальные колеса. Чистые колеса Python - это пакеты, такие как conman, у которых нет кода расширения C.
Платформенные колеса имеют код расширения C. Универсальные колеса - это чистые колеса Python, которые совместимы как с Python 2, так и с Python 3 с одинаковой базой кода (они не требуют даже 2to3). Если у вас есть чистый пакет Python и вы хотите, чтобы ваш пакет поддерживал как Python 2, так и Python 3 (становится все более и более важным), вы можете создать единую универсальную сборку вместо одного колеса для Python 2 и одного колеса для Python 3.
Если в вашем пакете есть код расширения C, вы должны создать колесо платформы для каждой платформы. Огромное преимущество колес, особенно для пакетов с расширениями C, заключается в том, что нет необходимости иметь компилятор и поддерживающие библиотеки, доступные на целевой машине. Колесо уже содержит встроенный пакет. Таким образом, вы знаете, что его гораздо быстрее установить, потому что это буквально просто копия. Люди, которые используют научные библиотеки, такие как Numpy и Pandas, действительно могут это оценить, поскольку установка таких пакетов требует много времени и может быть неудачной, если какая-то библиотека отсутствует или компилятор не настроен должным образом.
Команда для создания чистых или платформенных колес: python setup.py bdist_wheel.
Setuptools - движок, который обеспечивает функцию setup(), будет автоматически обнаруживаться, если требуется чистое или платформенное колесо.
1 |
running bdist_wheel |
2 |
|
3 |
running build |
4 |
|
5 |
running build_py |
6 |
|
7 |
creating build |
8 |
|
9 |
creating build/lib |
10 |
|
11 |
creating build/lib/conman |
12 |
|
13 |
copying conman/__init__.py -> build/lib/conman |
14 |
|
15 |
copying conman/conman_base.py -> build/lib/conman |
16 |
|
17 |
copying conman/conman_etcd.py -> build/lib/conman |
18 |
|
19 |
copying conman/conman_file.py -> build/lib/conman |
20 |
|
21 |
installing to build/bdist.macosx-10.9-x86_64/wheel |
22 |
|
23 |
running install
|
24 |
|
25 |
running install_lib |
26 |
|
27 |
creating build/bdist.macosx-10.9-x86_64 |
28 |
|
29 |
creating build/bdist.macosx-10.9-x86_64/wheel |
30 |
|
31 |
creating build/bdist.macosx-10.9-x86_64/wheel/conman |
32 |
|
33 |
copying build/lib/conman/__init__.py -> build/bdist.macosx-10.9-x86_64/wheel/conman |
34 |
|
35 |
copying build/lib/conman/conman_base.py -> build/bdist.macosx-10.9-x86_64/wheel/conman |
36 |
|
37 |
copying build/lib/conman/conman_etcd.py -> build/bdist.macosx-10.9-x86_64/wheel/conman |
38 |
|
39 |
copying build/lib/conman/conman_file.py -> build/bdist.macosx-10.9-x86_64/wheel/conman |
40 |
|
41 |
running install_egg_info |
42 |
|
43 |
running egg_info |
44 |
|
45 |
creating conman.egg-info |
46 |
|
47 |
writing conman.egg-info/PKG-INFO |
48 |
|
49 |
writing top-level names to conman.egg-info/top_level.txt |
50 |
|
51 |
writing dependency_links to conman.egg-info/dependency_links.txt |
52 |
|
53 |
writing manifest file 'conman.egg-info/SOURCES.txt'
|
54 |
|
55 |
reading manifest file 'conman.egg-info/SOURCES.txt'
|
56 |
|
57 |
reading manifest template 'MANIFEST.in'
|
58 |
|
59 |
writing manifest file 'conman.egg-info/SOURCES.txt'
|
60 |
|
61 |
Copying conman.egg-info to build/bdist.macosx-10.9-x86_64/wheel/conman-0.3-py2.7.egg-info |
62 |
|
63 |
running install_scripts |
64 |
|
65 |
creating build/bdist.macosx-10.9-x86_64/wheel/conman-0.3.dist-info/WHEEL |
При проверке каталога dist вы можете увидеть, что было создано чистое колесо Python:
1 |
ls -la dist |
2 |
|
3 |
|
4 |
|
5 |
dist/ |
6 |
|
7 |
total 32 |
8 |
|
9 |
-rw-r--r-- 1 gigi staff 5.5K Feb 29 07:57 conman-0.3-py2-none-any.whl
|
10 |
|
11 |
-rw-r--r-- 1 gigi staff 4.4K Feb 28 23:33 conman-0.3.tar.gz
|
Название «conman-0.3-py2-none-any.whl» имеет несколько компонентов: имя пакета, версию пакета, версию Python, версию платформы и, наконец, расширение «whl».
Чтобы создавать универсальные пакеты, вы просто добавляете --universal, как в python setup.py bdist_wheel --universal.
Полученное колесо называется "conman-0.3-py2.py3-none-any.whl".
Обратите внимание, что вы несете ответственность за то, чтобы ваш код действительно работал как с Python 2, так и с Python 3, если вы создаете универсальный пакет.
Заключение
Написание собственных пакетов Python требует использования большого количества инструментов, определения большого количества метаданных и тщательного анализа ваших зависимостей и целевой аудитории. Но награда велика.
Если вы правильно напишите полезный код и упакуете его, люди смогут легко его установить и извлечь из него выгоду.



