newsmode
search
Меню
arrow_back Назад

How to Set Up a Python Project For Automation and Collaboration

auto_awesomeКраткое саммари

Статья описывает пошаговую настройку Python-проекта для автоматизации проверок и удобной командной работы. Рассматриваются pyenv для управления версиями Python, venv и pip для виртуальных окружений, pytest для юнит-тестов, Coverage.py для измерения покрытия кода, pylint для линтинга и mypy для статической проверки типов. Все команды объединяются в Makefile, чтобы запускать полный набор проверок одной командой make check. Для непрерывной интеграции используется GitHub Actions — автоматические проверки выполняются при каждом git push и pull request. По оценке автора, такой подход сэкономил его командам около 50–60% усилий на код-ревью.

Как настроить Python-проект для автоматизации и командной работы

[ engineering production python productivity ] · 20 мин. чтения

По мере роста вашего Python-проекта управлять им становится всё сложнее.

Как можно автоматизировать проверки (например, юнит-тесты, проверку типов, линтинг)? Как можно минимизировать накладные расходы на совместную работу (например, код-ревью, единообразие кода)? Как можно максимально улучшить опыт разработчика, добавляя минимум дополнительных шагов?

В этой статье я поделюсь подходом, который хорошо работает для меня. К концу у нас будет автоматизированный рабочий процесс с юнит-тестами, отчётами о покрытии, проверками линтера и проверками типов, который будет выявлять большинство ошибок и облегчать совместную работу. Этот процесс будет запускаться локально одной командой (make check) и в удалённом репозитории при каждом git push.

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

Следите за материалом вместе с сопроводительным репозиторием здесь 🌟.

(Примечание: эти шаги должны работать на современных Mac или Linux. Для Windows потребуется включить подсистему Linux.)

Установка менеджера версий Python

Для начала нам понадобится менеджер версий Python. Он позволяет устанавливать и управлять несколькими версиями Python на локальной машине. Стандарт де-факто — pyenv. Вот как его установить через pyenv-installer.

# Установка pyenv curl https://pyenv.run | bash # Перезапуск оболочки (чтобы изменения PATH для pyenv вступили в силу) exec $SHELL

Теперь мы готовы устанавливать и переключаться между версиями Python. (Примечание: pyenv добавляет директорию с shim-файлами в ваш PATH. Подробнее об этом здесь.)

# Это системный python $ python --version Python 2.7.16 # Установка Python 3.8.2 $ pyenv install 3.8.2 python-build: use [email protected] from homebrew python-build: use readline from homebrew Downloading Python-3.8.2.tar.xz... -> https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tar.xz Installing Python-3.8.2... python-build: use readline from homebrew python-build: use zlib from xcode sdk Installed Python-3.8.2 to /Users/eugene/.pyenv/versions/3.8.2 # Устанавливаем локальную версию python $ pyenv local 3.8.2 $ python --version Python 3.8.2 # Проверяем различные версии python $ pyenv versions system 3.7.7 3.8.0 * 3.8.2 (set by /Users/eugene/projects/python-collab-template/.python-version)

Python 3.8.2 будет версией по умолчанию для этого проекта. То есть, когда вы находитесь в этой директории, вызов python будет использовать Python 3.8.2.

При обновлении версий Python — как узнать, что установка прошла корректно? Вот совет: запустите встроенный набор тестов Python.

$ python -m test 0:00:00 load avg: 1.89 Run tests sequentially 0:00:00 load avg: 1.89 [ 1/423] test_grammar 0:00:00 load avg: 1.89 [ 2/423] test_opcodes 0:00:00 load avg: 1.89 [ 3/423] test_dict 0:00:00 load avg: 1.89 [ 4/423] test_builtin 0:00:00 load avg: 1.98 [ 5/423] test_exceptions 0:00:01 load avg: 1.98 [ 6/423] test_types 0:00:01 load avg: 1.98 [ 7/423] test_unittest 0:00:03 load avg: 1.98 [ 8/423] test_doctest ... Total duration: 18 min 9 sec Tests result: SUCCESS

В этой версии Python содержит 423 внутренних теста. Просто расслабьтесь и наслаждайтесь тем, как тесты проходят один за другим. 😎

Настройка виртуального окружения и установка пакетов

Для управления Python-окружениями и установки пакетов мы будем использовать самые базовые инструменты — venv и pip. Оба поставляются со стандартным Python, просты в использовании, и, скорее всего, большинство начинающих Python-разработчиков уже с ними знакомы.

# Создание виртуального окружения python -m venv .venv # Активация виртуального окружения source .venv/bin/activate # Обновление pip pip install --upgrade pip

# Создание проекта poetry poetry init --no-interaction # Добавление numpy как зависимости poetry add numpy # Пересоздание проекта на основе pyproject.toml poetry install # Получение пути к виртуальному окружению poetry (для PyCharm) poetry env info

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

dev: используются только для разработки (например, тестирование, линтинг и т. д.) и не нужны в продакшене. prod: необходимы в продакшене (например, обработка данных, машинное обучение и т. д.).

# Установка dev-пакетов, которые будут использоваться для тестирования, линтинга, проверки типов и т. д. pip install pytest pytest-cov pylint mypy codecov # Фиксация dev-зависимостей pip freeze > requirements.dev # Установка prod-пакетов pip install pandas # Фиксация prod-зависимостей pip freeze > requirements.prod

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

$ cat requirements.dev codecov==2.1.7 mypy==0.780 pylint==2.5.3 pytest==5.4.3 pytest-cov==2.10.0 Sphinx==3.1.1 $ cat requirements.prod numpy==1.18.5 pandas==1.0.4

Когда у вас большой проект с множеством пакетов, обновление любого из них может сломать остальные. Эту проблему можно смягчить с помощью инструмента управления зависимостями. Такие инструменты также должны позволять легко разделять зависимости для dev и prod.

Я не включил это в основную часть статьи, так как единого мнения по ним пока нет, но вот несколько вариантов:

pipenv: выглядел многообещающе в 2018 году. Но я нашёл его глючным и медленным, что снижало скорость сборки. Тем не менее, с обновлениями 2020 года ситуация, похоже, улучшилась. pip-tools: работает со стандартным requirements.txt и позволяет просто обновлять зависимости через pip-compile --upgrade. Выглядит перспективно. poetry: в последнее время набирает популярность и позволяет напрямую собирать и публиковать пакеты в PyPI. Версия 1.0.0 была выпущена в декабре 2019 года. Когда-нибудь может стать стандартом де-факто.

Другие альтернативы: pyflow, dephell и flit. Мой друг Yu Xuan написал о средах Python и различных инструментах. Подробнее об этом здесь.

Альтернатива: Docker в качестве среды разработки

Альтернатива управлению версиями Python (через pyenv) и зависимостями (через venv) — использование Docker в качестве среды разработки. Таким образом команда может гарантировать единообразие среды ОС (а не только Python-окружения). Кроме того, некоторые ML-платформы, например SageMaker, запускают обучение и деплой через Docker-контейнеры — поэтому хорошей практикой является разработка в тех же контейнерах. Сейчас я рекомендую именно этот подход.

Начнём с создания простого Dockerfile, который базируется на Python3.8, копирует наши requirements и устанавливает зависимости.

ARG BASE_IMAGE=python:3.8 FROM ${BASE_IMAGE} as base LABEL maintainer='eugeneyan <[email protected]>' # Используем директорию opt как рабочую директорию для разработки WORKDIR /opt ENV PYTHONUNBUFFERED TRUE COPY requirements.dev . COPY requirements.prod . # Установка python-зависимостей RUN pip install --upgrade pip \ && pip install --no-cache-dir wheel \ && pip install --no-cache-dir -r requirements.dev \ && pip install --no-cache-dir -r requirements.prod \ && pip list

Чтобы собрать наше окружение, просто выполняем:

DOCKER_BUILDKIT=1 docker build -t dev -f Dockerfile .

Затем мы работаем через IDE на локальной машине как обычно. А когда нужно запустить тесты в Docker-контейнере, поднимаем его следующим образом. Эта команда монтирует текущую директорию в директорию /opt внутри Docker-контейнера и выполняет команду bash.

docker run --rm -it --name run-checks -v $(pwd):/opt -t dev bash

Создание единообразной структуры проекта

Тут немного что сказать — у нас будут стандартные директории src и tests. Директория tests должна повторять структуру директории src.

├── requirements.dev ├── requirements.prod ├── src │ ├── __init__.py │ └── data_prep └── tests ├── __init__.py └── data_prep

Добавление базовых методов

Прежде чем двигаться дальше, нам понадобится код для тестирования.

Для этой статьи я написал несколько базовых методов для обработки данных из набора данных Titanic. (Говорят, никогда не стоит писать о датасетах Titanic или MNIST, но вы же понимаете, что фокус этой статьи не на данных, верно?)

У нас есть модуль categorical для обработки имён и извлечения титулов (например, Mr, Mrs, Miss) и модуль continuous для заполнения пропущенных значений (в частности, возраста). Вот как выглядит метод extract_title. Остальной код src находится здесь.

def extract_title(df: pd.DataFrame, col: str, replace_dict: dict = None, title_col: str = 'title') -> pd.DataFrame: """Извлекает титулы в новый столбец title Args: df: DataFrame, из которого извлекаются титулы col: Столбец в DataFrame, из которого извлекаются титулы replace_dict (Optional): Необязательный словарь для сопоставления титулов title_col: Имя нового столбца с извлечёнными титулами Returns: DataFrame с дополнительным столбцом извлечённых титулов """ df[title_col] = df[col].str.extract(r' ([A-Za-z]+)\\.', expand=False) if replace_dict: df[title_col] = np.where(df[title_col].isin(replace_dict.keys()), df[title_col].map(replace_dict), df[title_col]) return df

Пишем юнит-тесты — наша страховочная сетка

Юнит-тесты проверяют работоспособность наших методов. Это важно для того, чтобы обновления ничего не ломали, особенно в data science, где ошибки в преобразовании данных и feature engineering обычно проходят незаметно. Поэтому мы включим тестовые данные (mock data) в наши тесты.

«Тесты — это истории, которые мы рассказываем следующему поколению программистов проекта.» — Roy Osherove

Стандарт де-факто для юнит-тестов — pytest. Также есть unittest, который поставляется с Python (хотя большинство просто используют pytest).

Чтобы протестировать наш метод extract_title, нужно создать тестовый DataFrame. Вот как это выглядит:

def test_extract_title(): string_col = ['futrelle, mme. jacques heath (lily may peel)', 'johnston, ms. catherine helen "carrie"', 'sloper, mr. william thompson', 'ostby, lady. engelhart cornelius', 'backstrom, major. karl alfred (maria mathilda gustafsson)'] df_dict = {'string': string_col} lowercased_df = pd.DataFrame(df_dict) result = extract_title(lowercased_df, col='string') assert result['title'].tolist() == ['mme', 'ms', 'mr', 'lady', 'major']

Запуск теста — это просто:

$ pytest ============================= test session starts ============================== platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 rootdir: /Users/eugene/projects/python-collab-template/tests/data_prep collected 1 item test_categorical.py::test_lowercase_column PASSED [100%] ============================== 1 passed in 0.24s ===============================

Вы заметите, что метод extract_title позволяет передать необязательный словарь для замены титулов. Например, мы можем захотеть заменить 'ms' на 'miss', а 'lady' и 'major' на 'rare'. Это тоже нужно протестировать.

Используйте фикстуры для повторного использования

Вместо копирования кода создания DataFrame можно оформить его как фикстуру для повторного использования. Это легко сделать с помощью декоратора pytest.fixture. Теперь мы можем использовать этот lowercase_df во всех тестах и соблюдать принцип DRY.

@pytest.fixture def lowercased_df(): string_col = ['futrelle, mme. jacques heath (lily may peel)', 'johnston, ms. catherine helen "carrie"', 'sloper, mr. william thompson', 'ostby, lady. engelhart cornelius', 'backstrom, major. karl alfred (maria mathilda gustafsson)'] df_dict = {'string': string_col} df = pd.DataFrame(df_dict) return df def test_extract_title(lowercased_df): result = extract_title(lowercased_df, col='string') assert result['title'].tolist() == ['mme', 'ms', 'mr', 'lady', 'major'] def test_extract_title_with_replacement(lowercased_df): title_replacement = {'mme': 'mrs', 'ms': 'miss', 'lady': 'rare', 'major': 'rare'} result = extract_title(lowercased_df, col='string', replace_dict=title_replacement) assert result['title'].tolist() == ['mrs', 'miss', 'mr', 'rare', 'rare']

Запустим тесты снова. Отлично. 👍

$ pytest ============================= test session starts ============================== platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 rootdir: /Users/eugene/projects/python-collab-template/tests/data_prep collected 2 items test_categorical.py::test_extract_title PASSED [ 50%] test_categorical.py::test_extract_title_with_replacement PASSED [100%] ============================== 2 passed in 0.30s ===============================

Тестируйте и исключения тоже

В методе fill_numeric пользователь может выбрать способ заполнения пропущенных значений, например mean, median и т. д. Если пользователь выберет некорректный способ, метод должен вернуть NotImplementedError. Это тоже можно протестировать в pytest.

def test_fill_numeric_not_implemented(dummy_df): with pytest.raises(NotImplementedError): fill_numeric(dummy_df, col='int', fill_type='random')

Чтобы лучше оценить ценность юнит-тестов, намеренно внесём баг: будем проверять, равен ли replace_dict значению True, вместо того чтобы проверять, был ли передан словарь.

def extract_title(df: pd.DataFrame, col: str, replace_dict: dict = None, title_col: str = 'title') -> pd.DataFrame: df[title_col] = df[col].str.extract(r' ([A-Za-z]+)\\.', expand=False) if replace_dict == True: # БАГ ВНЕСЁН ЗДЕСЬ df[title_col] = np.where(df[title_col].isin(replace_dict.keys()), df[title_col].map(replace_dict), df[title_col]) return df

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

$ pytest --pyargs tests/data_prep/test_categorical.py ============================= test session starts ============================== platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 rootdir: /Users/eugene/projects/python-collab-template plugins: cov-2.10.0 collected 4 items tests/data_prep/test_categorical.py ...F [100%] =================================== FAILURES =================================== _____________________ test_extract_title_with_replacement ______________________ def test_extract_title_with_replacement(lowercased_df): title_replacement = {'mme': 'mrs', 'ms': 'miss', 'lady': 'rare', 'major': 'rare'} result = extract_title(lowercased_df, col='string', replace_dict=title_replacement) > assert result['title'].tolist() == ['mrs', 'miss', 'mr', 'rare', 'rare'] E AssertionError: assert ['mme', 'ms',...ady', 'major'] == ['mrs', 'miss...rare', 'rare'] E At index 0 diff: 'mme' != 'mrs' E Use -v to get the full diff tests/data_prep/test_categorical.py:50: AssertionError =========================== short test summary info ============================ FAILED tests/data_prep/test_categorical.py::test_extract_title_with_replacement

Таким образом мы узнаём, когда какие-либо изменения что-то ломают или вызывают регрессию.

Проверка покрытия: насколько полны наши тесты?

Теперь, когда у нас есть тесты, можно проверить, насколько они полны (то есть покрытие кода). Какие части кода были выполнены (через юнит-тесты), а какие пропущены? Для этого мы используем Coverage.py и устанавливаем его вместе с pytest-cov, который интегрирует Coverage.py с pytest.

Измерить покрытие так же просто, как добавить опцию --cov.

$ pytest --cov=src ============================= test session starts ============================== platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 rootdir: /Users/eugene/projects/python-collab-template plugins: cov-2.10.0 collected 9 items tests/data_prep/test_categorical.py .... [ 44%] tests/data_prep/test_continuous.py ..... [100%] ---------- coverage: platform darwin, python 3.8.2-final-0 ----------- Name Stmts Miss Cover -------------------------------------------------- src/__init__.py 0 0 100% src/data_prep/__init__.py 0 0 100% src/data_prep/categorical.py 12 0 100% src/data_prep/continuous.py 11 0 100% -------------------------------------------------- TOTAL 23 0 100% ============================== 9 passed in 0.49s ===============================

В примере выше покрытие кода составляет 100%. Что произойдёт, если оно не 100%? Для демонстрации удалим test_extract_title_with_replacement и снова запустим pytest. Строки кода без покрытия можно определить с помощью --cov-report=term-missing.

pytest --cov=src --cov-report=term-missing ============================= test session starts ============================== platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 rootdir: /Users/eugene/projects/python-collab-template plugins: cov-2.10.0 collected 8 items tests/data_prep/test_categorical.py ... [ 37%] tests/data_prep/test_continuous.py ..... [100%] ---------- coverage: platform darwin, python 3.8.2-final-0 ----------- Name Stmts Miss Cover Missing ------------------------------------------------------------ src/__init__.py 0 0 100% src/data_prep/__init__.py 0 0 100% src/data_prep/categorical.py 12 1 92% 50 src/data_prep/continuous.py 11 0 100% ------------------------------------------------------------ TOTAL 23 1 96% ============================== 8 passed in 0.44s ===============================

Здесь покрытие data_prep.categorical составляет 92%, причём строка 50 не выполняется нашими юнит-тестами. Восстановление test_extract_title_with_replacement исправляет это.

Линтинг для обеспечения единообразия (между проектами)

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

В этой статье мы используем pylint. Популярная альтернатива — flake8. В чём разница? Если коротко, pylint считается надмножеством flake8 и может выдавать ложные срабатывания.

Также обратите внимание на недавно выпущенный GitHub Super Linter.

Вот результат запуска pylint на нашем (изначально непоследовательном) коде. Он указывает, в каких строках есть проблемы, и даёт рекомендации по их исправлению. Мы также получаем сводку ошибок линтинга и оценку (4.17/10 😢).

$ pylint src.data_prep.categorical --reports=y ************* Module src.data_prep.categorical src/data_prep/categorical.py:20:0: C0330: Wrong continued indentation (add 9 spaces). df[title_col].map(replace_dict), ^ | (bad-continuation) src/data_prep/categorical.py:21:0: C0330: Wrong continued indentation (add 9 spaces). df[title_col]) ^ | (bad-continuation) src/data_prep/categorical.py:16:12: W1401: Anomalous backslash in string: '\.'. String constant might be missing an r prefix. (anomalous-backslash-in-string) src/data_prep/categorical.py:1:0: C0114: Missing module docstring (missing-module-docstring) src/data_prep/categorical.py:5:0: C0116: Missing function or method docstring (missing-function-docstring) src/data_prep/categorical.py:9:0: C0116: Missing function or method docstring (missing-function-docstring) src/data_prep/categorical.py:14:0: C0116: Missing function or method docstring (missing-function-docstring) Report ====== 12 statements analysed. ... Messages -------- +------------------------------+------------+ |message id |occurrences | +==============================+============+ |missing-function-docstring |3 | +------------------------------+------------+ |bad-continuation |2 | +------------------------------+------------+ |missing-module-docstring |1 | +------------------------------+------------+ |anomalous-backslash-in-string |1 | +------------------------------+------------+ ----------------------------------- Your code has been rated at 4.17/10

Приведём код в порядок и запустим pylint снова.

pylint src.data_prep.categorical --reports=y Report ====== ... ------------------------------------------------------------------- Your code has been rated at 10.00/10 (previous run: 4.17/10, +5.83)

Выглядит гораздо лучше (10/10).

Проверка типов для предотвращения ошибок

Кто здесь любит аннотации типов? 🙋🏻‍♂️

С тех пор как я начал работать со Scala (и Spark), я полюбил указывать типы. Я был в восторге, когда модуль typing появился в Python 3.5. Указание типов даёт несколько преимуществ:

Информативность: пользователи знают, что передавать на вход и что ожидать на выходе Обработка ошибок: если передан неправильный тип, код выдаст предупреждение Простота поддержки и отладки

Среда выполнения Python не проверяет аннотации типов — язык динамически типизирован и проверяет типы (во время выполнения) только через утиную типизацию. Тем не менее, мы можем использовать аннотации типов и статический анализатор для проверки корректности типов в нашем коде. Для этого мы будем использовать mypy.

$ mypy src Success: no issues found in 4 source files

Примечание: некоторые компании из FAANG также выпустили собственные проверщики типов. Стоит обратить внимание на pytype (Google), pyre (Facebook) и pyright (Microsoft).

Что если типы указаны неправильно? Для демонстрации обновим fill_numeric, чтобы он заполнял пропущенные значения строкой '-1' (тип str) вместо числа -1 (тип int). mypy немедленно укажет на несовместимые типы.

$ mypy src src/data_prep/continuous.py:23: error: Incompatible types in assignment (expression has type "str", variable has type "float") Found 1 error in 1 file (checked 4 source files)

Обёртка для удобства разработчика

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

Мы воспользуемся make и обернём эти команды в Makefile. Makefile определяет набор задач и команд, необходимых для их выполнения. Цель — предоставить простой интерфейс и стимулировать использование наших проверок. Вот как выглядит задача test в Makefile — она запускает команду pytest с параметрами.

test: . .venv/bin/activate && py.test tests --cov=src --cov-report=term-missing --cov-fail-under 95

И как это запустить.

$ make test . .venv/bin/activate && py.test tests --cov=src --cov-report=term-missing --cov-fail-under 95 ============================= test session starts ============================== platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 rootdir: /Users/eugene/projects/python-collab-template plugins: cov-2.10.0 collected 8 items tests/data_prep/test_categorical.py ... [ 37%] tests/data_prep/test_continuous.py ..... [100%] ---------- coverage: platform darwin, python 3.8.2-final-0 ----------- Name Stmts Miss Cover Missing ------------------------------------------------------------ src/__init__.py 0 0 100% src/data_prep/__init__.py 0 0 100% src/data_prep/categorical.py 12 0 100% src/data_prep/continuous.py 11 0 100% ------------------------------------------------------------ TOTAL 23 0 100% Required test coverage of 95% reached. Total coverage: 100.00% ============================== 8 passed in 1.05s ===============================

Таким образом, вместо активации окружения, запоминания параметров и ввода длинной команды — или копирования — можно просто набрать make test.

Перед запуском тестов также полезно очистить существующие файлы pyc и отчёт о покрытии. Их тоже можно добавить в Makefile и связать с задачей test следующим образом.

clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + find . -name '__pycache__' -exec rm -fr {} + clean-test: rm -f .coverage rm -f .coverage.* clean: clean-pyc clean-test test: clean . .venv/bin/activate && py.test tests --cov=src --cov-report=term-missing --cov-fail-under 95

Затем можно создать задачу make check для запуска полного набора проверок. Вот как это выглядит в Makefile. (Полный Makefile смотрите в репозитории.)

checks: test lint mypy

Автоматизация проверок при каждом git push (и pull request)

Код-ревью занимает много времени. Мы можем снизить нагрузку на ревьюеров, убедившись, что код работает — запуская проверки перед каждым pull request. Ревью ошибочного pull request — пустая трата времени; такие ошибки можно и нужно минимизировать с помощью автоматических проверок.

Кроме того, хотя Makefile упрощает запуск проверок локально, иногда что-то проскальзывает. Мы можем торопиться исправить критический баг и забыть запустить проверки перед созданием и мержем хотфикса, тем самым создав ещё больше багов.

«Чем раньше вы обнаруживаете дефекты, тем дешевле их исправлять.» — David Farley

Поэтому мы будем автоматически запускать проверки перед каждым pull request (при каждом событии push). Это можно сделать с помощью GitHub Actions, добавив файл tests.yml в директорию .github/workflows.

# .github/workflows/tests.yml name: Tests on: push jobs: tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v1 with: python-version: 3.8 architecture: x64 - run: make setup-venv - run: make checks

При каждом push рабочий процесс tests будет запускаться на последнем образе ubuntu с Python версии 3.8. Сначала он настраивает окружение (make setup), устанавливая необходимые пакеты, а затем запускает проверки (make check).

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

Мы экономим время, не проводя ревью ошибочных pull request.

После обновления pull request с исправлением бага GitHub сообщает, что все проверки пройдены — теперь можно проводить ревью. По грубым оценкам, это сэкономило моим предыдущим командам около 50–60% усилий на код-ревью. Да, мы вносили (и, к счастью, предотвращали) ошибки с каждым PR.

Теперь можно приступить к код-ревью.

Этот pull request можно посмотреть здесь.

Поддержание ветки master без ошибок даёт нам этот блестящий бейдж.

Мы можем подключить сервис отчётов о покрытии, чтобы покрытие кода было более наглядным. Для этого используем codecov.

Зарегистрируйте аккаунт на Codecov и добавьте туда свой репозиторий. Затем следуйте инструкциям и загрузите CODECOV_TOKEN как секрет в ваш репозиторий. Также добавим последнюю строку в наш файл tests.yml, чтобы он выглядел так:

# .github/workflows/tests.yml name: Tests on: push jobs: tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v1 with: python-version: 3.8 architecture: x64 - run: make setup - run: make check - run: bash <(curl -s https://codecov.io/bash)

Это позволяет добавить бейдж codecov.

Применяйте эти практики и извлекайте выгоду

Спасибо, что дочитали до этого места. Мы рассмотрели основы настройки Python и управления пакетами, юнит-тестирование, покрытие кода, линтинг, проверку типов, Makefile и автоматический запуск проверок при каждом push (и pull request).

Репозиторий с кодом к этой статье доступен здесь 🌟.

Эти практики обеспечивают качество кода, уменьшают количество багов и экономят усилия. Начните с форка репозитория, обновите код (намеренно с багами) и создайте pull request. Или клонируйте репозиторий и попробуйте что-нибудь сломать. Расскажите, как всё прошло, в комментариях ниже.

P.S. Была ли эта статья полезной для вас? Если да, пожалуйста, поделитесь этим твитом. 👇

Дополнительное чтение

Благодарности Yang Xinyi, Stew Fortier и Joel Christiansen за чтение черновиков этой статьи.

P.S. Многому из этого я научился у Zhao Chuxin. Всё хорошее — его заслуга; ошибки — только мои.

Если вы нашли это полезным, пожалуйста, цитируйте эту статью следующим образом:

Yan, Ziyou. (Jun 2020). How to Set Up a Python Project For Automation and Collaboration. eugeneyan.com. https://eugeneyan.com/writing/setting-up-python-project-for-automation-and-collaboration/.

или

@article{yan2020python, title = {How to Set Up a Python Project For Automation and Collaboration}, author = {Yan, Ziyou}, journal = {eugeneyan.com}, year = {2020}, month = {Jun}, url = {https://eugeneyan.com/writing/setting-up-python-project-for-automation-and-collaboration/} }



Присоединяйтесь к 11 800+ читателям, получающим обновления о машинном обучении, RecSys, LLM и инженерии.