Skip to content

Commit 5f0fef4

Browse files
brymutatodorov
authored andcommitted
Refs kiwitcms/Kiwi#1774 New linter: warn about missing backwards migrations
1 parent 4d606f6 commit 5f0fef4

File tree

6 files changed

+90
-9
lines changed

6 files changed

+90
-9
lines changed

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@
1515
* [psrb](https://github.com/psrb)
1616
* [WayneLambert](https://github.com/WayneLambert)
1717
* [alejandro-angulo](https://github.com/alejandro-angulo)
18+
* [brymut](https://github.com/brymut)
1819

pylint_django/checkers/db_performance.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pylint.checkers import utils
1515
from pylint_django.__pkginfo__ import BASE_ID
1616
from pylint_django import compat
17+
from pylint_django.utils import is_migrations_module
1718

1819

1920
def _is_addfield_with_default(call):
@@ -37,13 +38,6 @@ def _is_addfield_with_default(call):
3738
return False
3839

3940

40-
def _is_migrations_module(node):
41-
if not isinstance(node, astroid.Module):
42-
return False
43-
44-
return 'migrations' in node.path[0] and not node.path[0].endswith('__init__.py')
45-
46-
4741
class NewDbFieldWithDefaultChecker(checkers.BaseChecker):
4842
"""
4943
Looks for migrations which add new model fields and these fields have a
@@ -69,7 +63,7 @@ class NewDbFieldWithDefaultChecker(checkers.BaseChecker):
6963
_possible_offences = {}
7064

7165
def visit_module(self, node):
72-
if _is_migrations_module(node):
66+
if is_migrations_module(node):
7367
self._migration_modules.append(node)
7468

7569
def visit_call(self, node):
@@ -78,7 +72,7 @@ def visit_call(self, node):
7872
except: # noqa: E722, pylint: disable=bare-except
7973
return
8074

81-
if not _is_migrations_module(module):
75+
if not is_migrations_module(module):
8276
return
8377

8478
if _is_addfield_with_default(node):
@@ -114,6 +108,38 @@ def _path(node):
114108
self.add_message('new-db-field-with-default', args=module.name, node=node)
115109

116110

111+
class MissingBackwardsMigrationChecker(checkers.BaseChecker):
112+
__implements__ = (interfaces.IAstroidChecker,)
113+
114+
name = 'missing-backwards-migration-callable'
115+
116+
msgs = {'W%s05' % BASE_ID: ('%s Always include backwards migration callable',
117+
'missing-backwards-migration-callable',
118+
'Always include a backwards/reverse callable counterpart'
119+
' so that the migration is not irreversable.')}
120+
121+
@utils.check_messages('missing-backwards-migration-callable')
122+
def visit_call(self, node):
123+
try:
124+
module = node.frame().parent
125+
except: # noqa: E722, pylint: disable=bare-except
126+
return
127+
128+
if not is_migrations_module(module):
129+
return
130+
131+
if node.func.as_string().endswith('RunPython') and len(node.args) < 2:
132+
if node.keywords:
133+
for keyword in node.keywords:
134+
if keyword.arg == 'reverse_code':
135+
return
136+
self.add_message('missing-backwards-migration-callable',
137+
args=module.name, node=node)
138+
else:
139+
self.add_message('missing-backwards-migration-callable',
140+
args=module.name, node=node)
141+
142+
117143
def load_configuration(linter):
118144
# don't blacklist migrations for this checker
119145
new_black_list = list(linter.config.black_list)
@@ -125,5 +151,6 @@ def load_configuration(linter):
125151
def register(linter):
126152
"""Required method to auto register this checker."""
127153
linter.register_checker(NewDbFieldWithDefaultChecker(linter))
154+
linter.register_checker(MissingBackwardsMigrationChecker(linter))
128155
if not compat.LOAD_CONFIGURATION_SUPPORTED:
129156
load_configuration(linter)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# pylint: disable=missing-docstring, invalid-name
2+
from django.db import migrations
3+
4+
5+
# pylint: disable=unused-argument
6+
def forwards_test(apps, schema_editor):
7+
pass
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
operations = [
13+
migrations.RunPython(), # [missing-backwards-migration-callable]
14+
migrations.RunPython( # [missing-backwards-migration-callable]
15+
forwards_test),
16+
migrations.RunPython( # [missing-backwards-migration-callable]
17+
code=forwards_test),
18+
migrations.RunPython( # [missing-backwards-migration-callable]
19+
code=forwards_test, atomic=False)
20+
]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
missing-backwards-migration-callable:13:Migration:pylint_django.tests.input.migrations.0003_without_backwards Always include backwards migration callable
2+
missing-backwards-migration-callable:14:Migration:pylint_django.tests.input.migrations.0003_without_backwards Always include backwards migration callable
3+
missing-backwards-migration-callable:16:Migration:pylint_django.tests.input.migrations.0003_without_backwards Always include backwards migration callable
4+
missing-backwards-migration-callable:18:Migration:pylint_django.tests.input.migrations.0003_without_backwards Always include backwards migration callable
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# pylint: disable=missing-docstring, invalid-name
2+
from django.db import migrations
3+
4+
5+
# pylint: disable=unused-argument
6+
def forwards_test(apps, schema_editor):
7+
pass
8+
9+
10+
# pylint: disable=unused-argument
11+
def backwards_test(apps, schema_editor):
12+
pass
13+
14+
15+
class Migration(migrations.Migration):
16+
17+
operations = [
18+
migrations.RunPython(forwards_test, backwards_test),
19+
migrations.RunPython(forwards_test, reverse_code=backwards_test),
20+
migrations.RunPython(code=forwards_test, reverse_code=backwards_test)
21+
]

pylint_django/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Utils."""
22
import sys
3+
import astroid
34

45
from astroid.bases import Instance
56
from astroid.exceptions import InferenceError
@@ -30,3 +31,10 @@ def node_is_subclass(cls, *subclass_names):
3031
continue
3132

3233
return False
34+
35+
36+
def is_migrations_module(node):
37+
if not isinstance(node, astroid.Module):
38+
return False
39+
40+
return 'migrations' in node.path[0] and not node.path[0].endswith('__init__.py')

0 commit comments

Comments
 (0)