Last active
September 6, 2024 21:31
-
-
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
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
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