Рабочий процесс проекта Python

Настройка проекта

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

Инициализировать проект

Сначала создадим новую папку для нашего проекта:

$ mkdir random-quote-generator
$ cd random-quote-generator

Инициализируйте проект с помощью Poetry:

$ poetry init

Package name [random_quote_generator]:
Version [0.1.0]:
Description []:
Author [Your name <your@email.com>, n to skip]:
License []:
Compatible Python versions [^3.10]:

Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Do you confirm generation? (yes/no) [yes]

Подробнее о поэзии читайте в статье Современные среды Python - управление зависимостями и рабочим пространством.

Имя вашего проекта должно быть уникальным, поскольку вы будете загружать его в PyPI. Поэтому, чтобы избежать столкновений имен, добавьте уникальную строку к имени пакета в pyproject.toml.

Например:

[tool.poetry]
name = "random-quote-generator-9308"
version = "0.1.0"
description = ""
authors = ["Michael Herman <notreal@gmail.com>"]

[tool.poetry.dependencies]
python = "^3.10"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

Создайте новый репозиторий на GitHub:

GitHub new repository

Далее, инициализируйте git-репозиторий внутри вашего проекта:

$ git init
$ git add pyproject.toml
$ git commit -m "first commit"
$ git branch -M main
$ git remote add origin git@github.com:<your-github-username>/random-quote-generator.git
$ git fetch
$ git branch --set-upstream-to=origin/main main
$ git pull origin main --rebase
$ git push -u origin main

После завершения базовой установки, давайте продолжим, добавив следующие зависимости для разработки:

Просмотрите статью Python Code Quality для получения подробной информации об этих зависимостях.

Install:

$ poetry add --dev pytest pytest-cov black isort flake8 bandit safety

Добавьте новый файл poetry.lock, а также обновленный файл pyproject.toml в git:

$ git add poetry.lock pyproject.toml

Создайте проект

После этого создайте новую папку под названием "random_quote_generator". Внутри этой папки добавьте файл __init__.py, чтобы он рассматривался как модуль, а также файл __quotes.py.

random-quote-generator
├── poetry.lock
├── pyproject.toml
└── random_quote_generator
    ├── __init__.py
    └── quotes.py

Внутри quotes.py добавьте:

quotes = [
    {
        "quote": "A long descriptive name is better than a short "
        "enigmatic name. A long descriptive name is better "
        "than a long descriptive comment.",
        "author": "Robert C. Martin",
    },
    {
        "quote": "You should name a variable using the same "
        "care with which you name a first-born child.",
        "author": "Robert C. Martin",
    },
    {
        "quote": "Any fool can write code that a computer "
        "can understand. Good programmers write code"
        " that humans can understand.",
        "author": "Martin Fowler",
    },
]

Там нет ничего особенного. Просто список словарей, по одному для каждой цитаты. Далее, создайте новую папку в корне проекта под названием "tests" и добавьте следующие файлы:

tests
├── __init__.py
└── test_get_quote.py

test_get_quote.py:

from random_quote_generator import get_quote
from random_quote_generator.quotes import quotes


def test_get_quote():
    """
    GIVEN
    WHEN get_quote is called
    THEN random quote from quotes is returned
    """

    quote = get_quote()

    assert quote in quotes

Запустите тест:

$ poetry run python -m pytest tests

Должен произойти сбой:

E   ImportError: cannot import name 'get_quote' from 'random_quote_generator'

Далее, добавьте новый файл в "random_quote_generator" под названием get_quote.py:

import random

from random_quote_generator.quotes import quotes


def get_quote() -> dict:
    """
    Get random quote

    Get randomly selected quote from database our programming quotes

    :return: selected quote
    :rtype: dict
    """

    return quotes[random.randint(0, len(quotes) - 1)]

Итак, цитата выбирается путем генерации случайного целого числа со значением random.randint между 0 и последним индексом.

Экспортируйте функцию в random_quote_generator/__init__.py:

"""
Random Quote Generator
======================

Get random quote from our database of programming wisdom
"""
from .get_quote import get_quote

__all__ = ["get_quote"]

Функция импортируется и перечисляется внутри атрибута __all__, который представляет собой список публичных объектов для модуля. Другими словами, когда кто-то использует from random_quote_generator import *, будут импортированы только имена, перечисленные в __all__.

Теперь тест должен пройти:

$ poetry run python -m pytest tests

Создайте файл .gitignore в корне вашего проекта:

__pycache__

Добавьте папки "random_quote_generator" и "tests" в git вместе с файлом .gitignore:

$ git add random_quote_generator/ tests/ .gitignore

Вот и все. Посылка готова к отправке.

Документирование проекта

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

Если вы не знакомы с docstrings или документацией как отдельным ресурсом, просмотрите статью Documenting Python Code and Projects.

Предполагая, что у вас установлен Sphinx installed, выполните следующее, чтобы разложить файлы и папки для Sphinx в корне проекта:

$ sphinx-quickstart docs

Вы будете продвигаться по службе с некоторыми вопросами:

> Separate source and build directories (y/n) [n]: n
> Project name: Random Quote Generator
> Author name(s): Your Name
> Project release []: 0.1.0
> Project language [en]: en

Далее, давайте обновим конфигурацию проекта. Откройте docs/conf.py и замените следующее:

# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))

С этим:

import os
import sys
sys.path.insert(0, os.path.abspath('..'))

Теперь autodoc, который используется для извлечения документации из docstrings, будет искать модули в родительской папке "docs".

Добавьте следующие расширения в список расширений:

extensions = [
    'sphinx.ext.autodoc',
]

Обновить docs/index.rst следующим образом:

.. Random Quote Generator documentation master file, created by
   sphinx-quickstart on Mon Dec 21 22:27:23 2020.
   You can adapt this file completely to your liking, but it should at least
   contain the root `toctree` directive.

Welcome to Random Quote Generator's documentation!
==================================================

.. automodule:: random_quote_generator
    :members:



Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

Этот файл должен быть исключен из Flake8, который мы скоро добавим. Поэтому создайте файл .flake8 в корне проекта:

[flake8]
exclude =
    docs/conf.py,

Добавьте папку "docs" и .flake8 в git:

$ git add docs/ .flake8

GitHub Actions

Далее, давайте подключим конвейер CI с помощью GitHub Actions.

Добавьте следующие файлы и папки в корень проекта:

.github
└── workflows
    └── branch.yaml

Внутри branch.yaml, добавьте:

name: Push
on: [push]

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        python-version: ['3.10']
        poetry-version: ['1.1.13']
        os: [ubuntu-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v3
        with:
          python-version: ${{ matrix.python-version }}
      - name: Run image
        uses: abatilo/actions-poetry@v2.1.4
        with:
          poetry-version: ${{ matrix.poetry-version }}
      - name: Install dependencies
        run: poetry install
      - name: Run tests
        run: poetry run pytest --cov=./ --cov-report=xml
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v2
  code-quality:
    strategy:
      fail-fast: false
      matrix:
        python-version: ['3.10']
        poetry-version: ['1.1.13']
        os: [ubuntu-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v3
        with:
          python-version: ${{ matrix.python-version }}
      - name: Run image
        uses: abatilo/actions-poetry@v2.1.4
        with:
          poetry-version: ${{ matrix.poetry-version }}
      - name: Install dependencies
        run: poetry install
      - name: Run black
        run: poetry run black . --check
      - name: Run isort
        run: poetry run isort . --check-only --profile black
      - name: Run flake8
        run: poetry run flake8 .
      - name: Run bandit
        run: poetry run bandit .
      - name: Run saftey
        run: poetry run safety check

Эта конфигурация:

  • запускается на каждом push в каждой ветке - on: [push]
  • работает на последней версии Ubuntu - ubuntu-latest
  • использует Python 3.10 - python-version: [3.10], python-version: ${{ matrix.python-version }}
  • использует Poetry версии 1.1.13 - poetry-version: [1.1.13], poetry-version: ${{ matrix.poetry-version }}

Определены два рабочих места: test и code-quality. Как следует из названий, тесты выполняются в тестовом задании, а наши проверки качества кода - в задании качества кода.

Теперь при каждом запуске в репозиторий GitHub будут запускаться тесты и задания качества кода.

Добавьте ".github" в git:

$ git add .github/

Запустите все проверки качества кода:

$ poetry run black .
$ poetry run isort . --profile black
$ poetry run flake8 .
$ poetry run bandit .
$ poetry run safety check

Обязательно добавьте в git все файлы, которые могли измениться. Затем зафиксируйте и отправьте изменения на GitHub:

$ git add docs/ random_quote_generator/ tests/
$ git commit -m 'Package ready'
$ git push -u origin main

Вы должны увидеть, что ваш рабочий процесс запущен на вкладке "Действия" в вашем репозитории GitHub. Убедитесь, что он прошел, прежде чем двигаться дальше.

CodeCov

Далее мы настроим CodeCov для отслеживания покрытия кода. Перейдите по адресу http://codecov.io/, войдите под своей учетной записью GitHub и найдите свой репозиторий.

Ознакомьтесь с руководством Quick Start, чтобы получить помощь в начале работы с CodeCov.

Снова запустите рабочий процесс GitHub Actions. После этого вы сможете увидеть отчет о покрытии на CodeCov:

CodeCov

Теперь при каждом запуске рабочего процесса будет создаваться отчет о покрытии и загружаться в CodeCov. Вы можете анализировать изменения в процентах покрытия для веток, коммитов и запросов на вытягивание, обращая внимание на увеличение и уменьшение покрытия с течением времени.

Документация

Мы будем использовать Read the Docs для размещения нашей документации. Перейдите по адресу https://readthedocs.org и войдите в систему, используя свою учетную запись GitHub.

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

Далее нажмите "Импортировать проект". После этого обновите проекты и добавьте проект генератора случайных цитат. Откройте проект и перейдите в раздел "Admin". Затем в разделе "Дополнительные настройки" установите ветвь по умолчанию на main. Не забудьте сохранить изменения.

Read the Docs

На сборку документации Read The Docs уйдет несколько минут. После этого вы сможете просмотреть документацию вашего проекта по адресу https://your-project-slug-on-readthedocs.readthedocs.io.

Read the Docs

По умолчанию документация будет перестраиваться при каждом переносе в ветку main. После этого остается только опубликовать ваш пакет в PyPI.

PyPI

Наконец, чтобы сделать проект "pip-installable", мы опубликуем его на PyPI.

Начните с добавления следующего раздела в pyproject.toml, чтобы модуль "random_quote_generator" был включен в дистрибутив PyPI:

packages = [
    { include = "random_quote_generator" },
]

Пример файла:

[tool.poetry]
name = "random-quote-generator-93618"
packages = [
    { include = "random_quote_generator" },
]
version = "0.1.0"
description = ""
authors = ["Amir Tadrisi <notreal@gmail.com>"]

[tool.poetry.dependencies]
python = "^3.10"

[tool.poetry.dev-dependencies]
pytest = "^7.1.2"
pytest-cov = "^3.0.0"
black = "^22.3.0"
isort = "^5.10.1"
flake8 = "^4.0.1"
bandit = "^1.7.4"
safety = "^1.10.3"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

Добавьте новый файл под названием release.yaml в ".github/workflows":

name: Release
on:
  release:
    types:
      - created

jobs:
  publish:
    strategy:
      fail-fast: false
      matrix:
        python-version: ['3.10']
        poetry-version: ['1.1.13']
        os: [ubuntu-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v3
        with:
          python-version: ${{ matrix.python-version }}
      - name: Run image
        uses: abatilo/actions-poetry@v2.1.4
        with:
          poetry-version: ${{ matrix.poetry-version }}
      - name: Publish
        env:
          PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
        run: |
          poetry config pypi-token.pypi $PYPI_TOKEN
          poetry publish --build

Итак, когда будет создан новый релиз, пакет будет опубликован в PyPI.

Далее нам потребуется создать токен PyPI.  Создайте аккаунт на PyPI, если у вас его еще нет. Затем, авторизовавшись, нажмите "Account settings" и добавьте новый токен API. Скопируйте токен. Теперь вам нужно добавить его в секреты вашего репозитория на GitHub. В репозитории перейдите на вкладку "Настройки", затем нажмите "Секреты" и выберите "Действия". Используйте PYPI_TOKEN для названия секрета и значение токена в качестве секретного значения.

Теперь вы готовы создать свой первый релиз.

Добавьте файл release.yaml в git, а также обновленный файл pyproject.toml, зафиксируйте и вытолкните:

$ git add .github/workflows/release.yaml pyproject.toml
$ git commit -m 'Ready for first release'
$ git push -u origin main

Чтобы создать новый релиз, перейдите в раздел https://github.com/<username>/<project-name>/releases/. Нажмите "Создать новый релиз". Введите 0.1.0 для тега и Initial Release для заголовка.

GitHub new release

Это запустит новый рабочий процесс на GitHub Actions. После завершения вы увидите свой пакет на PyPI.

Теперь вы должны иметь возможность установить и использовать ваш пакет из PyPI:

>>> from random_quote_generator import get_quote
>>>
>>> print(get_quote())
{'quote': 'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.', 'author': 'Martin Fowler'}

Заключение

Мы создали простой CI/CD конвейер для пакета Python, опубликованного на PyPI. Код в каждой ветке проверяется тестами и качеством кода. Вы можете проверить покрытие кода в CodeCov. При выпуске новые версии развертываются на PyPI, а документация обновляется. Это может значительно упростить вашу жизнь.

Подобные автоматизированные конвейеры помогают обеспечить неизменность рабочего процесса изо дня в день. Тем не менее, вы все равно должны заботиться о культуре работы вашей команды. Тесты запускаются при нажатии, но они должны присутствовать. Автоматизированный запуск тестов мало чем поможет, если у вас нет тестов для запуска. Автоматизация также не будет иметь большого значения для размера ваших изменений. Старайтесь, чтобы они были небольшими, часто объединяясь в main. Небольшие изменения вместе с тестово-ориентированной разработкой (TDD) и конвейером CI/CD могут иметь огромное значение для качества программного обеспечения, которое вы поставляете. Только не забывайте, что все всегда начинается и заканчивается культурой команды.

Вернуться на верх