diff --git a/netbox_custom_objects/__init__.py b/netbox_custom_objects/__init__.py index dc4cdc1..764f551 100644 --- a/netbox_custom_objects/__init__.py +++ b/netbox_custom_objects/__init__.py @@ -18,6 +18,36 @@ def is_running_migration(): return False +def is_in_clear_cache(): + """ + Check if the code is currently being called from Django's clear_cache() method. + + TODO: This is fairly ugly, but in models.CustomObjectType.get_model() we call + meta = type() which calls clear_cache on the model which causes a call to + get_models() which in-turn calls get_model and therefore recurses. + + This catches the specific case of a recursive call to get_models() from + clear_cache() which is the only case we care about, so should be relatively + safe. An alternative should be found for this. + """ + import inspect + + frame = inspect.currentframe() + try: + # Walk up the call stack to see if we're being called from clear_cache + while frame: + if ( + frame.f_code.co_name == "clear_cache" + and "django/apps/registry.py" in frame.f_code.co_filename + ): + return True + frame = frame.f_back + return False + finally: + # Clean up the frame reference + del frame + + def check_custom_object_type_table_exists(): """ Check if the CustomObjectType table exists in the database. @@ -49,6 +79,7 @@ class CustomObjectsPluginConfig(PluginConfig): default_settings = {} required_settings = [] template_extensions = "template_content.template_extensions" + _in_get_models = False # Recursion guard def get_model(self, model_name, require_ready=True): try: @@ -84,33 +115,45 @@ def get_model(self, model_name, require_ready=True): def get_models(self, include_auto_created=False, include_swapped=False): """Return all models for this plugin, including custom object type models.""" + # Get the regular Django models first for model in super().get_models(include_auto_created, include_swapped): yield model - # Suppress warnings about database calls during model loading - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", category=RuntimeWarning, message=".*database.*" - ) - warnings.filterwarnings( - "ignore", category=UserWarning, message=".*database.*" - ) + # Prevent recursion + if self._in_get_models and is_in_clear_cache(): + # Skip dynamic model creation if we're in a recursive get_models call + return - # Skip custom object type model loading if running during migration - if is_running_migration() or not check_custom_object_type_table_exists(): - return - - # Add custom object type models - from .models import CustomObjectType - - custom_object_types = CustomObjectType.objects.all() - for custom_type in custom_object_types: - # Only yield already cached models during discovery - if CustomObjectType.is_model_cached(custom_type.id): - model = CustomObjectType.get_cached_model(custom_type.id) + self._in_get_models = True + try: + # Suppress warnings about database calls during model loading + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=RuntimeWarning, message=".*database.*" + ) + warnings.filterwarnings( + "ignore", category=UserWarning, message=".*database.*" + ) + + # Skip custom object type model loading if running during migration + if ( + is_running_migration() + or not check_custom_object_type_table_exists() + ): + return + + # Add custom object type models + from .models import CustomObjectType + + custom_object_types = CustomObjectType.objects.all() + for custom_type in custom_object_types: + model = custom_type.get_model() if model: yield model + finally: + # Clean up the recursion guard + self._in_get_models = False config = CustomObjectsPluginConfig