Last active
July 1, 2024 09:20
-
-
Save hakib/e2e50d41d19a6984dc63bd94580c8647 to your computer and use it in GitHub Desktop.
Custom django checks using Django check framework, inspect and ast.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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 |
great! 😄
good job!
Source where it was mentioned: https://hakibenita.com/automating-the-boring-stuff-in-django-using-the-check-framework#putting-it-all-together
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
super nice =)