Viewing file: migrations.py (6.28 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
# Copyright (c) 2018, 2020 Alexander Todorov <atodorov@MrSenko.com> # Copyright (c) 2020 Bryan Mutai <mutaiwork@gmail.com>
# Licensed under the GPL 2.0: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint-django/blob/master/LICENSE """ Various suggestions around migrations. Disabled by default! Enable with pylint --load-plugins=pylint_django.checkers.migrations """
import astroid from pylint import checkers, interfaces from pylint.checkers import utils from pylint_plugin_utils import suppress_message
from pylint_django import compat from pylint_django.__pkginfo__ import BASE_ID from pylint_django.utils import is_migrations_module
def _is_addfield_with_default(call): if not isinstance(call.func, astroid.Attribute): return False
if not call.func.attrname == "AddField": return False
for keyword in call.keywords: # looking for AddField(..., field=XXX(..., default=Y, ...), ...) if keyword.arg == "field" and isinstance(keyword.value, astroid.Call): # loop over XXX's keywords # NOTE: not checking if XXX is an actual field type because there could # be many types we're not aware of. Also the migration will probably break # if XXX doesn't instantiate a field object! for field_keyword in keyword.value.keywords: if field_keyword.arg == "default": return True
return False
class NewDbFieldWithDefaultChecker(checkers.BaseChecker): """ Looks for migrations which add new model fields and these fields have a default value. According to Django docs this may have performance penalties especially on large tables: https://docs.djangoproject.com/en/2.0/topics/migrations/#postgresql
The preferred way is to add a new DB column with null=True because it will be created instantly and then possibly populate the table with the desired default values. """
__implements__ = (interfaces.IAstroidChecker,)
# configuration section name name = "new-db-field-with-default" msgs = { f"W{BASE_ID}98": ( "%s AddField with default value", "new-db-field-with-default", "Used when Pylint detects migrations adding new fields with a default value.", ) }
_migration_modules = [] _possible_offences = {}
def visit_module(self, node): if is_migrations_module(node): self._migration_modules.append(node)
def visit_call(self, node): try: module = node.frame().parent except: # noqa: E722, pylint: disable=bare-except return
if not is_migrations_module(module): return
if _is_addfield_with_default(node): if module not in self._possible_offences: self._possible_offences[module] = []
if node not in self._possible_offences[module]: self._possible_offences[module].append(node)
@utils.check_messages("new-db-field-with-default") def close(self): def _path(node): return node.path
# sort all migrations by name in reverse order b/c # we need only the latest ones self._migration_modules.sort(key=_path, reverse=True)
# filter out the last migration modules under each distinct # migrations directory, iow leave only the latest migrations # for each application last_name_space = "" latest_migrations = [] for module in self._migration_modules: name_space = module.path[0].split("migrations")[0] if name_space != last_name_space: last_name_space = name_space latest_migrations.append(module)
for module, nodes in self._possible_offences.items(): if module in latest_migrations: for node in nodes: self.add_message("new-db-field-with-default", args=module.name, node=node)
class MissingBackwardsMigrationChecker(checkers.BaseChecker): __implements__ = (interfaces.IAstroidChecker,)
name = "missing-backwards-migration-callable"
msgs = { f"W{BASE_ID}97": ( "Always include backwards migration callable", "missing-backwards-migration-callable", "Always include a backwards/reverse callable counterpart so that the migration is not irreversable.", ) }
@utils.check_messages("missing-backwards-migration-callable") def visit_call(self, node): try: module = node.frame().parent except: # noqa: E722, pylint: disable=bare-except return
if not is_migrations_module(module): return
if node.func.as_string().endswith("RunPython") and len(node.args) < 2: if node.keywords: for keyword in node.keywords: if keyword.arg == "reverse_code": return self.add_message("missing-backwards-migration-callable", node=node) else: self.add_message("missing-backwards-migration-callable", node=node)
def is_in_migrations(node): """ RunPython() migrations receive forward/backwards functions with signature:
def func(apps, schema_editor):
which could be unused. This augmentation will suppress all 'unused-argument' messages coming from functions in migration modules. """ return is_migrations_module(node.parent)
def load_configuration(linter): # TODO this is redundant and can be removed # don't blacklist migrations for this checker new_black_list = list(linter.config.black_list) if "migrations" in new_black_list: new_black_list.remove("migrations") linter.config.black_list = new_black_list
def register(linter): """Required method to auto register this checker.""" linter.register_checker(NewDbFieldWithDefaultChecker(linter)) linter.register_checker(MissingBackwardsMigrationChecker(linter)) if not compat.LOAD_CONFIGURATION_SUPPORTED: load_configuration(linter)
# apply augmentations for migration checkers # Unused arguments for migrations suppress_message( linter, checkers.variables.VariablesChecker.leave_functiondef, "unused-argument", is_in_migrations, )
|