فهرست منبع

Merge pull request #4813 from glennmatthews/gfm-issue-4805

Don't ignore ImportErrors raised when loading a plugin. Fixes #4805
Jeremy Stretch 5 سال پیش
والد
کامیت
9078cb29cc
4فایلهای تغییر یافته به همراه51 افزوده شده و 23 حذف شده
  1. 6 9
      netbox/extras/plugins/__init__.py
  2. 6 9
      netbox/extras/plugins/urls.py
  3. 33 0
      netbox/extras/plugins/utils.py
  4. 6 5
      netbox/extras/plugins/views.py

+ 6 - 9
netbox/extras/plugins/__init__.py

@@ -6,11 +6,12 @@ from django.apps import AppConfig
 from django.conf import settings
 from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.core.exceptions import ImproperlyConfigured
 from django.template.loader import get_template
 from django.template.loader import get_template
-from django.utils.module_loading import import_string
 
 
 from extras.registry import registry
 from extras.registry import registry
 from utilities.choices import ButtonColorChoices
 from utilities.choices import ButtonColorChoices
 
 
+from extras.plugins.utils import import_object
+
 
 
 # Initialize plugin registry stores
 # Initialize plugin registry stores
 registry['plugin_template_extensions'] = collections.defaultdict(list)
 registry['plugin_template_extensions'] = collections.defaultdict(list)
@@ -60,18 +61,14 @@ class PluginConfig(AppConfig):
     def ready(self):
     def ready(self):
 
 
         # Register template content
         # Register template content
-        try:
-            template_extensions = import_string(f"{self.__module__}.{self.template_extensions}")
+        template_extensions = import_object(f"{self.__module__}.{self.template_extensions}")
+        if template_extensions is not None:
             register_template_extensions(template_extensions)
             register_template_extensions(template_extensions)
-        except ImportError:
-            pass
 
 
         # Register navigation menu items (if defined)
         # Register navigation menu items (if defined)
-        try:
-            menu_items = import_string(f"{self.__module__}.{self.menu_items}")
+        menu_items = import_object(f"{self.__module__}.{self.menu_items}")
+        if menu_items is not None:
             register_menu_items(self.verbose_name, menu_items)
             register_menu_items(self.verbose_name, menu_items)
-        except ImportError:
-            pass
 
 
     @classmethod
     @classmethod
     def validate(cls, user_config):
     def validate(cls, user_config):

+ 6 - 9
netbox/extras/plugins/urls.py

@@ -3,7 +3,8 @@ from django.conf import settings
 from django.conf.urls import include
 from django.conf.urls import include
 from django.contrib.admin.views.decorators import staff_member_required
 from django.contrib.admin.views.decorators import staff_member_required
 from django.urls import path
 from django.urls import path
-from django.utils.module_loading import import_string
+
+from extras.plugins.utils import import_object
 
 
 from . import views
 from . import views
 
 
@@ -24,19 +25,15 @@ for plugin_path in settings.PLUGINS:
     base_url = getattr(app, 'base_url') or app.label
     base_url = getattr(app, 'base_url') or app.label
 
 
     # Check if the plugin specifies any base URLs
     # Check if the plugin specifies any base URLs
-    try:
-        urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns")
+    urlpatterns = import_object(f"{plugin_path}.urls.urlpatterns")
+    if urlpatterns is not None:
         plugin_patterns.append(
         plugin_patterns.append(
             path(f"{base_url}/", include((urlpatterns, app.label)))
             path(f"{base_url}/", include((urlpatterns, app.label)))
         )
         )
-    except ImportError:
-        pass
 
 
     # Check if the plugin specifies any API URLs
     # Check if the plugin specifies any API URLs
-    try:
-        urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns")
+    urlpatterns = import_object(f"{plugin_path}.api.urls.urlpatterns")
+    if urlpatterns is not None:
         plugin_api_patterns.append(
         plugin_api_patterns.append(
             path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
             path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
         )
         )
-    except ImportError:
-        pass

+ 33 - 0
netbox/extras/plugins/utils.py

@@ -0,0 +1,33 @@
+import importlib.util
+import sys
+
+
+def import_object(module_and_object):
+    """
+    Import a specific object from a specific module by name, such as "extras.plugins.utils.import_object".
+
+    Returns the imported object, or None if it doesn't exist.
+    """
+    target_module_name, object_name = module_and_object.rsplit('.', 1)
+    module_hierarchy = target_module_name.split('.')
+
+    # Iterate through the module hierarchy, checking for the existence of each successive submodule.
+    # We have to do this rather than jumping directly to calling find_spec(target_module_name)
+    # because find_spec will raise a ModuleNotFoundError if any parent module of target_module_name does not exist.
+    module_name = ""
+    for module_component in module_hierarchy:
+        module_name = f"{module_name}.{module_component}" if module_name else module_component
+        spec = importlib.util.find_spec(module_name)
+        if spec is None:
+            # No such module
+            return None
+
+    # Okay, target_module_name exists. Load it if not already loaded
+    if target_module_name in sys.modules:
+        module = sys.modules[target_module_name]
+    else:
+        module = importlib.util.module_from_spec(spec)
+        sys.modules[target_module_name] = module
+        spec.loader.exec_module(module)
+
+    return getattr(module, object_name, None)

+ 6 - 5
netbox/extras/plugins/views.py

@@ -4,13 +4,14 @@ from django.apps import apps
 from django.conf import settings
 from django.conf import settings
 from django.shortcuts import render
 from django.shortcuts import render
 from django.urls.exceptions import NoReverseMatch
 from django.urls.exceptions import NoReverseMatch
-from django.utils.module_loading import import_string
 from django.views.generic import View
 from django.views.generic import View
 from rest_framework import permissions
 from rest_framework import permissions
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.reverse import reverse
 from rest_framework.reverse import reverse
 from rest_framework.views import APIView
 from rest_framework.views import APIView
 
 
+from extras.plugins.utils import import_object
+
 
 
 class InstalledPluginsAdminView(View):
 class InstalledPluginsAdminView(View):
     """
     """
@@ -60,9 +61,9 @@ class PluginsAPIRootView(APIView):
 
 
     @staticmethod
     @staticmethod
     def _get_plugin_entry(plugin, app_config, request, format):
     def _get_plugin_entry(plugin, app_config, request, format):
-        try:
-            api_app_name = import_string(f"{plugin}.api.urls.app_name")
-        except (ImportError, ModuleNotFoundError):
+        # Check if the plugin specifies any API URLs
+        api_app_name = import_object(f"{plugin}.api.urls.app_name")
+        if api_app_name is None:
             # Plugin does not expose an API
             # Plugin does not expose an API
             return None
             return None
 
 
@@ -73,7 +74,7 @@ class PluginsAPIRootView(APIView):
                 format=format
                 format=format
             ))
             ))
         except NoReverseMatch:
         except NoReverseMatch:
-            # The plugin does not include an api-root
+            # The plugin does not include an api-root url
             entry = None
             entry = None
 
 
         return entry
         return entry