Browse Source

Merge branch 'release/1.10.x' into incoming

rdb 6 years ago
parent
commit
665d2fc56b

+ 4 - 4
README.md

@@ -24,7 +24,7 @@ Installing Panda3D
 ==================
 
 The latest Panda3D SDK can be downloaded from
-[this page](https://www.panda3d.org/download/sdk-1-10-4/).
+[this page](https://www.panda3d.org/download/sdk-1-10-4-1/).
 If you are familiar with installing Python packages, you can use
 the following comand:
 
@@ -64,8 +64,8 @@ depending on whether you are on a 32-bit or 64-bit system, or you can
 [click here](https://github.com/rdb/panda3d-thirdparty) for instructions on
 building them from source.
 
-https://www.panda3d.org/download/panda3d-1.10.4/panda3d-1.10.4-tools-win64.zip
-https://www.panda3d.org/download/panda3d-1.10.4/panda3d-1.10.4-tools-win32.zip
+https://www.panda3d.org/download/panda3d-1.10.4.1/panda3d-1.10.4.1-tools-win64.zip
+https://www.panda3d.org/download/panda3d-1.10.4.1/panda3d-1.10.4.1-tools-win32.zip
 
 After acquiring these dependencies, you may simply build Panda3D from the
 command prompt using the following command.  (Change `14.1` to `14` if you are
@@ -135,7 +135,7 @@ macOS
 -----
 
 On macOS, you will need to download a set of precompiled thirdparty packages in order to
-compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.4/panda3d-1.10.4-tools-mac.tar.gz).
+compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.4.1/panda3d-1.10.4.1-tools-mac.tar.gz).
 
 After placing the thirdparty directory inside the panda3d source directory,
 you may build Panda3D using a command like the following:

+ 1 - 1
direct/src/showbase/Loader.py

@@ -230,7 +230,7 @@ class Loader(DirectObject):
 
         """
 
-        assert Loader.notify.debug("Loading model: %s" % (modelPath))
+        assert Loader.notify.debug("Loading model: %s" % (modelPath,))
         if loaderOptions is None:
             loaderOptions = LoaderOptions()
         else:

+ 1 - 1
makepanda/makepackage.py

@@ -1039,7 +1039,7 @@ def MakeInstaller(version, **kwargs):
 
 
 if __name__ == "__main__":
-    version = ParsePandaVersion("dtool/PandaVersion.pp")
+    version = GetMetadataValue('version')
 
     parser = OptionParser()
     parser.add_option('', '--version', dest='version', help='Panda3D version number (default: %s)' % (version), default=version)

+ 2 - 2
makepanda/makewheel.py

@@ -15,7 +15,7 @@ import tempfile
 import subprocess
 from distutils.sysconfig import get_config_var
 from optparse import OptionParser
-from makepandacore import ColorText, LocateBinary, ParsePandaVersion, GetExtensionSuffix, SetVerbose, GetVerbose, GetMetadataValue
+from makepandacore import ColorText, LocateBinary, GetExtensionSuffix, SetVerbose, GetVerbose, GetMetadataValue
 from base64 import urlsafe_b64encode
 
 
@@ -730,7 +730,7 @@ if __debug__:
 
 
 if __name__ == "__main__":
-    version = ParsePandaVersion("dtool/PandaVersion.pp")
+    version = GetMetadataValue('version')
 
     parser = OptionParser()
     parser.add_option('', '--version', dest = 'version', help = 'Panda3D version number (default: %s)' % (version), default = version)

+ 35 - 0
panda/src/pgraph/loaderFileTypeRegistry.cxx

@@ -113,6 +113,41 @@ register_deferred_type(const string &extension, const string &library) {
   _deferred_types[dcextension] = library;
 }
 
+/**
+ * Removes a type previously registered using register_type.
+ */
+void LoaderFileTypeRegistry::
+unregister_type(LoaderFileType *type) {
+  Types::iterator it = find(_types.begin(), _types.end(), type);
+  if (it == _types.end()) {
+    if (loader_cat.is_debug()) {
+      loader_cat.debug()
+        << "Attempt to unregister LoaderFileType " << type->get_name()
+        << " (" << type->get_type() << "), which was not registered.\n";
+    }
+    return;
+  }
+
+  _types.erase(it);
+
+  {
+    std::string dcextension = downcase(type->get_extension());
+    Extensions::iterator ei = _extensions.find(dcextension);
+    if (ei != _extensions.end() && ei->second == type) {
+      _extensions.erase(ei);
+    }
+  }
+
+  vector_string words;
+  extract_words(type->get_additional_extensions(), words);
+  for (const std::string &word : words) {
+    Extensions::iterator ei = _extensions.find(downcase(word));
+    if (ei != _extensions.end() && ei->second == type) {
+      _extensions.erase(ei);
+    }
+  }
+}
+
 /**
  * Returns the total number of types registered.
  */

+ 4 - 0
panda/src/pgraph/loaderFileTypeRegistry.h

@@ -35,10 +35,14 @@ public:
   void register_type(LoaderFileType *type);
   void register_deferred_type(const std::string &extension, const std::string &library);
 
+  void unregister_type(LoaderFileType *type);
+
 PUBLISHED:
   EXTENSION(void register_type(PyObject *type));
   EXTENSION(void register_deferred_type(PyObject *entry_point));
 
+  EXTENSION(void unregister_type(PyObject *type));
+
   int get_num_types() const;
   LoaderFileType *get_type(int n) const;
   MAKE_SEQ(get_types, get_num_types, get_type);

+ 48 - 0
panda/src/pgraph/loaderFileTypeRegistry_ext.cxx

@@ -17,6 +17,8 @@
 
 #include "pythonLoaderFileType.h"
 
+extern struct Dtool_PyTypedObject Dtool_LoaderFileType;
+
 /**
  * Registers a loader file type that is implemented in Python.
  */
@@ -64,4 +66,50 @@ register_deferred_type(PyObject *entry_point) {
   _this->register_type(loader);
 }
 
+/**
+ * If the given loader type is registered, unregisters it.
+ */
+void Extension<LoaderFileTypeRegistry>::
+unregister_type(PyObject *type) {
+  // Are we passing in a C++ file type object?
+  LoaderFileType *extracted_type;
+  if (DtoolInstance_GetPointer(type, extracted_type, Dtool_LoaderFileType)) {
+    _this->unregister_type(extracted_type);
+    return;
+  }
+
+  // If not, we may be passing in a Python file type.
+  PyObject *load_func = PyObject_GetAttrString(type, "load_file");
+  PyObject *save_func = PyObject_GetAttrString(type, "save_file");
+  PyErr_Clear();
+
+  if (load_func == nullptr && save_func == nullptr) {
+    Dtool_Raise_TypeError("expected loader type");
+    return;
+  }
+
+  // Keep looping until we've removed all instances of it.
+  bool found_any;
+  do {
+    found_any = false;
+    size_t num_types = _this->get_num_types();
+    for (size_t i = 0; i < num_types; ++i) {
+      LoaderFileType *type = _this->get_type(i);
+      if (type->is_of_type(PythonLoaderFileType::get_class_type())) {
+        PythonLoaderFileType *python_type = (PythonLoaderFileType *)type;
+        if (python_type->_load_func == load_func &&
+            python_type->_save_func == save_func) {
+          _this->unregister_type(python_type);
+          delete python_type;
+          found_any = true;
+          break;
+        }
+      }
+    }
+  } while (found_any);
+
+  Py_XDECREF(load_func);
+  Py_XDECREF(save_func);
+}
+
 #endif

+ 2 - 0
panda/src/pgraph/loaderFileTypeRegistry_ext.h

@@ -31,6 +31,8 @@ class Extension<LoaderFileTypeRegistry> : public ExtensionBase<LoaderFileTypeReg
 public:
   void register_type(PyObject *type);
   void register_deferred_type(PyObject *entry_point);
+
+  void unregister_type(PyObject *type);
 };
 
 #endif  // HAVE_PYTHON

+ 17 - 0
panda/src/pgraph/pythonLoaderFileType.cxx

@@ -83,10 +83,27 @@ init(PyObject *loader) {
   // it must occur in the list.
   PyObject *extensions = PyObject_GetAttrString(loader, "extensions");
   if (extensions != nullptr) {
+    if (PyUnicode_Check(extensions)
+#if PY_MAJOR_VERSION < 3
+      || PyString_Check(extensions)
+#endif
+      ) {
+      Dtool_Raise_TypeError("extensions list should be a list or tuple");
+      Py_DECREF(extensions);
+      return false;
+    }
+
     PyObject *sequence = PySequence_Fast(extensions, "extensions must be a sequence");
     PyObject **items = PySequence_Fast_ITEMS(sequence);
     Py_ssize_t num_items = PySequence_Fast_GET_SIZE(sequence);
     Py_DECREF(extensions);
+
+    if (num_items == 0) {
+      PyErr_SetString(PyExc_ValueError, "extensions list may not be empty");
+      Py_DECREF(sequence);
+      return false;
+    }
+
     bool found_extension = false;
 
     for (Py_ssize_t i = 0; i < num_items; ++i) {

+ 3 - 0
panda/src/pgraph/pythonLoaderFileType.h

@@ -19,6 +19,7 @@
 #ifdef HAVE_PYTHON
 
 #include "loaderFileType.h"
+#include "extension.h"
 
 /**
  * This defines a Python-based loader plug-in.  An instance of this can be
@@ -57,6 +58,8 @@ private:
   PyObject *_save_func = nullptr;
   bool _supports_compressed = false;
 
+  friend class Extension<LoaderFileTypeRegistry>;
+
 public:
   static TypeHandle get_class_type() {
     return _type_handle;

+ 220 - 0
tests/pgraph/test_loader_types.py

@@ -0,0 +1,220 @@
+from panda3d.core import LoaderFileTypeRegistry, ModelRoot, Loader, LoaderOptions, Filename
+import pytest
+import tempfile
+import os
+from contextlib import contextmanager
+
+
[email protected]
+def test_filename():
+    """Fixture returning a filename to an existent .test file."""
+    fp = tempfile.NamedTemporaryFile(suffix='.test', delete=False)
+    fp.write(b"test")
+    fp.close()
+    filename = Filename.from_os_specific(fp.name)
+    filename.make_true_case()
+    yield filename
+    os.unlink(fp.name)
+
+
[email protected]
+def test_pz_filename():
+    """Fixture returning a filename to an existent .test.pz file."""
+    fp = tempfile.NamedTemporaryFile(suffix='.test.pz', delete=False)
+    fp.write(b"test")
+    fp.close()
+    filename = Filename.from_os_specific(fp.name)
+    filename.make_true_case()
+    yield filename
+    os.unlink(fp.name)
+
+
+@contextmanager
+def registered_type(type):
+    """Convenience method allowing use of register_type in a with block."""
+    registry = LoaderFileTypeRegistry.get_global_ptr()
+    registry.register_type(type)
+    yield
+    registry.unregister_type(type)
+
+
+class DummyLoader:
+    """The simplest possible successful LoaderFileType."""
+
+    extensions = ["test"]
+
+    @staticmethod
+    def load_file(path, options, record=None):
+        return ModelRoot("loaded")
+
+
+def test_loader_invalid():
+    """Tests that registering a malformed loader fails."""
+
+    class MissingExtensionsLoader:
+        pass
+
+    class InvalidTypeExtensionsLoader:
+        extensions = "abc"
+
+    class EmptyExtensionsLoader:
+        extensions = []
+
+    class InvalidExtensionsLoader:
+        extensions = [123, None]
+
+    registry = LoaderFileTypeRegistry.get_global_ptr()
+
+    with pytest.raises(Exception):
+        registry.register_type("invalid")
+
+    with pytest.raises(Exception):
+        registry.register_type(MissingExtensionsLoader)
+
+    with pytest.raises(TypeError):
+        registry.register_type(InvalidTypeExtensionsLoader)
+
+    with pytest.raises(ValueError):
+        registry.register_type(EmptyExtensionsLoader)
+
+    with pytest.raises(TypeError):
+        registry.register_type(InvalidExtensionsLoader)
+
+
+def test_loader_success(test_filename):
+    """Tests that a normal dummy loader successfully loads."""
+
+    with registered_type(DummyLoader):
+        model = Loader.get_global_ptr().load_sync(test_filename, LoaderOptions(LoaderOptions.LF_no_cache))
+        assert model is not None
+        assert model.name == "loaded"
+
+
+def test_loader_extensions(test_filename):
+    """Tests multi-extension loaders."""
+
+    class MultiExtensionLoader:
+        extensions = ["test1", "teSt2"]
+
+        @staticmethod
+        def load_file(path, options, record=None):
+            return ModelRoot("loaded")
+
+    fp1 = tempfile.NamedTemporaryFile(suffix='.test1', delete=False)
+    fp1.write(b"test1")
+    fp1.close()
+    fn1 = Filename.from_os_specific(fp1.name)
+    fn1.make_true_case()
+
+    fp2 = tempfile.NamedTemporaryFile(suffix='.TEST2', delete=False)
+    fp2.write(b"test2")
+    fp2.close()
+    fn2 = Filename.from_os_specific(fp2.name)
+    fn2.make_true_case()
+
+    try:
+        with registered_type(MultiExtensionLoader):
+            model1 = Loader.get_global_ptr().load_sync(fn1, LoaderOptions(LoaderOptions.LF_no_cache))
+            assert model1 is not None
+            assert model1.name == "loaded"
+
+            model2 = Loader.get_global_ptr().load_sync(fn2, LoaderOptions(LoaderOptions.LF_no_cache))
+            assert model2 is not None
+            assert model2.name == "loaded"
+    finally:
+        os.unlink(fp1.name)
+        os.unlink(fp2.name)
+
+    # Ensure that both were unregistered.
+    registry = LoaderFileTypeRegistry.get_global_ptr()
+    assert not registry.get_type_from_extension("test1")
+    assert not registry.get_type_from_extension("test2")
+
+
+def test_loader_nonexistent():
+    """Verifies that non-existent files fail before calling load_file."""
+    flag = [False]
+
+    class AssertiveLoader:
+        extensions = ["test"]
+
+        @staticmethod
+        def load_file(path, options, record=None):
+            flag[0] = True
+            assert False, "should never get here"
+
+    with registered_type(AssertiveLoader):
+        model = Loader.get_global_ptr().load_sync("/non-existent", LoaderOptions(LoaderOptions.LF_no_cache))
+        assert model is None
+        assert not flag[0]
+
+
+def test_loader_exception(test_filename):
+    """Tests for a loader that raises an exception."""
+
+    class FailingLoader:
+        extensions = ["test"]
+
+        @staticmethod
+        def load_file(path, options, record=None):
+            raise Exception("test error")
+
+    with registered_type(FailingLoader):
+        model = Loader.get_global_ptr().load_sync(test_filename, LoaderOptions(LoaderOptions.LF_no_cache))
+        assert model is None
+
+
+def test_loader_compressed(test_pz_filename):
+    """Tests for loading .pz files and the supports_compressed flag."""
+
+    class TestLoader:
+        extensions = ["test"]
+
+        @staticmethod
+        def load_file(path, options, record=None):
+            return ModelRoot("loaded")
+
+    # Test with property absent
+    with registered_type(TestLoader):
+        model = Loader.get_global_ptr().load_sync(test_pz_filename, LoaderOptions(LoaderOptions.LF_no_cache))
+        assert model is None
+
+    # Test with property False, should give same result
+    TestLoader.supports_compressed = False
+    with registered_type(TestLoader):
+        model = Loader.get_global_ptr().load_sync(test_pz_filename, LoaderOptions(LoaderOptions.LF_no_cache))
+        assert model is None
+
+    # Test with property True, should work
+    TestLoader.supports_compressed = True
+    with registered_type(TestLoader):
+        model = Loader.get_global_ptr().load_sync(test_pz_filename, LoaderOptions(LoaderOptions.LF_no_cache))
+        assert model is not None
+        assert model.name == "loaded"
+
+    # Test with property invalid type, should not register
+    TestLoader.supports_compressed = None
+    with pytest.raises(TypeError):
+        LoaderFileTypeRegistry.get_global_ptr().register_type(TestLoader)
+
+
+def test_loader_ram_cache(test_filename):
+    """Tests that the Python loader plug-ins write to the RAM cache."""
+
+    # Ensure a clean slate.
+    from panda3d.core import ModelPool
+    ModelPool.release_all_models()
+
+    with registered_type(DummyLoader):
+        model1 = Loader.get_global_ptr().load_sync(test_filename, LoaderOptions(LoaderOptions.LF_no_disk_cache | LoaderOptions.LF_allow_instance))
+        assert model1 is not None
+        assert model1.name == "loaded"
+
+        assert ModelPool.has_model(test_filename)
+        assert ModelPool.get_model(test_filename, True) == model1
+
+        model2 = Loader.get_global_ptr().load_sync(test_filename, LoaderOptions(LoaderOptions.LF_cache_only | LoaderOptions.LF_allow_instance))
+        assert model2 is not None
+        assert model1 == model2
+
+        ModelPool.release_model(model2)

+ 70 - 0
tests/showbase/test_Loader.py

@@ -0,0 +1,70 @@
+from panda3d.core import Filename, NodePath
+from direct.showbase.Loader import Loader
+import pytest
+
+
[email protected]
+def loader():
+    return Loader(base=None)
+
+
[email protected]
+def temp_model():
+    from panda3d.core import ModelPool, ModelRoot
+
+    root = ModelRoot('model')
+    root.fullpath = '/test-model.bam'
+
+    ModelPool.add_model(root.fullpath, root)
+    yield root.fullpath
+    ModelPool.release_model(root.fullpath)
+
+
+def test_load_model_filename(loader, temp_model):
+    model = loader.load_model(Filename(temp_model))
+    assert model
+    assert isinstance(model, NodePath)
+    assert model.name == 'model'
+
+
+def test_load_model_str(loader, temp_model):
+    model = loader.load_model(str(temp_model))
+    assert model
+    assert isinstance(model, NodePath)
+    assert model.name == 'model'
+
+
+def test_load_model_list(loader, temp_model):
+    models = loader.load_model([temp_model, temp_model])
+    assert models
+    assert isinstance(models, list)
+    assert len(models) == 2
+    assert isinstance(models[0], NodePath)
+    assert isinstance(models[1], NodePath)
+
+
+def test_load_model_tuple(loader, temp_model):
+    models = loader.load_model((temp_model, temp_model))
+    assert models
+    assert isinstance(models, list)
+    assert len(models) == 2
+    assert isinstance(models[0], NodePath)
+    assert isinstance(models[1], NodePath)
+
+
+def test_load_model_set(loader, temp_model):
+    models = loader.load_model({temp_model})
+    assert models
+    assert isinstance(models, list)
+    assert len(models) == 1
+    assert isinstance(models[0], NodePath)
+
+
+def test_load_model_missing(loader):
+    with pytest.raises(IOError):
+        loader.load_model('/nonexistent.bam')
+
+
+def test_load_model_okmissing(loader):
+    model = loader.load_model('/nonexistent.bam', okMissing=True)
+    assert model is None