Skip to content

Instantly share code, notes, and snippets.

@hakib
Last active July 1, 2024 09:20
Show Gist options
  • Save hakib/e2e50d41d19a6984dc63bd94580c8647 to your computer and use it in GitHub Desktop.
Save hakib/e2e50d41d19a6984dc63bd94580c8647 to your computer and use it in GitHub Desktop.
Custom django checks using Django check framework, inspect and ast.
"""
Custom django checks.
H001: Field has no verbose name.
H002: Verbose name should use gettext.
H003: Words in verbose name must be all upper case or all lower case.
H004: Help text should use gettext.
H005: Model must define class Meta.
H006: Model has no verbose name.
H007: Model has no verbose name plural.
H008: Must set db_index explicitly on a ForeignKey field.
"""
import inspect
import ast
import django.apps
import django.core.checks
from django.db.models import FieldDoesNotExist
def get_argument(node, arg):
for kw in node.value.keywords:
if kw.arg == arg:
return kw
return None
def is_gettext_node(node):
if not isinstance(node, ast.Call):
return False
# We assume gettext is aliased '_'.
if not node.func.id == '_': # type: ignore
return False
return True
def check_model(model):
"""Check a single model.
Yields (django.checks.CheckMessage)
"""
model_source = inspect.getsource(model)
model_node = ast.parse(model_source)
assert isinstance(model_node, ast.Module)
class_meta = None
for node in model_node.body[0].body: # type: ignore
if isinstance(node, ast.ClassDef):
# class Meta
if node.name == 'Meta':
class_meta = node
# Fields
elif isinstance(node, ast.Assign):
if len(node.targets) != 1:
continue
if not isinstance(node.targets[0], ast.Name):
continue
field_name = node.targets[0].id
try:
field = model._meta.get_field(field_name)
except FieldDoesNotExist:
continue
verbose_name = get_argument(node, 'verbose_name')
if verbose_name is None:
yield django.core.checks.Warning(
'Field has no verbose name',
hint='Set verbose name on the field.',
obj=field,
id='H001',
)
else:
if not is_gettext_node(verbose_name.value):
yield django.core.checks.Warning(
'Verbose name should use gettext',
hint='Use gettext on the verbose name.',
obj=field,
id='H002',
)
else:
value = verbose_name.value.args[0].s # type: ignore
if not all(w.islower() or w.isupper() or w.isdigit() for w in value.split(' ')):
yield django.core.checks.Warning(
'Words in verbose name must be all upper case or all lower case',
hint='Change verbose name to "{}".'.format(value.lower()),
obj=field,
id='H003',
)
help_text = get_argument(node, 'help_text')
if help_text is not None:
if not is_gettext_node(help_text.value):
yield django.core.checks.Warning(
'Help text should use gettext',
hint='Use gettext on the help text.',
obj=field,
id='H004',
)
if field.many_to_one:
db_index = get_argument(node, 'db_index')
if db_index is None:
yield django.core.checks.Warning(
'Must set db_index explicitly on a ForeignKey field',
hint='Set db_index on the field.',
obj=field,
id='H008',
)
if class_meta is None:
yield django.core.checks.Warning(
'Model "{}" must define class Meta'.format(model._meta.model_name),
hint='Add class Meta to model "{}".'.format(model._meta.model_name),
obj=model,
id='H005',
)
else:
verbose_name = None
verbose_name_plural = None
for node in ast.iter_child_nodes(class_meta):
if not isinstance(node, ast.Assign):
continue
if not isinstance(node.targets[0], ast.Name):
continue
attr = node.targets[0].id
if attr == 'verbose_name':
verbose_name = node
if attr == 'verbose_name_plural':
verbose_name_plural = node
if verbose_name is None:
yield django.core.checks.Warning(
'Model has no verbose name',
hint='Add verbose_name to class Meta.',
obj=model,
id='H006',
)
elif not is_gettext_node(verbose_name.value):
yield django.core.checks.Warning(
'Verbose name in class Meta should use gettext',
hint='Use gettext on the verbose_name of class Meta "{}".'.format(model._meta.model_name),
obj=model,
id='H002',
)
if verbose_name_plural is None:
yield django.core.checks.Warning(
'Model has no verbose name plural',
hint='Add verbose_name_plural to class Meta.',
obj=model,
id='H007',
)
elif not is_gettext_node(verbose_name_plural.value):
yield django.core.checks.Warning(
'Verbose name plural in class Meta should use gettext',
hint='Use gettext on the verbose_name_plural of class Meta "{}".'.format(model._meta.model_name),
obj=model,
id='H002',
)
@django.core.checks.register(django.core.checks.Tags.models)
def check_models(app_configs, **kwargs):
errors = []
for app in django.apps.apps.get_app_configs():
# Skip third party apps.
if app.path.find('site-packages') > -1:
continue
for model in app.get_models():
for check_message in check_model(model):
errors.append(check_message)
return errors
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment