Доброго дня!
Меня зовут Соболев Андрей и сегодня я вам расскажу как мы приготовили .pre-commit hook на нашем проекте.
Для начала пару слов, о том что такое в целом хуки (hooks) и для чего они могут быть нужны. Git «из коробки» предоставляет инструмент, который умеет запускать ваши скрипты при наступлении какого либо события (к примеру пуш на сервер и т.п.)
.pre-commit это удобная надстройка над дефолтным git pre-commit hook, которая запускает скрипты описанные в .pre-commit-config.yaml перед созданием коммита. В теории звучит просто, перейдем к практике.
Установим необходимые зависимости:
Выскажу свое мнение по поводу flake-8 и линтеров в целом. Если у вас уже большой проект с кучей legacy кода, то можете линтеры смело удалять. Затраты которые будут потрачены на «приведение к идеалу», начальство не оценит. Линтеры ставим для новых (и небольших) проектов. Повторюсь, это лично мое мнение, никому его не навязываю.
Заходим в корневой каталог среды разработки и выполняем следующие команды
В корневом каталоге среды создаем файл .pre-commit-config.yaml
Помимо проверки и форматирования кода мы будем выполнять тесты на этапе создания коммита. Для этого мы будем использовать pytest (https://docs.pytest.org/en/latest/) и настроим его для наших нужд.
В корневом каталоге нашей среды создадим файл pytest.ini
Далее создадим папку tests и поместим туда следующие файлы
test_example_without_db.py, test_example_with_db.py
Для удобства настроим тесты таким образом, чтобы можно было использовать текущую базу данных (к примеру копию базы данных с боевого сервера), а не создавать каждый раз новую.
Простой тест test_example_without_db.py
В простых тестах мы можем подключить например webbot и обходить узлы нашей системы, чтобы автоматизировать ручной труд тестировщика.
Тест с использованием базы данных test_example_with_db.py
Пример довольно искусственный и создан исключительно для данной заметки, но тем не менее он позволяет нам обратиться к текущей базе данных, и провести сложные тесты, которые выходят за рамки ручного тестирования.
Чтобы подключить тесты нам потребуется shell script в корневом каталоге среды, который мы назовем tests.sh:
Его содержание весьма очевидное, но вы можете заметить что активация виртуального окружения явным образом прописана в коде. Это может быть неудобно, если ваша команда ведет разработку на разных рабочих станциях (к примеру кто развернул среду на локальной машине, а кто-то разрабатывает на сервере).
Можно решить эту проблему через переменные в .env
Пример реализации:
github.com/Sobolev5/starlette-vue-backend/blob/master/.env.example (обратите внимание на переменную ENV_ACTIVATE)
github.com/Sobolev5/starlette-vue-backend/blob/master/tests.sh (парсим ENV_ACTIVATE и активируем среду)
Теперь осталось создать коммит и посмотреть как это работает
Коммит теперь создается в «два этапа». На первом этапе хуки выполняют форматирование кода, поэтому после их работы нам нужно просто «повторить» команды.
Получается следующая последовательность.
На этом все, спасибо за внимание.
> Полный список хуков
Меня зовут Соболев Андрей и сегодня я вам расскажу как мы приготовили .pre-commit hook на нашем проекте.
Вступление
Для начала пару слов, о том что такое в целом хуки (hooks) и для чего они могут быть нужны. Git «из коробки» предоставляет инструмент, который умеет запускать ваши скрипты при наступлении какого либо события (к примеру пуш на сервер и т.п.)
.pre-commit это удобная надстройка над дефолтным git pre-commit hook, которая запускает скрипты описанные в .pre-commit-config.yaml перед созданием коммита. В теории звучит просто, перейдем к практике.
Установка
Установим необходимые зависимости:
pre-commit
# основной пакет https://pre-commit.com/
autoflake
# для удаления неиспользуемых импортов (в нашем случае)
black
# форматируем код
pyupgrade
# приводим его к последней версии
reorder-python-imports
# делаем красивые импорты
yesqa
# удаляем неиспользуемые noqa комментарии (для линтеров)
# линтеры
flake8
flake8-annotations
flake8-annotations-coverage
flake8-bandit
flake8-broken-line
flake8-bugbear
flake8-builtins
flake8-commas
flake8-comprehensions
flake8-debugger
flake8-eradicate
flake8-executable
flake8-fixme
flake8-future-import
flake8-pyi
flake8-pytest
flake8-pytest-style
flake8-mutable
flake8-string-format
flake8-todo
flake8-unused-arguments
# тесты
pytest
pytest-django
Выскажу свое мнение по поводу flake-8 и линтеров в целом. Если у вас уже большой проект с кучей legacy кода, то можете линтеры смело удалять. Затраты которые будут потрачены на «приведение к идеалу», начальство не оценит. Линтеры ставим для новых (и небольших) проектов. Повторюсь, это лично мое мнение, никому его не навязываю.
Интеграция в среду
Заходим в корневой каталог среды разработки и выполняем следующие команды
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
$ pre-commit --version
pre-commit 2.4.0
Если .pre-commit будет ругаться на sqlite, то вам нужно будет его установить (к примеру $ yum install sqlite) и собрать python заново
Настройка файла .pre-commit-config.yaml
В корневом каталоге среды создаем файл .pre-commit-config.yaml
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: "v2.5.0"
hooks:
- id: check-merge-conflict
- id: debug-statements
- repo: local
hooks:
- id: black
name: black
entry: black
language: system
types: [python]
args: [--line-length=200, --target-version=py37]
- id: autoflake
name: autoflake
entry: autoflake
language: system
types: [python]
args: [--in-place, --remove-all-unused-imports, --remove-duplicate-keys]
# - id: flake8
# name: flake8
# entry: flake8
# language: system
# types: [python]
# args: [
# "--ignore=E203,W503,FI10,FI11,FI12,FI13,FI14,FI15,FI16,FI17,FI58,PT013",
# # black
# # E203 whitespace before ':'
# # W503 line break before binary operator
# # flake8-future-import
# # FI10 __future__ import "division" missing
# # FI11 __future__ import "absolute_import" missing
# # FI12 __future__ import "with_statement" missing
# # FI13 __future__ import "print_function" missing
# # FI14 __future__ import "unicode_literals" missing
# # FI15 __future__ import "generator_stop" missing
# # FI16 __future__ import "nested_scopes" missing
# # FI17 __future__ import "generators" missing
# # FI58 __future__ import "annotations" present
# # flake8-pytest-style
# # PT013 found incorrect import of pytest, use simple 'import pytest' instead
# "--max-line-length=110",
# "--per-file-ignores=tests/*.py:S101"
# # S101 Use of assert detected
# ]
- id: pyupgrade
name: pyupgrade
entry: pyupgrade
language: system
types: [python]
args: [--py37-plus]
- id: reorder-python-imports
name: reorder-python-imports
entry: reorder-python-imports
language: system
types: [python]
args: [--py37-plus]
- id: yesqa
name: yesqa
entry: yesqa
language: system
types: [python]
- id: tests
name: Run tests
entry: "bash tests.sh"
language: system
verbose: true
Тесты
Помимо проверки и форматирования кода мы будем выполнять тесты на этапе создания коммита. Для этого мы будем использовать pytest (https://docs.pytest.org/en/latest/) и настроим его для наших нужд.
В корневом каталоге нашей среды создадим файл pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = settings
python_files = tests.py test_*.py *_tests.py
Далее создадим папку tests и поместим туда следующие файлы
test_example_without_db.py, test_example_with_db.py
Для удобства настроим тесты таким образом, чтобы можно было использовать текущую базу данных (к примеру копию базы данных с боевого сервера), а не создавать каждый раз новую.
Простой тест test_example_without_db.py
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 4
В простых тестах мы можем подключить например webbot и обходить узлы нашей системы, чтобы автоматизировать ручной труд тестировщика.
Тест с использованием базы данных test_example_with_db.py
import pytest
import settings
from chat.models import ChatRoom
from settings import POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_PORT
@pytest.fixture(scope='session')
def django_db_setup():
settings.DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': POSTGRES_DB,
'USER': POSTGRES_USER,
'PASSWORD': POSTGRES_PASSWORD,
'HOST': POSTGRES_HOST,
'PORT': POSTGRES_PORT,
}
@pytest.fixture
def db_access_without_rollback_and_truncate(request, django_db_setup, django_db_blocker):
django_db_blocker.unblock()
request.addfinalizer(django_db_blocker.restore)
def chat():
return ChatRoom.objects.all().count()
@pytest.mark.django_db
def test_chat():
assert chat() > 0
Пример довольно искусственный и создан исключительно для данной заметки, но тем не менее он позволяет нам обратиться к текущей базе данных, и провести сложные тесты, которые выходят за рамки ручного тестирования.
Подключаем тесты в .pre-commit
Чтобы подключить тесты нам потребуется shell script в корневом каталоге среды, который мы назовем tests.sh:
source ../../python38_env/bin/activate && python -m pytest -v tests
Его содержание весьма очевидное, но вы можете заметить что активация виртуального окружения явным образом прописана в коде. Это может быть неудобно, если ваша команда ведет разработку на разных рабочих станциях (к примеру кто развернул среду на локальной машине, а кто-то разрабатывает на сервере).
Можно решить эту проблему через переменные в .env
Пример реализации:
github.com/Sobolev5/starlette-vue-backend/blob/master/.env.example (обратите внимание на переменную ENV_ACTIVATE)
github.com/Sobolev5/starlette-vue-backend/blob/master/tests.sh (парсим ENV_ACTIVATE и активируем среду)
Создаем коммит
Теперь осталось создать коммит и посмотреть как это работает
$ git add .
$ git commit -m Sobolev:TestPreCommitHook
Check for merge conflicts................................................Passed
Debug Statements (Python)................................................Passed
black....................................................................Failed
- hook id: black
- files were modified by this hook
reformatted /var/www/file.py
All done!
1 file reformatted, 2 files left unchanged.
autoflake................................................................Passed
pyupgrade................................................................Passed
reorder-python-imports...................................................Failed
- hook id: reorder-python-imports
- exit code: 1
- files were modified by this hook
Reordering imports in file.py
yesqa....................................................................Passed
Run tests................................................................Passed
- hook id: tests
- duration: 2.85s
tests/test_example_with_db.py::test_chat PASSED [ 66%]
tests/test_example_without_db.py::test_answer PASSED [100%]
Коммит теперь создается в «два этапа». На первом этапе хуки выполняют форматирование кода, поэтому после их работы нам нужно просто «повторить» команды.
Получается следующая последовательность.
$ git add .
$ git commit -m Sobolev:TestPreCommitHook
$ git add .
$ git commit -m Sobolev:TestPreCommitHook
На этом все, спасибо за внимание.
Дополнительные ссылки
> Полный список хуков
eumorozov
В серьезном проекте тесты могут выполняться несколько минут, а то и десятков минут. Если добавить их в pre-commit, то работа превращается в пытку.
Учитывая, что разработчики в принципе хейтят хуки. Сколько я не старался в своих проектах протолкнуть хотя бы идею хуков с flake8, всегда один ответ: «Мы не любим хуки, и не будем их устанавливать».
Кстати, максимальный размер строки в 200 символов — ужас-ужас. Не на всех мониторах можно будет вертикальный сплит использовать, а 3-way merge совершенно точно превратится в муку.
quarckster
Не надо устанавливать хуки на машинах разработчиков специально. Надо добавить их в CI, и вот когда пайплайны будут падать, разработчики сами установят pre-commit.
Mogost
Не установят. Увидят нотификацию об упавшем билде и сделают коммит с фиксом.