diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py index 1bdb3187..909d45f1 100644 --- a/django_migration_linter/cache.py +++ b/django_migration_linter/cache.py @@ -16,7 +16,7 @@ import os import pickle -from django_migration_linter.utils import split_path, compose_migration_path +from .utils import split_path class Cache(dict): @@ -42,8 +42,7 @@ def save(self): with open(self.filename, "wb") as f: pickle.dump(self, f, protocol=2) - def md5(self, app_name, migration): - path = compose_migration_path(self.django_folder, app_name, migration) + def md5(self, path): hash_md5 = hashlib.md5() with open(path, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): diff --git a/django_migration_linter/migration.py b/django_migration_linter/migration.py new file mode 100644 index 00000000..004f64c0 --- /dev/null +++ b/django_migration_linter/migration.py @@ -0,0 +1,12 @@ +from django_migration_linter.utils import split_migration_path + + +class Migration(object): + def __init__(self, abs_path): + self.abs_path = abs_path + self.app_name, self.name = split_migration_path(self.abs_path) + + def __lt__(self, other): + if not isinstance(other, Migration): + return NotImplemented + return self.abs_path < other.abs_path diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index a392881d..26a0ffb6 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -19,10 +19,10 @@ from subprocess import Popen, PIPE import sys -from django_migration_linter.cache import Cache -from django_migration_linter.constants import DEFAULT_CACHE_PATH, MIGRATION_FOLDER_NAME -from django_migration_linter.utils import split_migration_path -from . import utils +from .cache import Cache +from .constants import DEFAULT_CACHE_PATH, MIGRATION_FOLDER_NAME +from .migration import Migration +from .utils import is_directory, is_django_project, is_git_project, clean_bytes_to_str from .sql_analyser import analyse_sql_statements logger = logging.getLogger(__name__) @@ -31,13 +31,13 @@ class MigrationLinter(object): def __init__(self, project_path, **kwargs): # Verify correctness - if not utils.is_directory(project_path): + if not is_directory(project_path): raise ValueError( "The given path {0} does not seem to be a directory.".format( project_path ) ) - if not utils.is_django_project(project_path): + if not is_django_project(project_path): raise ValueError( ("The given path {0} does not " "seem to be a django project.").format( project_path @@ -72,11 +72,13 @@ def __init__(self, project_path, **kwargs): if not self.no_cache: self.old_cache.load() - def lint_migration(self, app_name, migration_name): + def lint_migration(self, migration): + app_name = migration.app_name + migration_name = migration.name print("({0}, {1})... ".format(app_name, migration_name), end="") self.nb_total += 1 - md5hash = self.old_cache.md5(app_name, migration_name) + md5hash = self.old_cache.md5(migration.abs_path) if md5hash in self.old_cache: if self.old_cache[md5hash]["result"] == "IGNORE": print("IGNORE (cached)") @@ -140,7 +142,7 @@ def lint_all_migrations(self, git_commit_id=None): # Lint those migrations for m in migrations: - self.lint_migration(*m) + self.lint_migration(m) if not self.no_cache: self.new_cache.save() @@ -182,9 +184,7 @@ def get_sql(self, app_name, migration_name): ) sql_statements = [] - for line in map( - utils.clean_bytes_to_str, sqlmigrate_process.stdout.readlines() - ): + for line in map(clean_bytes_to_str, sqlmigrate_process.stdout.readlines()): sql_statements.append(line) sqlmigrate_process.wait() if sqlmigrate_process.returncode != 0: @@ -203,19 +203,18 @@ def _gather_migrations_git(self, git_commit_id): ).format(self.django_path, git_commit_id) logger.info("Executing {0}".format(git_diff_command)) diff_process = Popen(git_diff_command, shell=True, stdout=PIPE, stderr=PIPE) - for line in map(utils.clean_bytes_to_str, diff_process.stdout.readlines()): + for line in map(clean_bytes_to_str, diff_process.stdout.readlines()): # Only gather lines that include added migrations if ( re.search(r"\/{0}\/.*\.py".format(MIGRATION_FOLDER_NAME), line) and "__init__" not in line ): - app_name, migration_name = split_migration_path(line) - migrations.append((app_name, migration_name)) + migrations.append(Migration(os.path.join(self.django_path, line))) diff_process.wait() if diff_process.returncode != 0: output = [] - for line in map(utils.clean_bytes_to_str, diff_process.stderr.readlines()): + for line in map(clean_bytes_to_str, diff_process.stderr.readlines()): output.append(line) logger.error("Error while git diff command:\n{}".format("".join(output))) raise Exception("Error while executing git diff command") @@ -231,8 +230,7 @@ def _gather_all_migrations(self): and file_name != "__init__.py" ): full_migration_path = os.path.join(root, file_name) - app_name, migration_name = split_migration_path(full_migration_path) - migrations.append((app_name, migration_name)) + migrations.append(Migration(full_migration_path)) return migrations def should_ignore_migration(self, app_name, migration_name): diff --git a/django_migration_linter/sql_analyser.py b/django_migration_linter/sql_analyser.py index de54f432..44b5246a 100644 --- a/django_migration_linter/sql_analyser.py +++ b/django_migration_linter/sql_analyser.py @@ -15,7 +15,7 @@ import re import logging -from django_migration_linter.operations import IGNORE_MIGRATION_SQL +from .operations import IGNORE_MIGRATION_SQL IGNORED_MIGRATION = "IGNORED_MIGRATION" diff --git a/django_migration_linter/utils.py b/django_migration_linter/utils.py index 5d34d1b7..0f50c26f 100644 --- a/django_migration_linter/utils.py +++ b/django_migration_linter/utils.py @@ -17,7 +17,7 @@ import os import sys -from django_migration_linter.constants import MIGRATION_FOLDER_NAME +from .constants import MIGRATION_FOLDER_NAME def is_django_project(path): diff --git a/tests/functional/test_cmd_line_call.py b/tests/functional/test_cmd_line_call.py index 59e28d98..055d2106 100644 --- a/tests/functional/test_cmd_line_call.py +++ b/tests/functional/test_cmd_line_call.py @@ -36,13 +36,14 @@ def test_call_linter_cmd_line_working(self): process.wait() self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) - self.assertEqual(len(lines), 5) + self.assertEqual(len(lines), 6) self.assertEqual( - sorted(lines[:3]), + sorted(lines[:4]), sorted([ "(test_app1, 0001_initial)... OK", "(test_app1, 0002_a_new_null_field)... OK", "(test_app2, 0001_foo)... OK", + "(test_app3, 0001_initial)... OK", ]) ) @@ -71,13 +72,14 @@ def test_call_linter_cmd_line_exclude_apps(self): process.wait() self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) - self.assertEqual(len(lines), 5) + self.assertEqual(len(lines), 6) self.assertEqual( - sorted(lines[:3]), + sorted(lines[:4]), sorted([ "(test_app1, 0001_initial)... OK", "(test_app1, 0002_a_new_null_field)... OK", "(test_app2, 0001_foo)... IGNORE", + "(test_app3, 0001_initial)... OK", ]) ) @@ -91,13 +93,14 @@ def test_call_linter_cmd_line_include_apps(self): process.wait() self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) - self.assertEqual(len(lines), 5) + self.assertEqual(len(lines), 6) self.assertEqual( - sorted(lines[:3]), + sorted(lines[:4]), sorted([ "(test_app1, 0001_initial)... IGNORE", "(test_app1, 0002_a_new_null_field)... IGNORE", "(test_app2, 0001_foo)... OK", + "(test_app3, 0001_initial)... IGNORE", ]) ) @@ -111,13 +114,14 @@ def test_call_linter_cmd_line_ignore_name(self): process.wait() self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) - self.assertEqual(len(lines), 5) + self.assertEqual(len(lines), 6) self.assertEqual( - sorted(lines[:3]), + sorted(lines[:4]), sorted([ "(test_app1, 0001_initial)... IGNORE", "(test_app1, 0002_a_new_null_field)... OK", "(test_app2, 0001_foo)... OK", + "(test_app3, 0001_initial)... IGNORE", ]) ) @@ -131,13 +135,14 @@ def test_call_linter_cmd_line_ignore_name_contains(self): process.wait() self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) - self.assertEqual(len(lines), 5) + self.assertEqual(len(lines), 6) self.assertEqual( - sorted(lines[:3]), + sorted(lines[:4]), sorted([ "(test_app1, 0001_initial)... IGNORE", "(test_app1, 0002_a_new_null_field)... OK", "(test_app2, 0001_foo)... IGNORE", + "(test_app3, 0001_initial)... IGNORE", ]) ) diff --git a/tests/functional/test_multiple_calls.py b/tests/functional/test_multiple_calls.py index b22f623e..e58a473a 100644 --- a/tests/functional/test_multiple_calls.py +++ b/tests/functional/test_multiple_calls.py @@ -13,8 +13,10 @@ # limitations under the License. import unittest +import os from django_migration_linter import MigrationLinter +from django_migration_linter.migration import Migration from tests import fixtures @@ -24,10 +26,10 @@ def test_multiple_linters(self): l2 = MigrationLinter(fixtures.RENAME_COLUMN_PROJECT) l3 = MigrationLinter(fixtures.CORRECT_PROJECT) - l1.lint_migration('test_app', '0002_add_new_not_null_field') - l2.lint_migration('test_app', '0002_rename_column') - l3.lint_migration('test_app1', '0001_initial') - l3.lint_migration('test_app1', '0002_a_new_null_field') + l1.lint_migration(Migration(os.path.join(fixtures.ADD_NOT_NULL_COLUMN_PROJECT, 'test_app/migrations/0002_add_new_not_null_field.py'))) + l2.lint_migration(Migration(os.path.join(fixtures.RENAME_COLUMN_PROJECT, 'test_app/migrations/0002_rename_column.py'))) + l3.lint_migration(Migration(os.path.join(fixtures.CORRECT_PROJECT, 'test_app1/migrations/0001_initial.py'))) + l3.lint_migration(Migration(os.path.join(fixtures.CORRECT_PROJECT, 'test_app1/migrations/0002_a_new_null_field.py'))) self.assertTrue(l1.has_errors) self.assertTrue(l2.has_errors) diff --git a/tests/test_project_fixtures/test_correct_project/apps/__init__.py b/tests/test_project_fixtures/test_correct_project/apps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/__init__.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/admin.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/tests/test_project_fixtures/test_correct_project/apps/test_app3/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/apps.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/apps.py new file mode 100644 index 00000000..7a1fd836 --- /dev/null +++ b/tests/test_project_fixtures/test_correct_project/apps/test_app3/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TestApp3Config(AppConfig): + name = 'test_app3' diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/migrations/0001_initial.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/migrations/0001_initial.py new file mode 100644 index 00000000..beaa7807 --- /dev/null +++ b/tests/test_project_fixtures/test_correct_project/apps/test_app3/migrations/0001_initial.py @@ -0,0 +1,21 @@ +# Generated by Django 2.1.4 on 2019-01-04 20:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='C', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('not_null_field', models.IntegerField()), + ], + ), + ] diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/migrations/__init__.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/models.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/models.py new file mode 100644 index 00000000..0124f428 --- /dev/null +++ b/tests/test_project_fixtures/test_correct_project/apps/test_app3/models.py @@ -0,0 +1,4 @@ +from django.db import models + +class C(models.Model): + not_null_field = models.IntegerField(null=False) diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/tests.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/tests/test_project_fixtures/test_correct_project/apps/test_app3/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/views.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/tests/test_project_fixtures/test_correct_project/apps/test_app3/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/tests/test_project_fixtures/test_correct_project/test_project/settings.py b/tests/test_project_fixtures/test_correct_project/test_project/settings.py index f0642cca..8145e446 100644 --- a/tests/test_project_fixtures/test_correct_project/test_project/settings.py +++ b/tests/test_project_fixtures/test_correct_project/test_project/settings.py @@ -11,10 +11,12 @@ """ import os +import sys # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(BASE_DIR, 'apps')) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ @@ -38,7 +40,8 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'test_app1', - 'test_app2' + 'test_app2', + 'apps.test_app3', ] MIDDLEWARE = [ diff --git a/tests/unit/test_linter.py b/tests/unit/test_linter.py index 5b3c4a55..25d1a659 100644 --- a/tests/unit/test_linter.py +++ b/tests/unit/test_linter.py @@ -13,8 +13,10 @@ # limitations under the License. import unittest +import os from django_migration_linter import MigrationLinter +from django_migration_linter.migration import Migration from tests import fixtures @@ -32,13 +34,16 @@ def test_has_errors(self): linter = MigrationLinter(project_path) self.assertFalse(linter.has_errors) - linter.lint_migration('test_app', '0001_create_table') + m = Migration(os.path.join(project_path, 'test_app/migrations/0001_create_table.py')) + linter.lint_migration(m) self.assertFalse(linter.has_errors) - linter.lint_migration('test_app', '0002_add_new_not_null_field') + m = Migration(os.path.join(project_path, 'test_app/migrations/0002_add_new_not_null_field.py')) + linter.lint_migration(m) self.assertTrue(linter.has_errors) - linter.lint_migration('test_app', '0001_create_table') + m = Migration(os.path.join(project_path, 'test_app/migrations/0001_create_table.py')) + linter.lint_migration(m) self.assertTrue(linter.has_errors) def test_linter_creation(self): @@ -80,12 +85,13 @@ def test_ignore_migration_full_name(self): def test_gather_all_migrations(self): linter = MigrationLinter(fixtures.CORRECT_PROJECT) migrations = linter._gather_all_migrations() - self.assertEqual(len(migrations), 3) + self.assertEqual(len(migrations), 4) self.assertEqual( - sorted(migrations), + sorted([(m.app_name, m.name) for m in migrations]), sorted([ ("test_app1", "0001_initial"), ("test_app1", "0002_a_new_null_field"), ("test_app2", "0001_foo"), + ("test_app3", "0001_initial"), ]) )