1. Code
  2. Python

Как написать, упаковать и распространять библиотеку на Python

Python - отличный язык программирования, но упаковка - одна из его самых слабых точек. Это общеизвестный в сообществе факт. За последние годы установка, импорт, использование и создание пакетов значительно улучшилась, но она все еще не соответствует новым языкам, таким как Go и Rust, которые многому научились в борьбе с Python и другими зрелыми языками.
Scroll to top

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. На этом этапе у вас должны быть все инструменты для написания и обмена вашими библиотеками с остальным миром.

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