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
This commit is contained in:
rdb 2023-10-14 11:22:09 +02:00
parent c77697a2c0
commit c1c035d5c9
2 changed files with 121 additions and 8 deletions

View File

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

View File

@ -1,6 +1,7 @@
from panda3d.core import Filename, NodePath from panda3d.core import Filename, NodePath, LoaderFileTypeRegistry
from direct.showbase.Loader import Loader from direct.showbase.Loader import Loader
import pytest import pytest
import sys
@pytest.fixture @pytest.fixture
@ -68,3 +69,105 @@ def test_load_model_missing(loader):
def test_load_model_okmissing(loader): def test_load_model_okmissing(loader):
model = loader.load_model('/nonexistent.bam', okMissing=True) model = loader.load_model('/nonexistent.bam', okMissing=True)
assert model is None 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()