Browse Source

python: convert more Trade assertions to Python exceptions.

next
Vladimír Vondruš 3 years ago
parent
commit
d1a7a7c6c8
  1. 32
      doc/python/magnum.trade.rst
  2. 1
      package/ci/appveyor-desktop-gles.bat
  3. 1
      package/ci/appveyor-desktop.bat
  4. 1
      package/ci/unix-desktop-gles.sh
  5. 1
      package/ci/unix-desktop.sh
  6. 108
      src/python/magnum/test/test_trade.py
  7. 79
      src/python/magnum/trade.cpp

32
doc/python/magnum.trade.rst

@ -374,6 +374,8 @@
behavior.
.. py:function:: magnum.trade.AbstractImporter.open_data
:raise AssertionError: If :ref:`trade.ImporterFeatures.OPEN_DATA` is not
supported
:raise RuntimeError: If file opening fails
.. py:function:: magnum.trade.AbstractImporter.open_file
@ -584,12 +586,21 @@
behavior.
.. py:function:: magnum.trade.AbstractSceneConverter.convert
:raise AssertionError: If :ref:`trade.SceneConverterFeatures.CONVERT_MESH`
is not supported
:raise RuntimeError: If conversion fails
.. py:function:: magnum.trade.AbstractSceneConverter.convert_in_place
:raise AssertionError: If :ref:`trade.SceneConverterFeatures.CONVERT_MESH_IN_PLACE`
is not supported
:raise RuntimeError: If conversion fails
.. py:function:: magnum.trade.AbstractSceneConverter.convert_to_file
:raise AssertionError: If neither
:ref:`SceneConverterFeatures.CONVERT_MESH_TO_FILE` nor the
combination of :ref:`SceneConverterFeatures.CONVERT_MULTIPLE_TO_FILE`
and :ref:`SceneConverterFeatures.ADD_MESHES`
is supported
:raise RuntimeError: If conversion fails
For compatibility with :ref:`os.path`, on Windows this function converts
@ -598,6 +609,9 @@
forward slashes as directory separators on all platforms.
.. py:function:: magnum.trade.AbstractSceneConverter.begin_file
:raise AssertionError: If neither
:ref:`SceneConverterFeatures.CONVERT_MULTIPLE_TO_FILE` nor
:ref:`SceneConverterFeatures.CONVERT_MESH_TO_FILE` is supported
:raise RuntimeError: If beginning the conversion fails
For compatibility with :ref:`os.path`, on Windows this function converts
@ -616,19 +630,36 @@
:raise AssertionError: If no conversion is in progress
.. py:function:: magnum.trade.AbstractSceneConverter.add
:raise AssertionError: If corresponding
:ref:`SceneConverterFeatures.ADD_* <SceneConverterFeatures>` is not
supported, or alternatively at least one of
:ref:`SceneConverterFeatures.CONVERT_MESH`,
:ref:`SceneConverterFeatures.CONVERT_MESH_TO_DATA` or
:ref:`SceneConverterFeatures.CONVERT_MESH_TO_FILE` is not supported
for meshes
:raise AssertionError: If no conversion is in progress
:raise RuntimeError: If adding the data fails
.. py:function:: magnum.trade.AbstractSceneConverter.set_mesh_attribute_name
:raise AssertionError: If none of
:ref:`SceneConverterFeatures.ADD_MESHES`,
:ref:`SceneConverterFeatures.CONVERT_MESH`,
:ref:`SceneConverterFeatures.CONVERT_MESH_IN_PLACE`,
:ref:`SceneConverterFeatures.CONVERT_MESH_TO_DATA` or
:ref:`SceneConverterFeatures.CONVERT_MESH_TO_FILE` is supported
:raise AssertionError: If no conversion is in progress
:raise AssertionError: If :p:`attribute` is not custom
.. py:function:: magnum.trade.AbstractSceneConverter.set_default_scene
:raise AssertionError: If :ref:`trade.SceneConverterFeatures.ADD_SCENES`
is not supported
:raise AssertionError: If no conversion is in progress
:raise AssertionError: If :p:`id` is negative or not less than
:ref:`scene_count`
.. py:function:: magnum.trade.AbstractSceneConverter.set_scene_field_name
:raise AssertionError: If :ref:`trade.SceneConverterFeatures.ADD_SCENES`
is not supported
:raise AssertionError: If no conversion is in progress
:raise AssertionError: If :p:`field` is not custom
@ -637,5 +668,6 @@
:raise RuntimeError: If adding the importer contents fails
.. py:function:: magnum.trade.AbstractSceneConverter.add_supported_importer_contents
:raise AssertionError: If :p:`importer` is not opened
:raise AssertionError: If no conversion is in progress
:raise RuntimeError: If adding the importer contents fails

1
package/ci/appveyor-desktop-gles.bat

@ -63,6 +63,7 @@ cmake .. ^
-DMAGNUM_WITH_WINDOWLESSWGLAPPLICATION=ON ^
-DMAGNUM_WITH_ANYIMAGEIMPORTER=ON ^
-DMAGNUM_WITH_ANYSCENECONVERTER=ON ^
-DMAGNUM_WITH_ANYSCENEIMPORTER=ON ^
-DMAGNUM_WITH_TGAIMPORTER=ON ^
-G Ninja || exit /b
cmake --build . || exit /b

1
package/ci/appveyor-desktop.bat

@ -73,6 +73,7 @@ cmake .. ^
-DMAGNUM_WITH_WINDOWLESSWGLAPPLICATION=ON ^
-DMAGNUM_WITH_ANYIMAGEIMPORTER=ON ^
-DMAGNUM_WITH_ANYSCENECONVERTER=ON ^
-DMAGNUM_WITH_ANYSCENEIMPORTER=ON ^
-DMAGNUM_WITH_TGAIMPORTER=ON ^
%COMPILER_EXTRA% -G Ninja || exit /b
cmake --build . || exit /b

1
package/ci/unix-desktop-gles.sh

@ -46,6 +46,7 @@ cmake .. \
-DMAGNUM_WITH_WINDOWLESSEGLAPPLICATION=ON \
-DMAGNUM_WITH_ANYIMAGEIMPORTER=ON \
-DMAGNUM_WITH_ANYSCENECONVERTER=ON \
-DMAGNUM_WITH_ANYSCENEIMPORTER=ON \
-DMAGNUM_WITH_TGAIMPORTER=ON \
-G Ninja
ninja install

1
package/ci/unix-desktop.sh

@ -47,6 +47,7 @@ cmake .. \
-DMAGNUM_WITH_WINDOWLESS${PLATFORM_GL_API}APPLICATION=ON \
-DMAGNUM_WITH_ANYIMAGEIMPORTER=ON \
-DMAGNUM_WITH_ANYSCENECONVERTER=ON \
-DMAGNUM_WITH_ANYSCENEIMPORTER=ON \
-DMAGNUM_WITH_TGAIMPORTER=ON \
-G Ninja

108
src/python/magnum/test/test_trade.py

@ -1424,6 +1424,12 @@ class Importer(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "opening data failed"):
importer.open_data(b'')
def test_open_data_not_supported(self):
importer = trade.ImporterManager().load_and_instantiate('AnySceneImporter')
with self.assertRaisesRegex(AssertionError, "feature not supported"):
importer.open_data(b'')
def test_scene(self):
# importer refcounting tested in image2d
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
@ -1719,6 +1725,12 @@ class SceneConverter(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "conversion failed"):
converted_mesh = converter.convert(mesh)
def test_mesh_not_supported(self):
converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter')
with self.assertRaisesRegex(AssertionError, "mesh conversion not supported"):
converter.convert(primitives.cube_solid())
def test_mesh_in_place(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))
@ -1747,6 +1759,12 @@ class SceneConverter(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "conversion failed"):
converter.convert_in_place(mesh)
def test_mesh_in_place_not_supported(self):
converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter')
with self.assertRaisesRegex(AssertionError, "mesh conversion not supported"):
converter.convert_in_place(primitives.cube_solid())
def test_mesh_to_file(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))
@ -1769,6 +1787,17 @@ class SceneConverter(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "conversion failed"):
converter.convert_to_file(mesh, os.path.join(tmp, "mesh.obj"))
def test_mesh_to_file_not_supported(self):
converter_manager = trade.SceneConverterManager()
if 'MeshOptimizerSceneConverter' not in converter_manager.plugin_list:
self.skipTest("MeshOptimizerSceneConverter plugin not available")
converter = converter_manager.load_and_instantiate('MeshOptimizerSceneConverter')
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaisesRegex(AssertionError, "mesh conversion not supported"):
converter.convert_to_file(primitives.cube_solid(), os.path.join(tmp, "mesh.foo"))
def test_batch_file(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))
@ -1812,6 +1841,17 @@ class SceneConverter(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "ending the conversion failed"):
converter.end_file()
def test_batch_file_not_supported(self):
converter_manager = trade.SceneConverterManager()
if 'MeshOptimizerSceneConverter' not in converter_manager.plugin_list:
self.skipTest("MeshOptimizerSceneConverter plugin not available")
converter = converter_manager.load_and_instantiate('MeshOptimizerSceneConverter')
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaisesRegex(AssertionError, "feature not supported"):
converter.begin_file(os.path.join(tmp, "mesh.foo"))
def test_batch_add_mesh_failed(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))
@ -1824,6 +1864,11 @@ class SceneConverter(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "adding the mesh failed"):
converter.add(mesh)
def test_batch_add_mesh_not_supported(self):
# TODO implement once there's a converter that doesn't support meshes
# or has only in-place conversion
pass
def test_batch_set_mesh_attribute_name(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))
@ -1851,6 +1896,10 @@ class SceneConverter(unittest.TestCase):
with self.assertRaisesRegex(AssertionError, "not a custom attribute"):
converter.set_mesh_attribute_name(trade.MeshAttribute.POSITION, 'foo')
def test_batch_set_mesh_attribute_name_not_supported(self):
# TODO implement once there's a converter that doesn't support meshes
pass
def test_batch_add_scene(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'scene.gltf'))
@ -1892,6 +1941,25 @@ class SceneConverter(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "adding the scene failed"):
converter.add(scene)
def test_batch_add_scene_not_supported(self):
# Static builds with non-static plugins cause assertions with non-owned
# array deleters used by PrimitiveImporter, skip in that case
if magnum.BUILD_STATIC:
self.skipTest("dynamic PrimitiveImporter doesn't work with a static build")
importer = trade.ImporterManager().load_and_instantiate('PrimitiveImporter')
importer.open_data(containers.ArrayView())
scene = importer.scene(0)
converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter')
with tempfile.TemporaryDirectory() as tmp:
converter.begin_file(os.path.join(tmp, "scene.ply"))
with self.assertRaisesRegex(AssertionError, "scene conversion not supported"):
converter.add(scene)
def test_batch_set_default_scene(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'scene.gltf'))
@ -1919,6 +1987,15 @@ class SceneConverter(unittest.TestCase):
with self.assertRaisesRegex(AssertionError, "index 1 out of range for 0 scenes"):
converter.set_default_scene(1)
def test_batch_set_default_scene_not_supported(self):
converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter')
with tempfile.TemporaryDirectory() as tmp:
converter.begin_file(os.path.join(tmp, "scene.ply"))
with self.assertRaisesRegex(AssertionError, "feature not supported"):
converter.set_default_scene(0)
def test_batch_set_scene_field_name(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'scene.gltf'))
@ -1954,6 +2031,15 @@ class SceneConverter(unittest.TestCase):
with self.assertRaisesRegex(AssertionError, "not a custom field"):
converter.set_scene_field_name(trade.SceneField.SCALING, 'foo')
def test_batch_set_scene_field_name_not_supported(self):
converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter')
with tempfile.TemporaryDirectory() as tmp:
converter.begin_file(os.path.join(tmp, "scene.ply"))
with self.assertRaisesRegex(AssertionError, "feature not supported"):
converter.set_scene_field_name(trade.SceneField.CUSTOM(1), 'foo')
def test_batch_add_importer_contents(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'two-meshes.gltf'))
@ -1983,6 +2069,17 @@ class SceneConverter(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "adding importer contents failed"):
converter.add_importer_contents(importer)
def test_batch_add_importer_contents_not_opened(self):
importer = trade.ImporterManager().load_and_instantiate('AnySceneImporter')
converter = trade.SceneConverterManager().load_and_instantiate('GltfSceneConverter')
with tempfile.TemporaryDirectory() as tmp:
converter.begin_file(os.path.join(tmp, "file.gltf"))
with self.assertRaisesRegex(AssertionError, "the importer is not opened"):
converter.add_importer_contents(importer)
def test_batch_add_supported_importer_contents(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'scene.gltf'))
@ -2016,6 +2113,17 @@ class SceneConverter(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "adding importer contents failed"):
converter.add_supported_importer_contents(importer)
def test_batch_add_supported_importer_contents_not_opened(self):
importer = trade.ImporterManager().load_and_instantiate('AnySceneImporter')
converter = trade.SceneConverterManager().load_and_instantiate('GltfSceneConverter')
with tempfile.TemporaryDirectory() as tmp:
converter.begin_file(os.path.join(tmp, "file.gltf"))
with self.assertRaisesRegex(AssertionError, "the importer is not opened"):
converter.add_supported_importer_contents(importer)
def test_batch_no_conversion_in_progress(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))

79
src/python/magnum/trade.cpp

@ -1532,6 +1532,11 @@ void trade(py::module_& m) {
}, "Importer flags")
.def_property_readonly("is_opened", &Trade::AbstractImporter::isOpened, "Whether any file is opened")
.def("open_data", [](Trade::AbstractImporter& self, Containers::ArrayView<const char> data) {
if(!(self.features() >= Trade::ImporterFeature::OpenData)) {
PyErr_SetString(PyExc_AssertionError, "feature not supported");
throw py::error_already_set{};
}
/** @todo log redirection -- but we'd need assertions to not be
part of that so when it dies, the user can still see why */
if(self.openData(data)) return;
@ -1759,6 +1764,11 @@ void trade(py::module_& m) {
self.setFlags(flags);
}, "Converter flags")
.def("convert", [](Trade::AbstractSceneConverter& self, const Trade::MeshData& mesh) {
if(!(self.features() >= Trade::SceneConverterFeature::ConvertMesh)) {
PyErr_SetString(PyExc_AssertionError, "mesh conversion not supported");
throw py::error_already_set{};
}
/** @todo log redirection -- but we'd need assertions to not be
part of that so when it dies, the user can still see why */
Containers::Optional<Trade::MeshData> out = self.convert(mesh);
@ -1769,6 +1779,11 @@ void trade(py::module_& m) {
return out;
}, "Convert a mesh", py::arg("mesh"))
.def("convert_in_place", [](Trade::AbstractSceneConverter& self, Trade::MeshData& mesh) {
if(!(self.features() >= Trade::SceneConverterFeature::ConvertMeshInPlace)) {
PyErr_SetString(PyExc_AssertionError, "mesh conversion not supported");
throw py::error_already_set{};
}
/** @todo log redirection -- but we'd need assertions to not be
part of that so when it dies, the user can still see why */
if(!self.convertInPlace(mesh)) {
@ -1779,6 +1794,12 @@ void trade(py::module_& m) {
/** @todo conversion to data */
/** @todo drop std::string in favor of our own string caster */
.def("convert_to_file", [](Trade::AbstractSceneConverter& self, const Trade::MeshData& mesh, const std::string& filename) {
if(!(self.features() >= (Trade::SceneConverterFeature::ConvertMeshToFile)) &&
!(self.features() >= (Trade::SceneConverterFeature::ConvertMultipleToFile|Trade::SceneConverterFeature::AddMeshes))) {
PyErr_SetString(PyExc_AssertionError, "mesh conversion not supported");
throw py::error_already_set{};
}
/** @todo log redirection -- but we'd need assertions to not be
part of that so when it dies, the user can still see why */
bool out = self.convertToFile(mesh,
@ -1801,6 +1822,12 @@ void trade(py::module_& m) {
/** @todo begin/end (MeshOptimizer), begin/end data */
/** @todo drop std::string in favor of our own string caster */
.def("begin_file", [](Trade::AbstractSceneConverter& self, const std::string& filename) {
if(!(self.features() >= Trade::SceneConverterFeature::ConvertMultipleToFile) &&
!(self.features() >= Trade::SceneConverterFeature::ConvertMeshToFile)) {
PyErr_SetString(PyExc_AssertionError, "feature not supported");
throw py::error_already_set{};
}
if(!self.beginFile(
#ifdef CORRADE_TARGET_WINDOWS
/* To allow people to conveniently use Python's os.path, we
@ -1828,6 +1855,10 @@ void trade(py::module_& m) {
}
}, "End converting a scene to a file")
.def("set_default_scene", [](Trade::AbstractSceneConverter& self, const UnsignedInt id) {
if(!(self.features() >= Trade::SceneConverterFeature::AddScenes)) {
PyErr_SetString(PyExc_AssertionError, "feature not supported");
throw py::error_already_set{};
}
if(!self.isConverting()) {
PyErr_SetString(PyExc_AssertionError, "no conversion in progress");
throw py::error_already_set{};
@ -1848,6 +1879,10 @@ void trade(py::module_& m) {
}, "Count of added scenes")
/** @todo drop std::string in favor of our own string caster */
.def("add", [](Trade::AbstractSceneConverter& self, const Trade::SceneData& scene, const std::string& name) {
if(!(self.features() >= Trade::SceneConverterFeature::AddScenes)) {
PyErr_SetString(PyExc_AssertionError, "scene conversion not supported");
throw py::error_already_set{};
}
if(!self.isConverting()) {
PyErr_SetString(PyExc_AssertionError, "no conversion in progress");
throw py::error_already_set{};
@ -1859,14 +1894,18 @@ void trade(py::module_& m) {
throw py::error_already_set{};
}, "Add a scene", py::arg("scene"), py::arg("name") = std::string{})
.def("set_scene_field_name", [](Trade::AbstractSceneConverter& self, const Trade::SceneField field, const std::string& name) {
if(!Trade::isSceneFieldCustom(field)) {
PyErr_SetString(PyExc_AssertionError, "not a custom field");
if(!(self.features() >= Trade::SceneConverterFeature::AddScenes)) {
PyErr_SetString(PyExc_AssertionError, "feature not supported");
throw py::error_already_set{};
}
if(!self.isConverting()) {
PyErr_SetString(PyExc_AssertionError, "no conversion in progress");
throw py::error_already_set{};
}
if(!Trade::isSceneFieldCustom(field)) {
PyErr_SetString(PyExc_AssertionError, "not a custom field");
throw py::error_already_set{};
}
self.setSceneFieldName(field, name);
}, "Set name of a custom scene field", py::arg("field"), py::arg("name"))
.def_property_readonly("mesh_count", [](Trade::AbstractSceneConverter& self) {
@ -1878,6 +1917,13 @@ void trade(py::module_& m) {
}, "Count of added meshes")
/** @todo drop std::string in favor of our own string caster */
.def("add", [](Trade::AbstractSceneConverter& self, const Trade::MeshData& mesh, const std::string& name) {
if(!(self.features() >= Trade::SceneConverterFeature::AddMeshes) &&
!(self.features() & (Trade::SceneConverterFeature::ConvertMesh|
Trade::SceneConverterFeature::ConvertMeshToData|
Trade::SceneConverterFeature::ConvertMeshToFile))) {
PyErr_SetString(PyExc_AssertionError, "mesh conversion not supported");
throw py::error_already_set{};
}
if(!self.isConverting()) {
PyErr_SetString(PyExc_AssertionError, "no conversion in progress");
throw py::error_already_set{};
@ -1890,12 +1936,25 @@ void trade(py::module_& m) {
}, "Add a mesh", py::arg("mesh"), py::arg("name") = std::string{})
/** @todo mesh levels */
.def("set_mesh_attribute_name", [](Trade::AbstractSceneConverter& self, const Trade::MeshAttribute attribute, const std::string& name) {
if(!Trade::isMeshAttributeCustom(attribute)) {
PyErr_SetString(PyExc_AssertionError, "not a custom attribute");
if(!(self.features() & (Trade::SceneConverterFeature::AddMeshes|
Trade::SceneConverterFeature::ConvertMesh|
Trade::SceneConverterFeature::ConvertMeshInPlace|
Trade::SceneConverterFeature::ConvertMeshToData|
Trade::SceneConverterFeature::ConvertMeshToFile))) {
PyErr_SetString(PyExc_AssertionError, "feature not supported");
throw py::error_already_set{};
}
/* Unless single mesh conversion is supported, allow this function
to be called only if begin*() was called before */
if(!(self.features() & (Trade::SceneConverterFeature::ConvertMesh|
Trade::SceneConverterFeature::ConvertMeshInPlace|
Trade::SceneConverterFeature::ConvertMeshToData|
Trade::SceneConverterFeature::ConvertMeshToFile)) && !self.isConverting()) {
PyErr_SetString(PyExc_AssertionError, "no conversion in progress");
throw py::error_already_set{};
}
if(!self.isConverting()) {
PyErr_SetString(PyExc_AssertionError, "no conversion in progress");
if(!Trade::isMeshAttributeCustom(attribute)) {
PyErr_SetString(PyExc_AssertionError, "not a custom attribute");
throw py::error_already_set{};
}
self.setMeshAttributeName(attribute, name);
@ -1905,6 +1964,10 @@ void trade(py::module_& m) {
PyErr_SetString(PyExc_AssertionError, "no conversion in progress");
throw py::error_already_set{};
}
if(!importer.isOpened()) {
PyErr_SetString(PyExc_AssertionError, "the importer is not opened");
throw py::error_already_set{};
}
/** @todo check if contents present in the file are supported? or
make that a runtime failure in Magnum for easier use? */
if(!self.addImporterContents(importer, contents)) {
@ -1917,6 +1980,10 @@ void trade(py::module_& m) {
PyErr_SetString(PyExc_AssertionError, "no conversion in progress");
throw py::error_already_set{};
}
if(!importer.isOpened()) {
PyErr_SetString(PyExc_AssertionError, "the importer is not opened");
throw py::error_already_set{};
}
/** @todo check if contents present in the file are supported? or
make that a runtime failure in Magnum for easier use? */
if(!self.addSupportedImporterContents(importer, contents)) {

Loading…
Cancel
Save