Mini Shell
# 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,
)
Zerion Mini Shell 1.0