Integrando .pre-commit hook en un proyecto Django

¡Buen día!

Mi nombre es Andrey Sobolev y hoy les diré cómo preparamos .pre-commit hook en nuestro proyecto.

Introducción


Para empezar, algunas palabras sobre qué son los ganchos en general y por qué podrían ser necesarios. Git fuera de la caja proporciona una herramienta que puede ejecutar sus scripts cuando ocurre un evento (por ejemplo, empujar a un servidor, etc.).

Pre-commit es un complemento conveniente para el gancho predeterminado de pre-commit de git que ejecuta los scripts descritos en .pre-commit-config.yaml antes de comprometerse. En teoría, suena simple, pasemos a la práctica.

Instalación


Establezca las dependencias necesarias:

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

Expresaré mi opinión sobre flake-8 y linter en general. Si ya tiene un proyecto grande con un montón de código heredado, puede eliminar linters de forma segura. Los costos que se gastarán en "llevar al ideal", las autoridades no lo apreciarán. Ponemos linters para proyectos nuevos (y pequeños). Repito, esta es mi opinión personal, no se la impongo a nadie.

Integración Ambiental


Vamos al directorio raíz del entorno de desarrollo y ejecutamos los siguientes comandos

$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
$ pre-commit --version
pre-commit 2.4.0

Si .pre-commit jurará en sqlite, entonces deberá instalarlo (por ejemplo, $ yum install sqlite) y construir python nuevamente

Configurar el archivo .pre-commit-config.yaml


En el directorio raíz del entorno, cree el archivo .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


Pruebas


Además de verificar y formatear el código, realizaremos pruebas en la etapa de creación de la confirmación. Para hacer esto, usaremos pytest (https://docs.pytest.org/en/latest/) y lo configuraremos para nuestras necesidades.

En el directorio raíz del entorno, cree la carpeta de pruebas y coloque los siguientes archivos
test_example_without_db.py, test_example_with_db.py allí

por conveniencia, configure las pruebas para que pueda usar la base de datos actual (por ejemplo, una copia de la base de datos del servidor de batalla), y no cree una nueva cada vez. .

Prueba simple test_example_without_db.py

def inc(x):
    return x + 1

def test_answer():
    assert inc(3) == 4

En pruebas simples, podemos conectar, por ejemplo, webbot y omitir los nodos de nuestro sistema para automatizar el trabajo manual del probador.

Prueba usando la base de datos test_example_with_db.py

import pytest
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

El ejemplo es bastante artificial y creado exclusivamente para esta nota, pero sin embargo nos permite acceder a la base de datos actual y realizar pruebas complejas que van más allá de las pruebas manuales.

Incluimos pruebas en .pre-commit


Para conectar las pruebas, necesitamos un script de shell en el directorio raíz del entorno, al que llamaremos tests.sh:

source ../../python38_env/bin/activate && python -m pytest -v tests

Su contenido es muy obvio, pero puede notar que la activación del entorno virtual está explícitamente escrita en el código. Esto puede ser inconveniente si su equipo está desarrollando en diferentes estaciones de trabajo (por ejemplo, quién implementó el entorno en una máquina local y alguien desarrolla en un servidor).

Puede resolver este problema a través de variables en .env

Ejemplo de implementación :

github.com/Sobolev5/starlette-vue-backend/blob/master/.env.example (tenga en cuenta la variable ENV_ACTIVATE)

github.com/Sobolev5/starlette-vue-backend /blob/master/tests.sh (analizar ENV_ACTIVATE y activar el entorno)

Crear un compromiso


Ahora queda crear un commit y ver cómo funciona

$ 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%]

Una confirmación ahora se crea en dos pasos. En la primera etapa, los ganchos formatean el código, por lo que después de su trabajo solo necesitamos "repetir" los comandos.

Resulta la siguiente secuencia.

$ git add .
$ git commit -m Sobolev:TestPreCommitHook
$ git add .
$ git commit -m Sobolev:TestPreCommitHook

Eso es todo, gracias por su atención.

Enlaces de sitio


Lista completa de ganchos

All Articles