How to Set Up a Python Project For Automation and Collaboration
Статья описывает пошаговую настройку Python-проекта для автоматизации проверок и удобной командной работы. Рассматриваются pyenv для управления версиями Python, venv и pip для виртуальных окружений, pytest для юнит-тестов, Coverage.py для измерения покрытия кода, pylint для линтинга и mypy для статической проверки типов. Все команды объединяются в Makefile, чтобы запускать полный набор проверок одной командой make check. Для непрерывной интеграции используется GitHub Actions — автоматические проверки выполняются при каждом git push и pull request. По оценке автора, такой подход сэкономил его командам около 50–60% усилий на код-ревью.
How to Set Up a Python Project For Automation and Collaboration
Как настроить Python-проект для автоматизации и командной работы
[ engineering production python productivity ] · 20 мин. чтения
As your Python project gets larger in scope, it can become difficult to manage.
По мере роста вашего Python-проекта управлять им становится всё сложнее.
Как можно автоматизировать проверки (например, юнит-тесты, проверку типов, линтинг)? Как можно минимизировать накладные расходы на совместную работу (например, код-ревью, единообразие кода)? Как можно максимально улучшить опыт разработчика, добавляя минимум дополнительных шагов?
In this article, I’ll share an approach that works well for me. When we’re done, we’ll have an automated workflow of units tests, coverage reports, lint checks, and type checks that’ll catch the the majority of errors and facilitate collaboration. This workflow will run in local with a single command (make check) and in your remote repository with each git push.
В этой статье я поделюсь подходом, который хорошо работает для меня. К концу у нас будет автоматизированный рабочий процесс с юнит-тестами, отчётами о покрытии, проверками линтера и проверками типов, который будет выявлять большинство ошибок и облегчать совместную работу. Этот процесс будет запускаться локально одной командой (make check) и в удалённом репозитории при каждом git push.
git pushУстановка менеджера версий Python Настройка виртуального окружения и установка пакетов Альтернатива: Docker в качестве среды разработки Создание единообразной структуры проекта Добавление базовых методов Написание юнит-тестов — наша страховочная сетка Проверка покрытия: насколько полны наши тесты? Линтинг для обеспечения единообразия (между проектами) Проверка типов для предотвращения ошибок Обёртка для удобства разработчика Автоматизация проверок при каждом git push Применяйте эти практики и извлекайте выгоду
To keep this article short and simple, we’ll use a single–most commonly used–tool for each step. I’ll also suggest popular alternatives.
Чтобы статья оставалась краткой и понятной, мы будем использовать один — наиболее популярный — инструмент для каждого шага. Я также предложу популярные альтернативы.
Follow along with the accompanying repository here 🌟.
Следите за материалом вместе с сопроводительным репозиторием здесь 🌟.
(Note: These steps should work on a recent Mac or Linux OS. For Windows, you’ll want to enable the Linux Subsystem.)
(Примечание: эти шаги должны работать на современных Mac или Linux. Для Windows потребуется включить подсистему Linux.)
Install a Python Version Manager
Установка менеджера версий Python
To start, we’ll need a version manager for Python. This allows us to install and manage multiple versions of Python on our local machine. The de facto standard is pyenv. Here’s how would we install it via pyenv-installer.
Для начала нам понадобится менеджер версий Python. Он позволяет устанавливать и управлять несколькими версиями Python на локальной машине. Стандарт де-факто — pyenv. Вот как его установить через pyenv-installer.
# Install pyenv
curl https://pyenv.run | bash
# Restart shell (so the pyenv path changes take effect)
exec $SHELL
# Установка pyenv curl https://pyenv.run | bash # Перезапуск оболочки (чтобы изменения PATH для pyenv вступили в силу) exec $SHELL
Now, we’re ready to install and switch between python versions. (Note: pyenv inserts a directory of shims in your PATH. Read more about it here.)
Теперь мы готовы устанавливать и переключаться между версиями Python. (Примечание: pyenv добавляет директорию с shim-файлами в ваш PATH. Подробнее об этом здесь.)
# This is the system python
$ python --version
Python 2.7.16
# Install 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
# Set our local python
$ pyenv local 3.8.2
$ python --version
Python 3.8.2
# Check your different python versions
$ pyenv versions
system
3.7.7
3.8.0
* 3.8.2 (set by /Users/eugene/projects/python-collab-template/.python-version)
# Это системный 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 will be the default version for this project. I.e., when you’re in this directory, invoking python will use Python 3.8.2.
Python 3.8.2 будет версией по умолчанию для этого проекта. То есть, когда вы находитесь в этой директории, вызов python будет использовать Python 3.8.2.
As you update python versions, how do you know if you’ve installed it properly on your system? Here’s a tip: Run Python’s built-in test suite.
При обновлении версий 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 -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
In this version, Python has 423 internal tests. Just chill and enjoy the satisfaction of seeing test after test passing. 😎
В этой версии Python содержит 423 внутренних теста. Просто расслабьтесь и наслаждайтесь тем, как тесты проходят один за другим. 😎
Set Up A Virtualenv and Install Packages
Настройка виртуального окружения и установка пакетов
To manage our Python environments and install packages, we’ll use the most basic venv and pip. Both come with standard python and are easy to use, and it’s likely most beginner Pythonistas will be familiar with them.
Для управления Python-окружениями и установки пакетов мы будем использовать самые базовые инструменты — venv и pip. Оба поставляются со стандартным Python, просты в использовании, и, скорее всего, большинство начинающих Python-разработчиков уже с ними знакомы.
# Create a virtual environment
python -m venv .venv
# Activate the virtual environment
source .venv/bin/activate
# Upgrade pip
pip install --upgrade pip
# Создание виртуального окружения python -m venv .venv # Активация виртуального окружения source .venv/bin/activate # Обновление pip pip install --upgrade pip
# Create a poetry project
poetry init --no-interaction
# Add numpy as dependency
poetry add numpy
# Recreate the project based on the pyproject.toml
poetry install
# To get the path to poetry venv (for PyCharm)
poetry env info
# Создание проекта poetry poetry init --no-interaction # Добавление numpy как зависимости poetry add numpy # Пересоздание проекта на основе pyproject.toml poetry install # Получение пути к виртуальному окружению poetry (для PyCharm) poetry env info
With our (virtual) environment set up and activated, we can proceed to install python packages. To keep our production environment as light as possible, we’ll want to separate the packages needed for dev and prod:
Когда виртуальное окружение настроено и активировано, можно приступить к установке Python-пакетов. Чтобы продакшен-окружение было максимально лёгким, стоит разделить пакеты, необходимые для dev и prod:
dev: These are only used for development (e.g., testing, linting, etc.) and are not required for production.prod: These are needed in production (e.g., data processing, machine learning, etc.).dev: используются только для разработки (например, тестирование, линтинг и т. д.) и не нужны в продакшене. prod: необходимы в продакшене (например, обработка данных, машинное обучение и т. д.).
# Install dev packages which we'll use for testing, linting, type-checking etc.
pip install pytest pytest-cov pylint mypy codecov
# Freeze dev requirements
pip freeze > requirements.dev
# Install prod packages
pip install pandas
# Freeze dev requirements
pip freeze > requirements.prod
# Установка dev-пакетов, которые будут использоваться для тестирования, линтинга, проверки типов и т. д. pip install pytest pytest-cov pylint mypy codecov # Фиксация dev-зависимостей pip freeze > requirements.dev # Установка prod-пакетов pip install pandas # Фиксация prod-зависимостей pip freeze > requirements.prod
In this simple project, I cleaned up the requirements files by hand (i.e., removed the lower level dependencies that the packages we installed above require). In bigger projects that involve multiple packages and dependencies, you’ll want to use a dependency management tool (discussed below). Here’s how the current requirements looks:
В этом простом проекте я вручную почистил файлы 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
$ 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
When you have a big project with multiple packages, updating any of them could break the others. You can mitigate this with a dependency management tool. These tools should also allow you to separate dev and prod requirements easily.
Когда у вас большой проект с множеством пакетов, обновление любого из них может сломать остальные. Эту проблему можно смягчить с помощью инструмента управления зависимостями. Такие инструменты также должны позволять легко разделять зависимости для dev и prod.
I didn’t cover this is the main section as there’s still no consensus on them, but here’s a couple of options:
Я не включил это в основную часть статьи, так как единого мнения по ним пока нет, но вот несколько вариантов:
pipenv: This looked promising in 2018. But I found it buggy and slow, which reduced build velocity. Nonetheless, things seem to have improved with the updates in 2020.pip-tools: This works with a standard requirements.txt and allows for simple upgrades via pip-compile --upgrade. Seems promising.poetry: This has gotten popular recently and allows for direct building and publishing to PyPI. Version 1.0.0 was released in December 2019. It could become the de facto someday.pipenv: выглядел многообещающе в 2018 году. Но я нашёл его глючным и медленным, что снижало скорость сборки. Тем не менее, с обновлениями 2020 года ситуация, похоже, улучшилась. pip-tools: работает со стандартным requirements.txt и позволяет просто обновлять зависимости через pip-compile --upgrade. Выглядит перспективно. poetry: в последнее время набирает популярность и позволяет напрямую собирать и публиковать пакеты в PyPI. Версия 1.0.0 была выпущена в декабре 2019 года. Когда-нибудь может стать стандартом де-факто.
Some other alternatives: pyflow, dephell, and flit. My friend Yu Xuan wrote about Python Environments and its various tools. Read more about it here.
Другие альтернативы: pyflow, dephell и flit. Мой друг Yu Xuan написал о средах Python и различных инструментах. Подробнее об этом здесь.
Alternatively, use Docker as a Dev Environment instead
Альтернатива: Docker в качестве среды разработки
An alternative to versioning Python (via pyenv) and Python dependencies (via venv) is using Docker as our dev environment. This way, the team can ensure the OS environment (and not just the Python environment) is consistent. Also, some ML platforms, such as SageMaker, run training workflows and deploy via Docker containers—thus it’s good practice to develop in the same containers. This is my recommended approach now.
Альтернатива управлению версиями Python (через pyenv) и зависимостями (через venv) — использование Docker в качестве среды разработки. Таким образом команда может гарантировать единообразие среды ОС (а не только Python-окружения). Кроме того, некоторые ML-платформы, например SageMaker, запускают обучение и деплой через Docker-контейнеры — поэтому хорошей практикой является разработка в тех же контейнерах. Сейчас я рекомендую именно этот подход.
We start by creating a simple Dockerfile that builds on Python3.8, copies our requirements, and installs the dependencies.
Начнём с создания простого Dockerfile, который базируется на Python3.8, копирует наши requirements и устанавливает зависимости.
ARG BASE_IMAGE=python:3.8
FROM ${BASE_IMAGE} as base
LABEL maintainer='eugeneyan <[email protected]>'
# Use the opt directory as our dev directory
WORKDIR /opt
ENV PYTHONUNBUFFERED TRUE
COPY requirements.dev .
COPY requirements.prod .
# Install python dependencies
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
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
To build our environment, we simply run:
Чтобы собрать наше окружение, просто выполняем:
DOCKER_BUILDKIT=1 docker build -t dev -f Dockerfile .
DOCKER_BUILDKIT=1 docker build -t dev -f Dockerfile .
Then, we develop on via an IDE on our local machine as usual. And when we want to run tests on our Docker container, we can spin it up as follows. This command mounts our current directory to the /opt dir on our docker container and runs the bash command.
Затем мы работаем через IDE на локальной машине как обычно. А когда нужно запустить тесты в Docker-контейнере, поднимаем его следующим образом. Эта команда монтирует текущую директорию в директорию /opt внутри Docker-контейнера и выполняет команду bash.
docker run --rm -it --name run-checks -v $(pwd):/opt -t dev bash
docker run --rm -it --name run-checks -v $(pwd):/opt -t dev bash
Set Up a Consistent Project Structure
Создание единообразной структуры проекта
Not much to say here, except we’ll have the standard src and tests directory. The tests directory should mirror the src directory.
Тут немного что сказать — у нас будут стандартные директории src и tests. Директория tests должна повторять структуру директории src.
├── requirements.dev
├── requirements.prod
├── src
│ ├── __init__.py
│ └── data_prep
└── tests
├── __init__.py
└── data_prep
├── requirements.dev ├── requirements.prod ├── src │ ├── __init__.py │ └── data_prep └── tests ├── __init__.py └── data_prep
Add Some Basic Methods
Добавление базовых методов
Before we proceed with the next steps, we’ll first need some code to test.
Прежде чем двигаться дальше, нам понадобится код для тестирования.
For this article, I’ve written some basic methods to process sample data from the Titanic data set. (They say you should never write about the Titanic or MNIST datasets, but you understand the focus of this article is not about the data, right?)
Для этой статьи я написал несколько базовых методов для обработки данных из набора данных Titanic. (Говорят, никогда не стоит писать о датасетах Titanic или MNIST, но вы же понимаете, что фокус этой статьи не на данных, верно?)
We have a categorical module to process names and extract titles (e.g., Mr, Mrs, Miss), and a continuous module to fill missing values (specifically, age). Here’s how the extract_title method looks like. The rest of src is here.
У нас есть модуль 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:
"""Extracts titles into a new title column
Args:
df: DataFrame to extract titles from
col: Column in DataFrame to extract titles from
replace_dict (Optional): Optional dictionary to map titles
title_col: Name of new column containing extracted titles
Returns:
A DataFrame with an additional column of extracted titles
"""
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
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
Write Some Unit Tests; They’re Our Safety Harness
Пишем юнит-тесты — наша страховочная сетка
Unit tests verify the functionality of our methods. This is important to ensure that updates don’t break anything, especially in data science where data transformation and feature engineering errors usually fail silently. Thus, we’ll include some mock data in our tests.
Юнит-тесты проверяют работоспособность наших методов. Это важно для того, чтобы обновления ничего не ломали, особенно в data science, где ошибки в преобразовании данных и feature engineering обычно проходят незаметно. Поэтому мы включим тестовые данные (mock data) в наши тесты.
“Tests are stories we tell the next generation of programmers on a project.” – Roy Osherove
«Тесты — это истории, которые мы рассказываем следующему поколению программистов проекта.» — Roy Osherove
The de facto for unit tests is pytest. There’s also unittest which comes with python (though most people just use pytest).
Стандарт де-факто для юнит-тестов — pytest. Также есть unittest, который поставляется с Python (хотя большинство просто используют pytest).
To test our extract_title method, we’ll want to create a sample DataFrame for testing. Here’s how it would look like:
Чтобы протестировать наш метод 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']
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']
Running the test is then as simple as:
Запуск теста — это просто:
$ 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 ===============================
$ 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 ===============================
You’ll notice that the extract_title method lets you pass in an optional dictionary to map titles. For example, we might want to map 'ms' to 'miss', and 'lady' and 'major' to 'rare'. We’ll want to test this too.
Вы заметите, что метод extract_title позволяет передать необязательный словарь для замены титулов. Например, мы можем захотеть заменить 'ms' на 'miss', а 'lady' и 'major' на 'rare'. Это тоже нужно протестировать.
Take Advantage of Fixtures for Reuse
Используйте фикстуры для повторного использования
Instead of copy-pasting the code for creating that DataFrame, we can make it a fixture for reuse. This is easily done with the pytest.fixture decorator. Now, we can use that lowercase_df across our tests and stay DRY.
Вместо копирования кода создания 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.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']
Let’s run the tests again. Perfect. 👍
Запустим тесты снова. Отлично. 👍
$ 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 ===============================
$ 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 ===============================
Test for Exceptions Too
Тестируйте и исключения тоже
In the fill_numeric method, we allow users to select the method to fill missing values, such as mean, median, etc. If the user selects an incorrect method, it should return a NotImplementedError. We can test for this in pytest too.
В методе 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')
def test_fill_numeric_not_implemented(dummy_df): with pytest.raises(NotImplementedError): fill_numeric(dummy_df, col='int', fill_type='random')
To better appreciate the value of unit tests, we’ll deliberately introduce a bug where we check if replace_dict is True, instead of whether a dictionary was passed in.
Чтобы лучше оценить ценность юнит-тестов, намеренно внесём баг: будем проверять, равен ли 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: # BUG INTRODUCED HERE
df[title_col] = np.where(df[title_col].isin(replace_dict.keys()),
df[title_col].map(replace_dict),
df[title_col])
return df
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
Running our tests again will immediately flag this bug, raise an error, and prevent the build from progressing.
Повторный запуск тестов немедленно обнаружит этот баг, выдаст ошибку и не позволит сборке продолжиться.
$ 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
$ 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
This way, we know when any changes break something or cause a regression.
Таким образом мы узнаём, когда какие-либо изменения что-то ломают или вызывают регрессию.
Check for Coverage; How Extensive Are Our Tests?
Проверка покрытия: насколько полны наши тесты?
Now that we have tests, we can check how comprehensive they are (i.e., code coverage). Which parts of our code were executed (via unit testing) and which were skipped? We’ll use Coverage.py for this and install it with pytest-cov which integrates Coverage.py with pytest.
Теперь, когда у нас есть тесты, можно проверить, насколько они полны (то есть покрытие кода). Какие части кода были выполнены (через юнит-тесты), а какие пропущены? Для этого мы используем Coverage.py и устанавливаем его вместе с pytest-cov, который интегрирует Coverage.py с pytest.
Measuring coverage is as simple as adding the --cov option.
Измерить покрытие так же просто, как добавить опцию --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 ===============================
$ 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 ===============================
In the example above, we have 100% code coverage. What happens when it’s not 100%? To do this, we’ll remove test_extract_title_with_replacement and run pytest again. We can identify the lines of code without coverage with --cov-report=term-missing.
В примере выше покрытие кода составляет 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 ===============================
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 ===============================
Here, we have 92% coverage on data_prep.categorical where line 50 is not executed by our unit tests. Restoring test_extract_title_with_replacement fixes this.
Здесь покрытие data_prep.categorical составляет 92%, причём строка 50 не выполняется нашими юнит-тестами. Восстановление test_extract_title_with_replacement исправляет это.
Lint to Ensure Consistency (Across Projects)
Линтинг для обеспечения единообразия (между проектами)
It can be difficult to ensure that code consistency within a project, not to mention across projects. Sloppy, inconsistent code makes it difficult to work on different projects; sometimes, they lead to bugs. Here’s where a linter can help. Linters analyze code to flag proramming errors, bugs, and deviations from standards.
Обеспечить единообразие кода внутри одного проекта бывает непросто, не говоря уже о нескольких проектах. Небрежный, непоследовательный код затрудняет работу над разными проектами, а иногда приводит к багам. Здесь на помощь приходит линтер. Линтеры анализируют код, выявляя ошибки программирования, баги и отклонения от стандартов.
In this article, we use pylint. A common alternative is flake8. What’s the difference? In a nutshell, pylint is seen as a superset of flake8, and can have false alarms.
В этой статье мы используем pylint. Популярная альтернатива — flake8. В чём разница? Если коротко, pylint считается надмножеством flake8 и может выдавать ложные срабатывания.
Also check out the recently released GitHub Super Linter.
Также обратите внимание на недавно выпущенный GitHub Super Linter.
Here’s running pylint on our (initially inconsistent) code. It calls out which lines have issues and includes suggestions on how to fix them. We also get a summary of the linting errors and a score (4.17/10 😢).
Вот результат запуска 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 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
Let’s tidy our code and run pylint again.
Приведём код в порядок и запустим 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)
pylint src.data_prep.categorical --reports=y Report ====== ... ------------------------------------------------------------------- Your code has been rated at 10.00/10 (previous run: 4.17/10, +5.83)
Looks much better now (10/10).
Выглядит гораздо лучше (10/10).
Check For Type Errors to Prevent Them
Проверка типов для предотвращения ошибок
Who here loves type annotations? 🙋🏻♂️
Кто здесь любит аннотации типов? 🙋🏻♂️
Since I started working with Scala (and Spark), I’ve grown to love specifying types. I was elated when typing was introduced in Python 3.5. There’re several benefits to specifying types, including:
С тех пор как я начал работать со Scala (и Spark), я полюбил указывать типы. Я был в восторге, когда модуль typing появился в Python 3.5. Указание типов даёт несколько преимуществ:
Информативность: пользователи знают, что передавать на вход и что ожидать на выходе Обработка ошибок: если передан неправильный тип, код выдаст предупреждение Простота поддержки и отладки
The Python runtime does not enforce type annotations; it’s dynamically typed and only verifies types (at runtime) via duck typing. Nonetheless, we can use type annotations and a static type checker to verify the type correctness of our code. We’ll use mypy for this.
Среда выполнения Python не проверяет аннотации типов — язык динамически типизирован и проверяет типы (во время выполнения) только через утиную типизацию. Тем не менее, мы можем использовать аннотации типов и статический анализатор для проверки корректности типов в нашем коде. Для этого мы будем использовать mypy.
$ mypy src
Success: no issues found in 4 source files
$ mypy src Success: no issues found in 4 source files
Note: Some of the FAANG companies have also released their own type checkers. You might want to check out pytype (Google), pyre (Facebook), and pyright (Microsoft).
Примечание: некоторые компании из FAANG также выпустили собственные проверщики типов. Стоит обратить внимание на pytype (Google), pyre (Facebook) и pyright (Microsoft).
What if the types are incorrect? To demonstrate, we’ll update fill_numeric to fill missing values with '-1' (str type) instead of -1 (int type). mypy immediately flags the incompatible types.
Что если типы указаны неправильно? Для демонстрации обновим 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)
$ 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)
Build a Wrapper For Developer Experience
Обёртка для удобства разработчика
Now we have the big pieces set up: unit tests, coverage reports, linting, type checking. As your project grows, the workflow will include more checks; it’ll be tedious to type out all these commands. The harder it is to use, the fewer users we’ll have.
Теперь у нас есть основные элементы: юнит-тесты, отчёты о покрытии, линтинг, проверка типов. По мере роста проекта рабочий процесс будет включать всё больше проверок, и набирать все эти команды вручную станет утомительно. Чем сложнее использование, тем меньше людей будут этим пользоваться.
We’ll rely on make and wrap these commands in a Makefile. The Makefile defines a set of tasks and the commands needed to execute them. The intent is to provide a simple interface and encourage usage of our checks. Here’s what in the Makefile for test; it runs the pytest command with the params.
Мы воспользуемся make и обернём эти команды в Makefile. Makefile определяет набор задач и команд, необходимых для их выполнения. Цель — предоставить простой интерфейс и стимулировать использование наших проверок. Вот как выглядит задача test в Makefile — она запускает команду pytest с параметрами.
test:
. .venv/bin/activate && py.test tests --cov=src --cov-report=term-missing --cov-fail-under 95
test: . .venv/bin/activate && py.test tests --cov=src --cov-report=term-missing --cov-fail-under 95
And how to run it.
И как это запустить.
$ 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 . .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 ===============================
So instead of activating the environment, remembering the params, and typing out that long command, or copy-pasting, we can just do make test.
Таким образом, вместо активации окружения, запоминания параметров и ввода длинной команды — или копирования — можно просто набрать make test.
Before running our tests, it’s also a good idea to clean up the existing pyc files and coverage report. We can add them to the Makefile too and chain it with test, like so.
Перед запуском тестов также полезно очистить существующие файлы 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
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
Then, we can have a make check task to run the full suite of checks. Here’s how it looks like in the Makefile. (For the full Makefile, check out the repo).
Затем можно создать задачу make check для запуска полного набора проверок. Вот как это выглядит в Makefile. (Полный Makefile смотрите в репозитории.)
checks: test lint mypy
checks: test lint mypy
Automate Checks with each git push (and pull request)
Автоматизация проверок при каждом git push (и pull request)
Code reviews take a lot of time. We can reduce the burden on reviewers by ensuring that our code is working by running the checks before each pull request. Reviewing an erroneous pull request is a waste of time; these errors can be—and should be—minimised with automated checks.
Код-ревью занимает много времени. Мы можем снизить нагрузку на ревьюеров, убедившись, что код работает — запуская проверки перед каждым pull request. Ревью ошибочного pull request — пустая трата времени; такие ошибки можно и нужно минимизировать с помощью автоматических проверок.
Furthermore, while the Makefile makes (no pun intended) it easy to run the checks on local, sometimes, things slip through. We might be in a rush to squash a critical bug and forget to run the checks before creating and merging a hotfix, thus creating more bugs.
Кроме того, хотя Makefile упрощает запуск проверок локально, иногда что-то проскальзывает. Мы можем торопиться исправить критический баг и забыть запустить проверки перед созданием и мержем хотфикса, тем самым создав ещё больше багов.
“The earlier you catch defects, the cheaper they are to fix.” ― David Farley
«Чем раньше вы обнаруживаете дефекты, тем дешевле их исправлять.» — David Farley
Thus, we’ll run our checks automatically before each pull request (with each push event). We can do this with GitHub Actions by adding tests.yml in our .github/workflows directory.
Поэтому мы будем автоматически запускать проверки перед каждым 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
# .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
With each push, the tests workflow will run on the latest ubuntu image with Python version 3.8. It first sets up the environment (make setup) by installing the necessary packages, before running the checks (make check).
При каждом push рабочий процесс tests будет запускаться на последнем образе ubuntu с Python версии 3.8. Сначала он настраивает окружение (make setup), устанавливая необходимые пакеты, а затем запускает проверки (make check).
Here’s what happens when a pull request introduces a bug. The workflow runs and reports that a check has failed. This should prompt us to fix the issues, and saves effort from unnecessary code reviews.
Вот что происходит, когда pull request вносит баг. Рабочий процесс запускается и сообщает, что одна из проверок не пройдена. Это должно побудить нас исправить проблемы и избавляет от ненужных код-ревью.
We can save time by not reviewing erroneous pull requests.
Мы экономим время, не проводя ревью ошибочных pull request.
After the pull request is updated to fix the bug, GitHub reports all checks passing; now we can review it. As a rough estimate, this has saved my previous teams about 50-60% of code review effort. Yea, we introduced (and thankfully prevented) errors with each PR.
После обновления pull request с исправлением бага GitHub сообщает, что все проверки пройдены — теперь можно проводить ревью. По грубым оценкам, это сэкономило моим предыдущим командам около 50–60% усилий на код-ревью. Да, мы вносили (и, к счастью, предотвращали) ошибки с каждым PR.
Now we can begin the code review.
Теперь можно приступить к код-ревью.
Keeping the master branch error-free earns us this shiny badge.
Поддержание ветки master без ошибок даёт нам этот блестящий бейдж.
We can add a coverage reporting service to make code coverage more visible. For this, we'll use codecov.
Мы можем подключить сервис отчётов о покрытии, чтобы покрытие кода было более наглядным. Для этого используем codecov.
Set up an account at Codecov and add your repository to it. Then, follow the steps and upload your CODECOV_TOKEN as a secret in your repo. We'll also add to the last line of our tests.yml file, so it looks like this:
Зарегистрируйте аккаунт на 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)
# .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)
This allows us to add the codecov badge.
Это позволяет добавить бейдж codecov.
Apply these Practices and Profit
Применяйте эти практики и извлекайте выгоду
Thanks for sticking with me so far. We covered the basics of setting up python and package management, unit testing, code coverage, linting, type checking, makefiles, and running automated checks with each push (and pull request).
Спасибо, что дочитали до этого места. Мы рассмотрели основы настройки Python и управления пакетами, юнит-тестирование, покрытие кода, линтинг, проверку типов, Makefile и автоматический запуск проверок при каждом push (и pull request).
Check out the Github repo of the code for this write-up here 🌟.
Репозиторий с кодом к этой статье доступен здесь 🌟.
These practices ensure code quality, reduce bugs, and save effort. Get started by forking the repository, updating the code (deliberately with bugs), and making a pull request. Or clone the repository and try breaking things. Let me know how it goes in the comments below.
Эти практики обеспечивают качество кода, уменьшают количество багов и экономят усилия. Начните с форка репозитория, обновите код (намеренно с багами) и создайте pull request. Или клонируйте репозиторий и попробуйте что-нибудь сломать. Расскажите, как всё прошло, в комментариях ниже.
P.S., Was this article useful for you? If so, please share this tweet. 👇
P.S. Была ли эта статья полезной для вас? Если да, пожалуйста, поделитесь этим твитом. 👇
Further reading
Дополнительное чтение
pyenvУправление несколькими версиями Python с помощью pyenv Как использовать статическую проверку типов в Python 3.6 Hypermodern Python
Thanks to Yang Xinyi, Stew Fortier and Joel Christiansen for reading drafts of this.
Благодарности Yang Xinyi, Stew Fortier и Joel Christiansen за чтение черновиков этой статьи.
P.S., I learnt much of this from Zhao Chuxin. The good stuff is due to him; the mistakes are mine alone.
P.S. Многому из этого я научился у Zhao Chuxin. Всё хорошее — его заслуга; ошибки — только мои.
If you found this useful, please cite this write-up as:
Если вы нашли это полезным, пожалуйста, цитируйте эту статью следующим образом:
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/.
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/.
or
или
@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/}
}
@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/} }
Join 11,800+ readers getting updates on machine learning, RecSys, LLMs, and engineering.
Присоединяйтесь к 11 800+ читателям, получающим обновления о машинном обучении, RecSys, LLM и инженерии.