diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 13e6769..12b26e6 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -42,7 +42,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install dependencies - run: python -m pip install tox tox-gh-actions + run: python -m pip install tox==3.28.0 tox-gh-actions - name: Test with tox ${{ matrix.python-version }} run: tox @@ -57,7 +57,7 @@ jobs: with: python-version: '3.10' - name: Install dependencies - run: python -m pip install tox + run: python -m pip install tox==3.28.0 - name: Lint with tox run: tox env: @@ -94,7 +94,7 @@ jobs: with: python-version: '3.10' - name: Install dependencies and coverage - run: python -m pip install tox + run: python -m pip install tox==3.28.0 - name: Test with tox with coverage run: tox env: diff --git a/CHANGELOG.md b/CHANGELOG.md index b964461..fcbbd8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ The crash is a sign to double-check the migration. But if you are certain the mi - Add support for Python 3.11 - Add support for Django 4.1 - Drop support for Django 2.2 +- Internally rename "migration tests" to "migration checks" +- Add dataclasses internally instead of custom dicts +- Add type checking using `mypy` ## 4.1.0 diff --git a/django_migration_linter/__init__.py b/django_migration_linter/__init__.py index 7230d42..cd9a98e 100644 --- a/django_migration_linter/__init__.py +++ b/django_migration_linter/__init__.py @@ -1,2 +1,4 @@ +from __future__ import annotations + from .migration_linter import * # noqa from .operations import * # noqa diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py index 9bdd1db..a8dca9f 100644 --- a/django_migration_linter/cache.py +++ b/django_migration_linter/cache.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import os import pickle class Cache(dict): - def __init__(self, django_folder, database, cache_path): + def __init__(self, django_folder: str | None, database: str, cache_path: str): self.filename = os.path.join( cache_path, "{}_{}.pickle".format(str(django_folder).replace(os.sep, "_"), database), @@ -14,7 +16,7 @@ class Cache(dict): super().__init__() - def load(self): + def load(self) -> None: try: with open(self.filename, "rb") as f: tmp_dict = pickle.load(f) @@ -22,6 +24,6 @@ class Cache(dict): except OSError: pass - def save(self): + def save(self) -> None: with open(self.filename, "wb") as f: pickle.dump(self, f, protocol=2) diff --git a/django_migration_linter/constants.py b/django_migration_linter/constants.py index f0a7fbb..3983c54 100644 --- a/django_migration_linter/constants.py +++ b/django_migration_linter/constants.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from appdirs import user_cache_dir __version__ = "4.1.0" diff --git a/django_migration_linter/management/commands/lintmigrations.py b/django_migration_linter/management/commands/lintmigrations.py index b8f31eb..7317061 100644 --- a/django_migration_linter/management/commands/lintmigrations.py +++ b/django_migration_linter/management/commands/lintmigrations.py @@ -1,12 +1,15 @@ +from __future__ import annotations + import configparser import itertools import os import sys from importlib import import_module +from typing import Any, Callable import toml from django.conf import settings -from django.core.management.base import BaseCommand +from django.core.management.base import BaseCommand, CommandParser from ...constants import __version__ from ...migration_linter import MessageType, MigrationLinter @@ -28,7 +31,7 @@ DEFAULT_CONFIG_FILES = ( class Command(BaseCommand): help = "Lint your migrations" - def add_arguments(self, parser): + def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "app_label", nargs="?", @@ -184,7 +187,7 @@ class Command(BaseCommand): sys.exit(1) @staticmethod - def read_django_settings(options): + def read_django_settings(options: dict[str, Any]) -> dict[str, Any]: django_settings_options = dict() django_migration_linter_settings = getattr( @@ -197,12 +200,13 @@ class Command(BaseCommand): return django_settings_options @staticmethod - def read_config_file(options): + def read_config_file(options: dict[str, Any]) -> dict[str, Any]: config_options = dict() config_parser = configparser.ConfigParser() config_parser.read(DEFAULT_CONFIG_FILES, encoding="utf-8") for key, value in options.items(): + config_get_fn: Callable if isinstance(value, bool): config_get_fn = config_parser.getboolean else: @@ -214,7 +218,7 @@ class Command(BaseCommand): return config_options @staticmethod - def read_toml_file(options): + def read_toml_file(options: dict[str, Any]) -> dict[str, Any]: toml_options = dict() if os.path.exists(PYPROJECT_TOML): @@ -226,5 +230,5 @@ class Command(BaseCommand): return toml_options - def get_version(self): + def get_version(self) -> str: return __version__ diff --git a/django_migration_linter/management/commands/makemigrations.py b/django_migration_linter/management/commands/makemigrations.py index 970990b..b842c0d 100644 --- a/django_migration_linter/management/commands/makemigrations.py +++ b/django_migration_linter/management/commands/makemigrations.py @@ -1,9 +1,13 @@ +from __future__ import annotations + import os from django.conf import settings +from django.core.management import CommandParser from django.core.management.commands.makemigrations import ( Command as MakeMigrationsCommand, ) +from django.db.migrations import Migration from django.db.migrations.questioner import InteractiveMigrationQuestioner from django.db.migrations.writer import MigrationWriter @@ -16,7 +20,7 @@ from ..utils import ( ) -def ask_should_keep_migration(): +def ask_should_keep_migration() -> bool: questioner = InteractiveMigrationQuestioner() msg = """ The migration linter detected that this migration is not backward compatible. @@ -33,7 +37,7 @@ def default_should_keep_migration(): class Command(MakeMigrationsCommand): help = "Creates new migration(s) for apps and lints them." - def add_arguments(self, parser): + def add_arguments(self, parser: CommandParser) -> None: super().add_arguments(parser) parser.add_argument( "--lint", @@ -51,7 +55,7 @@ class Command(MakeMigrationsCommand): configure_logging(options["verbosity"]) return super().handle(*app_labels, **options) - def write_migration_files(self, changes): + def write_migration_files(self, changes: dict[str, list[Migration]]) -> None: super().write_migration_files(changes) if ( @@ -79,7 +83,7 @@ class Command(MakeMigrationsCommand): all_warnings_as_errors, ) = extract_warnings_as_errors_option(self.warnings_as_errors) - # Lint migrations + # Lint migrations. linter = MigrationLinter( path=os.environ["DJANGO_SETTINGS_MODULE"], database=self.database, @@ -103,7 +107,7 @@ class Command(MakeMigrationsCommand): self.delete_migration(migration) linter.reset_counters() - def delete_migration(self, migration): + def delete_migration(self, migration: Migration) -> None: writer = MigrationWriter(migration) os.remove(writer.path) diff --git a/django_migration_linter/management/utils.py b/django_migration_linter/management/utils.py index 49a0b7d..effc29d 100644 --- a/django_migration_linter/management/utils.py +++ b/django_migration_linter/management/utils.py @@ -1,9 +1,13 @@ +from __future__ import annotations + import logging +from django.core.management import CommandParser + from ..sql_analyser.analyser import ANALYSER_STRING_MAPPING -def register_linting_configuration_options(parser): +def register_linting_configuration_options(parser: CommandParser) -> None: parser.add_argument( "--database", type=str, @@ -37,7 +41,7 @@ def register_linting_configuration_options(parser): ) -def configure_logging(verbosity): +def configure_logging(verbosity: int) -> None: logger = logging.getLogger("django_migration_linter") if verbosity > 1: @@ -48,7 +52,9 @@ def configure_logging(verbosity): logging.basicConfig(format="%(message)s") -def extract_warnings_as_errors_option(warnings_as_errors): +def extract_warnings_as_errors_option( + warnings_as_errors: list[str] | None, +) -> tuple[list[str] | None, bool]: if isinstance(warnings_as_errors, list): warnings_as_errors_tests = warnings_as_errors # If the option is specified but without any test codes, diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index fabb997..f73636a 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import hashlib import inspect import logging @@ -5,11 +7,13 @@ import os import re from enum import Enum, unique from subprocess import PIPE, Popen +from typing import Callable, Iterable from django.conf import settings from django.core.management import call_command from django.db import DEFAULT_DB_ALIAS, ProgrammingError, connections -from django.db.migrations import RunPython, RunSQL +from django.db.migrations import Migration, RunPython, RunSQL +from django.db.migrations.operations.base import Operation from .cache import Cache from .constants import ( @@ -19,6 +23,7 @@ from .constants import ( ) from .operations import IgnoreMigration from .sql_analyser import analyse_sql_statements, get_sql_analyser_class +from .sql_analyser.base import Issue from .utils import clean_bytes_to_str, get_migration_abspath, split_migration_path logger = logging.getLogger("django_migration_linter") @@ -32,38 +37,38 @@ class MessageType(Enum): ERROR = "error" @staticmethod - def values(): + def values() -> list[str]: return list(map(lambda c: c.value, MessageType)) class MigrationLinter: def __init__( self, - path=None, - ignore_name_contains=None, - ignore_name=None, - include_name_contains=None, - include_name=None, - include_apps=None, - exclude_apps=None, - database=DEFAULT_DB_ALIAS, - cache_path=DEFAULT_CACHE_PATH, - no_cache=False, - only_applied_migrations=False, - only_unapplied_migrations=False, - exclude_migration_tests=None, - quiet=None, - warnings_as_errors_tests=None, - all_warnings_as_errors=False, - no_output=False, - analyser_string=None, + path: str | None = None, + ignore_name_contains: str | None = None, + ignore_name: Iterable[str] | None = None, + include_name_contains: str | None = None, + include_name: Iterable[str] | None = None, + include_apps: Iterable[str] | None = None, + exclude_apps: Iterable[str] | None = None, + database: str = DEFAULT_DB_ALIAS, + cache_path: str = DEFAULT_CACHE_PATH, + no_cache: bool = False, + only_applied_migrations: bool = False, + only_unapplied_migrations: bool = False, + exclude_migration_tests: Iterable[str] | None = None, + quiet: Iterable[str] | None = None, + warnings_as_errors_tests: Iterable[str] | None = None, + all_warnings_as_errors: bool = False, + no_output: bool = False, + analyser_string: str | None = None, ): # Store parameters and options self.django_path = path self.ignore_name_contains = ignore_name_contains - self.ignore_name = ignore_name or tuple() + self.ignore_name = ignore_name or [] self.include_name_contains = include_name_contains - self.include_name = include_name or tuple() + self.include_name = include_name or [] self.include_apps = include_apps self.exclude_apps = exclude_apps self.exclude_migration_tests = exclude_migration_tests or [] @@ -84,7 +89,7 @@ class MigrationLinter: # Initialise counters self.reset_counters() - # Initialise cache. Read from old, write to new to prune old entries. + # Initialise cache. Read from old, write to new, in order to prune old entries. if self.should_use_cache(): self.old_cache = Cache(self.django_path, self.database, self.cache_path) self.new_cache = Cache(self.django_path, self.database, self.cache_path) @@ -97,31 +102,31 @@ class MigrationLinter: connection=connections[self.database], load=True ) - def reset_counters(self): + def reset_counters(self) -> None: self.nb_valid = 0 self.nb_ignored = 0 self.nb_warnings = 0 self.nb_erroneous = 0 self.nb_total = 0 - def should_use_cache(self): - return self.django_path and not self.no_cache + def should_use_cache(self) -> bool: + return bool(self.django_path and not self.no_cache) def lint_all_migrations( self, - app_label=None, - migration_name=None, - git_commit_id=None, - migrations_file_path=None, - ): - # Collect migrations + app_label: str | None = None, + migration_name: str | None = None, + git_commit_id: str | None = None, + migrations_file_path: str | None = None, + ) -> None: + # Collect migrations. migrations_list = self.read_migrations_list(migrations_file_path) if git_commit_id: migrations = self._gather_migrations_git(git_commit_id, migrations_list) else: migrations = self._gather_all_migrations(migrations_list) - # Lint those migrations + # Lint those migrations. sorted_migrations = sorted( migrations, key=lambda migration: (migration.app_label, migration.name) ) @@ -145,7 +150,7 @@ class MigrationLinter: if self.should_use_cache(): self.new_cache.save() - def lint_migration(self, migration): + def lint_migration(self, migration: Migration) -> None: app_label = migration.app_label migration_name = migration.name operations = migration.operations @@ -185,7 +190,7 @@ class MigrationLinter: elif self.warnings_as_errors_tests: new_warnings = [] for w in warnings: - if w["code"] in self.warnings_as_errors_tests: + if w.code in self.warnings_as_errors_tests: errors.append(w) else: new_warnings.append(w) @@ -222,14 +227,16 @@ class MigrationLinter: self.new_cache[md5hash] = value_to_cache @staticmethod - def get_migration_hash(app_label, migration_name): + def get_migration_hash(app_label: str, migration_name: str) -> str: hash_md5 = hashlib.md5() with open(get_migration_abspath(app_label, migration_name), "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) return hash_md5.hexdigest() - def lint_cached_migration(self, app_label, migration_name, md5hash): + def lint_cached_migration( + self, app_label: str, migration_name: str, md5hash: str + ) -> None: cached_value = self.old_cache[md5hash] if cached_value["result"] == "IGNORE": self.print_linting_msg( @@ -259,35 +266,37 @@ class MigrationLinter: self.new_cache[md5hash] = cached_value - def print_linting_msg(self, app_label, migration_name, msg, lint_result): + def print_linting_msg( + self, app_label: str, migration_name: str, msg: str, lint_result: MessageType + ) -> None: if lint_result.value in self.quiet: return if not self.no_output: print(f"({app_label}, {migration_name})... {msg}") - def print_errors(self, errors): + def print_errors(self, errors: list[Issue]) -> None: if MessageType.ERROR.value in self.quiet: return for err in errors: - error_str = "\t{}".format(err["msg"]) - if err.get("table"): - error_str += " (table: {}".format(err["table"]) - if err.get("column"): - error_str += ", column: {}".format(err["column"]) + error_str = "\t{}".format(err.message) + if err.table: + error_str += " (table: {}".format(err.table) + if err.column: + error_str += ", column: {}".format(err.column) error_str += ")" if not self.no_output: print(error_str) - def print_warnings(self, warnings): + def print_warnings(self, warnings: list[Issue]) -> None: if MessageType.WARNING.value in self.quiet: return for warning_details in warnings: - warn_str = "\t{}".format(warning_details["msg"]) + warn_str = "\t{}".format(warning_details.message) if not self.no_output: print(warn_str) - def print_summary(self): + def print_summary(self) -> None: if self.no_output: return print("*** Summary ***") @@ -297,10 +306,10 @@ class MigrationLinter: print(f"Ignored migrations: {self.nb_ignored}/{self.nb_total}") @property - def has_errors(self): + def has_errors(self) -> bool: return self.nb_erroneous > 0 - def get_sql(self, app_label, migration_name): + def get_sql(self, app_label: str, migration_name: str) -> list[str]: logger.info(f"Calling sqlmigrate command {app_label} {migration_name}") try: with open(os.devnull, "w") as dev_null: @@ -321,16 +330,18 @@ class MigrationLinter: return sql_statement.splitlines() @staticmethod - def is_migration_file(filename): + def is_migration_file(filename: str) -> bool: from django.db.migrations.loader import MIGRATIONS_MODULE_NAME - return ( + return bool( re.search(rf"/{MIGRATIONS_MODULE_NAME}/.*\.py", filename) and "__init__" not in filename ) @classmethod - def read_migrations_list(cls, migrations_file_path): + def read_migrations_list( + cls, migrations_file_path: str | None + ) -> list[tuple[str, str]] | None: """ Returning an empty list is different from returning None here. None: no file was specified and we should consider all migrations @@ -357,7 +368,9 @@ class MigrationLinter: ) return migrations - def _gather_migrations_git(self, git_commit_id, migrations_list=None): + def _gather_migrations_git( + self, git_commit_id: str, migrations_list: list[tuple[str, str]] | None = None + ) -> Iterable[Migration]: migrations = [] # Get changes since specified commit git_diff_command = ( @@ -365,7 +378,9 @@ class MigrationLinter: ).format(self.django_path, git_commit_id) logger.info(f"Executing {git_diff_command}") diff_process = Popen(git_diff_command, shell=True, stdout=PIPE, stderr=PIPE) - for line in map(clean_bytes_to_str, diff_process.stdout.readlines()): + for line in map( + clean_bytes_to_str, diff_process.stdout.readlines() # type: ignore + ): # Only gather lines that include added migrations if self.is_migration_file(line): app_label, name = split_migration_path(line) @@ -386,13 +401,17 @@ class MigrationLinter: if diff_process.returncode != 0: output = [] - for line in map(clean_bytes_to_str, diff_process.stderr.readlines()): + for line in map( + clean_bytes_to_str, diff_process.stderr.readlines() # type: ignore + ): output.append(line) logger.error("Error while git diff command:\n{}".format("".join(output))) raise Exception("Error while executing git diff command") return migrations - def _gather_all_migrations(self, migrations_list=None): + def _gather_all_migrations( + self, migrations_list: list[tuple[str, str]] | None = None + ) -> Iterable[Migration]: for ( (app_label, name), migration, @@ -401,7 +420,9 @@ class MigrationLinter: if migrations_list is None or (app_label, name) in migrations_list: yield migration - def should_ignore_migration(self, app_label, migration_name, operations=()): + def should_ignore_migration( + self, app_label: str, migration_name: str, operations: Iterable[Operation] = () + ) -> bool: return ( (self.include_apps and app_label not in self.include_apps) or (self.exclude_apps and app_label in self.exclude_apps) @@ -428,7 +449,9 @@ class MigrationLinter: ) ) - def analyse_data_migration(self, migration): + def analyse_data_migration( + self, migration: Migration + ) -> tuple[list[Issue], list[Issue], list[Issue]]: errors = [] ignored = [] warnings = [] @@ -450,7 +473,9 @@ class MigrationLinter: return errors, ignored, warnings - def lint_runpython(self, runpython): + def lint_runpython( + self, runpython: RunPython + ) -> tuple[list[Issue], list[Issue], list[Issue]]: function_name = runpython.code.__name__ error = [] ignored = [] @@ -458,13 +483,13 @@ class MigrationLinter: # Detect warning on missing reverse operation if not runpython.reversible: - issue = { - "code": "RUNPYTHON_REVERSIBLE", - "msg": "'{}': RunPython data migration is not reversible".format( + issue = Issue( + code="RUNPYTHON_REVERSIBLE", + message="'{}': RunPython data migration is not reversible".format( function_name ), - } - if issue["code"] in self.exclude_migration_tests: + ) + if issue.code in self.exclude_migration_tests: ignored.append(issue) else: warning.append(issue) @@ -472,14 +497,14 @@ class MigrationLinter: # Detect warning for argument naming convention args_spec = inspect.getfullargspec(runpython.code) if tuple(args_spec.args) != EXPECTED_DATA_MIGRATION_ARGS: - issue = { - "code": "RUNPYTHON_ARGS_NAMING_CONVENTION", - "msg": ( + issue = Issue( + code="RUNPYTHON_ARGS_NAMING_CONVENTION", + message=( "'{}': By convention, " "RunPython names the two arguments: apps, schema_editor" ).format(function_name), - } - if issue["code"] in self.exclude_migration_tests: + ) + if issue.code in self.exclude_migration_tests: ignored.append(issue) else: warning.append(issue) @@ -488,7 +513,7 @@ class MigrationLinter: # Forward issues = self.get_runpython_model_import_issues(runpython.code) for issue in issues: - if issue["code"] in self.exclude_migration_tests: + if issue.code in self.exclude_migration_tests: ignored.append(issue) else: error.append(issue) @@ -497,7 +522,7 @@ class MigrationLinter: if runpython.reversible: issues = self.get_runpython_model_import_issues(runpython.reverse_code) for issue in issues: - if issue and issue["code"] in self.exclude_migration_tests: + if issue and issue.code in self.exclude_migration_tests: ignored.append(issue) else: error.append(issue) @@ -505,7 +530,7 @@ class MigrationLinter: # Detect warning if model variable name is not the same as model class issues = self.get_runpython_model_variable_naming_issues(runpython.code) for issue in issues: - if issue and issue["code"] in self.exclude_migration_tests: + if issue and issue.code in self.exclude_migration_tests: ignored.append(issue) else: warning.append(issue) @@ -515,7 +540,7 @@ class MigrationLinter: runpython.reverse_code ) for issue in issues: - if issue and issue["code"] in self.exclude_migration_tests: + if issue and issue.code in self.exclude_migration_tests: ignored.append(issue) else: warning.append(issue) @@ -523,7 +548,7 @@ class MigrationLinter: return error, ignored, warning @staticmethod - def get_runpython_model_import_issues(code): + def get_runpython_model_import_issues(code: Callable) -> list[Issue]: model_object_regex = re.compile(r"[^a-zA-Z0-9._]?([a-zA-Z0-9._]+?)\.objects") function_name = code.__name__ @@ -542,19 +567,19 @@ class MigrationLinter: ) if not has_get_model_call: issues.append( - { - "code": "RUNPYTHON_MODEL_IMPORT", - "msg": ( + Issue( + code="RUNPYTHON_MODEL_IMPORT", + message=( "'{}': Could not find an 'apps.get_model(\"...\", \"{}\")' " "call. Importing the model directly is incorrect for " "data migrations." ).format(function_name, model), - } + ) ) return issues @staticmethod - def get_runpython_model_variable_naming_issues(code): + def get_runpython_model_variable_naming_issues(code: Callable) -> list[Issue]: model_object_regex = re.compile(r"[^a-zA-Z]?([a-zA-Z0-9]+?)\.objects") function_name = code.__name__ @@ -583,29 +608,31 @@ class MigrationLinter: ) if not has_same_model_name: issues.append( - { - "code": "RUNPYTHON_MODEL_VARIABLE_NAME", - "msg": ( + Issue( + code="RUNPYTHON_MODEL_VARIABLE_NAME", + message=( "'{}': Model variable name {} is different from the " "model class name that was found in the " "apps.get_model(...) call." ).format(function_name, model), - } + ) ) return issues - def lint_runsql(self, runsql): + def lint_runsql( + self, runsql: RunSQL + ) -> tuple[list[Issue], list[Issue], list[Issue]]: error = [] ignored = [] warning = [] # Detect warning on missing reverse operation if not runsql.reversible: - issue = { - "code": "RUNSQL_REVERSIBLE", - "msg": "RunSQL data migration is not reversible", - } - if issue["code"] in self.exclude_migration_tests: + issue = Issue( + code="RUNSQL_REVERSIBLE", + message="RunSQL data migration is not reversible", + ) + if issue.code in self.exclude_migration_tests: ignored.append(issue) else: warning.append(issue) diff --git a/django_migration_linter/operations.py b/django_migration_linter/operations.py index f6e460e..ede67eb 100644 --- a/django_migration_linter/operations.py +++ b/django_migration_linter/operations.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db.migrations.operations.base import Operation diff --git a/django_migration_linter/sql_analyser/__init__.py b/django_migration_linter/sql_analyser/__init__.py index ad16b0d..ff678e8 100644 --- a/django_migration_linter/sql_analyser/__init__.py +++ b/django_migration_linter/sql_analyser/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .base import BaseAnalyser # noqa from .mysql import MySqlAnalyser # noqa from .postgresql import PostgresqlAnalyser # noqa diff --git a/django_migration_linter/sql_analyser/analyser.py b/django_migration_linter/sql_analyser/analyser.py index 3915fa2..9ea3d54 100644 --- a/django_migration_linter/sql_analyser/analyser.py +++ b/django_migration_linter/sql_analyser/analyser.py @@ -1,6 +1,13 @@ +from __future__ import annotations + import logging +from typing import TYPE_CHECKING, Iterable, Type + +if TYPE_CHECKING: + from sql_analyser.base import Issue from django_migration_linter.sql_analyser import ( + BaseAnalyser, MySqlAnalyser, PostgresqlAnalyser, SqliteAnalyser, @@ -8,20 +15,22 @@ from django_migration_linter.sql_analyser import ( logger = logging.getLogger("django_migration_linter") -ANALYSER_STRING_MAPPING = { +ANALYSER_STRING_MAPPING: dict[str, Type[BaseAnalyser]] = { "sqlite": SqliteAnalyser, "mysql": MySqlAnalyser, "postgresql": PostgresqlAnalyser, } -def get_sql_analyser_class(database_vendor, analyser_string=None): +def get_sql_analyser_class( + database_vendor: str, analyser_string: str | None = None +) -> Type[BaseAnalyser]: if analyser_string: return get_sql_analyser_from_string(analyser_string) return get_sql_analyser_class_from_db_vendor(database_vendor) -def get_sql_analyser_from_string(analyser_string): +def get_sql_analyser_from_string(analyser_string: str) -> Type[BaseAnalyser]: if analyser_string not in ANALYSER_STRING_MAPPING: raise ValueError( "Unknown SQL analyser '{}'. Known values: '{}'".format( @@ -32,7 +41,8 @@ def get_sql_analyser_from_string(analyser_string): return ANALYSER_STRING_MAPPING[analyser_string] -def get_sql_analyser_class_from_db_vendor(database_vendor): +def get_sql_analyser_class_from_db_vendor(database_vendor: str) -> Type[BaseAnalyser]: + sql_analyser_class: Type[BaseAnalyser] if "mysql" in database_vendor: sql_analyser_class = MySqlAnalyser elif "postgre" in database_vendor: @@ -51,8 +61,10 @@ def get_sql_analyser_class_from_db_vendor(database_vendor): def analyse_sql_statements( - sql_analyser_class, sql_statements, exclude_migration_tests=None -): + sql_analyser_class: Type[BaseAnalyser], + sql_statements: list[str], + exclude_migration_tests: Iterable[str] | None = None, +) -> tuple[list[Issue], list[Issue], list[Issue]]: sql_analyser = sql_analyser_class(exclude_migration_tests) sql_analyser.analyse(sql_statements) return sql_analyser.errors, sql_analyser.ignored, sql_analyser.warnings diff --git a/django_migration_linter/sql_analyser/base.py b/django_migration_linter/sql_analyser/base.py index b27c2d8..2fa0646 100644 --- a/django_migration_linter/sql_analyser/base.py +++ b/django_migration_linter/sql_analyser/base.py @@ -1,12 +1,36 @@ +from __future__ import annotations + import logging import re - -from .utils import update_migration_tests +from copy import deepcopy +from dataclasses import dataclass +from enum import Enum +from typing import Callable, Iterable logger = logging.getLogger("django_migration_linter") -def has_not_null_column(sql_statements, **kwargs): +def find_check_from_code(checks: list[Check], code: str) -> Check | None: + return next((c for c in checks if c.code == code), None) + + +def update_migration_checks( + base_checks: list[Check], specific_checks: list[Check] +) -> list[Check]: + new_checks = deepcopy(base_checks) + for override_check in specific_checks: + migration_check = find_check_from_code(new_checks, override_check.code) + + if migration_check is None or not override_check.code: + migration_check = deepcopy(override_check) + new_checks.append(migration_check) + + for field in override_check.__dataclass_fields__: + setattr(migration_check, field, getattr(override_check, field)) + return new_checks + + +def has_not_null_column(sql_statements: list[str], **kwargs) -> bool: # TODO: improve to detect that the same column is concerned ends_with_default = False for sql in sql_statements: @@ -23,7 +47,7 @@ def has_not_null_column(sql_statements, **kwargs): ) -def has_add_unique(sql_statements, **kwargs): +def has_add_unique(sql_statements: list[str], **kwargs) -> bool: regex_result = None for sql in sql_statements: regex_result = re.search( @@ -41,130 +65,167 @@ def has_add_unique(sql_statements, **kwargs): return not table_is_added_in_transaction +class CheckMode(Enum): + """ + Defines whether the Check.fn gets a str or a list[str] as first parameter. + """ + + ONE_LINER = 1 + TRANSACTION = 2 + + +class CheckType(Enum): + ERROR = 1 + WARNING = 2 + + +@dataclass +class Check: + """ + Represents a check that will be done against SQL statement(s). + """ + + code: str + fn: Callable + message: str + mode: CheckMode + type: CheckType + + +@dataclass +class Issue: + code: str + message: str + table: str | None = None + column: str | None = None + + class BaseAnalyser: - base_migration_tests = [ - { - "code": "RENAME_TABLE", - "fn": lambda sql, **kw: re.search("RENAME TABLE", sql) + base_migration_checks: list[Check] = [ + Check( + code="RENAME_TABLE", + fn=lambda sql, **kw: re.search("RENAME TABLE", sql) or re.search("ALTER TABLE .* RENAME TO", sql), - "msg": "RENAMING tables", - "mode": "one_liner", - "type": "error", - }, - { - "code": "NOT_NULL", - "fn": has_not_null_column, - "msg": "NOT NULL constraint on columns", - "mode": "transaction", - "type": "error", - }, - { - "code": "DROP_COLUMN", - "fn": lambda sql, **kw: re.search("DROP COLUMN", sql), - "msg": "DROPPING columns", - "mode": "one_liner", - "type": "error", - }, - { - "code": "DROP_TABLE", - "fn": lambda sql, **kw: sql.startswith("DROP TABLE"), - "msg": "DROPPING table", - "mode": "one_liner", - "type": "error", - }, - { - "code": "RENAME_COLUMN", - "fn": lambda sql, **kw: re.search("ALTER TABLE .* CHANGE", sql) + message="RENAMING tables", + mode=CheckMode.ONE_LINER, + type=CheckType.ERROR, + ), + Check( + code="NOT_NULL", + fn=has_not_null_column, + message="NOT NULL constraint on columns", + mode=CheckMode.TRANSACTION, + type=CheckType.ERROR, + ), + Check( + code="DROP_COLUMN", + fn=lambda sql, **kw: re.search("DROP COLUMN", sql), + message="DROPPING columns", + mode=CheckMode.ONE_LINER, + type=CheckType.ERROR, + ), + Check( + code="DROP_TABLE", + fn=lambda sql, **kw: sql.startswith("DROP TABLE"), + message="DROPPING table", + mode=CheckMode.ONE_LINER, + type=CheckType.ERROR, + ), + Check( + code="RENAME_COLUMN", + fn=lambda sql, **kw: re.search("ALTER TABLE .* CHANGE", sql) or re.search("ALTER TABLE .* RENAME COLUMN", sql), - "msg": "RENAMING columns", - "mode": "one_liner", - "type": "error", - }, - { - "code": "ALTER_COLUMN", - "fn": lambda sql, **kw: re.search( - "ALTER TABLE .* ALTER COLUMN .* TYPE", sql - ), - "msg": ( + message="RENAMING columns", + mode=CheckMode.ONE_LINER, + type=CheckType.ERROR, + ), + Check( + code="ALTER_COLUMN", + fn=lambda sql, **kw: re.search("ALTER TABLE .* ALTER COLUMN .* TYPE", sql), + message=( "ALTERING columns (Could be backward compatible. " "You may ignore this migration.)" ), - "mode": "one_liner", - "type": "error", - }, - { - "code": "ADD_UNIQUE", - "fn": has_add_unique, - "msg": "ADDING unique constraint", - "mode": "transaction", - "type": "error", - }, + mode=CheckMode.ONE_LINER, + type=CheckType.ERROR, + ), + Check( + code="ADD_UNIQUE", + fn=has_add_unique, + message="ADDING unique constraint", + mode=CheckMode.TRANSACTION, + type=CheckType.ERROR, + ), ] - migration_tests = [] + migration_checks: list[Check] = [] - def __init__(self, exclude_migration_tests): - self.exclude_migration_tests = exclude_migration_tests or [] - self.errors = [] - self.warnings = [] - self.ignored = [] - self.migration_tests = update_migration_tests( - self.base_migration_tests, self.migration_tests + def __init__(self, exclude_migration_tests: Iterable[str] | None): + self.exclude_migration_tests: Iterable[str] = exclude_migration_tests or [] + self.errors: list[Issue] = [] + self.warnings: list[Issue] = [] + self.ignored: list[Issue] = [] + self.migration_checks = update_migration_checks( + self.base_migration_checks, self.migration_checks ) - def analyse(self, sql_statements): + def analyse(self, sql_statements: list[str]) -> None: for statement in sql_statements: - for test in self.one_line_migration_tests: - self._test_sql(test, sql=statement) + for test in self.one_line_migration_checks: + self._check_sql(test, sql=statement) - for test in self.transaction_migration_tests: - self._test_sql(test, sql=sql_statements) + for test in self.transaction_migration_checks: + self._check_sql(test, sql=sql_statements) @property - def one_line_migration_tests(self): - return (test for test in self.migration_tests if test["mode"] == "one_liner") + def one_line_migration_checks(self) -> Iterable[Check]: + return (c for c in self.migration_checks if c.mode == CheckMode.ONE_LINER) @property - def transaction_migration_tests(self): - return (test for test in self.migration_tests if test["mode"] == "transaction") + def transaction_migration_checks(self) -> Iterable[Check]: + return (c for c in self.migration_checks if c.mode == CheckMode.TRANSACTION) - def _test_sql(self, test, sql): - if test["fn"](sql, errors=self.errors): - if test["code"] in self.exclude_migration_tests: + def _check_sql(self, check: Check, sql: list[str] | str) -> None: + if check.fn(sql, errors=self.errors): + if check.code in self.exclude_migration_tests: action = "IGNORED" list_to_add = self.ignored - elif test["type"] == "warning": + elif check.type == CheckType.WARNING: action = "WARNING" list_to_add = self.warnings else: action = "ERROR" list_to_add = self.errors logger.debug("Testing %s -- %s", sql, action) - err = self.build_error_dict(migration_test=test, sql_statement=sql) - list_to_add.append(err) + issue = self.build_issue(migration_check=check, sql_statement=sql) + list_to_add.append(issue) else: logger.debug("Testing %s -- PASSED", sql) - def build_error_dict(self, migration_test, sql_statement): + def build_issue( + self, migration_check: Check, sql_statement: list[str] | str + ) -> Issue: table = self.detect_table(sql_statement) col = self.detect_column(sql_statement) - return { - "msg": migration_test["msg"], - "code": migration_test["code"], - "table": table, - "column": col, - } + return Issue( + code=migration_check.code, + message=migration_check.message, + table=table, + column=col, + ) @staticmethod - def detect_table(sql): + def detect_table(sql: list[str] | str) -> str | None: if isinstance(sql, str): regex_result = re.search("TABLE [`\"'](.*?)[`\"']", sql, re.IGNORECASE) if regex_result: return regex_result.group(1) + return None @staticmethod - def detect_column(sql): + def detect_column(sql: list[str] | str) -> str | None: if isinstance(sql, str): regex_result = re.search("COLUMN [`\"'](.*?)[`\"']", sql, re.IGNORECASE) if regex_result: return regex_result.group(1) + return None diff --git a/django_migration_linter/sql_analyser/mysql.py b/django_migration_linter/sql_analyser/mysql.py index 487a85e..90d3a72 100644 --- a/django_migration_linter/sql_analyser/mysql.py +++ b/django_migration_linter/sql_analyser/mysql.py @@ -1,22 +1,26 @@ +from __future__ import annotations + import re -from .base import BaseAnalyser +from .base import BaseAnalyser, Check, CheckMode, CheckType class MySqlAnalyser(BaseAnalyser): - migration_tests = [ - { - "code": "ALTER_COLUMN", - "fn": lambda sql, **kw: re.search( - "ALTER TABLE .* MODIFY .* (?!NULL);?$", sql + migration_checks: list[Check] = [ + Check( + code="ALTER_COLUMN", + fn=lambda sql, **kw: re.search("ALTER TABLE .* MODIFY .* (?!NULL);?$", sql), + message=( + "ALTERING columns (Could be backward compatible. " + "You may ignore this migration.)" ), - "mode": "one_liner", - "type": "error", - } + mode=CheckMode.ONE_LINER, + type=CheckType.ERROR, + ), ] @staticmethod - def detect_column(sql): + def detect_column(sql: list[str] | str) -> str | None: if isinstance(sql, str): regex_result = re.search("COLUMN [`\"'](.*?)[`\"']", sql, re.IGNORECASE) if regex_result: @@ -24,3 +28,4 @@ class MySqlAnalyser(BaseAnalyser): regex_result = re.search("MODIFY [`\"'](.*?)[`\"']", sql, re.IGNORECASE) if regex_result: return regex_result.group(1) + return None diff --git a/django_migration_linter/sql_analyser/postgresql.py b/django_migration_linter/sql_analyser/postgresql.py index 36173b9..33e0b23 100644 --- a/django_migration_linter/sql_analyser/postgresql.py +++ b/django_migration_linter/sql_analyser/postgresql.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import re -from .base import BaseAnalyser +from .base import BaseAnalyser, Check, CheckMode, CheckType -def has_create_index(sql_statements, **kwargs): +def has_create_index(sql_statements: list[str], **kwargs) -> bool: regex_result = None for sql in sql_statements: regex_result = re.search(r"CREATE (UNIQUE )?INDEX.*ON (.*) \(", sql) @@ -22,27 +24,27 @@ def has_create_index(sql_statements, **kwargs): class PostgresqlAnalyser(BaseAnalyser): - migration_tests = [ - { - "code": "CREATE_INDEX", - "fn": has_create_index, - "msg": "CREATE INDEX locks table", - "mode": "transaction", - "type": "warning", - }, - { - "code": "DROP_INDEX", - "fn": lambda sql, **kw: re.search("DROP INDEX", sql) + migration_checks: list[Check] = [ + Check( + code="CREATE_INDEX", + fn=has_create_index, + message="CREATE INDEX locks table", + mode=CheckMode.TRANSACTION, + type=CheckType.WARNING, + ), + Check( + code="DROP_INDEX", + fn=lambda sql, **kw: re.search("DROP INDEX", sql) and not re.search("INDEX CONCURRENTLY", sql), - "msg": "DROP INDEX locks table", - "mode": "one_liner", - "type": "warning", - }, - { - "code": "REINDEX", - "fn": lambda sql, **kw: sql.startswith("REINDEX"), - "msg": "REINDEX locks table", - "mode": "one_liner", - "type": "warning", - }, + message="DROP INDEX locks table", + mode=CheckMode.ONE_LINER, + type=CheckType.WARNING, + ), + Check( + code="REINDEX", + fn=lambda sql, **kw: sql.startswith("REINDEX"), + message="REINDEX locks table", + mode=CheckMode.ONE_LINER, + type=CheckType.WARNING, + ), ] diff --git a/django_migration_linter/sql_analyser/sqlite.py b/django_migration_linter/sql_analyser/sqlite.py index 530788a..92d4475 100644 --- a/django_migration_linter/sql_analyser/sqlite.py +++ b/django_migration_linter/sql_analyser/sqlite.py @@ -1,31 +1,35 @@ +from __future__ import annotations + import re -from .base import BaseAnalyser +from .base import BaseAnalyser, Check, CheckMode, CheckType class SqliteAnalyser(BaseAnalyser): - migration_tests = [ - { - "code": "RENAME_TABLE", - "fn": lambda sql, **kw: re.search("ALTER TABLE .* RENAME TO", sql) + migration_checks: list[Check] = [ + Check( + code="RENAME_TABLE", + fn=lambda sql, **kw: re.search("ALTER TABLE .* RENAME TO", sql) and "__old" not in sql and "new__" not in sql, - "type": "error", - }, - { - "code": "DROP_TABLE", + message="RENAMING tables", + mode=CheckMode.ONE_LINER, + type=CheckType.ERROR, + ), + Check( + code="DROP_TABLE", # TODO: improve to detect that the table names overlap - "fn": lambda sql_statements, **kw: any( + fn=lambda sql_statements, **kw: any( sql.startswith("DROP TABLE") for sql in sql_statements ) and not any(sql.startswith("CREATE TABLE") for sql in sql_statements), - "msg": "DROPPING table", - "mode": "transaction", - "type": "error", - }, - { - "code": "NOT_NULL", - "fn": lambda sql_statements, **kw: any( + message="DROPPING table", + mode=CheckMode.TRANSACTION, + type=CheckType.ERROR, + ), + Check( + code="NOT_NULL", + fn=lambda sql_statements, **kw: any( re.search("NOT NULL(?! PRIMARY)(?! DEFAULT)", sql) for sql in sql_statements ) @@ -34,13 +38,14 @@ class SqliteAnalyser(BaseAnalyser): and ("__old" in sql or "new__" in sql) for sql in sql_statements ), - "mode": "transaction", - "type": "error", - }, + message="NOT NULL constraint on columns", + mode=CheckMode.TRANSACTION, + type=CheckType.ERROR, + ), ] @staticmethod - def detect_table(sql): + def detect_table(sql: list[str] | str) -> str | None: if isinstance(sql, str): regex_result = re.search("TABLE [`\"'](.*?)[`\"']", sql, re.IGNORECASE) if regex_result: @@ -48,3 +53,4 @@ class SqliteAnalyser(BaseAnalyser): regex_result = re.search("ON [`\"'](.*?)[`\"']", sql, re.IGNORECASE) if regex_result: return regex_result.group(1) + return None diff --git a/django_migration_linter/sql_analyser/utils.py b/django_migration_linter/sql_analyser/utils.py deleted file mode 100644 index e7e6c2c..0000000 --- a/django_migration_linter/sql_analyser/utils.py +++ /dev/null @@ -1,21 +0,0 @@ -from copy import deepcopy - - -def find_error_dict_with_code(tests, code): - return next((test_dict for test_dict in tests if test_dict["code"] == code), None) - - -def update_migration_tests(base_tests, specific_tests): - base_tests = deepcopy(base_tests) - for override_test in specific_tests: - migration_test_dict = find_error_dict_with_code( - base_tests, override_test["code"] - ) - - if migration_test_dict is None or not override_test["code"]: - migration_test_dict = {} - base_tests.append(migration_test_dict) - - for key in override_test.keys(): - migration_test_dict[key] = override_test[key] - return base_tests diff --git a/django_migration_linter/utils.py b/django_migration_linter/utils.py index 17a3270..8757e5b 100644 --- a/django_migration_linter/utils.py +++ b/django_migration_linter/utils.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import os from importlib import import_module -def split_path(path): - decomposed_path = [] +def split_path(path: str) -> list[str]: + decomposed_path: list[str] = [] while 1: head, tail = os.path.split(path) if head == path: # sentinel for absolute paths @@ -21,20 +23,21 @@ def split_path(path): return decomposed_path -def split_migration_path(migration_path): +def split_migration_path(migration_path: str) -> tuple[str, str]: from django.db.migrations.loader import MIGRATIONS_MODULE_NAME decomposed_path = split_path(migration_path) for i, p in enumerate(decomposed_path): if p == MIGRATIONS_MODULE_NAME: return decomposed_path[i - 1], os.path.splitext(decomposed_path[i + 1])[0] + return "", "" -def clean_bytes_to_str(byte_input): +def clean_bytes_to_str(byte_input: bytes) -> str: return byte_input.decode("utf-8").strip() -def get_migration_abspath(app_label, migration_name): +def get_migration_abspath(app_label: str, migration_name: str) -> str: from django.db.migrations.loader import MigrationLoader module_name, _ = MigrationLoader.migrations_module(app_label) @@ -42,6 +45,9 @@ def get_migration_abspath(app_label, migration_name): migration_module = import_module(migration_path) migration_file = migration_module.__file__ + if not migration_file: + raise ValueError("Migration file not found") + if migration_file.endswith(".pyc"): migration_file = migration_file[:-1] return migration_file diff --git a/manage.py b/manage.py index 8512e65..4e458ca 100755 --- a/manage.py +++ b/manage.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +from __future__ import annotations + import os import sys diff --git a/requirements/dev.txt b/requirements/dev.txt index 0ae3055..a367549 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,3 +1,6 @@ flake8==6.0.0 black==22.12.0 -isort==5.11.3 +isort==5.11.4 +mypy==0.991 +types-toml==0.10.8.1 +ruff==0.0.194 diff --git a/setup.cfg b/setup.cfg index 97dbfa5..1cd7b95 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,9 @@ [flake8] max-line-length = 88 +[ruff] +line-length = 88 + [isort] multi_line_output = 3 include_trailing_comma = True @@ -8,3 +11,8 @@ force_grid_wrap = 0 use_parentheses = True ensure_newline_before_comments = True line_length = 88 +add_imports = + from __future__ import annotations + +[mypy] +ignore_missing_imports = True \ No newline at end of file diff --git a/setup.py b/setup.py index 7333fdc..5b47e5b 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import ast import re from os import path diff --git a/tests/fixtures.py b/tests/fixtures.py index 571924d..7262d1e 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,3 +1,5 @@ +from __future__ import annotations + CREATE_TABLE_WITH_NOT_NULL_COLUMN = "app_create_table_with_not_null_column" ADD_NOT_NULL_COLUMN = "app_add_not_null_column" DROP_COLUMN = "app_drop_column" diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 7876851..3e38582 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import unittest import unittest.mock as mock @@ -7,6 +9,7 @@ from django.db.migrations import Migration from django_migration_linter import ( IgnoreMigration, + Issue, MigrationLinter, analyse_sql_statements, get_migration_abspath, @@ -29,7 +32,7 @@ class CacheTestCase(unittest.TestCase): ], ) def test_cache_normal(self, *args): - linter = MigrationLinter(self.test_project_path) + linter = MigrationLinter(self.test_project_path, database="mysql") linter.old_cache.clear() linter.old_cache.save() @@ -43,22 +46,20 @@ class CacheTestCase(unittest.TestCase): cache = linter.new_cache cache.load() - self.assertEqual("OK", cache["4a3770a405738d457e2d23e17fb1f3aa"]["result"]) - self.assertEqual("ERR", cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["result"]) + self.assertEqual("OK", cache["ab2eef5ceb020d0e6eaa42dda2a754c5"]["result"]) + self.assertEqual("ERR", cache["52000d9d2c43dfda982945000dc6ba54"]["result"]) self.assertListEqual( [ - { - "msg": "NOT NULL constraint on columns", - "code": "NOT_NULL", - "table": None, - "column": None, - } + Issue( + code="NOT_NULL", + message="NOT NULL constraint on columns", + ), ], - cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["errors"], + cache["52000d9d2c43dfda982945000dc6ba54"]["errors"], ) # Start the Linter again -> should use cache now. - linter = MigrationLinter(self.test_project_path) + linter = MigrationLinter(self.test_project_path, database="mysql") with mock.patch( "django_migration_linter.migration_linter.analyse_sql_statements", @@ -95,18 +96,16 @@ class CacheTestCase(unittest.TestCase): cache = linter.new_cache cache.load() - self.assertEqual("OK", cache["4a3770a405738d457e2d23e17fb1f3aa"]["result"]) - self.assertEqual("ERR", cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["result"]) + self.assertEqual("OK", cache["ab2eef5ceb020d0e6eaa42dda2a754c5"]["result"]) + self.assertEqual("ERR", cache["52000d9d2c43dfda982945000dc6ba54"]["result"]) self.assertListEqual( [ - { - "msg": "NOT NULL constraint on columns", - "code": "NOT_NULL", - "table": None, - "column": None, - } + Issue( + code="NOT_NULL", + message="NOT NULL constraint on columns", + ), ], - cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["errors"], + cache["52000d9d2c43dfda982945000dc6ba54"]["errors"], ) # Start the Linter again but with different database, should not be the same cache @@ -122,18 +121,16 @@ class CacheTestCase(unittest.TestCase): cache = linter.new_cache cache.load() - self.assertEqual("OK", cache["4a3770a405738d457e2d23e17fb1f3aa"]["result"]) - self.assertEqual("ERR", cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["result"]) + self.assertEqual("OK", cache["ab2eef5ceb020d0e6eaa42dda2a754c5"]["result"]) + self.assertEqual("ERR", cache["52000d9d2c43dfda982945000dc6ba54"]["result"]) self.assertListEqual( [ - { - "msg": "NOT NULL constraint on columns", - "code": "NOT_NULL", - "table": None, - "column": None, - } + Issue( + code="NOT_NULL", + message="NOT NULL constraint on columns", + ), ], - cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["errors"], + cache["52000d9d2c43dfda982945000dc6ba54"]["errors"], ) self.assertTrue(linter.has_errors) @@ -169,7 +166,7 @@ class CacheTestCase(unittest.TestCase): ], ) def test_cache_modified(self, *args): - linter = MigrationLinter(self.test_project_path) + linter = MigrationLinter(self.test_project_path, database="mysql") linter.old_cache.clear() linter.old_cache.save() @@ -183,7 +180,7 @@ class CacheTestCase(unittest.TestCase): cache = linter.new_cache cache.load() - self.assertEqual("ERR", cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["result"]) + self.assertEqual("ERR", cache["52000d9d2c43dfda982945000dc6ba54"]["result"]) # Get the content of the migration file and mock the open call to append # some content to change the hash @@ -209,9 +206,9 @@ class CacheTestCase(unittest.TestCase): cache = linter.new_cache cache.load() - self.assertNotIn("19fd3ea688fc05e2cc2a6e67c0b7aa17", cache) + self.assertNotIn("52000d9d2c43dfda982945000dc6ba54", cache) self.assertEqual(1, len(cache)) - self.assertEqual("ERR", cache["a25768641a0ad526fad199f97c303784"]["result"]) + self.assertEqual("ERR", cache["edeb40ecc1f5550ebef876c8e8ca093d"]["result"]) @mock.patch( "django_migration_linter.MigrationLinter._gather_all_migrations", @@ -221,7 +218,7 @@ class CacheTestCase(unittest.TestCase): ], ) def test_ignore_cached_migration(self, *args): - linter = MigrationLinter(self.test_project_path) + linter = MigrationLinter(self.test_project_path, database="mysql") linter.old_cache.clear() linter.old_cache.save() @@ -235,23 +232,23 @@ class CacheTestCase(unittest.TestCase): cache = linter.new_cache cache.load() - self.assertEqual("OK", cache["4a3770a405738d457e2d23e17fb1f3aa"]["result"]) - self.assertEqual("ERR", cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["result"]) + self.assertEqual("OK", cache["ab2eef5ceb020d0e6eaa42dda2a754c5"]["result"]) + self.assertEqual("ERR", cache["52000d9d2c43dfda982945000dc6ba54"]["result"]) self.assertListEqual( [ - { - "msg": "NOT NULL constraint on columns", - "code": "NOT_NULL", - "table": None, - "column": None, - } + Issue( + code="NOT_NULL", + message="NOT NULL constraint on columns", + ), ], - cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["errors"], + cache["52000d9d2c43dfda982945000dc6ba54"]["errors"], ) # Start the Linter again -> should use cache now but ignore the erroneous linter = MigrationLinter( - self.test_project_path, ignore_name_contains="0002_add_new_not_null_field" + self.test_project_path, + ignore_name_contains="0002_add_new_not_null_field", + database="mysql", ) with mock.patch( @@ -266,4 +263,4 @@ class CacheTestCase(unittest.TestCase): cache = linter.new_cache cache.load() self.assertEqual(1, len(cache)) - self.assertEqual("OK", cache["4a3770a405738d457e2d23e17fb1f3aa"]["result"]) + self.assertEqual("OK", cache["ab2eef5ceb020d0e6eaa42dda2a754c5"]["result"]) diff --git a/tests/functional/test_data_migrations.py b/tests/functional/test_data_migrations.py index 2bb039c..98ef8da 100644 --- a/tests/functional/test_data_migrations.py +++ b/tests/functional/test_data_migrations.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import unittest @@ -307,13 +309,13 @@ class RunSQLMigrationTestCase(unittest.TestCase): runsql = migrations.RunSQL("sql;") error, ignored, warning = self.linter.lint_runsql(runsql) - self.assertEqual("RUNSQL_REVERSIBLE", warning[0]["code"]) + self.assertEqual("RUNSQL_REVERSIBLE", warning[0].code) def test_sql_linting_error(self): runsql = migrations.RunSQL("ALTER TABLE t DROP COLUMN t;") error, ignored, warning = self.linter.lint_runsql(runsql) - self.assertEqual("DROP_COLUMN", error[0]["code"]) + self.assertEqual("DROP_COLUMN", error[0].code) def test_sql_linting_error_array(self): runsql = migrations.RunSQL( @@ -321,11 +323,11 @@ class RunSQLMigrationTestCase(unittest.TestCase): ) error, ignored, warning = self.linter.lint_runsql(runsql) - self.assertEqual("DROP_COLUMN", error[0]["code"]) - self.assertEqual("RENAME_COLUMN", error[1]["code"]) + self.assertEqual("DROP_COLUMN", error[0].code) + self.assertEqual("RENAME_COLUMN", error[1].code) def test_sql_linting_error_args(self): runsql = migrations.RunSQL([("ALTER TABLE %s DROP COLUMN %s;", ("t", "c"))]) error, ignored, warning = self.linter.lint_runsql(runsql) - self.assertEqual("DROP_COLUMN", error[0]["code"]) + self.assertEqual("DROP_COLUMN", error[0].code) diff --git a/tests/functional/test_lintmigrations_command.py b/tests/functional/test_lintmigrations_command.py index be011f1..17fa44c 100644 --- a/tests/functional/test_lintmigrations_command.py +++ b/tests/functional/test_lintmigrations_command.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest.mock import patch from django.core.management import call_command diff --git a/tests/functional/test_makemigrations_command.py b/tests/functional/test_makemigrations_command.py index c1978a0..2882247 100644 --- a/tests/functional/test_makemigrations_command.py +++ b/tests/functional/test_makemigrations_command.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import shutil import tempfile diff --git a/tests/functional/test_migration_linter.py b/tests/functional/test_migration_linter.py index 1ecc4b3..6b19170 100644 --- a/tests/functional/test_migration_linter.py +++ b/tests/functional/test_migration_linter.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import unittest diff --git a/tests/test_project/app_add_manytomany_field/migrations/0001_initial.py b/tests/test_project/app_add_manytomany_field/migrations/0001_initial.py index e39c60c..225dc73 100644 --- a/tests/test_project/app_add_manytomany_field/migrations/0001_initial.py +++ b/tests/test_project/app_add_manytomany_field/migrations/0001_initial.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2019-09-20 20:01 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_add_manytomany_field/migrations/0002_b_many_to_many.py b/tests/test_project/app_add_manytomany_field/migrations/0002_b_many_to_many.py index e1dda63..bfed66b 100644 --- a/tests/test_project/app_add_manytomany_field/migrations/0002_b_many_to_many.py +++ b/tests/test_project/app_add_manytomany_field/migrations/0002_b_many_to_many.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2019-09-20 20:02 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_add_manytomany_field/models.py b/tests/test_project/app_add_manytomany_field/models.py index 852c271..0118b09 100644 --- a/tests/test_project/app_add_manytomany_field/models.py +++ b/tests/test_project/app_add_manytomany_field/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_add_not_null_column/migrations/0001_create_table.py b/tests/test_project/app_add_not_null_column/migrations/0001_create_table.py index f790609..4e43ba8 100644 --- a/tests/test_project/app_add_not_null_column/migrations/0001_create_table.py +++ b/tests/test_project/app_add_not_null_column/migrations/0001_create_table.py @@ -1,5 +1,7 @@ # Generated by Django 2.1.4 on 2019-03-19 20:59 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_add_not_null_column/migrations/0002_add_new_not_null_field.py b/tests/test_project/app_add_not_null_column/migrations/0002_add_new_not_null_field.py index e1cffb4..a74b55d 100644 --- a/tests/test_project/app_add_not_null_column/migrations/0002_add_new_not_null_field.py +++ b/tests/test_project/app_add_not_null_column/migrations/0002_add_new_not_null_field.py @@ -1,5 +1,7 @@ # Generated by Django 2.1.4 on 2019-03-19 21:05 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_add_not_null_column/models.py b/tests/test_project/app_add_not_null_column/models.py index 8bf59ee..b0c264d 100644 --- a/tests/test_project/app_add_not_null_column/models.py +++ b/tests/test_project/app_add_not_null_column/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_add_not_null_column_followed_by_default/migrations/0001_initial.py b/tests/test_project/app_add_not_null_column_followed_by_default/migrations/0001_initial.py index 9c56ab3..da93e37 100644 --- a/tests/test_project/app_add_not_null_column_followed_by_default/migrations/0001_initial.py +++ b/tests/test_project/app_add_not_null_column_followed_by_default/migrations/0001_initial.py @@ -1,6 +1,8 @@ # Generated by Django 1.11.18 on 2019-04-14 14:54 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_add_not_null_column_followed_by_default/migrations/0002_a_not_null_field.py b/tests/test_project/app_add_not_null_column_followed_by_default/migrations/0002_a_not_null_field.py index 690985e..c6a426b 100644 --- a/tests/test_project/app_add_not_null_column_followed_by_default/migrations/0002_a_not_null_field.py +++ b/tests/test_project/app_add_not_null_column_followed_by_default/migrations/0002_a_not_null_field.py @@ -1,6 +1,8 @@ # Generated by Django 1.11.18 on 2019-04-14 14:55 +from __future__ import annotations + from django.db import migrations, models from django_add_default_value import AddDefaultValue diff --git a/tests/test_project/app_add_not_null_column_followed_by_default/models.py b/tests/test_project/app_add_not_null_column_followed_by_default/models.py index de80dc8..cfb119c 100644 --- a/tests/test_project/app_add_not_null_column_followed_by_default/models.py +++ b/tests/test_project/app_add_not_null_column_followed_by_default/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_alter_column/migrations/0001_initial.py b/tests/test_project/app_alter_column/migrations/0001_initial.py index d5b655f..506280b 100644 --- a/tests/test_project/app_alter_column/migrations/0001_initial.py +++ b/tests/test_project/app_alter_column/migrations/0001_initial.py @@ -1,6 +1,8 @@ # Generated by Django 1.11.18 on 2019-04-14 14:56 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_alter_column/migrations/0002_auto_20190414_1456.py b/tests/test_project/app_alter_column/migrations/0002_auto_20190414_1456.py index 066a4f5..7a0e360 100644 --- a/tests/test_project/app_alter_column/migrations/0002_auto_20190414_1456.py +++ b/tests/test_project/app_alter_column/migrations/0002_auto_20190414_1456.py @@ -1,6 +1,8 @@ # Generated by Django 1.11.18 on 2019-04-14 14:56 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_alter_column/models.py b/tests/test_project/app_alter_column/models.py index 8f7d60b..14f5721 100644 --- a/tests/test_project/app_alter_column/models.py +++ b/tests/test_project/app_alter_column/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_alter_column_drop_not_null/migrations/0001_create_table.py b/tests/test_project/app_alter_column_drop_not_null/migrations/0001_create_table.py index 81539c0..b91d595 100644 --- a/tests/test_project/app_alter_column_drop_not_null/migrations/0001_create_table.py +++ b/tests/test_project/app_alter_column_drop_not_null/migrations/0001_create_table.py @@ -1,5 +1,7 @@ # Generated by Django 2.1.4 on 2019-03-19 20:59 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_alter_column_drop_not_null/migrations/0002_drop_not_null.py b/tests/test_project/app_alter_column_drop_not_null/migrations/0002_drop_not_null.py index 7c71a17..ba9c86e 100644 --- a/tests/test_project/app_alter_column_drop_not_null/migrations/0002_drop_not_null.py +++ b/tests/test_project/app_alter_column_drop_not_null/migrations/0002_drop_not_null.py @@ -1,5 +1,7 @@ # Generated by Django 2.1.4 on 2019-03-19 21:05 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_alter_column_drop_not_null/models.py b/tests/test_project/app_alter_column_drop_not_null/models.py index 6ad7e37..6545041 100644 --- a/tests/test_project/app_alter_column_drop_not_null/models.py +++ b/tests/test_project/app_alter_column_drop_not_null/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_correct/migrations/0001_initial.py b/tests/test_project/app_correct/migrations/0001_initial.py index 9ce2453..6edadcf 100644 --- a/tests/test_project/app_correct/migrations/0001_initial.py +++ b/tests/test_project/app_correct/migrations/0001_initial.py @@ -1,5 +1,7 @@ # Generated by Django 2.1.4 on 2019-03-19 21:08 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_correct/migrations/0002_foo.py b/tests/test_project/app_correct/migrations/0002_foo.py index 0ccdf22..567d794 100644 --- a/tests/test_project/app_correct/migrations/0002_foo.py +++ b/tests/test_project/app_correct/migrations/0002_foo.py @@ -1,5 +1,7 @@ # Generated by Django 2.1.4 on 2019-03-19 21:08 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_correct/models.py b/tests/test_project/app_correct/models.py index 2382369..21f732f 100644 --- a/tests/test_project/app_correct/models.py +++ b/tests/test_project/app_correct/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_create_table_with_not_null_column/migrations/0001_initial.py b/tests/test_project/app_create_table_with_not_null_column/migrations/0001_initial.py index fcb3211..6ecddc3 100644 --- a/tests/test_project/app_create_table_with_not_null_column/migrations/0001_initial.py +++ b/tests/test_project/app_create_table_with_not_null_column/migrations/0001_initial.py @@ -1,6 +1,8 @@ # Generated by Django 1.11.18 on 2019-04-14 14:59 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_create_table_with_not_null_column/models.py b/tests/test_project/app_create_table_with_not_null_column/models.py index f6d4b02..a272755 100644 --- a/tests/test_project/app_create_table_with_not_null_column/models.py +++ b/tests/test_project/app_create_table_with_not_null_column/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_data_migrations/migrations/0001_initial.py b/tests/test_project/app_data_migrations/migrations/0001_initial.py index b62d558..22c666b 100644 --- a/tests/test_project/app_data_migrations/migrations/0001_initial.py +++ b/tests/test_project/app_data_migrations/migrations/0001_initial.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2020-01-26 17:43 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_data_migrations/migrations/0002_missing_reverse.py b/tests/test_project/app_data_migrations/migrations/0002_missing_reverse.py index 0913d82..3f22a8f 100644 --- a/tests/test_project/app_data_migrations/migrations/0002_missing_reverse.py +++ b/tests/test_project/app_data_migrations/migrations/0002_missing_reverse.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2020-01-26 17:43 +from __future__ import annotations + from django.db import migrations diff --git a/tests/test_project/app_data_migrations/migrations/0003_incorrect_arguments.py b/tests/test_project/app_data_migrations/migrations/0003_incorrect_arguments.py index 4993201..b93b465 100644 --- a/tests/test_project/app_data_migrations/migrations/0003_incorrect_arguments.py +++ b/tests/test_project/app_data_migrations/migrations/0003_incorrect_arguments.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2020-01-26 17:47 +from __future__ import annotations + from django.db import migrations diff --git a/tests/test_project/app_data_migrations/models.py b/tests/test_project/app_data_migrations/models.py index 9ca985e..e34b568 100644 --- a/tests/test_project/app_data_migrations/models.py +++ b/tests/test_project/app_data_migrations/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_drop_column/migrations/0001_initial.py b/tests/test_project/app_drop_column/migrations/0001_initial.py index c0d4bfc..0d7889e 100644 --- a/tests/test_project/app_drop_column/migrations/0001_initial.py +++ b/tests/test_project/app_drop_column/migrations/0001_initial.py @@ -1,6 +1,8 @@ # Generated by Django 1.11.18 on 2019-04-14 14:59 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_drop_column/migrations/0002_remove_a_field_b.py b/tests/test_project/app_drop_column/migrations/0002_remove_a_field_b.py index 990bd3c..619b790 100644 --- a/tests/test_project/app_drop_column/migrations/0002_remove_a_field_b.py +++ b/tests/test_project/app_drop_column/migrations/0002_remove_a_field_b.py @@ -1,6 +1,8 @@ # Generated by Django 1.11.18 on 2019-04-14 14:59 +from __future__ import annotations + from django.db import migrations diff --git a/tests/test_project/app_drop_column/models.py b/tests/test_project/app_drop_column/models.py index 9983b1d..d62e9ba 100644 --- a/tests/test_project/app_drop_column/models.py +++ b/tests/test_project/app_drop_column/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_drop_table/migrations/0001_initial.py b/tests/test_project/app_drop_table/migrations/0001_initial.py index a071e60..521446b 100644 --- a/tests/test_project/app_drop_table/migrations/0001_initial.py +++ b/tests/test_project/app_drop_table/migrations/0001_initial.py @@ -1,6 +1,8 @@ # Generated by Django 1.11.18 on 2019-04-14 14:59 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_drop_table/migrations/0002_delete_a.py b/tests/test_project/app_drop_table/migrations/0002_delete_a.py index 1acf99c..0163403 100644 --- a/tests/test_project/app_drop_table/migrations/0002_delete_a.py +++ b/tests/test_project/app_drop_table/migrations/0002_delete_a.py @@ -1,5 +1,7 @@ # Generated by Django 2.1.10 on 2019-07-19 09:50 +from __future__ import annotations + from django.db import migrations diff --git a/tests/test_project/app_drop_table/models.py b/tests/test_project/app_drop_table/models.py index 6dcef22..0e64a1d 100644 --- a/tests/test_project/app_drop_table/models.py +++ b/tests/test_project/app_drop_table/models.py @@ -1,4 +1,4 @@ -from django.db import models +# from django.db import models # class A(models.Model): # field_a = models.IntegerField() diff --git a/tests/test_project/app_ignore_migration/migrations/0001_initial.py b/tests/test_project/app_ignore_migration/migrations/0001_initial.py index d3dcbd7..72f4b57 100644 --- a/tests/test_project/app_ignore_migration/migrations/0001_initial.py +++ b/tests/test_project/app_ignore_migration/migrations/0001_initial.py @@ -1,5 +1,7 @@ # Generated by Django 2.1.4 on 2019-03-21 20:40 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_ignore_migration/migrations/0002_ignore_migration.py b/tests/test_project/app_ignore_migration/migrations/0002_ignore_migration.py index 11a37bf..3f15839 100644 --- a/tests/test_project/app_ignore_migration/migrations/0002_ignore_migration.py +++ b/tests/test_project/app_ignore_migration/migrations/0002_ignore_migration.py @@ -1,5 +1,7 @@ # Generated by Django 2.1.4 on 2019-03-21 20:41 +from __future__ import annotations + from django.db import migrations, models import django_migration_linter as linter diff --git a/tests/test_project/app_ignore_migration/models.py b/tests/test_project/app_ignore_migration/models.py index 950aae0..9d3e055 100644 --- a/tests/test_project/app_ignore_migration/models.py +++ b/tests/test_project/app_ignore_migration/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_make_not_null_with_django_default/migrations/0001_initial.py b/tests/test_project/app_make_not_null_with_django_default/migrations/0001_initial.py index 62f9610..d462d87 100644 --- a/tests/test_project/app_make_not_null_with_django_default/migrations/0001_initial.py +++ b/tests/test_project/app_make_not_null_with_django_default/migrations/0001_initial.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2020-06-20 15:17 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_make_not_null_with_django_default/migrations/0002_make_not_null_with_django_default.py b/tests/test_project/app_make_not_null_with_django_default/migrations/0002_make_not_null_with_django_default.py index c8cb410..60241b7 100644 --- a/tests/test_project/app_make_not_null_with_django_default/migrations/0002_make_not_null_with_django_default.py +++ b/tests/test_project/app_make_not_null_with_django_default/migrations/0002_make_not_null_with_django_default.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2020-06-20 15:17 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_make_not_null_with_django_default/models.py b/tests/test_project/app_make_not_null_with_django_default/models.py index f4000fa..153e85b 100644 --- a/tests/test_project/app_make_not_null_with_django_default/models.py +++ b/tests/test_project/app_make_not_null_with_django_default/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_make_not_null_with_lib_default/migrations/0001_initial.py b/tests/test_project/app_make_not_null_with_lib_default/migrations/0001_initial.py index 5e20102..ec2ca59 100644 --- a/tests/test_project/app_make_not_null_with_lib_default/migrations/0001_initial.py +++ b/tests/test_project/app_make_not_null_with_lib_default/migrations/0001_initial.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2020-06-20 15:23 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_make_not_null_with_lib_default/migrations/0002_make_not_null_with_lib_default.py b/tests/test_project/app_make_not_null_with_lib_default/migrations/0002_make_not_null_with_lib_default.py index 35d842f..84e51d0 100644 --- a/tests/test_project/app_make_not_null_with_lib_default/migrations/0002_make_not_null_with_lib_default.py +++ b/tests/test_project/app_make_not_null_with_lib_default/migrations/0002_make_not_null_with_lib_default.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2020-06-20 15:23 +from __future__ import annotations + from django.db import migrations, models from django_add_default_value import AddDefaultValue diff --git a/tests/test_project/app_make_not_null_with_lib_default/models.py b/tests/test_project/app_make_not_null_with_lib_default/models.py index a6acbc5..20ac670 100644 --- a/tests/test_project/app_make_not_null_with_lib_default/models.py +++ b/tests/test_project/app_make_not_null_with_lib_default/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_rename_column/migrations/0001_initial.py b/tests/test_project/app_rename_column/migrations/0001_initial.py index 25f8dcd..32cb8cb 100644 --- a/tests/test_project/app_rename_column/migrations/0001_initial.py +++ b/tests/test_project/app_rename_column/migrations/0001_initial.py @@ -1,6 +1,8 @@ # Generated by Django 1.11.18 on 2019-04-14 15:02 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_rename_column/migrations/0002_auto_20190414_1502.py b/tests/test_project/app_rename_column/migrations/0002_auto_20190414_1502.py index b7a0a4c..6c5b3b7 100644 --- a/tests/test_project/app_rename_column/migrations/0002_auto_20190414_1502.py +++ b/tests/test_project/app_rename_column/migrations/0002_auto_20190414_1502.py @@ -1,6 +1,8 @@ # Generated by Django 1.11.18 on 2019-04-14 15:02 +from __future__ import annotations + from django.db import migrations diff --git a/tests/test_project/app_rename_column/models.py b/tests/test_project/app_rename_column/models.py index a7164cd..561e458 100644 --- a/tests/test_project/app_rename_column/models.py +++ b/tests/test_project/app_rename_column/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_rename_table/migrations/0001_initial.py b/tests/test_project/app_rename_table/migrations/0001_initial.py index 981ff33..62f9acd 100644 --- a/tests/test_project/app_rename_table/migrations/0001_initial.py +++ b/tests/test_project/app_rename_table/migrations/0001_initial.py @@ -1,6 +1,8 @@ # Generated by Django 1.11.18 on 2019-04-14 15:00 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_rename_table/migrations/0002_auto_20190414_1500.py b/tests/test_project/app_rename_table/migrations/0002_auto_20190414_1500.py index 9d4ac65..5ae4cf2 100644 --- a/tests/test_project/app_rename_table/migrations/0002_auto_20190414_1500.py +++ b/tests/test_project/app_rename_table/migrations/0002_auto_20190414_1500.py @@ -1,6 +1,8 @@ # Generated by Django 1.11.18 on 2019-04-14 15:00 +from __future__ import annotations + from django.db import migrations diff --git a/tests/test_project/app_rename_table/models.py b/tests/test_project/app_rename_table/models.py index 19deeff..14c3e5e 100644 --- a/tests/test_project/app_rename_table/models.py +++ b/tests/test_project/app_rename_table/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/app_unique_together/migrations/0001_initial.py b/tests/test_project/app_unique_together/migrations/0001_initial.py index 68c5d91..eaf9c1a 100644 --- a/tests/test_project/app_unique_together/migrations/0001_initial.py +++ b/tests/test_project/app_unique_together/migrations/0001_initial.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2019-07-29 21:21 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/app_unique_together/migrations/0002_auto_20190729_2122.py b/tests/test_project/app_unique_together/migrations/0002_auto_20190729_2122.py index 0786991..465730a 100644 --- a/tests/test_project/app_unique_together/migrations/0002_auto_20190729_2122.py +++ b/tests/test_project/app_unique_together/migrations/0002_auto_20190729_2122.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2019-07-29 21:22 +from __future__ import annotations + from django.db import migrations diff --git a/tests/test_project/app_unique_together/migrations/0003_auto_20190729_2122.py b/tests/test_project/app_unique_together/migrations/0003_auto_20190729_2122.py index e9307f2..2c5778f 100644 --- a/tests/test_project/app_unique_together/migrations/0003_auto_20190729_2122.py +++ b/tests/test_project/app_unique_together/migrations/0003_auto_20190729_2122.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2019-07-29 21:22 +from __future__ import annotations + from django.db import migrations diff --git a/tests/test_project/app_unique_together/models.py b/tests/test_project/app_unique_together/models.py index 917cb06..42adf61 100644 --- a/tests/test_project/app_unique_together/models.py +++ b/tests/test_project/app_unique_together/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/makemigrations_backward_incompatible_migration_missing/migrations/0001_initial.py b/tests/test_project/makemigrations_backward_incompatible_migration_missing/migrations/0001_initial.py index ff05a7c..da756e3 100644 --- a/tests/test_project/makemigrations_backward_incompatible_migration_missing/migrations/0001_initial.py +++ b/tests/test_project/makemigrations_backward_incompatible_migration_missing/migrations/0001_initial.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2020-06-26 17:49 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/makemigrations_backward_incompatible_migration_missing/models.py b/tests/test_project/makemigrations_backward_incompatible_migration_missing/models.py index 3ffc3e6..88b5f7e 100644 --- a/tests/test_project/makemigrations_backward_incompatible_migration_missing/models.py +++ b/tests/test_project/makemigrations_backward_incompatible_migration_missing/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/makemigrations_correct_migration_missing/migrations/0001_initial.py b/tests/test_project/makemigrations_correct_migration_missing/migrations/0001_initial.py index 9aea7ed..54bab96 100644 --- a/tests/test_project/makemigrations_correct_migration_missing/migrations/0001_initial.py +++ b/tests/test_project/makemigrations_correct_migration_missing/migrations/0001_initial.py @@ -1,5 +1,7 @@ # Generated by Django 2.2 on 2020-06-26 15:57 +from __future__ import annotations + from django.db import migrations, models diff --git a/tests/test_project/makemigrations_correct_migration_missing/models.py b/tests/test_project/makemigrations_correct_migration_missing/models.py index 101fd38..c15040f 100644 --- a/tests/test_project/makemigrations_correct_migration_missing/models.py +++ b/tests/test_project/makemigrations_correct_migration_missing/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models diff --git a/tests/test_project/settings.py b/tests/test_project/settings.py index 53d9c8b..271dc4c 100644 --- a/tests/test_project/settings.py +++ b/tests/test_project/settings.py @@ -10,11 +10,11 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.1/ref/settings/ """ +from __future__ import annotations + import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) -from collections import OrderedDict - BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/tests/test_project/urls.py b/tests/test_project/urls.py index 2dee2b5..7e3d7c2 100644 --- a/tests/test_project/urls.py +++ b/tests/test_project/urls.py @@ -13,6 +13,8 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from __future__ import annotations + from django.contrib import admin from django.urls import path diff --git a/tests/test_project/wsgi.py b/tests/test_project/wsgi.py index 9c142be..4aecd9e 100644 --- a/tests/test_project/wsgi.py +++ b/tests/test_project/wsgi.py @@ -7,6 +7,8 @@ For more information on this file, see https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ """ +from __future__ import annotations + import os from django.core.wsgi import get_wsgi_application diff --git a/tests/unit/test_linter.py b/tests/unit/test_linter.py index 084cbd1..8c2b57b 100644 --- a/tests/unit/test_linter.py +++ b/tests/unit/test_linter.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import tempfile import unittest diff --git a/tests/unit/test_sql_analyser.py b/tests/unit/test_sql_analyser.py index e673441..d7349fc 100644 --- a/tests/unit/test_sql_analyser.py +++ b/tests/unit/test_sql_analyser.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import unittest from django_migration_linter.sql_analyser import ( @@ -28,9 +30,9 @@ class SqlAnalyserTestCase(unittest.TestCase): self.assertNotEqual(0, len(errors), "Found no errors in sql") if code: self.assertTrue( - any(err["code"] == code for err in errors), + any(err.code == code for err in errors), "Didn't find error code {} in returned errors ({})".format( - code, [err["code"] for err in errors] + code, [err.code for err in errors] ), ) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 2556a47..733b5c8 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import unittest from django_migration_linter.utils import split_migration_path, split_path diff --git a/tox.ini b/tox.ini index abfc3a1..7505e74 100644 --- a/tox.ini +++ b/tox.ini @@ -32,6 +32,8 @@ commands = flake8 django_migration_linter/ black --check django_migration_linter/ tests/ manage.py setup.py isort --check-only . + mypy django_migration_linter/ + ruff django_migration_linter/ [testenv:coverage] commands =