Статьи

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

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

В этом руководстве вы узнаете все, что вам нужно знать о написании, упаковке и распространении ваших собственных пакетов.

Библиотека Python — это связная коллекция модулей Python, организованная в виде пакета Python. В общем, это означает, что все модули находятся в одном каталоге и этот каталог находится в пути поиска Python.

Давайте быстро напишем небольшой пакет Python 3 и проиллюстрируем все эти концепции.

Python 3 имеет отличный объект Path, который является огромным улучшением по сравнению с неуклюжим модулем Python 2 os.path. Но ему не хватает одной важной возможности — найти путь к текущему сценарию. Это очень важно, когда вы хотите найти файлы доступа относительно текущего скрипта.

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

Вот как вы делаете это в Python:

1
2
3
import pathlib
 
script_dir = pathlib.Path(__file__).parent.resolve()

Для доступа к файлу с именем file.txt в подкаталоге data текущего каталога скрипта вы можете использовать следующий код: print(open(str(script_dir/'data/file.txt').read())

С пакетом pathology у вас есть встроенный метод script_dir , и вы используете его следующим образом:

1
2
3
from pathology.Path import script_dir
 
print(open(str(script_dir()/’data/file.txt’).read())

Да, это полный рот. Пакет патологии очень прост. Он извлекает свой собственный класс Path из Pathlib’s Path и добавляет статический script_dir (), который всегда возвращает путь вызывающего скрипта.

Вот реализация:

1
2
3
4
5
6
7
8
9
import pathlib
import inspect
 
class Path(type(pathlib.Path())):
    @staticmethod
    def script_dir():
        print(inspect.stack()[1].filename)
        p = pathlib.Path(inspect.stack()[1].filename)
        return p.parent.resolve()

Из-за кросс-платформенной реализации pathlib.Path , вы можете наследовать его напрямую и должны наследовать от определенного подкласса ( PosixPath или WindowsPath ). Разрешение dir скрипта использует модуль inspect для поиска вызывающего абонента, а затем его атрибута имени файла.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import os
import shutil
from unittest import TestCase
from pathology.path import Path
 
 
class PathTest(TestCase):
    def test_script_dir(self):
        expected = os.path.abspath(os.path.dirname(__file__))
        actual = str(Path.script_dir())
        self.assertEqual(expected, actual)
 
    def test_file_access(self):
        script_dir = os.path.abspath(os.path.dirname(__file__))
        subdir = os.path.join(script_dir, ‘test_data’)
        if Path(subdir).is_dir():
            shutil.rmtree(subdir)
        os.makedirs(subdir)
        file_path = str(Path(subdir)/’file.txt’)
        content = ‘123’
        open(file_path, ‘w’).write(content)
        test_path = Path.script_dir()/subdir/’file.txt’
        actual = open(str(test_path)).read()
 
        self.assertEqual(content, actual)

Пакеты Python должны быть установлены где-то на пути поиска Python для импорта модулями Python. Путь поиска Python представляет собой список каталогов и всегда доступен в sys.path . Вот мой текущий sys.path:

1
2
3
4
5
6
7
>>> print(‘\n’.join(sys.path))
 
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python36.zip
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python3.6
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python3.6/lib-dynload
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python3.6/site-packages
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python3.6/site-packages/setuptools-27.2.0-py3.6.egg

Обратите внимание, что первая пустая строка вывода представляет текущий каталог, поэтому вы можете импортировать модули из текущего рабочего каталога, каким бы он ни был. Вы можете напрямую добавлять или удалять каталоги в / из sys.path.

Вы также можете определить переменную среды PYTHONPATH , и есть несколько других способов управления ею. Стандартные site-packages включены по умолчанию, и именно здесь вы устанавливаете пакеты с помощью pip go.

Теперь, когда у нас есть наш код и тесты, давайте упаковываем все это в правильную библиотеку. Python предоставляет простой способ через модуль установки. Вы создаете файл с именем setup.py в корневом каталоге вашего пакета. Затем, чтобы создать исходный дистрибутив, вы запускаете: python setup.py sdist

Чтобы создать бинарный дистрибутив, называемый колесом, вы запускаете: python setup.py bdist_wheel

Вот файл setup.py пакета патологии:

01
02
03
04
05
06
07
08
09
10
11
12
from setuptools import setup, find_packages
 
setup(name=’pathology’,
      version=’0.1′,
      url=’https://github.com/the-gigi/pathology’,
      license=’MIT’,
      author=’Gigi Sayfan’,
      author_email=’the.gigi@gmail.com’,
      description=’Add static script_dir() method to Path’,
      packages=find_packages(exclude=[‘tests’]),
      long_description=open(‘README.md’).read(),
      zip_safe=False)

Он включает в себя множество метаданных в дополнение к элементу «packages», который использует find_packages() импортированную из setuptools для поиска find_packages() .

Давайте создадим исходный дистрибутив:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ python setup.py sdist
running sdist
running egg_info
creating pathology.egg-info
writing pathology.egg-info/PKG-INFO
writing dependency_links to pathology.egg-info/dependency_links.txt
writing top-level names to pathology.egg-info/top_level.txt
writing manifest file ‘pathology.egg-info/SOURCES.txt’
reading manifest file ‘pathology.egg-info/SOURCES.txt’
writing manifest file ‘pathology.egg-info/SOURCES.txt’
warning: sdist: standard file not found: should have one of README, README.rst, README.txt
 
running check
creating pathology-0.1
creating pathology-0.1/pathology
creating pathology-0.1/pathology.egg-info
copying files to pathology-0.1…
copying setup.py -> pathology-0.1
copying pathology/__init__.py -> pathology-0.1/pathology
copying pathology/path.py -> pathology-0.1/pathology
copying pathology.egg-info/PKG-INFO -> pathology-0.1/pathology.egg-info
copying pathology.egg-info/SOURCES.txt -> pathology-0.1/pathology.egg-info
copying pathology.egg-info/dependency_links.txt -> pathology-0.1/pathology.egg-info
copying pathology.egg-info/not-zip-safe -> pathology-0.1/pathology.egg-info
copying pathology.egg-info/top_level.txt -> pathology-0.1/pathology.egg-info
Writing pathology-0.1/setup.cfg
creating dist
Creating tar archive
removing ‘pathology-0.1’ (and everything under it)

Предупреждение связано с тем, что я использовал нестандартный файл README.md. Это безопасно игнорировать. Результатом является tar-gzipped файл в каталоге dist:

1
2
3
4
5
$ ls -la dist
total 8
drwxr-xr-x 3 gigi.sayfan gigi.sayfan 102 Apr 18 21:20 .
drwxr-xr-x 12 gigi.sayfan gigi.sayfan 408 Apr 18 21:20 ..
-rw-r—r— 1 gigi.sayfan gigi.sayfan 1223 Apr 18 21:20 pathology-0.1.tar.gz

А вот бинарный дистрибутив:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ python setup.py bdist_wheel
running bdist_wheel
running build
running build_py
creating build
creating build/lib
creating build/lib/pathology
copying pathology/__init__.py -> build/lib/pathology
copying pathology/path.py -> build/lib/pathology
installing to build/bdist.macosx-10.7-x86_64/wheel
running install
running install_lib
creating build/bdist.macosx-10.7-x86_64
creating build/bdist.macosx-10.7-x86_64/wheel
creating build/bdist.macosx-10.7-x86_64/wheel/pathology
copying build/lib/pathology/__init__.py -> build/bdist.macosx-10.7-x86_64/wheel/pathology
copying build/lib/pathology/path.py -> build/bdist.macosx-10.7-x86_64/wheel/pathology
running install_egg_info
running egg_info
writing pathology.egg-info/PKG-INFO
writing dependency_links to pathology.egg-info/dependency_links.txt
writing top-level names to pathology.egg-info/top_level.txt
reading manifest file ‘pathology.egg-info/SOURCES.txt’
writing manifest file ‘pathology.egg-info/SOURCES.txt’
Copying pathology.egg-info to build/bdist.macosx-10.7-x86_64/wheel/pathology-0.1-py3.6.egg-info
running install_scripts
creating build/bdist.macosx-10.7-x86_64/wheel/pathology-0.1.dist-info/WHEEL

Пакет pathology содержит только чистые модули Python, поэтому можно создать универсальный пакет. Если ваш пакет включает расширения C, вам придется создать отдельное колесо для каждой платформы:

1
2
3
4
5
6
$ ls -la dist
total 16
drwxr-xr-x 4 gigi.sayfan gigi.sayfan 136 Apr 18 21:24 .
drwxr-xr-x 13 gigi.sayfan gigi.sayfan 442 Apr 18 21:24 ..
-rw-r—r— 1 gigi.sayfan gigi.sayfan 2695 Apr 18 21:24 pathology-0.1-py3-none-any.whl
-rw-r—r— 1 gigi.sayfan gigi.sayfan 1223 Apr 18 21:20 pathology-0.1.tar.gz

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

Python имеет центральное хранилище пакетов, называемое PyPI (индекс пакетов Python). Когда вы устанавливаете пакет Python с помощью pip, он загружает пакет из PyPI (если вы не укажете другой репозиторий). Чтобы распространять наш пакет патологий, нам нужно загрузить его в PyPI и предоставить некоторые дополнительные метаданные, необходимые PyPI. Шаги:

  • Создайте аккаунт на PyPI (только один раз).
  • Зарегистрируйте свой пакет.
  • Загрузите свой пакет.

Вы можете создать учетную запись на сайте PyPI . Затем создайте файл .pypirc в вашем домашнем каталоге:

1
2
3
4
5
6
[distutils]
index-servers=pypi
  
[pypi]
repository = https://pypi.python.org/pypi
username = the_gigi

В целях тестирования вы можете добавить индексный сервер «pypitest» к вашему. файл pypirc :

01
02
03
04
05
06
07
08
09
10
11
12
[distutils]
index-servers=
    pypi
    pypitest
 
[pypitest]
repository = https://testpypi.python.org/pypi
username = the_gigi
 
[pypi]
repository = https://pypi.python.org/pypi
username = the_gigi

Если это первый выпуск вашего пакета, вам необходимо зарегистрировать его в PyPI. Используйте команду register из setup.py. Он попросит вас ввести пароль. Обратите внимание, что я указываю на тестовый репозиторий здесь:

01
02
03
04
05
06
07
08
09
10
11
12
$ python setup.py register -r pypitest
running register
running egg_info
writing pathology.egg-info/PKG-INFO
writing dependency_links to pathology.egg-info/dependency_links.txt
writing top-level names to pathology.egg-info/top_level.txt
reading manifest file ‘pathology.egg-info/SOURCES.txt’
writing manifest file ‘pathology.egg-info/SOURCES.txt’
running check
Password:
Registering pathology to https://testpypi.python.org/pypi
Server response (200): OK

Теперь, когда пакет зарегистрирован, мы можем загрузить его. Я рекомендую использовать шпагат , который является более безопасным. Установите его, как обычно, используя pip install twine . Затем загрузите вашу посылку, используя шпагат, и введите свой пароль (отредактировано ниже):

1
2
3
4
5
6
$ twine upload -r pypitest -p <redacted> dist/*
Uploading distributions to https://testpypi.python.org/pypi
Uploading pathology-0.1-py3-none-any.whl
[================================] 5679/5679 — 00:00:02
Uploading pathology-0.1.tar.gz
[================================] 4185/4185 — 00:00:01

Чтобы глубже погрузиться в тему распространения ваших пакетов, ознакомьтесь с разделом Как поделиться своими пакетами Python .

В этом уроке мы прошли полный процесс написания библиотеки Python, ее упаковки и распространения через PyPI. К этому моменту у вас должны быть все инструменты для написания ваших библиотек и обмена ими со всем миром.

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