Merge branch 'release/1.10.x' into incoming

This commit is contained in:
rdb 2019-08-23 10:53:09 +02:00
commit 665d2fc56b
12 changed files with 407 additions and 8 deletions

View File

@ -24,7 +24,7 @@ Installing Panda3D
================== ==================
The latest Panda3D SDK can be downloaded from 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 If you are familiar with installing Python packages, you can use
the following comand: 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 [click here](https://github.com/rdb/panda3d-thirdparty) for instructions on
building them from source. 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.1/panda3d-1.10.4.1-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-win32.zip
After acquiring these dependencies, you may simply build Panda3D from the 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 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 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, After placing the thirdparty directory inside the panda3d source directory,
you may build Panda3D using a command like the following: you may build Panda3D using a command like the following:

View File

@ -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: if loaderOptions is None:
loaderOptions = LoaderOptions() loaderOptions = LoaderOptions()
else: else:

View File

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

View File

@ -15,7 +15,7 @@ import tempfile
import subprocess import subprocess
from distutils.sysconfig import get_config_var from distutils.sysconfig import get_config_var
from optparse import OptionParser 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 from base64 import urlsafe_b64encode
@ -730,7 +730,7 @@ if __debug__:
if __name__ == "__main__": if __name__ == "__main__":
version = ParsePandaVersion("dtool/PandaVersion.pp") version = GetMetadataValue('version')
parser = OptionParser() parser = OptionParser()
parser.add_option('', '--version', dest = 'version', help = 'Panda3D version number (default: %s)' % (version), default = version) parser.add_option('', '--version', dest = 'version', help = 'Panda3D version number (default: %s)' % (version), default = version)

View File

@ -113,6 +113,41 @@ register_deferred_type(const string &extension, const string &library) {
_deferred_types[dcextension] = 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. * Returns the total number of types registered.
*/ */

View File

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

View File

@ -17,6 +17,8 @@
#include "pythonLoaderFileType.h" #include "pythonLoaderFileType.h"
extern struct Dtool_PyTypedObject Dtool_LoaderFileType;
/** /**
* Registers a loader file type that is implemented in Python. * Registers a loader file type that is implemented in Python.
*/ */
@ -64,4 +66,50 @@ register_deferred_type(PyObject *entry_point) {
_this->register_type(loader); _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 #endif

View File

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

View File

@ -83,10 +83,27 @@ init(PyObject *loader) {
// it must occur in the list. // it must occur in the list.
PyObject *extensions = PyObject_GetAttrString(loader, "extensions"); PyObject *extensions = PyObject_GetAttrString(loader, "extensions");
if (extensions != nullptr) { 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 *sequence = PySequence_Fast(extensions, "extensions must be a sequence");
PyObject **items = PySequence_Fast_ITEMS(sequence); PyObject **items = PySequence_Fast_ITEMS(sequence);
Py_ssize_t num_items = PySequence_Fast_GET_SIZE(sequence); Py_ssize_t num_items = PySequence_Fast_GET_SIZE(sequence);
Py_DECREF(extensions); 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; bool found_extension = false;
for (Py_ssize_t i = 0; i < num_items; ++i) { for (Py_ssize_t i = 0; i < num_items; ++i) {

View File

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

View File

@ -0,0 +1,220 @@
from panda3d.core import LoaderFileTypeRegistry, ModelRoot, Loader, LoaderOptions, Filename
import pytest
import tempfile
import os
from contextlib import contextmanager
@pytest.fixture
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)
@pytest.fixture
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)

View File

@ -0,0 +1,70 @@
from panda3d.core import Filename, NodePath
from direct.showbase.Loader import Loader
import pytest
@pytest.fixture
def loader():
return Loader(base=None)
@pytest.fixture
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