Glenn Matthews 5 лет назад
Родитель
Сommit
0fd3c83861
4 измененных файлов с 58 добавлено и 64 удалено
  1. 8 20
      netbox/extras/plugins/__init__.py
  2. 12 27
      netbox/extras/plugins/urls.py
  3. 33 0
      netbox/extras/plugins/utils.py
  4. 5 17
      netbox/extras/plugins/views.py

+ 8 - 20
netbox/extras/plugins/__init__.py

@@ -1,7 +1,5 @@
 import collections
 import collections
-import importlib
 import inspect
 import inspect
-import sys
 from packaging import version
 from packaging import version
 
 
 from django.apps import AppConfig
 from django.apps import AppConfig
@@ -12,6 +10,8 @@ from django.template.loader import get_template
 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)
@@ -61,26 +61,14 @@ class PluginConfig(AppConfig):
     def ready(self):
     def ready(self):
 
 
         # Register template content
         # Register template content
-        module, attr = f"{self.__module__}.{self.template_extensions}".rsplit('.', 1)
-        spec = importlib.util.find_spec(module)
-        if spec is not None:
-            template_content = importlib.util.module_from_spec(spec)
-            sys.modules[module] = template_content
-            spec.loader.exec_module(template_content)
-            if hasattr(template_content, attr):
-                template_extensions = getattr(template_content, attr)
-                register_template_extensions(template_extensions)
+        template_extensions = import_object(f"{self.__module__}.{self.template_extensions}")
+        if template_extensions is not None:
+            register_template_extensions(template_extensions)
 
 
         # Register navigation menu items (if defined)
         # Register navigation menu items (if defined)
-        module, attr = f"{self.__module__}.{self.menu_items}".rsplit('.', 1)
-        spec = importlib.util.find_spec(module)
-        if spec is not None:
-            navigation = importlib.util.module_from_spec(spec)
-            sys.modules[module] = navigation
-            spec.loader.exec_module(navigation)
-            if hasattr(navigation, attr):
-                menu_items = getattr(navigation, attr)
-                register_menu_items(self.verbose_name, 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)
 
 
     @classmethod
     @classmethod
     def validate(cls, user_config):
     def validate(cls, user_config):

+ 12 - 27
netbox/extras/plugins/urls.py

@@ -1,12 +1,11 @@
-import importlib
-import sys
-
 from django.apps import apps
 from django.apps import apps
 from django.conf import settings
 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 extras.plugins.utils import import_object
+
 from . import views
 from . import views
 
 
 # Initialize URL base, API, and admin URL patterns for plugins
 # Initialize URL base, API, and admin URL patterns for plugins
@@ -26,29 +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
-    spec = importlib.util.find_spec(f"{plugin_path}.urls")
-    if spec is not None:
-        # The plugin has a .urls module - import it
-        urls = importlib.util.module_from_spec(spec)
-        sys.modules[f"{plugin_path}.urls"] = urls
-        spec.loader.exec_module(urls)
-        if hasattr(urls, "urlpatterns"):
-            urlpatterns = urls.urlpatterns
-            plugin_patterns.append(
-                path(f"{base_url}/", include((urlpatterns, app.label)))
-            )
+    urlpatterns = import_object(f"{plugin_path}.urls.urlpatterns")
+    if urlpatterns is not None:
+        plugin_patterns.append(
+            path(f"{base_url}/", include((urlpatterns, app.label)))
+        )
 
 
     # Check if the plugin specifies any API URLs
     # Check if the plugin specifies any API URLs
-    spec = importlib.util.find_spec(f"{plugin_path}.api")
-    if spec is not None:
-        spec = importlib.util.find_spec(f"{plugin_path}.api.urls")
-        if spec is not None:
-            # The plugin has a .api.urls module - import it
-            api_urls = importlib.util.module_from_spec(spec)
-            sys.modules[f"{plugin_path}.api.urls"] = api_urls
-            spec.loader.exec_module(api_urls)
-            if hasattr(api_urls, "urlpatterns"):
-                urlpatterns = api_urls.urlpatterns
-                plugin_api_patterns.append(
-                    path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
-                )
+    urlpatterns = import_object(f"{plugin_path}.api.urls.urlpatterns")
+    if urlpatterns is not None:
+        plugin_api_patterns.append(
+            path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
+        )

+ 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)

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

@@ -1,6 +1,4 @@
 from collections import OrderedDict
 from collections import OrderedDict
-import importlib
-import sys
 
 
 from django.apps import apps
 from django.apps import apps
 from django.conf import settings
 from django.conf import settings
@@ -12,6 +10,8 @@ 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):
     """
     """
@@ -62,22 +62,10 @@ class PluginsAPIRootView(APIView):
     @staticmethod
     @staticmethod
     def _get_plugin_entry(plugin, app_config, request, format):
     def _get_plugin_entry(plugin, app_config, request, format):
         # Check if the plugin specifies any API URLs
         # Check if the plugin specifies any API URLs
-        spec = importlib.util.find_spec(f"{plugin}.api")
-        if spec is None:
-            # There is no plugin.api module
-            return None
-        spec = importlib.util.find_spec(f"{plugin}.api.urls")
-        if spec is None:
-            # There is no plugin.api.urls module
-            return None
-        # The plugin has a .api.urls module - import it
-        api_urls = importlib.util.module_from_spec(spec)
-        sys.modules[f"{plugin}.api.urls"] = api_urls
-        spec.loader.exec_module(api_urls)
-        if not hasattr(api_urls, "app_name"):
-            # The plugin api.urls does not declare an app_name string
+        api_app_name = import_object(f"{plugin}.api.urls.app_name")
+        if api_app_name is None:
+            # Plugin does not expose an API
             return None
             return None
-        api_app_name = api_urls.app_name
 
 
         try:
         try:
             entry = (getattr(app_config, 'base_url', app_config.label), reverse(
             entry = (getattr(app_config, 'base_url', app_config.label), reverse(