Skip to content

Instantly share code, notes, and snippets.

@dmwyatt
Last active September 6, 2024 21:31
Show Gist options
  • Save dmwyatt/ae51ae75c2eb611f49eb1e6440456d0e to your computer and use it in GitHub Desktop.
Save dmwyatt/ae51ae75c2eb611f49eb1e6440456d0e to your computer and use it in GitHub Desktop.
Check that Django models inherit from a common base model and that they explictly define db_table
import ast
import inspect
from pathlib import Path
from django.apps import apps
from django.conf import settings
from django.core.checks import Error, register, Tags
from .models import UniversalBaseModel
@register(Tags.models)
def check_model_inheritance(app_configs, **kwargs):
"""Enforce all app models inherit from a common base model.
For example, you may want all models to inherit from a Timestamped abstract model.
"""
errors = []
project_dir = Path(settings.BASE_DIR).resolve()
for app_config in apps.get_app_configs():
app_path = Path(app_config.path).resolve()
# Check if the app is within the project directory
if not app_path.is_relative_to(project_dir) or app_path.parent != project_dir:
continue
for model in app_config.get_models():
if not issubclass(model, UniversalBaseModel):
errors.append(
Error(
f"{model.__name__} from app {app_config.name} does not "
f"inherit from UniversalBaseModel",
hint=f"Ensure that {model.__name__} inherits from "
f"UniversalBaseModel",
obj=model,
id=f"{settings.SITE_NAME}.E001",
)
)
return errors
def find_meta_class(node):
"""Search for a Meta class definition within a given AST node."""
for child in ast.iter_child_nodes(node):
if isinstance(child, ast.ClassDef) and child.name == "Meta":
return child
return None
def check_db_table_in_meta(meta_class):
"""Check if 'db_table' is explicitly defined in the Meta class."""
return (
any(
isinstance(child, ast.Assign)
and any(target.id == "db_table" for target in child.targets)
for child in meta_class.body
)
if meta_class
else False
)
@register()
def check_db_table_defined(app_configs, **kwargs):
"""Enforce all project models explicitly define a db table name."""
errors = []
project_dir = Path(settings.BASE_DIR).resolve()
parsed_files = {} # Cache to store parsed ASTs by file path
# Loop through all installed apps
for app_config in apps.get_app_configs():
app_path = Path(app_config.path).resolve()
# Check if the app is within the project directory and directly under it
if not app_path.is_relative_to(project_dir) or app_path.parent != project_dir:
continue
# Analyze each model class
for model_class in app_config.get_models():
try:
module_file = Path(inspect.getsourcefile(model_class)).resolve()
except TypeError:
# Handle cases where source file is not found
errors.append(
Error(
f"No source file found for the model {model_class._meta.object_name} in app {app_config.label}.",
hint="Ensure the model's module is accessible and not generated dynamically or obscured in a way that prevents file inspection.",
obj=model_class,
id="myapp.E003",
)
)
continue
if module_file not in parsed_files:
try:
parsed_files[module_file] = ast.parse(
module_file.read_text("utf8"), filename=str(module_file)
)
except FileNotFoundError:
# Handle file not found after getting a path
errors.append(
Error(
f"Source file {module_file} not found for the model {model_class._meta.object_name} in app {app_config.label}.",
hint="Check if the file was moved or deleted, and ensure the environment is correctly configured.",
obj=model_class,
id="myapp.E003",
)
)
continue
tree = parsed_files[module_file]
class_name = model_class.__name__
meta_class_found = False
db_table_defined = False
# Search for a class definition that matches the model class name
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef) and node.name == class_name:
meta_class = find_meta_class(node)
if meta_class:
meta_class_found = True
db_table_defined = check_db_table_in_meta(meta_class)
break # Stop after checking the specific class
if not meta_class_found:
errors.append(
Error(
f"The model {model_class._meta.object_name} in app {app_config.label} lacks a Meta class.",
hint="Define a Meta class within your model.",
obj=model_class,
id="myapp.E002",
)
)
elif not db_table_defined:
errors.append(
Error(
f"The model {model_class._meta.object_name} in app {app_config.label} has a Meta class but lacks an explicitly defined 'db_table' attribute.",
hint="Add a 'db_table' attribute to the Meta class of your model.",
obj=model_class,
id="myapp.E001",
)
)
# Sort errors by app label and then by model name
errors.sort(key=lambda e: (e.obj._meta.app_label, e.obj._meta.object_name))
return errors
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment