Skip to content

Commit

Permalink
Add pre-commit hook
Browse files Browse the repository at this point in the history
This pre-commit hook checks the migration files in the current
repository and ensures that they all have a `safe` property
defined. While we can think of easy ways to be break this, for
all the use-cases I've had this covers it quite nicely, so I
think it's worth merging even if we think it's a bit rough.

Tim Schilling wrote this in
#39
and I'm very happy to take that almost verbatim and prove it.
The only thing I changed was to make a new function to handle
sys.exit(), so that I could set it as a console_script to use
with pre-commit.
  • Loading branch information
ryanhiebert committed Sep 12, 2023
1 parent e4e5300 commit 714b06e
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ db.sqlite3
htmlcov/
dist/
.tox/
.venv/
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ repos:
rev: v0.12.1
hooks:
- id: validate-pyproject
- repo: .
rev: HEAD
hooks:
- id: check
6 changes: 6 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- id: check
name: Mark Migrations for Safety
description: Ensure that all local migrations have been marked for safety.
entry: safemigrate-check
language: python
types: [text]
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ django = ">=3.2,<5.0"

[tool.poetry.dev-dependencies]
tox = "*"

[tool.poetry.plugins."console_scripts"]
"safemigrate-check" = "django_safemigrate.check:check"
69 changes: 69 additions & 0 deletions src/django_safemigrate/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
Search our application's migrations are using django_safemigrate.
This is fairly rudimentary and won't work if the class doesn't explicitly inherit
from ``Migration``.
"""

import os
import re
import sys

MIGRATION_PATTERN = re.compile(r"class\s+(?P<MigrationClass>\w+)\s?\(.*Migration\):")
MIGRATION_FILE_PATTERN = re.compile(r"^\d{4}_.+\.py$")

MISSING_SAFE_MESSAGE = (
"{file_path}: {migration_class} is missing the 'safe' attribute.\n"
)
FAILURE_MESSAGE = (
"\n"
"Add the following to the migration class:\n"
"\n"
"from django_safemigrate import Safe\n"
"class Migration(migrations.Migration):\n"
" safe = Safe.before_deploy\n"
"\n"
"You can also use the following:\n"
" safe = Safe.always\n"
" safe = Safe.after_deploy\n"
)


def find_migration_files(directory):
for root, _, files in os.walk(directory):
for file in files:
file_is_migration = MIGRATION_FILE_PATTERN.match(file)
if file_is_migration and file.endswith(".py"):
yield os.path.join(root, file)


def validate_migrations():
success = True
for file_path in find_migration_files("."):
# Strip the leading ./ it's a byproduct of os.walk on the current directory
file_path = file_path[2:] if file_path.startswith("./") else file_path
with open(file_path, "r") as f:
content = f.read()

match = MIGRATION_PATTERN.search(content)
if match:
migration_class = match.group("MigrationClass")
if "safe = Safe." not in content:
success = False
sys.stdout.write(
MISSING_SAFE_MESSAGE.format(
file_path=file_path, migration_class=migration_class
)
)
if not success:
sys.stdout.write(FAILURE_MESSAGE)
return success


def check():
if not validate_migrations():
sys.exit(1)


if __name__ == "__main__":
check()

0 comments on commit 714b06e

Please sign in to comment.