Как написать, упаковать и распространять библиотеку на Python
Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)
Python - отличный язык программирования, но упаковка - одна из его самых слабых точек. Это общеизвестный в сообществе факт. За последние годы установка, импорт, использование и создание пакетов значительно улучшилась, но она все еще не соответствует новым языкам, таким как Go и Rust, которые многому научились в борьбе с Python и другими зрелыми языками.
В этом уроке вы узнаете все, что вам нужно знать о написании, упаковке и распространении собственных пакетов.
Как написать библиотеку Python
Библиотека Python представляет собой согласованный набор модулей Python, который организован как пакет Python. В общем, это означает, что все модули живут под одним и тем же каталогом и этот каталог находится на пути поиска Python.
Давайте быстро напишем небольшой пакет Python 3 и проиллюстрируем все эти понятия.
Пакет Pathology
Python 3 имеет отличный объект Path, который является большим улучшением по сравнению с неудобным модулем Os.path Python 2. Но ему не хватает одной важной возможности - найти путь к текущему сценарию. Это очень важно, если вы хотите найти файлы доступа относительно текущего скрипта.
Во многих случаях сценарий может быть установлен в любом месте, поэтому вы не можете использовать абсолютные пути, а рабочий каталог может быть установлен на любое значение, поэтому вы не можете использовать относительный путь. Если вы хотите получить доступ к файлу в подкаталоге или родительском каталоге, вы должны иметь возможность определить текущий каталог сценариев.
Вот как вы это делаете в Python:
1 |
import pathlib |
2 |
|
3 |
script_dir = pathlib.Path(__file__).parent.resolve() |
Чтобы получить доступ к файлу с именем 'file.txt' в подкаталоге данных в каталоге текущего скрипта, вы можете использовать следующий код:print(open(str(script_dir/'data/file.txt').read())
С пакетом pathology у вас есть встроенный метод script_dir, и вы используете его следующим образом:
1 |
from pathology.Path import script_dir |
2 |
|
3 |
print(open(str(script_dir()/'data/file.txt').read()) |
4 |
Да, это глоток свежего воздуха. Пакет патологии очень прост. Он выводит свой собственный класс Path из Pathlib Path и добавляет статический script_dir(), который всегда возвращает путь вызывающего скрипта.
Вот реализация:
1 |
import pathlib |
2 |
import inspect |
3 |
|
4 |
class Path(type(pathlib.Path())): |
5 |
@staticmethod |
6 |
def script_dir(): |
7 |
print(inspect.stack()[1].filename) |
8 |
p = pathlib.Path(inspect.stack()[1].filename) |
9 |
return p.parent.resolve() |
Из-за кросс-платформенной реализации pathlib.Path вы можете получить непосредственно от него и должны быть получены из определенного подкласса (PosixPath или WindowsPath). Разрешение dir-файла сценария использует модуль проверки, чтобы найти вызывающего, а затем его атрибут имени файла.
Тестирование пакета патологии
Всякий раз, когда вы пишете нечто более сложное, вы должны его протестировать. Модуль патологии не является исключением. Вот тесты с использованием стандартной модульной тестовой платформы:
1 |
import os |
2 |
import shutil |
3 |
from unittest import TestCase |
4 |
from pathology.path import Path |
5 |
|
6 |
|
7 |
class PathTest(TestCase): |
8 |
def test_script_dir(self): |
9 |
expected = os.path.abspath(os.path.dirname(__file__)) |
10 |
actual = str(Path.script_dir()) |
11 |
self.assertEqual(expected, actual) |
12 |
|
13 |
def test_file_access(self): |
14 |
script_dir = os.path.abspath(os.path.dirname(__file__)) |
15 |
subdir = os.path.join(script_dir, 'test_data') |
16 |
if Path(subdir).is_dir(): |
17 |
shutil.rmtree(subdir) |
18 |
os.makedirs(subdir) |
19 |
file_path = str(Path(subdir)/'file.txt') |
20 |
content = '123' |
21 |
open(file_path, 'w').write(content) |
22 |
test_path = Path.script_dir()/subdir/'file.txt' |
23 |
actual = open(str(test_path)).read() |
24 |
|
25 |
self.assertEqual(content, actual) |
Путь Python
Пакеты Python должны быть установлены где-то на пути поиска Python, который должен быть импортирован модулями Python. Путь поиска Python представляет собой список каталогов и всегда доступен в sys.path. Вот мой текущий sys.path:
1 |
>>> print('\n'.join(sys.path)) |
2 |
|
3 |
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python36.zip |
4 |
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python3.6 |
5 |
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python3.6/lib-dynload |
6 |
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python3.6/site-packages |
7 |
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python3.6/site-packages/setuptools-27.2.0-py3.6.egg |
Обратите внимание, что первая пустая строка вывода представляет текущий каталог, поэтому вы можете импортировать модули из текущего рабочего каталога, что бы это ни было. Вы можете напрямую добавлять или удалять каталоги в / из sys.path.
Вы также можете определить переменную среды PYTHONPATH, и есть несколько других способов ее контролировать. Стандартные site-packages включены по умолчанию, и именно там устанавливаются пакеты, которые вы устанавливаете с помощью pip.
Как упаковать библиотеку Python
Теперь, когда у нас есть наш код и тесты, давайте упакуем все это в нужную библиотеку. Python обеспечивает простой способ через модуль настройки. Вы создаете файл setup.py в корневом каталоге вашего пакета. Затем, чтобы создать исходный дистрибутив, вы запустите: python setup.py sdist
Чтобы создать двоичный дистрибутив, называемый колесом, вы запускаете: python setup.py bdist_wheel
Вот файл setup.py пакета патологии:
1 |
from setuptools import setup, find_packages |
2 |
|
3 |
setup(name='pathology', |
4 |
version='0.1', |
5 |
url='https://github.com/the-gigi/pathology', |
6 |
license='MIT', |
7 |
author='Gigi Sayfan', |
8 |
author_email='the.gigi@gmail.com', |
9 |
description='Add static script_dir() method to Path', |
10 |
packages=find_packages(exclude=['tests']), |
11 |
long_description=open('README.md').read(), |
12 |
zip_safe=False) |
Он включает в себя множество метаданных в дополнение к элементу 'packages', который использует функцию find_packages(), импортированную из setuptools, чтобы найти подпакеты.
Давайте построим дистрибутив источника:
1 |
$ python setup.py sdist |
2 |
running sdist |
3 |
running egg_info |
4 |
creating pathology.egg-info |
5 |
writing pathology.egg-info/PKG-INFO |
6 |
writing dependency_links to pathology.egg-info/dependency_links.txt |
7 |
writing top-level names to pathology.egg-info/top_level.txt |
8 |
writing manifest file 'pathology.egg-info/SOURCES.txt' |
9 |
reading manifest file 'pathology.egg-info/SOURCES.txt' |
10 |
writing manifest file 'pathology.egg-info/SOURCES.txt' |
11 |
warning: sdist: standard file not found: should have one of README, README.rst, README.txt |
12 |
|
13 |
running check |
14 |
creating pathology-0.1 |
15 |
creating pathology-0.1/pathology |
16 |
creating pathology-0.1/pathology.egg-info |
17 |
copying files to pathology-0.1... |
18 |
copying setup.py -> pathology-0.1 |
19 |
copying pathology/__init__.py -> pathology-0.1/pathology |
20 |
copying pathology/path.py -> pathology-0.1/pathology |
21 |
copying pathology.egg-info/PKG-INFO -> pathology-0.1/pathology.egg-info |
22 |
copying pathology.egg-info/SOURCES.txt -> pathology-0.1/pathology.egg-info |
23 |
copying pathology.egg-info/dependency_links.txt -> pathology-0.1/pathology.egg-info |
24 |
copying pathology.egg-info/not-zip-safe -> pathology-0.1/pathology.egg-info |
25 |
copying pathology.egg-info/top_level.txt -> pathology-0.1/pathology.egg-info |
26 |
Writing pathology-0.1/setup.cfg |
27 |
creating dist |
28 |
Creating tar archive |
29 |
removing 'pathology-0.1' (and everything under it) |
Предупреждение связано с тем, что я использовал нестандартный файл README.md. Это безопасно поэтому игнорируем. Результатом является файл tar-gzipped в каталоге dist:
1 |
$ ls -la dist |
2 |
total 8 |
3 |
drwxr-xr-x 3 gigi.sayfan gigi.sayfan 102 Apr 18 21:20 .
|
4 |
drwxr-xr-x 12 gigi.sayfan gigi.sayfan 408 Apr 18 21:20 .. |
5 |
-rw-r--r-- 1 gigi.sayfan gigi.sayfan 1223 Apr 18 21:20 pathology-0.1.tar.gz
|
И вот двоичное распределение:
1 |
$ python setup.py bdist_wheel |
2 |
running bdist_wheel |
3 |
running build |
4 |
running build_py |
5 |
creating build |
6 |
creating build/lib |
7 |
creating build/lib/pathology |
8 |
copying pathology/__init__.py -> build/lib/pathology |
9 |
copying pathology/path.py -> build/lib/pathology |
10 |
installing to build/bdist.macosx-10.7-x86_64/wheel |
11 |
running install |
12 |
running install_lib |
13 |
creating build/bdist.macosx-10.7-x86_64 |
14 |
creating build/bdist.macosx-10.7-x86_64/wheel |
15 |
creating build/bdist.macosx-10.7-x86_64/wheel/pathology |
16 |
copying build/lib/pathology/__init__.py -> build/bdist.macosx-10.7-x86_64/wheel/pathology |
17 |
copying build/lib/pathology/path.py -> build/bdist.macosx-10.7-x86_64/wheel/pathology |
18 |
running install_egg_info |
19 |
running egg_info |
20 |
writing pathology.egg-info/PKG-INFO |
21 |
writing dependency_links to pathology.egg-info/dependency_links.txt |
22 |
writing top-level names to pathology.egg-info/top_level.txt |
23 |
reading manifest file 'pathology.egg-info/SOURCES.txt' |
24 |
writing manifest file 'pathology.egg-info/SOURCES.txt' |
25 |
Copying pathology.egg-info to build/bdist.macosx-10.7-x86_64/wheel/pathology-0.1-py3.6.egg-info |
26 |
running install_scripts |
27 |
creating build/bdist.macosx-10.7-x86_64/wheel/pathology-0.1.dist-info/WHEEL |
Пакет патологии содержит только чистые модули Python, поэтому можно создать универсальный пакет. Если ваш пакет включает расширения C, вам нужно будет создать отдельное колесо для каждой платформы:
1 |
$ ls -la dist |
2 |
total 16 |
3 |
drwxr-xr-x 4 gigi.sayfan gigi.sayfan 136 Apr 18 21:24 .
|
4 |
drwxr-xr-x 13 gigi.sayfan gigi.sayfan 442 Apr 18 21:24 .. |
5 |
-rw-r--r-- 1 gigi.sayfan gigi.sayfan 2695 Apr 18 21:24 pathology-0.1-py3-none-any.whl
|
6 |
-rw-r--r-- 1 gigi.sayfan gigi.sayfan 1223 Apr 18 21:20 pathology-0.1.tar.gz
|
Для более глубокого погружения в тему упаковки библиотек Python ознакомьтесь, как писать свои собственные пакеты Python.
Как раздавать пакет Python
Python имеет центральный репозиторий пакетов, называемый PyPI (индекс пакетов Python). Когда вы устанавливаете пакет Python с помощью pip, он загружает пакет из PyPI (если вы не укажете другой репозиторий). Чтобы распространять наш пакет патологии, нам нужно загрузить его в PyPI и предоставить некоторые дополнительные метаданные, которые требуется PyPI. Шаги:
- Создайте учетную запись на PyPI (только один раз).
- Зарегистрируйте свой пакет.
- Загрузите свой пакет.
Создайте аккаунт
Вы можете создать учетную запись на веб-сайте PyPI. Затем создайте файл .pypirc в своем домашнем каталоге:
1 |
[distutils] |
2 |
index-servers=pypi |
3 |
|
4 |
[pypi] |
5 |
repository = https://pypi.python.org/pypi |
6 |
username = the_gigi |
В целях тестирования вы можете добавить «pypitest» индексный сервер в ваш .pypirc файл:
1 |
[distutils] |
2 |
index-servers= |
3 |
pypi
|
4 |
pypitest
|
5 |
|
6 |
[pypitest] |
7 |
repository = https://testpypi.python.org/pypi |
8 |
username = the_gigi |
9 |
|
10 |
[pypi] |
11 |
repository = https://pypi.python.org/pypi |
12 |
username = the_gigi |
Зарегистрируйте свой пакет
Если это первый выпуск вашего пакета, вам необходимо зарегистрировать его с помощью PyPI. Используйте команду register setup.py. Она попросит вас ввести пароль. Обратите внимание, что я указываю его на тестовый репозиторий:
1 |
$ python setup.py register -r pypitest |
2 |
running register |
3 |
running egg_info |
4 |
writing pathology.egg-info/PKG-INFO |
5 |
writing dependency_links to pathology.egg-info/dependency_links.txt |
6 |
writing top-level names to pathology.egg-info/top_level.txt |
7 |
reading manifest file 'pathology.egg-info/SOURCES.txt'
|
8 |
writing manifest file 'pathology.egg-info/SOURCES.txt'
|
9 |
running check |
10 |
Password: |
11 |
Registering pathology to https://testpypi.python.org/pypi |
12 |
Server response (200): OK |
Загрузите свой пакет
Теперь, когда пакет зарегистрирован, мы можем его загрузить. Я рекомендую использовать twine, который более безопасен. Установите его, как обычно, с помощью pip install twine. Затем загрузите свой пакет с помощью twine и укажите свой пароль (отредактированный ниже):
1 |
$ twine upload -r pypitest -p <redacted> dist/* |
2 |
Uploading distributions to https://testpypi.python.org/pypi |
3 |
Uploading pathology-0.1-py3-none-any.whl |
4 |
[================================] 5679/5679 - 00:00:02 |
5 |
Uploading pathology-0.1.tar.gz |
6 |
[================================] 4185/4185 - 00:00:01 |
Для более глубокого погружения в тему распространения ваших пакетов ознакомьтесь с разделом «Пакеты Python».
Заключение
В этом уроке мы прошли полноценный процесс написания библиотеки Python, ее упаковки и распространения через PyPI. На этом этапе у вас должны быть все инструменты для написания и обмена вашими библиотеками с остальным миром.
Кроме того, не стесняйтесь посмотреть, что у нас есть для продажи и для изучения на рынке, и, пожалуйста, задавайте любые вопросы и предоставляйте свою ценную обратную связь, используя приведенный ниже канал.



