From c1c035d5c9fd7910b6e9f94d4507eed6bd511fe3 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 14 Oct 2023 11:22:09 +0200 Subject: [PATCH] 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 --- direct/src/showbase/Loader.py | 24 +++++--- tests/showbase/test_Loader.py | 105 +++++++++++++++++++++++++++++++++- 2 files changed, 121 insertions(+), 8 deletions(-) diff --git a/direct/src/showbase/Loader.py b/direct/src/showbase/Loader.py index 711a40f30e..0237fbe9b2 100644 --- a/direct/src/showbase/Loader.py +++ b/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 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 pkg_resources: + 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 diff --git a/tests/showbase/test_Loader.py b/tests/showbase/test_Loader.py index 0dadecb384..2414e0f385 100644 --- a/tests/showbase/test_Loader.py +++ b/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()