Browse Source

showbase: Switch Loader entry point detection to importlib.metadata

Only in Python 3.8 and up, where this module is available, otherwise it falls back to pkg_resources

Add unit test for custom entry point loaders
rdb 2 years ago
parent
commit
c1c035d5c9
2 changed files with 122 additions and 9 deletions
  1. 18 8
      direct/src/showbase/Loader.py
  2. 104 1
      tests/showbase/test_Loader.py

+ 18 - 8
direct/src/showbase/Loader.py

@@ -8,6 +8,7 @@ from panda3d.core import *
 from panda3d.core import Loader as PandaLoader
 from direct.directnotify.DirectNotifyGlobal import *
 from direct.showbase.DirectObject import DirectObject
+import sys
 
 # You can specify a phaseChecker callback to check
 # a modelPath to see if it is being loaded in the correct
@@ -167,16 +168,25 @@ class Loader(DirectObject):
         if not ConfigVariableBool('loader-support-entry-points', True):
             return
 
-        import importlib
-        try:
-            pkg_resources = importlib.import_module('pkg_resources')
-        except ImportError:
-            pkg_resources = None
-
-        if pkg_resources:
+        if sys.version_info >= (3, 8):
+            from importlib.metadata import entry_points
+            eps = entry_points()
+            if isinstance(eps, dict): # Python 3.8 and 3.9
+                loaders = eps.get('panda3d.loaders', ())
+            else:
+                loaders = entry_points().select(group='panda3d.loaders')
+        else:
+            import importlib
+            try:
+                pkg_resources = importlib.import_module('pkg_resources')
+                loaders = pkg_resources.iter_entry_points('panda3d.loaders')
+            except ImportError:
+                loaders = ()
+
+        if loaders:
             registry = LoaderFileTypeRegistry.getGlobalPtr()
 
-            for entry_point in pkg_resources.iter_entry_points('panda3d.loaders'):
+            for entry_point in loaders:
                 registry.register_deferred_type(entry_point)
 
             cls._loadedPythonFileTypes = True

+ 104 - 1
tests/showbase/test_Loader.py

@@ -1,6 +1,7 @@
-from panda3d.core import Filename, NodePath
+from panda3d.core import Filename, NodePath, LoaderFileTypeRegistry
 from direct.showbase.Loader import Loader
 import pytest
+import sys
 
 
 @pytest.fixture
@@ -68,3 +69,105 @@ def test_load_model_missing(loader):
 def test_load_model_okmissing(loader):
     model = loader.load_model('/nonexistent.bam', okMissing=True)
     assert model is None
+
+
+def test_loader_entry_points(tmp_path):
+    # A dummy loader for .fnrgl files.
+    (tmp_path / "fnargle.py").write_text("""
+from panda3d.core import ModelRoot
+import sys
+
+sys._fnargle_loaded = True
+
+class FnargleLoader:
+    name = "Fnargle"
+    extensions = ['fnrgl']
+    supports_compressed = False
+
+    @staticmethod
+    def load_file(path, options, record=None):
+        return ModelRoot("fnargle")
+""")
+    (tmp_path / "fnargle.dist-info").mkdir()
+    (tmp_path / "fnargle.dist-info" / "METADATA").write_text("""
+Metadata-Version: 2.0
+Name: fnargle
+Version: 1.0.0
+""")
+    (tmp_path / "fnargle.dist-info" / "entry_points.txt").write_text("""
+[panda3d.loaders]
+fnrgl = fnargle:FnargleLoader
+""")
+
+    model_path = tmp_path / "test.fnrgl"
+    model_path.write_text("")
+
+    if sys.version_info >= (3, 11):
+        import sysconfig
+        stdlib = sysconfig.get_path("stdlib")
+        platstdlib = sysconfig.get_path("platstdlib")
+    else:
+        from distutils import sysconfig
+        stdlib = sysconfig.get_python_lib(False, True)
+        platstdlib = sysconfig.get_python_lib(True, True)
+
+    if sys.version_info < (3, 8):
+        # Older Python versions don't have importlib.metadata, so we rely on
+        # pkg_resources - but this caches the results once.  Fortunately, it
+        # provides this function for reinitializing the cached entry points.
+        # See pypa/setuptools#373
+        pkg_resources = pytest.importorskip("pkg_resources")
+        if not hasattr(pkg_resources, "_initialize_master_working_set"):
+            pytest.skip("pkg_resources too old")
+
+    registry = LoaderFileTypeRegistry.get_global_ptr()
+    prev_loaded = Loader._loadedPythonFileTypes
+    prev_path = sys.path
+    file_type = None
+    try:
+        # We do this so we don't re-register thirdparty loaders
+        sys.path = [str(tmp_path), platstdlib, stdlib]
+        if sys.version_info < (3, 8):
+            pkg_resources._initialize_master_working_set()
+
+        Loader._loadedPythonFileTypes = False
+
+        # base parameter is only used for audio
+        loader = Loader(None)
+        assert Loader._loadedPythonFileTypes
+
+        # Should be registered, not yet loaded
+        file_type = registry.get_type_from_extension('fnrgl')
+        assert file_type is not None
+        assert not hasattr(sys, '_fnargle_loaded')
+
+        assert file_type.supports_load()
+        assert not file_type.supports_save()
+        assert not file_type.supports_compressed()
+        assert file_type.get_extension() == 'fnrgl'
+
+        # The above should have caused it to load
+        assert sys._fnargle_loaded
+        assert 'fnargle' in sys.modules
+
+        # Now try loading a fnargle file
+        model = loader.load_model(model_path)
+        assert model is not None
+        assert model.name == "fnargle"
+
+    finally:
+        # Set everything back to what it was
+        Loader._loadedPythonFileTypes = prev_loaded
+        sys.path = prev_path
+
+        if hasattr(sys, '_fnargle_loaded'):
+            del sys._fnargle_loaded
+
+        if 'fnargle' in sys.modules:
+            del sys.modules['fnargle']
+
+        if file_type is not None:
+            registry.unregister_type(file_type)
+
+        if sys.version_info < (3, 8):
+            pkg_resources._initialize_master_working_set()