diff --git a/doc/python/conf.py b/doc/python/conf.py index a862b0d..7857b43 100644 --- a/doc/python/conf.py +++ b/doc/python/conf.py @@ -7,6 +7,7 @@ sys.path = [os.path.join(os.path.dirname(__file__), '../../build/src/python/')] import corrade import corrade.containers import corrade.pluginmanager +import corrade.utility import magnum import magnum.gl @@ -24,7 +25,7 @@ import magnum.trade # So the doc see everything # TODO: use just +=, m.css should reorder this on its own -corrade.__all__ = ['containers', 'pluginmanager', 'BUILD_DEPRECATED', 'BUILD_STATIC', 'BUILD_MULTITHREADED', 'TARGET_UNIX', 'TARGET_APPLE', 'TARGET_IOS', 'TARGET_IOS_SIMULATOR', 'TARGET_WINDOWS', 'TARGET_WINDOWS_RT', 'TARGET_EMSCRIPTEN', 'TARGET_ANDROID'] +corrade.__all__ = ['containers', 'pluginmanager', 'utility', 'BUILD_DEPRECATED', 'BUILD_STATIC', 'BUILD_MULTITHREADED', 'TARGET_UNIX', 'TARGET_APPLE', 'TARGET_IOS', 'TARGET_IOS_SIMULATOR', 'TARGET_WINDOWS', 'TARGET_WINDOWS_RT', 'TARGET_EMSCRIPTEN', 'TARGET_ANDROID'] magnum.__all__ = ['math', 'gl', 'meshtools', 'platform', 'primitives', 'shaders', 'scenegraph', 'text', 'trade', 'BUILD_DEPRECATED', 'BUILD_STATIC', 'TARGET_GL', 'TARGET_GLES', 'TARGET_GLES2', 'TARGET_WEBGL', 'TARGET_EGL', 'TARGET_VK'] + magnum.__all__ # hide values of the preprocessor defines to avoid confusion by assigning a @@ -176,6 +177,7 @@ INPUT_DOCS = [ 'corrade.rst', 'corrade.containers.rst', 'corrade.pluginmanager.rst', + 'corrade.utility.rst', 'magnum.rst', 'magnum.gl.rst', diff --git a/doc/python/corrade.utility.rst b/doc/python/corrade.utility.rst new file mode 100644 index 0000000..8bbdb27 --- /dev/null +++ b/doc/python/corrade.utility.rst @@ -0,0 +1,33 @@ +.. + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +.. + +.. py:function:: corrade.utility.ConfigurationGroup.group + :raise KeyError: If group :p:`name` doesn't exist + +.. py:function:: corrade.utility.Configuration.__init__ + :raise IOError: If :p:`filename` contains a parse error + +.. py:function:: corrade.utility.Configuration.save + :raise IOError: If the file can't be saved diff --git a/doc/python/pages/changelog.rst b/doc/python/pages/changelog.rst index 58d5198..99f6998 100644 --- a/doc/python/pages/changelog.rst +++ b/doc/python/pages/changelog.rst @@ -123,6 +123,8 @@ Changelog - Exposed :ref:`Color3.red()` and other convenience constructors (see :gh:`mosra/magnum-bindings#12`) - Exposed the :ref:`text` library +- Exposed the minimal interface of :ref:`utility.ConfigurationGroup` and + :ref:`utility.Configuration` - Fixed issues with an in-source build (see :gh:`mosra/magnum-bindings#13`) - All CMake build options are now prefixed with ``MAGNUM_``. For backwards compatibility, unless ``MAGNUM_BUILD_DEPRECATED`` is disabled and unless a diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 8621d61..a6928ee 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -62,6 +62,7 @@ add_subdirectory(magnum) foreach(target corrade_containers corrade_pluginmanager + corrade_utility magnum_gl magnum_meshtools magnum_primitives diff --git a/src/python/corrade/CMakeLists.txt b/src/python/corrade/CMakeLists.txt index 5dbdb23..df2cf93 100644 --- a/src/python/corrade/CMakeLists.txt +++ b/src/python/corrade/CMakeLists.txt @@ -43,6 +43,9 @@ set(corrade_containers_SRCS set(corrade_pluginmanager_SRCS pluginmanager.cpp) +set(corrade_utility_SRCS + utility.cpp) + # If Corrade is not built as static, compile the sub-libraries as separate # modules if(NOT CORRADE_BUILD_STATIC) @@ -57,6 +60,16 @@ if(NOT CORRADE_BUILD_STATIC) OUTPUT_NAME "containers" LIBRARY_OUTPUT_DIRECTORY ${output_dir}/corrade) + pybind11_add_module(corrade_utility ${pybind11_add_module_SYSTEM} ${corrade_utility_SRCS}) + target_include_directories(corrade_utility PRIVATE + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/src/python) + target_link_libraries(corrade_utility PRIVATE + Corrade::Utility) + set_target_properties(corrade_utility PROPERTIES + OUTPUT_NAME "utility" + LIBRARY_OUTPUT_DIRECTORY ${output_dir}/corrade) + if(Corrade_PluginManager_FOUND) pybind11_add_module(corrade_pluginmanager ${pybind11_add_module_SYSTEM} ${corrade_pluginmanager_SRCS}) target_include_directories(corrade_pluginmanager PRIVATE @@ -76,8 +89,12 @@ else() configure_file(${CMAKE_CURRENT_SOURCE_DIR}/staticconfigure.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/staticconfigure.h) - list(APPEND corrade_SRCS ${corrade_containers_SRCS}) - list(APPEND corrade_LIBS Corrade::Containers) + list(APPEND corrade_SRCS + ${corrade_containers_SRCS} + ${corrade_utility_SRCS}) + list(APPEND corrade_LIBS + Corrade::Containers + Corrade::Utility) if(Corrade_PluginManager_FOUND) list(APPEND corrade_SRCS ${corrade_pluginmanager_SRCS}) diff --git a/src/python/corrade/__init__.py b/src/python/corrade/__init__.py index 3e5e2b2..2c06f2e 100644 --- a/src/python/corrade/__init__.py +++ b/src/python/corrade/__init__.py @@ -33,7 +33,7 @@ import sys # _corrade. The following feels extremely hackish, but without that it wouldn't # be possible to do `import corrade.containers`, which is weird # (`from corrade import containers` works, tho, for whatever reason) -for i in ['containers', 'pluginmanager']: +for i in ['containers', 'pluginmanager', 'utility']: if i in globals(): sys.modules['corrade.' + i] = globals()[i] # Prevent all submodules being pulled in when saying `from corrade import *` -- diff --git a/src/python/corrade/bootstrap.h b/src/python/corrade/bootstrap.h index bd82d4f..386ebbc 100644 --- a/src/python/corrade/bootstrap.h +++ b/src/python/corrade/bootstrap.h @@ -48,6 +48,7 @@ namespace py = pybind11; void containers(py::module_& m); void pluginmanager(py::module_& m); +void utility(py::module_& m); } diff --git a/src/python/corrade/corrade.cpp b/src/python/corrade/corrade.cpp index 96bbe44..c6bacfb 100644 --- a/src/python/corrade/corrade.cpp +++ b/src/python/corrade/corrade.cpp @@ -132,5 +132,8 @@ PYBIND11_MODULE(_corrade, m) { py::module_ pluginmanager = m.def_submodule("pluginmanager"); corrade::pluginmanager(pluginmanager); #endif + + py::module_ utility = m.def_submodule("utility"); + corrade::utility(utility); #endif } diff --git a/src/python/corrade/test/broken.conf b/src/python/corrade/test/broken.conf new file mode 100644 index 0000000..bd3032b --- /dev/null +++ b/src/python/corrade/test/broken.conf @@ -0,0 +1 @@ +]]] yes this is invalid!!! diff --git a/src/python/corrade/test/file.conf b/src/python/corrade/test/file.conf new file mode 100644 index 0000000..da8a8f8 --- /dev/null +++ b/src/python/corrade/test/file.conf @@ -0,0 +1,6 @@ +someKey=42 +[someGroup] +value=hello +[someGroup/subgroup] +anotherValue=another +[emptyGroup] diff --git a/src/python/corrade/test/test_utility.py b/src/python/corrade/test/test_utility.py new file mode 100644 index 0000000..28f57f6 --- /dev/null +++ b/src/python/corrade/test/test_utility.py @@ -0,0 +1,141 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021, 2022 Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import os +import sys +import tempfile +import unittest + +from corrade import utility + +# Tests also the ConfigurationGroup bindings, as a ConfigurationGroup cannot be +# constructed as a standalone type +class Configuration(unittest.TestCase): + def test_open(self): + a = utility.Configuration(os.path.join(os.path.dirname(__file__), "file.conf")) + a_refcount = sys.getrefcount(a) + self.assertEqual(a['someKey'], '42') + self.assertEqual(a['nonexistent'], '') + + b = a.group('someGroup') + b_refcount = sys.getrefcount(b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(b['value'], 'hello') + + c = b.group('subgroup') + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + self.assertEqual(c['anotherValue'], 'another') + + del c + self.assertEqual(sys.getrefcount(b), b_refcount) + + del b + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_nonexistent_group(self): + a = utility.Configuration(os.path.join(os.path.dirname(__file__), "file.conf")) + a_refcount = sys.getrefcount(a) + + with self.assertRaises(KeyError): + a.group('nonexistent') + + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_save(self): + with tempfile.TemporaryDirectory() as tmp: + filename = os.path.join(tmp, "file.conf") + + a = utility.Configuration(filename) + a['value'] = 'hello' + + a_refcount = sys.getrefcount(a) + b = a.add_group('someGroup') + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + b['someKey'] = '42' + + # This should not delete the group from the configuration + del b + self.assertEqual(sys.getrefcount(a), a_refcount) + + a.save() + + with open(filename, 'r') as f: + self.assertEqual(f.read(), "value=hello\n[someGroup]\nsomeKey=42\n") + + def test_save_different_filename(self): + a = utility.Configuration() + a['value'] = 'hello' + + with tempfile.TemporaryDirectory() as tmp: + filename = os.path.join(tmp, "file.conf") + a.save(filename) + + with open(filename, 'r') as f: + self.assertEqual(f.read(), "value=hello\n") + + def test_save_implicit(self): + with tempfile.TemporaryDirectory() as tmp: + filename = os.path.join(tmp, "file.conf") + + a = utility.Configuration(filename) + a['value'] = 'hello' + self.assertFalse(os.path.exists(filename)) + + del a + self.assertTrue(os.path.exists(filename)) + + with open(filename, 'r') as f: + self.assertEqual(f.read(), "value=hello\n") + + def test_open_nonexistent(self): + # This should not raise any exception as it's a valid use case (i.e, + # opening an app for the first time) + a = utility.Configuration("nonexistent.conf") + self.assertFalse(a.has_groups) + self.assertFalse(a.has_values) + + def test_open_failed(self): + with self.assertRaises(IOError): + utility.Configuration(os.path.join(os.path.dirname(__file__), "broken.conf")) + + @unittest.skipIf(os.access("/", os.W_OK), "root dir is writable") + def test_save_failed(self): + # The file doesn't exist, which means nothing is parsed during + # construction. But saving will fail as the directory is not writable. + a = utility.Configuration("/nonexistent.conf") + self.assertFalse(a.has_groups) + self.assertFalse(a.has_values) + + with self.assertRaises(IOError): + a.save() + + def test_save_different_filename_failed(self): + a = utility.Configuration() + + with self.assertRaises(IOError): + a.save("/some/path/that/does/not/exist.conf") + diff --git a/src/python/corrade/utility.cpp b/src/python/corrade/utility.cpp new file mode 100644 index 0000000..54bea40 --- /dev/null +++ b/src/python/corrade/utility.cpp @@ -0,0 +1,95 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include + +#include "corrade/bootstrap.h" + +namespace corrade { + +void utility(py::module_& m) { + m.doc() = "Utilities"; + + py::class_{m, "ConfigurationGroup", "Group of values in a configuration file"} + .def_property_readonly("has_groups", &Utility::ConfigurationGroup::hasGroups, "Whether this group has any subgroups") + .def("group", [](Utility::ConfigurationGroup& self, const std::string& name) { + Utility::ConfigurationGroup* group = self.group(name); + if(!group) { + PyErr_SetNone(PyExc_KeyError); + throw py::error_already_set{}; + } + return group; + }, "Group", py::arg("name"), py::return_value_policy::reference_internal) + .def("add_group", [](Utility::ConfigurationGroup& self, const std::string& name) { + Utility::ConfigurationGroup* group = self.addGroup(name); + CORRADE_INTERNAL_ASSERT(group); + return group; + }, "Add a group", py::arg("name"), py::return_value_policy::reference_internal) + .def_property_readonly("has_values", &Utility::ConfigurationGroup::hasValues, "Whether this group has any values") + .def("__getitem__", [](Utility::ConfigurationGroup& self, const std::string& key) { + /** @todo should return an Optional once ConfigurationGroup is + reworked */ + return self.value(key); + }, "Value", py::arg("key")) + .def("__setitem__", [](Utility::ConfigurationGroup& self, const std::string& key, const std::string& value) { + self.setValue(key, value); + }, "Set a value", py::arg("key"), py::arg("value")); + + py::class_{m, "Configuration", "Parser and writer for configuration files"} + .def(py::init(), "Construct an empty configuration") + .def(py::init([](const std::string& filename) { + std::unique_ptr self{new Utility::Configuration{filename}}; + if(!self->isValid()) { + PyErr_SetNone(PyExc_IOError); + throw py::error_already_set{}; + } + return self; + }), "Parse a configuration file", py::arg("filename")) + .def("save", [](Utility::Configuration& self) { + if(!self.save()) { + PyErr_SetNone(PyExc_IOError); + throw py::error_already_set{}; + } + }, "Save the configuration") + .def("save", [](Utility::Configuration& self, const std::string& filename) { + if(!self.save(filename)) { + PyErr_SetNone(PyExc_IOError); + throw py::error_already_set{}; + } + }, "Save the configuration to another file", py::arg("filename")); +} + +} + +#ifndef CORRADE_BUILD_STATIC +/* TODO: remove declaration when https://github.com/pybind/pybind11/pull/1863 + is released */ +extern "C" PYBIND11_EXPORT PyObject* PyInit_utility(); +PYBIND11_MODULE(utility, m) { + corrade::utility(m); +} +#endif diff --git a/src/python/setup.py.cmake b/src/python/setup.py.cmake index c67964e..a6b7e6b 100644 --- a/src/python/setup.py.cmake +++ b/src/python/setup.py.cmake @@ -36,6 +36,7 @@ extension_paths = { '_corrade': '$', 'corrade.containers': '${corrade_containers_file}', 'corrade.pluginmanager': '${corrade_pluginmanager_file}', + 'corrade.utility': '${corrade_utility_file}', '_magnum': '$', 'magnum.gl': '${magnum_gl_file}', 'magnum.meshtools': '${magnum_meshtools_file}',