From a9270ff78fe13f0f30a66fcc0097322e5a239184 Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Tue, 25 Jul 2023 22:33:18 -0500 Subject: [PATCH 1/9] Add python bindings for `sdf::Element` Signed-off-by: Addisu Z. Taddese --- include/sdf/Param.hh | 4 +- python/CMakeLists.txt | 2 + python/src/sdf/_gz_sdformat_pybind11.cc | 2 + python/src/sdf/pyElement.cc | 285 ++++++++++++++++++++++++ python/src/sdf/pyElement.hh | 41 ++++ python/test/pyElement_TEST.py | 147 ++++++++++++ src/Exception.cc | 2 - 7 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 python/src/sdf/pyElement.cc create mode 100644 python/src/sdf/pyElement.hh create mode 100644 python/test/pyElement_TEST.py diff --git a/include/sdf/Param.hh b/include/sdf/Param.hh index 29c5b90e5..084ed4b6a 100644 --- a/include/sdf/Param.hh +++ b/include/sdf/Param.hh @@ -613,12 +613,12 @@ namespace sdf /// \brief Data type to string mapping /// \return The type as a string, empty string if unknown type public: template - std::string TypeToString() const; + static std::string TypeToString(); }; /////////////////////////////////////////////// template - std::string ParamPrivate::TypeToString() const + std::string ParamPrivate::TypeToString() { // cppcheck-suppress syntaxError if constexpr (std::is_same_v) diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index a84167f70..e62548d7c 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -53,6 +53,7 @@ pybind11_add_module(${BINDINGS_MODULE_NAME} MODULE src/sdf/pyCapsule.cc src/sdf/pyCollision.cc src/sdf/pyCylinder.cc + src/sdf/pyElement.cc src/sdf/pyEllipsoid.cc src/sdf/pyError.cc src/sdf/pyForceTorque.cc @@ -132,6 +133,7 @@ if (BUILD_TESTING AND NOT WIN32) pyCapsule_TEST pyCollision_TEST pyCylinder_TEST + pyElement_TEST pyEllipsoid_TEST pyError_TEST pyForceTorque_TEST diff --git a/python/src/sdf/_gz_sdformat_pybind11.cc b/python/src/sdf/_gz_sdformat_pybind11.cc index a6f587193..78a38f969 100644 --- a/python/src/sdf/_gz_sdformat_pybind11.cc +++ b/python/src/sdf/_gz_sdformat_pybind11.cc @@ -27,6 +27,7 @@ #include "pyCapsule.hh" #include "pyCollision.hh" #include "pyCylinder.hh" +#include "pyElement.hh" #include "pyEllipsoid.hh" #include "pyError.hh" #include "pyExceptions.hh" @@ -84,6 +85,7 @@ PYBIND11_MODULE(BINDINGS_MODULE_NAME, m) { sdf::python::defineCollision(m); sdf::python::defineContact(m); sdf::python::defineCylinder(m); + sdf::python::defineElement(m); sdf::python::defineEllipsoid(m); sdf::python::defineError(m); sdf::python::defineForceTorque(m); diff --git a/python/src/sdf/pyElement.cc b/python/src/sdf/pyElement.cc new file mode 100644 index 000000000..c9a9888be --- /dev/null +++ b/python/src/sdf/pyElement.cc @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2023 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "pyElement.hh" + +#include +#include +#include + +#include "sdf/Element.hh" +#include "pybind11_helpers.hh" + +#include "sdf/config.hh" + +using namespace pybind11::literals; +namespace py = pybind11; +namespace sdf +{ +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +namespace python +{ + +using PyClassElement = py::class_; + +template +std::string computeSuffix() +{ + // TypeToString returns a value with a space, which would not be a valid + // python function name, so we override that here. + if constexpr (std::is_same_v) + return "unsigned_int"; + return ParamPrivate::TypeToString(); +} + +template +struct DefineElementGetSet +{ + void operator()(PyClassElement &_cls) + { + const std::string getFuncName = "get_" + computeSuffix(); + const std::string setFuncName = "set_" + computeSuffix(); + _cls.def( + getFuncName.c_str(), + [](const sdf::Element &_self, const std::string &_key) + { + sdf::Errors errors; + T val = _self.Get(errors, _key); + ThrowIfErrors(errors); + return val; + }, + "Get the value of a key. This function assumes the _key exists.") + .def( + getFuncName.c_str(), + [](const sdf::Element &_self, const std::string &_key, + const T &_defaultValue) + { + sdf::Errors errors; + std::pair val = + _self.Get(errors, _key, _defaultValue); + ThrowIfErrors(errors); + return val; + }, + "Get the value of a key. This function assumes the _key exists.") + .def( + setFuncName.c_str(), + [](sdf::Element &_self, const T &_value) + { + sdf::Errors errors; + bool result = _self.Set(errors, _value); + ThrowIfErrors(errors); + return result; + }, + "Get the value of a key. This function assumes the _key exists."); + } +}; + +template +struct DefineElementGetSet> +{ + void operator()(PyClassElement &_cls) + { + (DefineElementGetSet()(_cls), ...); + } +}; + +///////////////////////////////////////////////// +// TODO(azeey) Interspersed between the `.def` calls is a list of +// functions that need to be implemented. I'm not 100% sure if all of them need +// to be implemented since some of them are mainly used internally by the +// parser. I've already excluded the following functions on this criteria: +// - SetReferenceSDF +// - ReferenceSDF +// - PrintDescription (Until https://github.com/gazebosim/sdformat/issues/1302 +// is resolved) +// - PrintValues (Because ToString is available) +// - PrintDocLeftPane (Helper function for generating documentation) +// - PrintDocRightPane (Helper function for generating documentation) +// - GetElementDescriptionCount (Until +// https://github.com/gazebosim/sdformat/issues/1302 is resolved) +// - GetElementDescription (Until +// https://github.com/gazebosim/sdformat/issues/1302 is resolved) +// - HasElementDescription (Until +// https://github.com/gazebosim/sdformat/issues/1302 is resolved) +// - HasUniqueChildNames (Used for error checking by the parser) +// - CountNamedElements (Used for error checking by the parser) +// - GetElement (FindElement and AddElement should be used instead) +// - GetDescription (Until https://github.com/gazebosim/sdformat/issues/1302 is +// resolved) +// - SetDescription (Until https://github.com/gazebosim/sdformat/issues/1302 is +// resolved) +// - AddElementDescription (Until +// https://github.com/gazebosim/sdformat/issues/1302 is resolved) +// - GetElementImpl (Use FindElement instead) +// - NameUniquenessExceptions (Used for error checking by the parser) +void defineElement(py::object module) +{ + auto elemClass = + PyClassElement(module, "Element") + .def(py::init<>()) + .def( + "clone", + [](const sdf::Element &_self) + { + sdf::Errors errors; + sdf::ElementPtr elem = _self.Clone(errors); + ThrowIfErrors(errors); + return elem; + }, + "Create a copy of this Element.") + .def("get_parent", &sdf::Element::GetParent, + "Get a pointer to this Element's parent.") + .def("set_parent", &sdf::Element::SetParent, + "Set the parent of this Element.") + .def("set_name", &sdf::Element::SetName, + "Set the name of the Element") + .def("get_name", &sdf::Element::GetName, "Get the Element's name.") + .def("set_required", &sdf::Element::SetRequired, + "Set the requirement type.") + .def("get_required", &sdf::Element::GetRequired, + "Get the requirement string.") + // print_description + .def("set_explicitly_set_in_file", + &sdf::Element::SetExplicitlySetInFile, + "Set if the element and children where set or default in the " + "original file") + .def("get_explicitly_set_in_file", + &sdf::Element::GetExplicitlySetInFile, + "Return if the element was been explicitly set in the file") + // to_string + .def( + "add_attribute", + [](sdf::Element &_self, const std::string &_key, + const std::string &_type, const std::string &_defaultvalue, + bool _required, const std::string &_description = "") + { + sdf::Errors errors; + _self.AddAttribute(_key, _type, _defaultvalue, _required, + errors, _description); + ThrowIfErrors(errors); + }, + "Add an attribute value.") + .def( + "add_value", + [](sdf::Element &_self, const std::string &_type, + const std::string &_defaultValue, bool _required, + const std::string &_description = "") + { + sdf::Errors errors; + _self.AddValue(_type, _defaultValue, _required, errors, + _description); + ThrowIfErrors(errors); + }, + "Add a value to this Element") + .def( + "add_value", + [](sdf::Element &_self, const std::string &_type, + const std::string &_defaultValue, bool _required, + const std::string &_minValue, const std::string &_maxValue, + const std::string &_description = "") + { + sdf::Errors errors; + _self.AddValue(_type, _defaultValue, _required, _minValue, + _maxValue, errors, _description); + ThrowIfErrors(errors); + }, + "Add a value to this Element") + // get_attribute (string) + .def("get_attribute_count", &sdf::Element::GetAttributeCount, + "Get the number of attributes.") + // get_attributes + // get_attribute (index) + // get_element_description_count + // get_element_description (index) + // get_element_description (string) + // has_element_description + .def("has_attribute", &sdf::Element::HasAttribute, + "Return true if an attribute exists.") + .def("get_attribute_set", &sdf::Element::GetAttributeSet, + "Return true if the attribute was set (i.e. not default value)") + // remove_attribute + // remove_all_attributes + // get_value + .def( + "get_any", + [](sdf::Element &_self, const std::string &_key = "") + { + sdf::Errors errors; + auto output = _self.GetAny(errors,_key); + ThrowIfErrors(errors); + return output; + }, + "Add a value to this Element") + .def("has_element", &sdf::Element::HasElement, + "Return true if the named element exists.") + .def("get_first_element", &sdf::Element::GetFirstElement, + "Get the first child element") + .def("get_next_element", &sdf::Element::GetNextElement, + "Get the first child Get the next sibling of this element.") + // get_element_type_names + .def("find_element", + py::overload_cast( + &sdf::Element::FindElement), + "Return a pointer to the child element with the provided name.") + .def( + "add_element", + [](sdf::Element &_self, const std::string &_name) + { + sdf::Errors errors; + auto output = _self.AddElement(_name, errors); + ThrowIfErrors(errors); + return output; + }, + "Add a value to this Element") + .def("insert_element", + py::overload_cast( + &sdf::Element::InsertElement), + "Add an element object, and optionally set the given element's " + "parent to this object", + "elem"_a, "set_parent_to_self"_a = false) + // remove_from_parent + // remove_child + // clear_elements + // clear + // update + // reset + .def("set_include_element", &sdf::Element::SetIncludeElement, + "Set the `` element that was used to load this element") + .def( + "get_include_element", &sdf::Element::GetIncludeElement, + "Get the `` element that was used to load this element.") + .def("set_file_path", &sdf::Element::SetFilePath, + "Set the path to the SDF document where this element came from.") + .def("file_path", &sdf::Element::FilePath, + "Get the path to the SDF document where this element came from") + .def("set_line_number", &sdf::Element::SetLineNumber, + "Set the line number of this element within the SDF document.") + .def("line_number", &sdf::Element::LineNumber, + "Get the line number of this element within the SDF document.") + .def("set_xml_path", &sdf::Element::SetXmlPath, + "Set the XML path of this element.") + .def("xml_path", &sdf::Element::XmlPath, + "Get the XML path of this element.") + .def("set_original_version", &sdf::Element::SetOriginalVersion, + "Set the spec version that this was originally parsed from.") + .def("original_version", &sdf::Element::OriginalVersion, + "Get the spec version that this was originally parsed from."); + DefineElementGetSet()(elemClass); +} +} // namespace python +} // namespace SDF_VERSION_NAMESPACE +} // namespace sdf diff --git a/python/src/sdf/pyElement.hh b/python/src/sdf/pyElement.hh new file mode 100644 index 000000000..619a9f555 --- /dev/null +++ b/python/src/sdf/pyElement.hh @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SDFORMAT_PYTHON_ELEMENT_HH_ +#define SDFORMAT_PYTHON_ELEMENT_HH_ + +#include + +#include "sdf/Element.hh" + +#include "sdf/config.hh" + +namespace sdf +{ +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +namespace python +{ +/// Define a pybind11 wrapper for an sdf::Element +/** + * \param[in] module a pybind11 module to add the definition to + */ +void defineElement(pybind11::object module); +} // namespace python +} // namespace SDF_VERSION_NAMESPACE +} // namespace sdf + +#endif // SDFORMAT_PYTHON_SCENE_HH_ diff --git a/python/test/pyElement_TEST.py b/python/test/pyElement_TEST.py new file mode 100644 index 000000000..35775145b --- /dev/null +++ b/python/test/pyElement_TEST.py @@ -0,0 +1,147 @@ +# Copyright (C) 2023 Open Source Robotics Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License") +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from gz_test_deps.sdformat import Element, SDFErrorsException +from gz_test_deps.math import Vector3d +import unittest + + +class ElementTEST(unittest.TestCase): + + def add_child_element(self, parent: Element, element_name: str, + add_name_attribute: bool, child_name: str): + child = Element() + child.set_parent(parent) + child.set_name(element_name) + parent.insert_element(child) + if add_name_attribute: + child.add_attribute("name", "string", child_name, False, + "description") + return child + + def test_child(self): + child = Element() + parent = Element() + self.assertIsNone(child.get_parent()) + child.set_parent(parent) + self.assertIsNotNone(child.get_parent()) + + def test_name(self): + elem = Element() + self.assertEqual(elem.get_name(), "") + elem.set_name("test") + self.assertEqual(elem.get_name(), "test") + + def test_required(self): + elem = Element() + self.assertEqual(elem.get_required(), "") + elem.set_required("1") + self.assertEqual(elem.get_required(), "1") + + def test_get_templates(self): + elem = Element() + + elem.add_attribute("test", "string", "foo", False, "foo description") + elem.add_attribute("test_int", "int", "5", False, "none") + elem.add_attribute("test_vector3", "vector3", "1 2 3", False, "none") + with self.assertRaises(SDFErrorsException): + elem.add_attribute("test_error", "int", "bad", False, "none") + + out = elem.get_string("test") + self.assertEqual(out, "foo") + + self.assertEqual(elem.get_int("test_int"), 5) + self.assertEqual(elem.get_vector3("test_vector3"), Vector3d(1, 2, 3)) + + pairout = elem.get_string("test", "def") + self.assertEqual(pairout[0], "foo") + self.assertEqual(pairout[1], True) + + def test_clone(self): + parent = Element() + child = Element() + # desc = Element() + + parent.set_name("parent") + child.set_name("child") + + parent.insert_element(child) + self.assertIsNotNone(parent.get_first_element()) + + # parent.add_element_description(desc) + # self.assertEqual(parent.get_element_description_count(), 1) + + parent.add_attribute("test", "string", "foo", False, "foo description") + self.assertEqual(parent.get_attribute_count(), 1) + + parent.add_value("string", "foo", False, "foo description") + + parent.set_file_path("/path/to/file.sdf") + parent.set_line_number(12) + parent.set_xml_path("/sdf/world[@name=\"default\"]") + parent.set_original_version("1.5") + + includeElemToStore = Element() + includeElemToStore.set_name("include") + parent.set_include_element(includeElemToStore) + + newelem = parent.clone() + + self.assertEqual("/path/to/file.sdf", newelem.file_path()) + self.assertIsNotNone(newelem.line_number()) + self.assertEqual(12, newelem.line_number()) + self.assertEqual("/sdf/world[@name=\"default\"]", newelem.xml_path()) + self.assertEqual("1.5", newelem.original_version()) + self.assertIsNotNone(newelem.get_first_element()) + # self.assertEqual(newelem.get_element_description_count(), 1) + self.assertEqual(newelem.get_attribute_count(), 1) + self.assertIsNotNone(newelem.get_include_element()) + self.assertEqual("include", newelem.get_include_element().get_name()) + self.assertTrue(newelem.get_explicitly_set_in_file()) + + # self.assertIsNotNone(parent.get_value().get_parent_element()) + # self.assertEqual(parent, parent.get_value().get_parent_element()) + # + # self.assertIsNotNone(newelem.get_value().get_parent_element()) + # self.assertEqual(newelem, newelem.get_value().get_parent_element()) + + # clonedAttribs = newelem.get_attributes() + # self.assertEqual(newelem, clonedAttribs[0].get_parent_element()) + + def test_find_element(self): + root = Element() + root.set_name("root") + elem_a = self.add_child_element(root, "elem_A", False, "") + self.add_child_element(elem_a, "child_elem_A", False, "") + elem_b = self.add_child_element(root, "elem_B", False, "") + self.add_child_element(elem_b, "child_elem_B", True, "first_child") + self.add_child_element(elem_b, "child_elem_B", False, "") + + test_elem_a = root.find_element("elem_A") + self.assertIsNotNone(test_elem_a) + self.assertIsNotNone(test_elem_a.find_element("child_elem_A")) + self.assertIsNone(test_elem_a.find_element("non_existent_elem")) + + elem_b = root.find_element("elem_B") + self.assertIsNotNone(elem_b) + # This should find the first "child_elem_B" element, which has the name + # attribute + child_elem_b = elem_b.find_element("child_elem_B") + self.assertTrue(child_elem_b.has_attribute("name")) + # self.assertEqual("first_child", + # child_elem_b.get_attribute("name").get_as_string()) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/Exception.cc b/src/Exception.cc index ebbf3b592..fa0f038ae 100644 --- a/src/Exception.cc +++ b/src/Exception.cc @@ -48,8 +48,6 @@ Exception::Exception(const char *_file, std::int64_t _line, std::string _msg) this->dataPtr->file = _file; this->dataPtr->line = _line; this->dataPtr->str = _msg; - // TODO(azeey) Remove Print for libsdformat 14.0 - this->Print(); } ////////////////////////////////////////////////// From 77364f63d19a7eb03f6135a4c3e2222804ddd9dc Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Sat, 12 Aug 2023 00:29:39 -0500 Subject: [PATCH 2/9] Add PrintConfig and Param Signed-off-by: Addisu Z. Taddese --- python/CMakeLists.txt | 3 + python/src/sdf/_gz_sdformat_pybind11.cc | 6 + python/src/sdf/pyElement.cc | 195 ++++++++------------- python/src/sdf/pyParam.cc | 218 ++++++++++++++++++++++++ python/src/sdf/pyParam.hh | 52 ++++++ python/src/sdf/pyPrintConfig.cc | 43 +++++ python/src/sdf/pyPrintConfig.hh | 41 +++++ python/src/sdf/pybind11_helpers.hh | 85 +++++++++ python/test/pyElement_TEST.py | 13 ++ python/test/pyParam_TEST.py | 39 +++++ 10 files changed, 574 insertions(+), 121 deletions(-) create mode 100644 python/src/sdf/pyParam.cc create mode 100644 python/src/sdf/pyParam.hh create mode 100644 python/src/sdf/pyPrintConfig.cc create mode 100644 python/src/sdf/pyPrintConfig.hh create mode 100644 python/test/pyParam_TEST.py diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index e62548d7c..575050636 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -73,6 +73,7 @@ pybind11_add_module(${BINDINGS_MODULE_NAME} MODULE src/sdf/pyModel.cc src/sdf/pyNavSat.cc src/sdf/pyNoise.cc + src/sdf/pyParam.cc src/sdf/pyParserConfig.cc src/sdf/pyParticleEmitter.cc src/sdf/pyPbr.cc @@ -80,6 +81,7 @@ pybind11_add_module(${BINDINGS_MODULE_NAME} MODULE src/sdf/pyPlane.cc src/sdf/pyPlugin.cc src/sdf/pyPolyline.cc + src/sdf/pyPrintConfig.cc src/sdf/pyProjector.cc src/sdf/pyRoot.cc src/sdf/pyScene.cc @@ -153,6 +155,7 @@ if (BUILD_TESTING AND NOT WIN32) pyModel_TEST pyNoise_TEST pyNavSat_TEST + pyParam_TEST pyParserConfig_TEST pyParticleEmitter_TEST pyPbr_TEST diff --git a/python/src/sdf/_gz_sdformat_pybind11.cc b/python/src/sdf/_gz_sdformat_pybind11.cc index 78a38f969..929b6f999 100644 --- a/python/src/sdf/_gz_sdformat_pybind11.cc +++ b/python/src/sdf/_gz_sdformat_pybind11.cc @@ -48,6 +48,7 @@ #include "pyModel.hh" #include "pyNavSat.hh" #include "pyNoise.hh" +#include "pyParam.hh" #include "pyParserConfig.hh" #include "pyParticleEmitter.hh" #include "pyPbr.hh" @@ -55,6 +56,7 @@ #include "pyPlane.hh" #include "pyPlugin.hh" #include "pyPolyline.hh" +#include "pyPrintConfig.hh" #include "pyProjector.hh" #include "pyRoot.hh" #include "pyScene.hh" @@ -109,6 +111,10 @@ PYBIND11_MODULE(BINDINGS_MODULE_NAME, m) { sdf::python::defineNavSat(m); sdf::python::defineNoise(m); sdf::python::defineODE(m); + // PrintConfig has to be defined before Param and Element because it's used as + // a default argument. + sdf::python::definePrintConfig(m); + sdf::python::defineParam(m); sdf::python::defineParserConfig(m); sdf::python::defineParticleEmitter(m); sdf::python::definePbr(m); diff --git a/python/src/sdf/pyElement.cc b/python/src/sdf/pyElement.cc index c9a9888be..23deb6113 100644 --- a/python/src/sdf/pyElement.cc +++ b/python/src/sdf/pyElement.cc @@ -18,11 +18,14 @@ #include #include + #include -#include "sdf/Element.hh" #include "pybind11_helpers.hh" - +#include "pyParam.hh" +#include "sdf/Element.hh" +#include "sdf/Param.hh" +#include "sdf/PrintConfig.hh" #include "sdf/config.hh" using namespace pybind11::literals; @@ -34,69 +37,41 @@ inline namespace SDF_VERSION_NAMESPACE { namespace python { -using PyClassElement = py::class_; - -template -std::string computeSuffix() -{ - // TypeToString returns a value with a space, which would not be a valid - // python function name, so we override that here. - if constexpr (std::is_same_v) - return "unsigned_int"; - return ParamPrivate::TypeToString(); -} template -struct DefineElementGetSet +struct DefineGetSetImpl { - void operator()(PyClassElement &_cls) + template + void operator()(py::class_ &_cls) { const std::string getFuncName = "get_" + computeSuffix(); const std::string setFuncName = "set_" + computeSuffix(); - _cls.def( - getFuncName.c_str(), - [](const sdf::Element &_self, const std::string &_key) - { - sdf::Errors errors; - T val = _self.Get(errors, _key); - ThrowIfErrors(errors); - return val; - }, - "Get the value of a key. This function assumes the _key exists.") - .def( - getFuncName.c_str(), - [](const sdf::Element &_self, const std::string &_key, - const T &_defaultValue) - { - sdf::Errors errors; - std::pair val = - _self.Get(errors, _key, _defaultValue); - ThrowIfErrors(errors); - return val; - }, - "Get the value of a key. This function assumes the _key exists.") - .def( - setFuncName.c_str(), - [](sdf::Element &_self, const T &_value) - { - sdf::Errors errors; - bool result = _self.Set(errors, _value); - ThrowIfErrors(errors); - return result; - }, - "Get the value of a key. This function assumes the _key exists."); + _cls.def(getFuncName.c_str(), + ErrorWrappedCast(&Class::template Get, + py::const_), + "Get the value of a key. This function assumes the _key exists.") + .def(getFuncName.c_str(), + ErrorWrappedCast( + &Class::template Get, py::const_), + "Get the value of a key. This function assumes the _key exists.") + .def(setFuncName.c_str(), + ErrorWrappedCast(&Class::template Set), + "Get the value of a key. This function assumes the _key exists."); } }; template -struct DefineElementGetSet> +struct DefineGetSetImpl> { - void operator()(PyClassElement &_cls) + template + void operator()(PyClass &_cls) const noexcept { - (DefineElementGetSet()(_cls), ...); + (DefineGetSetImpl()(_cls), ...); } }; +static constexpr DefineGetSetImpl defineGetSet = {}; + ///////////////////////////////////////////////// // TODO(azeey) Interspersed between the `.def` calls is a list of // functions that need to be implemented. I'm not 100% sure if all of them need @@ -128,58 +103,49 @@ struct DefineElementGetSet> // - NameUniquenessExceptions (Used for error checking by the parser) void defineElement(py::object module) { + using PyClassElement = py::class_; auto elemClass = PyClassElement(module, "Element") .def(py::init<>()) - .def( - "clone", - [](const sdf::Element &_self) - { - sdf::Errors errors; - sdf::ElementPtr elem = _self.Clone(errors); - ThrowIfErrors(errors); - return elem; - }, - "Create a copy of this Element.") - .def("get_parent", &sdf::Element::GetParent, + .def("clone", ErrorWrappedCast<>(&Element::Clone, py::const_), + "Create a copy of this Element.") + .def("get_parent", &Element::GetParent, "Get a pointer to this Element's parent.") - .def("set_parent", &sdf::Element::SetParent, + .def("set_parent", &Element::SetParent, "Set the parent of this Element.") - .def("set_name", &sdf::Element::SetName, - "Set the name of the Element") - .def("get_name", &sdf::Element::GetName, "Get the Element's name.") - .def("set_required", &sdf::Element::SetRequired, + .def("set_name", &Element::SetName, "Set the name of the Element") + .def("get_name", &Element::GetName, "Get the Element's name.") + .def("set_required", &Element::SetRequired, "Set the requirement type.") - .def("get_required", &sdf::Element::GetRequired, + .def("get_required", &Element::GetRequired, "Get the requirement string.") // print_description - .def("set_explicitly_set_in_file", - &sdf::Element::SetExplicitlySetInFile, + .def("set_explicitly_set_in_file", &Element::SetExplicitlySetInFile, "Set if the element and children where set or default in the " "original file") - .def("get_explicitly_set_in_file", - &sdf::Element::GetExplicitlySetInFile, + .def("get_explicitly_set_in_file", &Element::GetExplicitlySetInFile, "Return if the element was been explicitly set in the file") // to_string .def( "add_attribute", - [](sdf::Element &_self, const std::string &_key, + [](Element &_self, const std::string &_key, const std::string &_type, const std::string &_defaultvalue, bool _required, const std::string &_description = "") { - sdf::Errors errors; + Errors errors; _self.AddAttribute(_key, _type, _defaultvalue, _required, errors, _description); ThrowIfErrors(errors); }, - "Add an attribute value.") + "Add an attribute value.", "key"_a, "type"_a, "default_value"_a, + "required"_a, "description"_a = "") .def( "add_value", - [](sdf::Element &_self, const std::string &_type, + [](Element &_self, const std::string &_type, const std::string &_defaultValue, bool _required, const std::string &_description = "") { - sdf::Errors errors; + Errors errors; _self.AddValue(_type, _defaultValue, _required, errors, _description); ThrowIfErrors(errors); @@ -187,19 +153,20 @@ void defineElement(py::object module) "Add a value to this Element") .def( "add_value", - [](sdf::Element &_self, const std::string &_type, + [](Element &_self, const std::string &_type, const std::string &_defaultValue, bool _required, const std::string &_minValue, const std::string &_maxValue, const std::string &_description = "") { - sdf::Errors errors; + Errors errors; _self.AddValue(_type, _defaultValue, _required, _minValue, _maxValue, errors, _description); ThrowIfErrors(errors); }, - "Add a value to this Element") + "Add a value to this Element", "type"_a, "default_value"_a, + "required"_a, "min_value"_a, "max_value"_a, "description"_a = "") // get_attribute (string) - .def("get_attribute_count", &sdf::Element::GetAttributeCount, + .def("get_attribute_count", &Element::GetAttributeCount, "Get the number of attributes.") // get_attributes // get_attribute (index) @@ -207,47 +174,33 @@ void defineElement(py::object module) // get_element_description (index) // get_element_description (string) // has_element_description - .def("has_attribute", &sdf::Element::HasAttribute, + .def("has_attribute", &Element::HasAttribute, "Return true if an attribute exists.") - .def("get_attribute_set", &sdf::Element::GetAttributeSet, + .def("get_attribute_set", &Element::GetAttributeSet, "Return true if the attribute was set (i.e. not default value)") // remove_attribute // remove_all_attributes - // get_value - .def( - "get_any", - [](sdf::Element &_self, const std::string &_key = "") - { - sdf::Errors errors; - auto output = _self.GetAny(errors,_key); - ThrowIfErrors(errors); - return output; - }, - "Add a value to this Element") - .def("has_element", &sdf::Element::HasElement, + .def("get_value", &Element::GetValue, + "Get the param of the elements value") + .def("get_any", + ErrorWrappedCast(&Element::GetAny, + py::const_), + "Add a value to this Element") + .def("has_element", &Element::HasElement, "Return true if the named element exists.") - .def("get_first_element", &sdf::Element::GetFirstElement, + .def("get_first_element", &Element::GetFirstElement, "Get the first child element") - .def("get_next_element", &sdf::Element::GetNextElement, + .def("get_next_element", &Element::GetNextElement, "Get the first child Get the next sibling of this element.") // get_element_type_names .def("find_element", - py::overload_cast( - &sdf::Element::FindElement), + py::overload_cast(&Element::FindElement), "Return a pointer to the child element with the provided name.") - .def( - "add_element", - [](sdf::Element &_self, const std::string &_name) - { - sdf::Errors errors; - auto output = _self.AddElement(_name, errors); - ThrowIfErrors(errors); - return output; - }, - "Add a value to this Element") + .def("add_element", + ErrorWrappedCast(&Element::AddElement), + "Add a value to this Element") .def("insert_element", - py::overload_cast( - &sdf::Element::InsertElement), + py::overload_cast(&Element::InsertElement), "Add an element object, and optionally set the given element's " "parent to this object", "elem"_a, "set_parent_to_self"_a = false) @@ -257,28 +210,28 @@ void defineElement(py::object module) // clear // update // reset - .def("set_include_element", &sdf::Element::SetIncludeElement, + .def("set_include_element", &Element::SetIncludeElement, "Set the `` element that was used to load this element") .def( - "get_include_element", &sdf::Element::GetIncludeElement, + "get_include_element", &Element::GetIncludeElement, "Get the `` element that was used to load this element.") - .def("set_file_path", &sdf::Element::SetFilePath, + .def("set_file_path", &Element::SetFilePath, "Set the path to the SDF document where this element came from.") - .def("file_path", &sdf::Element::FilePath, + .def("file_path", &Element::FilePath, "Get the path to the SDF document where this element came from") - .def("set_line_number", &sdf::Element::SetLineNumber, + .def("set_line_number", &Element::SetLineNumber, "Set the line number of this element within the SDF document.") - .def("line_number", &sdf::Element::LineNumber, + .def("line_number", &Element::LineNumber, "Get the line number of this element within the SDF document.") - .def("set_xml_path", &sdf::Element::SetXmlPath, + .def("set_xml_path", &Element::SetXmlPath, "Set the XML path of this element.") - .def("xml_path", &sdf::Element::XmlPath, + .def("xml_path", &Element::XmlPath, "Get the XML path of this element.") - .def("set_original_version", &sdf::Element::SetOriginalVersion, + .def("set_original_version", &Element::SetOriginalVersion, "Set the spec version that this was originally parsed from.") - .def("original_version", &sdf::Element::OriginalVersion, + .def("original_version", &Element::OriginalVersion, "Get the spec version that this was originally parsed from."); - DefineElementGetSet()(elemClass); + defineGetSet(elemClass); } } // namespace python } // namespace SDF_VERSION_NAMESPACE diff --git a/python/src/sdf/pyParam.cc b/python/src/sdf/pyParam.cc new file mode 100644 index 000000000..ac79cc3a0 --- /dev/null +++ b/python/src/sdf/pyParam.cc @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2023 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "pyParam.hh" + +#include +#include + +#include + +#include "pybind11_helpers.hh" +#include "sdf/Element.hh" +#include "sdf/Param.hh" +#include "sdf/PrintConfig.hh" +#include "sdf/config.hh" + +using namespace pybind11::literals; +namespace py = pybind11; +namespace sdf +{ +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +namespace python +{ + + + +template +struct ForEachParamTypeImpl; + +template +struct ForEachParamTypeImpl> +{ + template + void operator()(Func _func) const + { + (ForEachParamTypeImpl{}(_func), ...); + } +}; + +template +struct ForEachParamTypeImpl +{ + template + void operator()(Func _func) const + { + _func(T{}); + } +}; + +static constexpr ForEachParamTypeImpl + forEachParamType = {}; + +void defineParam(py::object module) +{ + using PyClassParam = py::class_; + auto paramClass = PyClassParam(module, "Param"); + + paramClass + .def(py::init( + [](const std::string &_key, const std::string &_typeName, + const std::string &_default, bool _required, + const std::string &_description) + { + Errors errors; + auto output = Param(_key, _typeName, _default, _required, + errors, _description); + ThrowIfErrors(errors); + return output; + }), + "key"_a, "type_name"_a, "default"_a, "required"_a, + "description"_a = "") + .def(py::init( + [](const std::string &_key, const std::string &_typeName, + const std::string &_default, bool _required, + const std::string &_minValue, const std::string &_maxValue, + const std::string &_description) + { + Errors errors; + auto output = + Param(_key, _typeName, _default, _required, _minValue, + _maxValue, errors, _description); + ThrowIfErrors(errors); + return output; + }), + "key"_a, "type_name"_a, "default"_a, "required"_a, "min_value"_a, + "max_value"_a, "description"_a = "") + .def(py::init()) + .def("get_as_string", + ErrorWrappedCast(&Param::GetAsString, + py::const_), + "Get the value as a string.", "config"_a = PrintConfig()) + .def("get_default_as_string", + ErrorWrappedCast(&Param::GetDefaultAsString, + py::const_), + "Get the default value as a string", "config"_a = PrintConfig()) + .def("get_min_value_as_string", + ErrorWrappedCast(&Param::GetMinValueAsString, + py::const_), + "Get the minimum allowed value as a string.", + "config"_a = PrintConfig()) + .def("set_from_string", + ErrorWrappedCast(&Param::SetFromString), + "Set the parameter value from a string.") + .def("set_from_string", + ErrorWrappedCast(&Param::SetFromString), + "Set the parameter value from a string.") + .def("get_parent_element", &Param::GetParentElement, + "Get the parent Element of this Param.") + .def("set_parent_element", + ErrorWrappedCast(&Param::SetParentElement), + "Set the parent Element of this Param.") + .def("reset", &Param::Reset, "Reset the parameter to the default value.") + .def("reparse", ErrorWrappedCast<>(&Param::Reparse), + "Set the parameter value from a string.") + .def("get_key", &Param::GetKey, "Get the key value.") + // is_type is defined below + .def("get_type_name", &Param::GetTypeName, "Get the type name value.") + .def("get_required", &Param::GetRequired, + "Return whether the parameter is required.") + .def("get_set", &Param::GetSet, + "Return true if the parameter has been set.") + .def("ignores_parent_element_attribute", + &Param::IgnoresParentElementAttribute, + "Return true if the parameter ignores the parent element's " + "attributes, or if the parameter has no parent element.") + .def("clone", &Param::Clone, "Clone the parameter.") + .def( + "set_update_func", + [](Param &_self, const py::object &_func) + { _self.SetUpdateFunc(_func); }, + "Set the update function. The updateFunc will be used to set the " + "parameter's value when Param::Update is called.") + .def("update", ErrorWrappedCast<>(&Param::Update), + "Set the parameter's value using the update_func.") + // set is defined below + .def("get_any", ErrorWrappedCast(&Param::GetAny, py::const_), + "Get the value as a string.") + // get is defined below + .def("set_description", &Param::SetDescription, + "Set the description of the parameter.") + .def("get_description", &Param::GetDescription, + "Get the description of the parameter.") + .def("validate_value", + ErrorWrappedCast<>(&Param::ValidateValue, py::const_), + "Validate the value against minimum and maximum allowed values"); + + // Definitions for `is_type`, `get`, `get_default`, and `set` + forEachParamType( + [¶mClass](auto &&arg) + { + using T = std::decay_t; + const std::string nameWithSuffix = "is_type_" + computeSuffix(); + paramClass.def(nameWithSuffix.c_str(), &Param::IsType, ""); + }); + + forEachParamType( + [¶mClass](auto &&arg) + { + using T = std::decay_t; + const std::string nameWithSuffix = "get_" + computeSuffix(); + paramClass.def( + nameWithSuffix.c_str(), + [](const Param &_self) + { + sdf::Errors errors; + T value; + bool rc = _self.Get(value, errors); + ThrowIfErrors(errors); + return py::make_tuple(value, rc); + }, + "Get the value of the parameter."); + }); + + forEachParamType( + [¶mClass](auto &&arg) + { + using T = std::decay_t; + const std::string nameWithSuffix = "get_default_" + computeSuffix(); + paramClass.def( + nameWithSuffix.c_str(), + [](const Param &_self) + { + sdf::Errors errors; + T value; + bool rc = _self.GetDefault(value, errors); + ThrowIfErrors(errors); + return py::make_tuple(value, rc); + }, + "Get the value of the parameter."); + }); + + forEachParamType( + [¶mClass](auto &&arg) + { + using T = std::decay_t; + const std::string nameWithSuffix = "set" + computeSuffix(); + paramClass.def(nameWithSuffix.c_str(), + ErrorWrappedCast(&Param::Set), + "Get the value of the parameter."); + }); +} +} // namespace python +} // namespace SDF_VERSION_NAMESPACE +} // namespace sdf diff --git a/python/src/sdf/pyParam.hh b/python/src/sdf/pyParam.hh new file mode 100644 index 000000000..f4ed2fd4c --- /dev/null +++ b/python/src/sdf/pyParam.hh @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SDFORMAT_PYTHON_PARAM_HH_ +#define SDFORMAT_PYTHON_PARAM_HH_ + +#include + +#include "sdf/Param.hh" + +#include "sdf/config.hh" + +namespace sdf +{ +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +namespace python +{ +// Define a pybind11 wrapper for an sdf::Param +/** + * \param[in] module a pybind11 module to add the definition to + */ +void defineParam(pybind11::object module); + +template +std::string computeSuffix() +{ + // TypeToString returns a value with a space, which would not be a valid + // python function name, so we override that here. + if constexpr (std::is_same_v) + return "unsigned_int"; + return ParamPrivate::TypeToString(); +} + +} // namespace python +} // namespace SDF_VERSION_NAMESPACE +} // namespace sdf + +#endif // SDFORMAT_PYTHON_SCENE_HH_ diff --git a/python/src/sdf/pyPrintConfig.cc b/python/src/sdf/pyPrintConfig.cc new file mode 100644 index 000000000..6a99e68ca --- /dev/null +++ b/python/src/sdf/pyPrintConfig.cc @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "pyPrintConfig.hh" + +#include +#include +#include + +#include "sdf/PrintConfig.hh" +#include "pybind11_helpers.hh" + +#include "sdf/config.hh" + +using namespace pybind11::literals; +namespace py = pybind11; +namespace sdf +{ +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +namespace python +{ + +void definePrintConfig(py::object module) +{ + py::class_(module, "PrintConfig").def(py::init<>()); +} +} // namespace python +} // namespace SDF_VERSION_NAMESPACE +} // namespace sdf diff --git a/python/src/sdf/pyPrintConfig.hh b/python/src/sdf/pyPrintConfig.hh new file mode 100644 index 000000000..eb0f47d27 --- /dev/null +++ b/python/src/sdf/pyPrintConfig.hh @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SDFORMAT_PYTHON_PRINTCONFIG_HH_ +#define SDFORMAT_PYTHON_PRINTCONFIG_HH_ + +#include + +#include "sdf/PrintConfig.hh" + +#include "sdf/config.hh" + +namespace sdf +{ +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +namespace python +{ +/// Define a pybind11 wrapper for an sdf::PrintConfig +/** + * \param[in] module a pybind11 module to add the definition to + */ +void definePrintConfig(pybind11::object module); +} // namespace python +} // namespace SDF_VERSION_NAMESPACE +} // namespace sdf + +#endif // SDFORMAT_PYTHON_SCENE_HH_ diff --git a/python/src/sdf/pybind11_helpers.hh b/python/src/sdf/pybind11_helpers.hh index 0a6eb2f8d..2d632d72f 100644 --- a/python/src/sdf/pybind11_helpers.hh +++ b/python/src/sdf/pybind11_helpers.hh @@ -14,6 +14,8 @@ * limitations under the License. * */ +#ifndef SDFORMAT_PYTHON_PYBIND11_HELPERS_HH_ +#define SDFORMAT_PYTHON_PYBIND11_HELPERS_HH_ #include @@ -32,6 +34,89 @@ namespace python /// \param[in] _errors The list of errors to check. /// \throws PySDFErrorsException void ThrowIfErrors(const sdf::Errors &_errors); + + +// NOTE: This currently only works for member funtions +template +struct ErrorWrappedCastImpl +{ + template + static auto ErrorFirst(Func pmf) + { + return [pmf](Class &_self, Args... args) + { + sdf::Errors errors; + if constexpr (std::is_same_v) + { + (_self.*pmf)(errors, std::forward(args)...); + ThrowIfErrors(errors); + } + else + { + auto output = (_self.*pmf)(errors, std::forward(args)...); + ThrowIfErrors(errors); + return output; + } + }; + } + + template + static auto ErrorLast(Func pmf) + { + return [pmf](Class &_self, Args... args) + { + sdf::Errors errors; + if constexpr (std::is_same_v) + { + (_self.*pmf)(std::forward(args)..., errors); + ThrowIfErrors(errors); + } + else + { + auto output = (_self.*pmf)(std::forward(args)..., errors); + ThrowIfErrors(errors); + return output; + } + }; + } + + template + auto operator()(Return (Class::*pmf)(Errors &, Args...) const, + std::true_type) const noexcept + { + return ErrorFirst(pmf); + } + template + auto operator()(Return (Class::*pmf)(Errors &, Args...), + std::false_type = {}) const noexcept + { + return ErrorFirst(pmf); + } + + template > + auto operator()(Return (Class::*pmf)(Args..., Errors &) const, + std::true_type) const noexcept + { + return ErrorLast(pmf); + } + + template > + auto operator()(Return (Class::*pmf)(Args..., Errors &), + std::false_type = {}) const noexcept + { + return ErrorLast(pmf); + } +}; + +template +static constexpr ErrorWrappedCastImpl ErrorWrappedCast = {}; + } // namespace python } // namespace SDF_VERSION_NAMESPACE } // namespace sdf + +#endif diff --git a/python/test/pyElement_TEST.py b/python/test/pyElement_TEST.py index 35775145b..6b5362801 100644 --- a/python/test/pyElement_TEST.py +++ b/python/test/pyElement_TEST.py @@ -119,6 +119,19 @@ def test_clone(self): # clonedAttribs = newelem.get_attributes() # self.assertEqual(newelem, clonedAttribs[0].get_parent_element()) + def test_add_value(self): + elem = Element() + elem.set_name("test") + elem.add_value("string", "foo", False, "foo description") + + param = elem.get_value() + # ASSERT_EQ(param->GetKey(), "test") + # ASSERT_EQ(param->GetTypeName(), "string") + self.assertEqual(param.get_default_as_string(), "foo") + # ASSERT_EQ(param->GetDescription(), "foo description") + self.assertNotEqual(param.get_parent_element(), None) + self.assertEqual(param.get_parent_element(), elem) + def test_find_element(self): root = Element() root.set_name("root") diff --git a/python/test/pyParam_TEST.py b/python/test/pyParam_TEST.py new file mode 100644 index 000000000..8bfb05372 --- /dev/null +++ b/python/test/pyParam_TEST.py @@ -0,0 +1,39 @@ +# Copyright (C) 2023 Open Source Robotics Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License") +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from gz_test_deps.sdformat import (Element, Param, PrintConfig, + SDFErrorsException) +from gz_test_deps.math import Vector3d +import unittest + + +class ParamTEST(unittest.TestCase): + + def test_bool(self): + bool_param = Param("key", "bool", "true", False, "description") + value, rc = bool_param.get_bool() + self.assertTrue(value) + value, rc = bool_param.get_int() + + def test_get_as_string(self): + param = Param("test_key", "int", "5", False, "bla") + param.get_as_string() + param.get_parent_element() + elem = Element() + param.set_parent_element(elem) + self.assertEqual(elem, param.get_parent_element()) + + +if __name__ == '__main__': + unittest.main() From d4a83441ceeb405f0e6c679d4d412c617e3c7c81 Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Tue, 15 Aug 2023 18:21:19 -0500 Subject: [PATCH 3/9] Add Param tests Signed-off-by: Addisu Z. Taddese --- python/src/sdf/pyParam.cc | 12 +-- python/test/pyParam_TEST.py | 144 +++++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 9 deletions(-) diff --git a/python/src/sdf/pyParam.cc b/python/src/sdf/pyParam.cc index ac79cc3a0..4c0271e6f 100644 --- a/python/src/sdf/pyParam.cc +++ b/python/src/sdf/pyParam.cc @@ -147,8 +147,7 @@ void defineParam(py::object module) .def("update", ErrorWrappedCast<>(&Param::Update), "Set the parameter's value using the update_func.") // set is defined below - .def("get_any", ErrorWrappedCast(&Param::GetAny, py::const_), - "Get the value as a string.") + // get_any is not supported since std::any is not supported in pybind11. // get is defined below .def("set_description", &Param::SetDescription, "Set the description of the parameter.") @@ -164,7 +163,8 @@ void defineParam(py::object module) { using T = std::decay_t; const std::string nameWithSuffix = "is_type_" + computeSuffix(); - paramClass.def(nameWithSuffix.c_str(), &Param::IsType, ""); + paramClass.def(nameWithSuffix.c_str(), &Param::IsType, + "Return true if the param is a particular type"); }); forEachParamType( @@ -200,17 +200,17 @@ void defineParam(py::object module) ThrowIfErrors(errors); return py::make_tuple(value, rc); }, - "Get the value of the parameter."); + "Get the default value of the parameter."); }); forEachParamType( [¶mClass](auto &&arg) { using T = std::decay_t; - const std::string nameWithSuffix = "set" + computeSuffix(); + const std::string nameWithSuffix = "set_" + computeSuffix(); paramClass.def(nameWithSuffix.c_str(), ErrorWrappedCast(&Param::Set), - "Get the value of the parameter."); + "Set the value of the parameter."); }); } } // namespace python diff --git a/python/test/pyParam_TEST.py b/python/test/pyParam_TEST.py index 8bfb05372..c9a623bb3 100644 --- a/python/test/pyParam_TEST.py +++ b/python/test/pyParam_TEST.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http:#www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -14,7 +14,7 @@ from gz_test_deps.sdformat import (Element, Param, PrintConfig, SDFErrorsException) -from gz_test_deps.math import Vector3d +from gz_test_deps.math import Vector2i, Vector3d, Pose3d import unittest @@ -24,7 +24,145 @@ def test_bool(self): bool_param = Param("key", "bool", "true", False, "description") value, rc = bool_param.get_bool() self.assertTrue(value) - value, rc = bool_param.get_int() + bool_param.set_bool(False) + self.assertFalse(bool_param.get_bool()[0]) + + str_param = Param("key", "string", "true", False, "description") + + self.assertTrue(str_param.get_bool()[0]) + + str_param.set_string("False") + self.assertFalse(str_param.get_bool()[0]) + + str_param.set_string("1") + self.assertTrue(str_param.get_bool()[0]) + + str_param.set_string("0") + value, rc = str_param.get_bool() + self.assertFalse(str_param.get_bool()[0]) + + str_param.set_string("True") + self.assertTrue(str_param.get_bool()[0]) + + str_param.set_string("TRUE") + self.assertTrue(str_param.get_bool()[0]) + + # Anything other than 1 or true is treated as a False value + str_param.set_string("%") + with self.assertRaises(SDFErrorsException): + str_param.get_bool() + + # Test getting and setting a floating point value + def test_double(self): + double_param = Param("key", "double", "0.0", False, "description") + + self.assertEqual(double_param.get_double(), (0.0, True)) + + double_param.set_double(1.0) + self.assertEqual(double_param.get_double(), (1.0, True)) + + sixteen_digits = 12345678.87654321 + double_param.set_double(sixteen_digits) + self.assertEqual(double_param.get_double(), (sixteen_digits, True)) + + # Test getting and setting a Vector3d + def test_Vector3d(self): + vector_param = Param("key", "vector3", "0 0 0", False, "description") + value, rc = vector_param.get_vector3() + self.assertTrue(rc) + self.assertAlmostEqual(0.0, value.x()) + self.assertAlmostEqual(0.0, value.y()) + self.assertAlmostEqual(0.0, value.z()) + + vector_param.set_vector3(Vector3d.ONE) + value, rc = vector_param.get_vector3() + self.assertTrue(rc) + self.assertAlmostEqual(1.0, value.x()) + self.assertAlmostEqual(1.0, value.y()) + self.assertAlmostEqual(1.0, value.z()) + + # Test getting and setting a Pose3d + def test_pose3d(self): + pose_param = Param("key", "pose", "0 0 0 0 0 0", False, "description") + value, rc = pose_param.get_pose() + self.assertAlmostEqual(0.0, value.pos().x()) + self.assertAlmostEqual(0.0, value.pos().y()) + self.assertAlmostEqual(0.0, value.pos().z()) + self.assertAlmostEqual(0.0, value.rot().euler().x()) + self.assertAlmostEqual(0.0, value.rot().euler().y()) + self.assertAlmostEqual(0.0, value.rot().euler().z()) + + pose_param.set_pose(Pose3d(1, 2, 3, 0.1, 0.2, 0.3)) + value, rc = pose_param.get_pose() + self.assertAlmostEqual(1.0, value.pos().x()) + self.assertAlmostEqual(2.0, value.pos().y()) + self.assertAlmostEqual(3.0, value.pos().z()) + self.assertAlmostEqual(0.1, value.rot().euler().x()) + self.assertAlmostEqual(0.2, value.rot().euler().y()) + self.assertAlmostEqual(0.3, value.rot().euler().z()) + + # Test decimal number + def test_set_from_string_decimals(self): + param = Param("number", "double", "0.0", True) + self.assertTrue(param.set_from_string("0.2345")) + + # Test setting and reading uint64_t values. + def test_uint64t(self): + uint64t_param = Param("key", "uint64_t", "1", False, "description") + self.assertEqual(uint64t_param.get_uint64_t(), (1, True)) + + # Unknown type, should fall back to stream operators + def test_UnknownType(self): + double_param = Param("key", "double", "1.0", False, "description") + value, rc = double_param.get_angle() + self.assertTrue(rc) + self.assertEqual(value.radian(), 1.0) + + # Test setting and reading vector2i values. + def test_Vector2i(self): + vect2i_param = Param("key", "vector2i", "0 0", False, "description") + self.assertEqual(vect2i_param.get_vector2i(), (Vector2i(0, 0), True)) + + def test_invalid_constructor(self): + with self.assertRaises(SDFErrorsException): + Param("key", "badtype", "0", False, "description"), + + def test_set_description(self): + uint64_param = Param("key", "uint64_t", "1", False, "description") + uint64_param.set_description("new desc") + self.assertEqual("new desc", uint64_param.get_description()) + + def test_setting_parent_element(self): + parent_element = Element() + double_param = Param("key", "double", "1.0", False, "description") + self.assertTrue(double_param.set_parent_element(parent_element)) + + self.assertIsNotNone(double_param.get_parent_element()) + self.assertEqual(parent_element, double_param.get_parent_element()) + + # Set a new parent Element + new_parent_element = Element() + + self.assertTrue(double_param.set_parent_element(new_parent_element)) + self.assertIsNotNone(double_param.get_parent_element()) + self.assertEqual(new_parent_element, double_param.get_parent_element()) + + # Remove the parent Element + self.assertTrue(double_param.set_parent_element(None)) + self.assertIsNone(double_param.get_parent_element()) + + def test_copy_constructor(self): + parent_element = Element() + + double_param = Param("key", "double", "1.0", False, "description") + self.assertTrue(double_param.set_parent_element(parent_element)) + + self.assertNotEqual(None, double_param.get_parent_element()) + self.assertEqual(parent_element, double_param.get_parent_element()) + + newParam = Param(double_param) + self.assertNotEqual(None, newParam.get_parent_element()) + self.assertEqual(parent_element, newParam.get_parent_element()) def test_get_as_string(self): param = Param("test_key", "int", "5", False, "bla") From 67f2f7c6b8cba1841db574139c71a64f7a99655d Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Wed, 16 Aug 2023 11:40:39 -0500 Subject: [PATCH 4/9] Add more Element tests Signed-off-by: Addisu Z. Taddese --- python/src/sdf/_gz_sdformat_pybind11.cc | 6 +- python/src/sdf/pyElement.cc | 36 ++- python/test/pyElement_TEST.py | 284 +++++++++++++++++++++++- 3 files changed, 302 insertions(+), 24 deletions(-) diff --git a/python/src/sdf/_gz_sdformat_pybind11.cc b/python/src/sdf/_gz_sdformat_pybind11.cc index 929b6f999..fd95925ad 100644 --- a/python/src/sdf/_gz_sdformat_pybind11.cc +++ b/python/src/sdf/_gz_sdformat_pybind11.cc @@ -87,6 +87,9 @@ PYBIND11_MODULE(BINDINGS_MODULE_NAME, m) { sdf::python::defineCollision(m); sdf::python::defineContact(m); sdf::python::defineCylinder(m); + // PrintConfig has to be defined before Param and Element because it's used as + // a default argument. + sdf::python::definePrintConfig(m); sdf::python::defineElement(m); sdf::python::defineEllipsoid(m); sdf::python::defineError(m); @@ -111,9 +114,6 @@ PYBIND11_MODULE(BINDINGS_MODULE_NAME, m) { sdf::python::defineNavSat(m); sdf::python::defineNoise(m); sdf::python::defineODE(m); - // PrintConfig has to be defined before Param and Element because it's used as - // a default argument. - sdf::python::definePrintConfig(m); sdf::python::defineParam(m); sdf::python::defineParserConfig(m); sdf::python::defineParticleEmitter(m); diff --git a/python/src/sdf/pyElement.cc b/python/src/sdf/pyElement.cc index 23deb6113..e65808ac9 100644 --- a/python/src/sdf/pyElement.cc +++ b/python/src/sdf/pyElement.cc @@ -119,13 +119,16 @@ void defineElement(py::object module) "Set the requirement type.") .def("get_required", &Element::GetRequired, "Get the requirement string.") - // print_description .def("set_explicitly_set_in_file", &Element::SetExplicitlySetInFile, "Set if the element and children where set or default in the " "original file") .def("get_explicitly_set_in_file", &Element::GetExplicitlySetInFile, "Return if the element was been explicitly set in the file") - // to_string + .def("to_string", + ErrorWrappedCast( + &Element::ToString, py::const_), + "Convert the element values to a string representation.", + "prefix"_a, "config"_a = PrintConfig()) .def( "add_attribute", [](Element &_self, const std::string &_key, @@ -150,7 +153,8 @@ void defineElement(py::object module) _description); ThrowIfErrors(errors); }, - "Add a value to this Element") + "Add a value to this Element", "type"_a, "default_value"_a, + "required"_a, "description"_a = "") .def( "add_value", [](Element &_self, const std::string &_type, @@ -165,11 +169,18 @@ void defineElement(py::object module) }, "Add a value to this Element", "type"_a, "default_value"_a, "required"_a, "min_value"_a, "max_value"_a, "description"_a = "") - // get_attribute (string) + .def("get_attribute", + py::overload_cast(&Element::GetAttribute, + py::const_), + "Get the param of an attribute.") .def("get_attribute_count", &Element::GetAttributeCount, "Get the number of attributes.") - // get_attributes - // get_attribute (index) + .def("get_attributes", &Element::GetAttributes, + "Get all the attribute params.") + .def("get_attribute", + py::overload_cast(&Element::GetAttribute, + py::const_), + "Get the param of an attribute.") // get_element_description_count // get_element_description (index) // get_element_description (string) @@ -178,8 +189,10 @@ void defineElement(py::object module) "Return true if an attribute exists.") .def("get_attribute_set", &Element::GetAttributeSet, "Return true if the attribute was set (i.e. not default value)") - // remove_attribute - // remove_all_attributes + .def("remove_attribute", &Element::RemoveAttribute, + "Remove an attribute.") + .def("remove_all_attributes", &Element::RemoveAllAttributes, + "Removes all attributes.") .def("get_value", &Element::GetValue, "Get the param of the elements value") .def("get_any", @@ -206,8 +219,11 @@ void defineElement(py::object module) "elem"_a, "set_parent_to_self"_a = false) // remove_from_parent // remove_child - // clear_elements - // clear + .def("clear_elements", &Element::ClearElements, + "Remove all child elements.") + .def("clear", &Element::Clear, + "Remove all child elements and reset file path and original " + "version.") // update // reset .def("set_include_element", &Element::SetIncludeElement, diff --git a/python/test/pyElement_TEST.py b/python/test/pyElement_TEST.py index 6b5362801..0b52a70da 100644 --- a/python/test/pyElement_TEST.py +++ b/python/test/pyElement_TEST.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http:#www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -49,6 +49,186 @@ def test_required(self): elem.set_required("1") self.assertEqual(elem.get_required(), "1") + def test_set_explicitly_set_in_file(self): + # The heirarchy in xml: + # + # + # + # + # + # + # + # + # + # + # + # + + parent = Element() + elem = Element() + parent.insert_element(elem, True) + elem2 = Element() + parent.insert_element(elem2, True) + + self.assertTrue(elem.get_explicitly_set_in_file()) + + elem.set_explicitly_set_in_file(False) + + self.assertFalse(elem.get_explicitly_set_in_file()) + + elem.set_explicitly_set_in_file(True) + + self.assertTrue(elem.get_explicitly_set_in_file()) + + # the childs and siblings of the element should all be + # set to the same value when using this function + child = Element() + child.set_parent(elem) + elem.insert_element(child, False) + + sibling = Element() + sibling.set_parent(elem) + elem.insert_element(sibling) + + sibling2 = Element() + sibling2.set_parent(elem) + elem.insert_element(sibling2) + + child2 = Element() + child2.set_parent(child) + child.insert_element(child2) + + grandChild = Element() + grandChild.set_parent(child) + child.insert_element(grandChild) + + self.assertTrue(elem.get_explicitly_set_in_file()) + self.assertTrue(child.get_explicitly_set_in_file()) + self.assertTrue(sibling.get_explicitly_set_in_file()) + self.assertTrue(sibling2.get_explicitly_set_in_file()) + self.assertTrue(child2.get_explicitly_set_in_file()) + self.assertTrue(grandChild.get_explicitly_set_in_file()) + self.assertTrue(elem2.get_explicitly_set_in_file()) + + elem.set_explicitly_set_in_file(False) + self.assertFalse(elem.get_explicitly_set_in_file()) + self.assertFalse(child.get_explicitly_set_in_file()) + self.assertFalse(sibling.get_explicitly_set_in_file()) + self.assertFalse(sibling2.get_explicitly_set_in_file()) + self.assertFalse(child2.get_explicitly_set_in_file()) + self.assertFalse(grandChild.get_explicitly_set_in_file()) + + # SetExplicitlySetInFile(False) is be called only on `elem`. We expect + # get_explicitly_set_in_file() to be False for all children and + # grandchildren of `elem`, but True for `elem2`, which is a sibling of + # `elem`. + self.assertTrue(elem2.get_explicitly_set_in_file()) + + def test_set_explicitly_set_in_file_with_insert(self): + parent = Element() + parent.set_explicitly_set_in_file(False) + child = Element() + child.set_parent(parent) + parent.insert_element(child) + + self.assertFalse(parent.get_explicitly_set_in_file()) + self.assertTrue(child.get_explicitly_set_in_file()) + + def test_add_value(self): + elem = Element() + elem.set_name("test") + elem.add_value("string", "foo", False, "foo description") + + param = elem.get_value() + # self.assertEqual(param.get_key(), "test") + # self.assertEqual(param.get_type_name(), "string") + self.assertEqual(param.get_default_as_string(), "foo") + # self.assertEqual(param.get_description(), "foo description") + self.assertNotEqual(param.get_parent_element(), None) + self.assertEqual(param.get_parent_element(), elem) + + def test_add_attribute(self): + elem = Element() + + self.assertEqual(elem.get_attribute_count(), 0) + + elem.add_attribute("test", "string", "foo", False, "foo description") + self.assertEqual(elem.get_attribute_count(), 1) + + elem.add_attribute("attr", "float", "0.0", False, "float description") + self.assertEqual(elem.get_attribute_count(), 2) + + param = elem.get_attribute("test") + self.assertTrue(param.is_type_string()) + self.assertEqual(param.get_key(), "test") + self.assertEqual(param.get_type_name(), "string") + self.assertEqual(param.get_default_as_string(), "foo") + self.assertEqual(param.get_description(), "foo description") + + param = elem.get_attribute("attr") + self.assertTrue(param.is_type_float()) + self.assertEqual(param.get_key(), "attr") + self.assertEqual(param.get_type_name(), "float") + self.assertEqual(param.get_default_as_string(), "0") + self.assertEqual(param.get_description(), "float description") + + def test_get_attribute_set(self): + elem = Element() + self.assertEqual(elem.get_attribute_count(), 0) + elem.add_attribute("test", "string", "foo", False, "foo description") + self.assertEqual(elem.get_attribute_count(), 1) + + self.assertFalse(elem.get_attribute_set("test")) + elem.get_attribute("test").set_string("asdf") + self.assertTrue(elem.get_attribute_set("test")) + + def test_remove_attribute(self): + elem = Element() + self.assertEqual(elem.get_attribute_count(), 0) + + elem.add_attribute("test", "string", "foo", False, "foo description") + elem.add_attribute("attr", "float", "0.0", False, "float description") + self.assertEqual(elem.get_attribute_count(), 2) + + elem.remove_attribute("test") + self.assertEqual(elem.get_attribute_count(), 1) + self.assertEqual(elem.get_attribute("test"), None) + self.assertNotEqual(elem.get_attribute("attr"), None) + + def test_remove_all_attributes(self): + elem = Element() + self.assertEqual(elem.get_attribute_count(), 0) + + elem.add_attribute("test", "string", "foo", False, "foo description") + elem.add_attribute("test2", "string", "foo", False, "foo description") + elem.add_attribute("attr", "float", "0.0", False, "float description") + self.assertEqual(elem.get_attribute_count(), 3) + + elem.remove_all_attributes() + self.assertEqual(elem.get_attribute_count(), 0) + self.assertEqual(elem.get_attribute("test"), None) + self.assertEqual(elem.get_attribute("test2"), None) + self.assertEqual(elem.get_attribute("attr"), None) + + def test_include(self): + elem = Element() + + # includeElemToStore = Element() + # includeElemToStore.set_name("include") + # uriDesc = Element() + # uriDesc.set_name("uri") + # uriDesc.add_value("string", "", True) + # # includeElemToStore.add_element_description(uriDesc) + # + # includeElemToStore.find_element("uri").set("foo.txt") + # elem.SetIncludeElement(includeElemToStore) + # + # includeElem = elem.get_include_element() + # self.assertNotEqual(None, includeElem) + # self.assertTrue(includeElem.has_element("uri")) + # self.assertEqual("foo.txt", + # includeElem.find_element("uri").get_string()) + def test_get_templates(self): elem = Element() @@ -119,18 +299,100 @@ def test_clone(self): # clonedAttribs = newelem.get_attributes() # self.assertEqual(newelem, clonedAttribs[0].get_parent_element()) - def test_add_value(self): + def test_clear_elements(self): + parent = Element() + child = Element() + + parent.set_file_path("/path/to/file.sdf") + parent.set_line_number(12) + parent.set_xml_path("/sdf/world[@name=\"default\"]") + parent.set_original_version("1.5") + child.set_parent(parent) + parent.insert_element(child) + + self.assertEqual("/path/to/file.sdf", parent.file_path()) + self.assertEqual(12, parent.line_number()) + self.assertEqual("/sdf/world[@name=\"default\"]", parent.xml_path()) + self.assertEqual("1.5", parent.original_version()) + self.assertNotEqual(parent.get_first_element(), None) + self.assertEqual("/path/to/file.sdf", + parent.get_first_element().file_path()) + self.assertEqual("1.5", parent.get_first_element().original_version()) + + parent.clear_elements() + + self.assertEqual(parent.get_first_element(), None) + self.assertEqual("/path/to/file.sdf", parent.file_path()) + self.assertEqual(12, parent.line_number()) + self.assertEqual("1.5", parent.original_version()) + + def test_clear(self): + parent = Element() + child = Element() + + parent.set_file_path("/path/to/file.sdf") + parent.set_line_number(12) + parent.set_xml_path("/sdf/world[@name=\"default\"]") + parent.set_original_version("1.5") + child.set_parent(parent) + parent.insert_element(child) + + self.assertEqual("/path/to/file.sdf", parent.file_path()) + self.assertEqual(12, parent.line_number()) + self.assertEqual("/sdf/world[@name=\"default\"]", parent.xml_path()) + self.assertEqual("1.5", parent.original_version()) + self.assertNotEqual(parent.get_first_element(), None) + self.assertEqual("/path/to/file.sdf", + parent.get_first_element().file_path()) + self.assertEqual("1.5", parent.get_first_element().original_version()) + + parent.clear() + + self.assertEqual(parent.get_first_element(), None) + self.assertEqual(parent.file_path(), "") + self.assertIsNone(parent.line_number()) + self.assertEqual(parent.xml_path(), "") + self.assertEqual(parent.original_version(), "") + + def test_to_string_elements(self): + parent = Element() + child = Element() + + parent.set_name("parent") + child.set_name("child") + + parent.insert_element(child) + self.assertNotEqual(parent.get_first_element(), None) + + parent.add_attribute("test", "string", "foo", False, "foo description") + self.assertEqual(parent.get_attribute_count(), 1) + + # attribute won't print unless it has been set + self.assertFalse(parent.get_attribute_set("test")) + self.assertEqual( + parent.to_string(""), "\n" + " \n" + "\n") + + test = parent.get_attribute("test") + self.assertNotEqual(None, test) + self.assertFalse(test.get_set()) + self.assertTrue(test.set_from_string("foo")) + self.assertTrue(test.get_set()) + self.assertTrue(parent.get_attribute_set("test")) + + self.assertEqual( + parent.to_string(""), + "\n" + " \n" + "\n") + + def test_set(self): elem = Element() - elem.set_name("test") - elem.add_value("string", "foo", False, "foo description") - param = elem.get_value() - # ASSERT_EQ(param->GetKey(), "test") - # ASSERT_EQ(param->GetTypeName(), "string") - self.assertEqual(param.get_default_as_string(), "foo") - # ASSERT_EQ(param->GetDescription(), "foo description") - self.assertNotEqual(param.get_parent_element(), None) - self.assertEqual(param.get_parent_element(), elem) + elem.add_value("string", "val", False, "val description") + + self.assertTrue(elem.set_string("hello")) def test_find_element(self): root = Element() From f4d9959976e4a55a3216340841614063fb472d39 Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Wed, 16 Aug 2023 17:25:17 -0500 Subject: [PATCH 5/9] Finish Element tests Signed-off-by: Addisu Z. Taddese --- python/src/sdf/pyElement.cc | 78 +++++++++++-------- python/test/pyElement_TEST.py | 143 ++++++++++++++++++++++++---------- 2 files changed, 147 insertions(+), 74 deletions(-) diff --git a/python/src/sdf/pyElement.cc b/python/src/sdf/pyElement.cc index e65808ac9..df18a819f 100644 --- a/python/src/sdf/pyElement.cc +++ b/python/src/sdf/pyElement.cc @@ -49,11 +49,12 @@ struct DefineGetSetImpl _cls.def(getFuncName.c_str(), ErrorWrappedCast(&Class::template Get, py::const_), - "Get the value of a key. This function assumes the _key exists.") + "Get the value of a key. This function assumes the _key exists.", + "key"_a = "") .def(getFuncName.c_str(), ErrorWrappedCast( &Class::template Get, py::const_), - "Get the value of a key. This function assumes the _key exists.") + "Get the value of a key.") .def(setFuncName.c_str(), ErrorWrappedCast(&Class::template Set), "Get the value of a key. This function assumes the _key exists."); @@ -73,33 +74,23 @@ struct DefineGetSetImpl> static constexpr DefineGetSetImpl defineGetSet = {}; ///////////////////////////////////////////////// -// TODO(azeey) Interspersed between the `.def` calls is a list of -// functions that need to be implemented. I'm not 100% sure if all of them need -// to be implemented since some of them are mainly used internally by the -// parser. I've already excluded the following functions on this criteria: -// - SetReferenceSDF -// - ReferenceSDF +// The following functions have been excluded from the bindings because they're +// mainly used internally by the parser or there are better alternatives. +// - GetCopyChildren (Used by the parser for handling unknown elements) +// - SetCopyChildren (Used by the parser for handling unknown elements) +// - SetReferenceSDF (Used only by the parser) +// - ReferenceSDF (Used only by the parser) // - PrintDescription (Until https://github.com/gazebosim/sdformat/issues/1302 // is resolved) // - PrintValues (Because ToString is available) // - PrintDocLeftPane (Helper function for generating documentation) // - PrintDocRightPane (Helper function for generating documentation) -// - GetElementDescriptionCount (Until -// https://github.com/gazebosim/sdformat/issues/1302 is resolved) -// - GetElementDescription (Until -// https://github.com/gazebosim/sdformat/issues/1302 is resolved) -// - HasElementDescription (Until -// https://github.com/gazebosim/sdformat/issues/1302 is resolved) // - HasUniqueChildNames (Used for error checking by the parser) // - CountNamedElements (Used for error checking by the parser) // - GetElement (FindElement and AddElement should be used instead) -// - GetDescription (Until https://github.com/gazebosim/sdformat/issues/1302 is // resolved) -// - SetDescription (Until https://github.com/gazebosim/sdformat/issues/1302 is -// resolved) -// - AddElementDescription (Until -// https://github.com/gazebosim/sdformat/issues/1302 is resolved) // - GetElementImpl (Use FindElement instead) +// - GetElementTypeNames // - NameUniquenessExceptions (Used for error checking by the parser) void defineElement(py::object module) { @@ -109,6 +100,8 @@ void defineElement(py::object module) .def(py::init<>()) .def("clone", ErrorWrappedCast<>(&Element::Clone, py::const_), "Create a copy of this Element.") + .def("copy", ErrorWrappedCast(&Element::Copy), + "Copy values from an Element.") .def("get_parent", &Element::GetParent, "Get a pointer to this Element's parent.") .def("set_parent", &Element::SetParent, @@ -181,10 +174,19 @@ void defineElement(py::object module) py::overload_cast(&Element::GetAttribute, py::const_), "Get the param of an attribute.") - // get_element_description_count - // get_element_description (index) - // get_element_description (string) - // has_element_description + .def("get_element_description_count", + &Element::GetElementDescriptionCount, + "Get the number of element descriptions.") + .def("get_element_description", + py::overload_cast(&Element::GetElementDescription, + py::const_), + "Get an element description using an index") + .def("get_element_description", + py::overload_cast( + &Element::GetElementDescription, py::const_), + "Get an element description using a key") + .def("has_element_description", &Element::HasElementDescription, + "Return true if an element description exists.") .def("has_attribute", &Element::HasAttribute, "Return true if an attribute exists.") .def("get_attribute_set", &Element::GetAttributeSet, @@ -205,27 +207,35 @@ void defineElement(py::object module) "Get the first child element") .def("get_next_element", &Element::GetNextElement, "Get the first child Get the next sibling of this element.") - // get_element_type_names .def("find_element", py::overload_cast(&Element::FindElement), "Return a pointer to the child element with the provided name.") .def("add_element", ErrorWrappedCast(&Element::AddElement), "Add a value to this Element") + .def("insert_element", + py::overload_cast(&Element::InsertElement), + "Add an element object.") .def("insert_element", py::overload_cast(&Element::InsertElement), "Add an element object, and optionally set the given element's " - "parent to this object", - "elem"_a, "set_parent_to_self"_a = false) - // remove_from_parent - // remove_child + "parent to this object") + .def("remove_from_parent", &Element::RemoveFromParent, + "Remove this element from its parent.") + .def("remove_child", + ErrorWrappedCast(&Element::RemoveChild), + "Remove a child element.") .def("clear_elements", &Element::ClearElements, "Remove all child elements.") .def("clear", &Element::Clear, "Remove all child elements and reset file path and original " "version.") - // update - // reset + .def("update", ErrorWrappedCast<>(&Element::Update), + "Call the Update() callback on each element, as well as the " + "embedded Param.") + .def("reset", &Element::Reset, + "Call reset on each element and element description before " + "deleting all of them. Also clear out the embedded Param.") .def("set_include_element", &Element::SetIncludeElement, "Set the `` element that was used to load this element") .def( @@ -246,7 +256,13 @@ void defineElement(py::object module) .def("set_original_version", &Element::SetOriginalVersion, "Set the spec version that this was originally parsed from.") .def("original_version", &Element::OriginalVersion, - "Get the spec version that this was originally parsed from."); + "Get the spec version that this was originally parsed from.") + .def("get_description", &Element::GetDescription, + "Get a text description of the element.") + .def("set_description", &Element::SetDescription, + "Set a text description for the element.") + .def("add_element_description", &Element::AddElementDescription, + "Add a new element description"); defineGetSet(elemClass); } } // namespace python diff --git a/python/test/pyElement_TEST.py b/python/test/pyElement_TEST.py index 0b52a70da..6b013ae8d 100644 --- a/python/test/pyElement_TEST.py +++ b/python/test/pyElement_TEST.py @@ -19,17 +19,6 @@ class ElementTEST(unittest.TestCase): - def add_child_element(self, parent: Element, element_name: str, - add_name_attribute: bool, child_name: str): - child = Element() - child.set_parent(parent) - child.set_name(element_name) - parent.insert_element(child) - if add_name_attribute: - child.add_attribute("name", "string", child_name, False, - "description") - return child - def test_child(self): child = Element() parent = Element() @@ -118,8 +107,8 @@ def test_set_explicitly_set_in_file(self): self.assertFalse(child2.get_explicitly_set_in_file()) self.assertFalse(grandChild.get_explicitly_set_in_file()) - # SetExplicitlySetInFile(False) is be called only on `elem`. We expect - # get_explicitly_set_in_file() to be False for all children and + # set_explicitly_set_in_file(False) is be called only on `elem`. We + # expect get_explicitly_set_in_file() to be False for all children and # grandchildren of `elem`, but True for `elem2`, which is a sibling of # `elem`. self.assertTrue(elem2.get_explicitly_set_in_file()) @@ -140,10 +129,10 @@ def test_add_value(self): elem.add_value("string", "foo", False, "foo description") param = elem.get_value() - # self.assertEqual(param.get_key(), "test") - # self.assertEqual(param.get_type_name(), "string") + self.assertEqual(param.get_key(), "test") + self.assertEqual(param.get_type_name(), "string") self.assertEqual(param.get_default_as_string(), "foo") - # self.assertEqual(param.get_description(), "foo description") + self.assertEqual(param.get_description(), "foo description") self.assertNotEqual(param.get_parent_element(), None) self.assertEqual(param.get_parent_element(), elem) @@ -213,21 +202,21 @@ def test_remove_all_attributes(self): def test_include(self): elem = Element() - # includeElemToStore = Element() - # includeElemToStore.set_name("include") - # uriDesc = Element() - # uriDesc.set_name("uri") - # uriDesc.add_value("string", "", True) - # # includeElemToStore.add_element_description(uriDesc) - # - # includeElemToStore.find_element("uri").set("foo.txt") - # elem.SetIncludeElement(includeElemToStore) - # - # includeElem = elem.get_include_element() - # self.assertNotEqual(None, includeElem) - # self.assertTrue(includeElem.has_element("uri")) - # self.assertEqual("foo.txt", - # includeElem.find_element("uri").get_string()) + include_elem_to_store = Element() + include_elem_to_store.set_name("include") + uri_desc = Element() + uri_desc.set_name("uri") + uri_desc.add_value("string", "", True) + include_elem_to_store.add_element_description(uri_desc) + + include_elem_to_store.add_element("uri").set_string("foo.txt") + elem.set_include_element(include_elem_to_store) + + include_elem = elem.get_include_element() + self.assertNotEqual(None, include_elem) + self.assertTrue(include_elem.has_element("uri")) + self.assertEqual("foo.txt", + include_elem.find_element("uri").get_string()) def test_get_templates(self): elem = Element() @@ -272,9 +261,9 @@ def test_clone(self): parent.set_xml_path("/sdf/world[@name=\"default\"]") parent.set_original_version("1.5") - includeElemToStore = Element() - includeElemToStore.set_name("include") - parent.set_include_element(includeElemToStore) + include_elem_to_store = Element() + include_elem_to_store.set_name("include") + parent.set_include_element(include_elem_to_store) newelem = parent.clone() @@ -290,14 +279,14 @@ def test_clone(self): self.assertEqual("include", newelem.get_include_element().get_name()) self.assertTrue(newelem.get_explicitly_set_in_file()) - # self.assertIsNotNone(parent.get_value().get_parent_element()) - # self.assertEqual(parent, parent.get_value().get_parent_element()) - # - # self.assertIsNotNone(newelem.get_value().get_parent_element()) - # self.assertEqual(newelem, newelem.get_value().get_parent_element()) + self.assertIsNotNone(parent.get_value().get_parent_element()) + self.assertEqual(parent, parent.get_value().get_parent_element()) + + self.assertIsNotNone(newelem.get_value().get_parent_element()) + self.assertEqual(newelem, newelem.get_value().get_parent_element()) - # clonedAttribs = newelem.get_attributes() - # self.assertEqual(newelem, clonedAttribs[0].get_parent_element()) + clonedAttribs = newelem.get_attributes() + self.assertEqual(newelem, clonedAttribs[0].get_parent_element()) def test_clear_elements(self): parent = Element() @@ -394,6 +383,74 @@ def test_set(self): self.assertTrue(elem.set_string("hello")) + def test_copy(self): + src = Element() + dest = Element() + + src.set_name("test") + src.set_file_path("/path/to/file.sdf") + src.set_line_number(12) + src.set_xml_path("/sdf/world[@name=\"default\"]") + src.set_original_version("1.5") + src.add_value("string", "val", False, "val description") + src.add_attribute("test", "string", "foo", False, "foo description") + src.insert_element(Element()) + + include_elem_to_store = Element() + include_elem_to_store.set_name("include") + src.set_include_element(include_elem_to_store) + + dest.copy(src) + + self.assertEqual("/path/to/file.sdf", dest.file_path()) + self.assertEqual(12, dest.line_number()) + self.assertEqual("/sdf/world[@name=\"default\"]", dest.xml_path()) + self.assertEqual("1.5", dest.original_version()) + + param = dest.get_value() + self.assertTrue(param.is_type_string()) + self.assertEqual(param.get_key(), "test") + self.assertEqual(param.get_type_name(), "string") + self.assertEqual(param.get_default_as_string(), "val") + self.assertEqual(param.get_description(), "val description") + self.assertNotEqual(param.get_parent_element(), None) + self.assertEqual(param.get_parent_element(), dest) + + self.assertEqual(dest.get_attribute_count(), 1) + self.assertTrue(dest.get_explicitly_set_in_file()) + param = dest.get_attribute("test") + self.assertTrue(param.is_type_string()) + self.assertEqual(param.get_key(), "test") + self.assertEqual(param.get_type_name(), "string") + self.assertEqual(param.get_default_as_string(), "foo") + self.assertEqual(param.get_description(), "foo description") + self.assertNotEqual(param.get_parent_element(), None) + self.assertEqual(param.get_parent_element(), dest) + + self.assertNotEqual(dest.get_first_element(), None) + self.assertNotEqual(dest.get_include_element(), None) + self.assertEqual("include", dest.get_include_element().get_name()) + + def test_get_next_element(self): + child = Element() + parent = Element() + + child.set_parent(parent) + + self.assertEqual(child.get_next_element("foo"), None) + + # Helper used in test_find_element + def add_child_element(self, parent: Element, element_name: str, + add_name_attribute: bool, child_name: str): + child = Element() + child.set_parent(parent) + child.set_name(element_name) + parent.insert_element(child) + if add_name_attribute: + child.add_attribute("name", "string", child_name, False, + "description") + return child + def test_find_element(self): root = Element() root.set_name("root") @@ -414,8 +471,8 @@ def test_find_element(self): # attribute child_elem_b = elem_b.find_element("child_elem_B") self.assertTrue(child_elem_b.has_attribute("name")) - # self.assertEqual("first_child", - # child_elem_b.get_attribute("name").get_as_string()) + self.assertEqual("first_child", + child_elem_b.get_attribute("name").get_as_string()) if __name__ == '__main__': From ec8696bce191069bdd1fefc208fd5f6644e3b404 Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Wed, 16 Aug 2023 17:54:06 -0500 Subject: [PATCH 6/9] Add PrintConfig Signed-off-by: Addisu Z. Taddese --- python/CMakeLists.txt | 1 + python/src/sdf/pyPrintConfig.cc | 42 +++++++++++- python/test/pyPrintConfig_TEST.py | 103 ++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 python/test/pyPrintConfig_TEST.py diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 575050636..62078d8db 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -163,6 +163,7 @@ if (BUILD_TESTING AND NOT WIN32) pyPlane_TEST pyPlugin_TEST pyPolyline_TEST + pyPrintConfig_TEST pyProjector_TEST pyRoot_TEST pyScene_TEST diff --git a/python/src/sdf/pyPrintConfig.cc b/python/src/sdf/pyPrintConfig.cc index 6a99e68ca..963d8227f 100644 --- a/python/src/sdf/pyPrintConfig.cc +++ b/python/src/sdf/pyPrintConfig.cc @@ -16,13 +16,14 @@ #include "pyPrintConfig.hh" +#include #include #include + #include -#include "sdf/PrintConfig.hh" #include "pybind11_helpers.hh" - +#include "sdf/PrintConfig.hh" #include "sdf/config.hh" using namespace pybind11::literals; @@ -36,7 +37,42 @@ namespace python void definePrintConfig(py::object module) { - py::class_(module, "PrintConfig").def(py::init<>()); + py::class_(module, "PrintConfig") + .def(py::init<>()) + .def(py::init()) + .def(py::self == py::self) + .def("set_rotation_in_degrees", &PrintConfig::SetRotationInDegrees, + "Sets the option for printing pose rotations in degrees if true, " + "otherwise they will be printed as radians by default.") + .def("rotation_in_degrees", &PrintConfig::RotationInDegrees, + "Returns whether or not pose rotations should be printed in " + "degrees.") + .def("set_rotation_snap_to_degrees", + ErrorWrappedCast( + &PrintConfig::SetRotationSnapToDegrees), + "Sets the option for printing pose rotation in degrees as well as " + "snapping the rotation to the desired interval, with the provided " + "tolerance.") + .def("rotation_snap_to_degrees", &PrintConfig::RotationSnapToDegrees, + "Returns the current degree value that pose rotations will snap to " + "when printed.") + .def("rotation_snap_tolerance", &PrintConfig::RotationSnapTolerance, + "Returns the tolerance for snapping degree values when printed.") + .def("set_preserve_includes", &PrintConfig::SetPreserveIncludes, + "Set print config to preserve tags.") + .def("preserve_includes", &PrintConfig::PreserveIncludes, + "Check if tags are to be preserved or expanded.") + .def("set_out_precision", &PrintConfig::SetOutPrecision, + "Set precision of output stream for float / double types. By " + "default, the output stream uses maximum precision.") + .def("out_precision", &PrintConfig::OutPrecision, + "Retrieve the output stream's set precision value.") + .def("__copy__", + [](const PrintConfig &self) { return PrintConfig(self); }) + .def( + "__deepcopy__", + [](const PrintConfig &self, py::dict) { return PrintConfig(self); }, + "memo"_a); } } // namespace python } // namespace SDF_VERSION_NAMESPACE diff --git a/python/test/pyPrintConfig_TEST.py b/python/test/pyPrintConfig_TEST.py new file mode 100644 index 000000000..92cb23b6f --- /dev/null +++ b/python/test/pyPrintConfig_TEST.py @@ -0,0 +1,103 @@ +# Copyright (C) 2023 Open Source Robotics Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License") +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from gz_test_deps.sdformat import PrintConfig, SDFErrorsException +import unittest +import sys + + +class PrintConfigTEST(unittest.TestCase): + + def test_construction(self): + config = PrintConfig() + self.assertFalse(config.rotation_in_degrees()) + self.assertFalse(config.rotation_snap_to_degrees()) + self.assertFalse(config.preserve_includes()) + + def test_rotation_in_degrees(self): + config = PrintConfig() + self.assertFalse(config.rotation_in_degrees()) + + config.set_rotation_in_degrees(True) + self.assertTrue(config.rotation_in_degrees()) + + config.set_rotation_in_degrees(False) + self.assertFalse(config.rotation_in_degrees()) + + def test_rotation_snap_to_degrees(self): + config = PrintConfig() + self.assertIsNone(config.rotation_snap_to_degrees()) + self.assertIsNone(config.rotation_snap_tolerance()) + + self.assertTrue(config.set_rotation_snap_to_degrees(5, 0.01)) + self.assertEqual(5, config.rotation_snap_to_degrees()) + self.assertEqual(0.01, config.rotation_snap_tolerance()) + + self.assertRaises(SDFErrorsException, + config.set_rotation_snap_to_degrees, 0, 0.01) + self.assertRaises(SDFErrorsException, + config.set_rotation_snap_to_degrees, 360 + 1, 0.01) + self.assertRaises(SDFErrorsException, + config.set_rotation_snap_to_degrees, 5, -1e6) + self.assertRaises(SDFErrorsException, + config.set_rotation_snap_to_degrees, 5, 360 + 1e-6) + + self.assertRaises(SDFErrorsException, + config.set_rotation_snap_to_degrees, 5, 5 + 1e-6) + self.assertTrue(config.set_rotation_snap_to_degrees(5, 5 - 1e-6)) + self.assertEqual(5, config.rotation_snap_to_degrees()) + self.assertEqual(5 - 1e-6, config.rotation_snap_tolerance()) + + def test_compare(self): + first = PrintConfig() + second = PrintConfig() + self.assertTrue(first == second) + self.assertTrue(second == first) + + first.set_rotation_in_degrees(True) + self.assertTrue(first.rotation_in_degrees()) + self.assertFalse(second.rotation_in_degrees()) + self.assertFalse(first == second) + self.assertFalse(second == first) + + second.set_rotation_in_degrees(True) + self.assertTrue(first == second) + self.assertTrue(second == first) + + self.assertTrue(first.set_rotation_snap_to_degrees(5, 0.01)) + self.assertFalse(first == second) + self.assertFalse(second == first) + + self.assertTrue(second.set_rotation_snap_to_degrees(5, 0.01)) + self.assertTrue(first == second) + self.assertTrue(second == first) + + def test_preserve_includes(self): + config = PrintConfig() + self.assertFalse(config.preserve_includes()) + config.set_preserve_includes(True) + self.assertTrue(config.preserve_includes()) + config.set_preserve_includes(False) + self.assertFalse(config.preserve_includes()) + + def test_out_precision(self): + config = PrintConfig() + config.set_out_precision(2) + self.assertEqual(2, config.out_precision()) + config.set_out_precision(8) + self.assertEqual(8, config.out_precision()) + + +if __name__ == '__main__': + unittest.main() From 8805c4f48fc476b484dec82e9575d4154821b06a Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Wed, 16 Aug 2023 18:11:19 -0500 Subject: [PATCH 7/9] Refactor, Codecheck, documentation Signed-off-by: Addisu Z. Taddese --- python/src/sdf/pyElement.cc | 76 ++++++++----------- python/src/sdf/pyParam.cc | 114 +++++++++-------------------- python/src/sdf/pyParam.hh | 62 +++++++++++++++- python/src/sdf/pybind11_helpers.hh | 68 +++++++++++++++-- python/test/pyParam_TEST.py | 3 +- python/test/pyPrintConfig_TEST.py | 1 - 6 files changed, 191 insertions(+), 133 deletions(-) diff --git a/python/src/sdf/pyElement.cc b/python/src/sdf/pyElement.cc index df18a819f..e0d97a1b3 100644 --- a/python/src/sdf/pyElement.cc +++ b/python/src/sdf/pyElement.cc @@ -20,9 +20,10 @@ #include #include +#include -#include "pybind11_helpers.hh" #include "pyParam.hh" +#include "pybind11_helpers.hh" #include "sdf/Element.hh" #include "sdf/Param.hh" #include "sdf/PrintConfig.hh" @@ -37,60 +38,22 @@ inline namespace SDF_VERSION_NAMESPACE { namespace python { - -template -struct DefineGetSetImpl -{ - template - void operator()(py::class_ &_cls) - { - const std::string getFuncName = "get_" + computeSuffix(); - const std::string setFuncName = "set_" + computeSuffix(); - _cls.def(getFuncName.c_str(), - ErrorWrappedCast(&Class::template Get, - py::const_), - "Get the value of a key. This function assumes the _key exists.", - "key"_a = "") - .def(getFuncName.c_str(), - ErrorWrappedCast( - &Class::template Get, py::const_), - "Get the value of a key.") - .def(setFuncName.c_str(), - ErrorWrappedCast(&Class::template Set), - "Get the value of a key. This function assumes the _key exists."); - } -}; - -template -struct DefineGetSetImpl> -{ - template - void operator()(PyClass &_cls) const noexcept - { - (DefineGetSetImpl()(_cls), ...); - } -}; - -static constexpr DefineGetSetImpl defineGetSet = {}; - ///////////////////////////////////////////////// -// The following functions have been excluded from the bindings because they're +// The following functions have been excluded from the bindings because they're // mainly used internally by the parser or there are better alternatives. // - GetCopyChildren (Used by the parser for handling unknown elements) // - SetCopyChildren (Used by the parser for handling unknown elements) // - SetReferenceSDF (Used only by the parser) // - ReferenceSDF (Used only by the parser) -// - PrintDescription (Until https://github.com/gazebosim/sdformat/issues/1302 -// is resolved) +// - PrintDescription (Use GetDescription() and print) // - PrintValues (Because ToString is available) // - PrintDocLeftPane (Helper function for generating documentation) // - PrintDocRightPane (Helper function for generating documentation) // - HasUniqueChildNames (Used for error checking by the parser) // - CountNamedElements (Used for error checking by the parser) // - GetElement (FindElement and AddElement should be used instead) -// resolved) // - GetElementImpl (Use FindElement instead) -// - GetElementTypeNames +// - GetElementTypeNames (Used for error checking by the parser) // - NameUniquenessExceptions (Used for error checking by the parser) void defineElement(py::object module) { @@ -238,8 +201,7 @@ void defineElement(py::object module) "deleting all of them. Also clear out the embedded Param.") .def("set_include_element", &Element::SetIncludeElement, "Set the `` element that was used to load this element") - .def( - "get_include_element", &Element::GetIncludeElement, + .def("get_include_element", &Element::GetIncludeElement, "Get the `` element that was used to load this element.") .def("set_file_path", &Element::SetFilePath, "Set the path to the SDF document where this element came from.") @@ -263,7 +225,31 @@ void defineElement(py::object module) "Set a text description for the element.") .def("add_element_description", &Element::AddElementDescription, "Add a new element description"); - defineGetSet(elemClass); + + // Definitions for `Get`, and `Set` which will bind to `get_bool`, + // `get_int`, etc. + forEachParamType( + [&elemClass](auto &&arg) + { + using T = std::decay_t; + const std::string getFuncName = "get_" + computeSuffix(); + const std::string setFuncName = "set_" + computeSuffix(); + elemClass + .def(getFuncName.c_str(), + ErrorWrappedCast(&Element::Get, + py::const_), + "Get the value of a key. This function assumes the _key " + "exists.", + "key"_a = "") + .def(getFuncName.c_str(), + ErrorWrappedCast( + &Element::Get, py::const_), + "Get the value of a key.") + .def(setFuncName.c_str(), + ErrorWrappedCast(&Element::Set), + "Get the value of a key. This function assumes the _key " + "exists."); + }); } } // namespace python } // namespace SDF_VERSION_NAMESPACE diff --git a/python/src/sdf/pyParam.cc b/python/src/sdf/pyParam.cc index 4c0271e6f..e4f50c34b 100644 --- a/python/src/sdf/pyParam.cc +++ b/python/src/sdf/pyParam.cc @@ -20,6 +20,7 @@ #include #include +#include #include "pybind11_helpers.hh" #include "sdf/Element.hh" @@ -36,34 +37,6 @@ inline namespace SDF_VERSION_NAMESPACE { namespace python { - - -template -struct ForEachParamTypeImpl; - -template -struct ForEachParamTypeImpl> -{ - template - void operator()(Func _func) const - { - (ForEachParamTypeImpl{}(_func), ...); - } -}; - -template -struct ForEachParamTypeImpl -{ - template - void operator()(Func _func) const - { - _func(T{}); - } -}; - -static constexpr ForEachParamTypeImpl - forEachParamType = {}; - void defineParam(py::object module) { using PyClassParam = py::class_; @@ -157,60 +130,45 @@ void defineParam(py::object module) ErrorWrappedCast<>(&Param::ValidateValue, py::const_), "Validate the value against minimum and maximum allowed values"); - // Definitions for `is_type`, `get`, `get_default`, and `set` - forEachParamType( - [¶mClass](auto &&arg) - { - using T = std::decay_t; - const std::string nameWithSuffix = "is_type_" + computeSuffix(); - paramClass.def(nameWithSuffix.c_str(), &Param::IsType, - "Return true if the param is a particular type"); - }); - - forEachParamType( - [¶mClass](auto &&arg) - { - using T = std::decay_t; - const std::string nameWithSuffix = "get_" + computeSuffix(); - paramClass.def( - nameWithSuffix.c_str(), - [](const Param &_self) - { - sdf::Errors errors; - T value; - bool rc = _self.Get(value, errors); - ThrowIfErrors(errors); - return py::make_tuple(value, rc); - }, - "Get the value of the parameter."); - }); - - forEachParamType( - [¶mClass](auto &&arg) - { - using T = std::decay_t; - const std::string nameWithSuffix = "get_default_" + computeSuffix(); - paramClass.def( - nameWithSuffix.c_str(), - [](const Param &_self) - { - sdf::Errors errors; - T value; - bool rc = _self.GetDefault(value, errors); - ThrowIfErrors(errors); - return py::make_tuple(value, rc); - }, - "Get the default value of the parameter."); - }); - + // Definitions for `IsType`, `Get`, `GetDefault`, and `Set`, which + // will bind to `is_type_bool`, `is_type_int`, etc. forEachParamType( [¶mClass](auto &&arg) { using T = std::decay_t; - const std::string nameWithSuffix = "set_" + computeSuffix(); - paramClass.def(nameWithSuffix.c_str(), - ErrorWrappedCast(&Param::Set), - "Set the value of the parameter."); + const std::string isTypeFuncName = "is_type_" + computeSuffix(); + const std::string getFuncName = "get_" + computeSuffix(); + const std::string getDefaultFuncName = + "get_default_" + computeSuffix(); + const std::string setFuncName = "set_" + computeSuffix(); + paramClass + .def(isTypeFuncName.c_str(), &Param::IsType, + "Return true if the param is a particular type") + .def( + getFuncName.c_str(), + [](const Param &_self) + { + sdf::Errors errors; + T value; + bool rc = _self.Get(value, errors); + ThrowIfErrors(errors); + return py::make_tuple(value, rc); + }, + "Get the value of the parameter.") + .def( + getDefaultFuncName.c_str(), + [](const Param &_self) + { + sdf::Errors errors; + T value; + bool rc = _self.GetDefault(value, errors); + ThrowIfErrors(errors); + return py::make_tuple(value, rc); + }, + "Get the default value of the parameter.") + .def(setFuncName.c_str(), + ErrorWrappedCast(&Param::Set), + "Set the value of the parameter."); }); } } // namespace python diff --git a/python/src/sdf/pyParam.hh b/python/src/sdf/pyParam.hh index f4ed2fd4c..6ff139e48 100644 --- a/python/src/sdf/pyParam.hh +++ b/python/src/sdf/pyParam.hh @@ -19,8 +19,9 @@ #include -#include "sdf/Param.hh" +#include +#include "sdf/Param.hh" #include "sdf/config.hh" namespace sdf @@ -35,16 +36,73 @@ namespace python */ void defineParam(pybind11::object module); +/// \brief Compute the suffix for `get_` and `set_` functions that are defined +/// for a set of types defined in ParamPrivate::ParamVariant template std::string computeSuffix() { - // TypeToString returns a value with a space, which would not be a valid + // TypeToString returns a value with a space, which would not be a valid // python function name, so we override that here. if constexpr (std::is_same_v) return "unsigned_int"; return ParamPrivate::TypeToString(); } +/// \brief Implementation for forEachParamType +/// +/// Base template for a single type. See specialization below +template +struct ForEachParamTypeImpl +{ + /// \brief Calls the passed in function with a default constructed `T` object, + /// which is used to determine the type within the callback lambda. + /// \tparam Func Callback function type (usually a lambda) + /// \param[in] _func Callback function + template + void operator()(Func _func) const + { + _func(T{}); + } +}; + + +/// \brief specialization of ForEachParamTypeImpl for std::variant +/// +/// This template will call the passed in function for each type in std::variant +template +struct ForEachParamTypeImpl> +{ + /// \brief Passes the passed in function to the single type specialization of + /// ForEachParamTypeImpl. + /// \tparam Func Callback function type (usually a lambda) + /// \param[in] _func Callback function + template + void operator()(Func _func) const + { + (ForEachParamTypeImpl{}(_func), ...); + } +}; +/// \brief Helper template for creating bindings for template functions in +/// `sdf::Param` and `sdf::Element`. +/// +/// Since only the types in `ParamPrivate::ParamVariant` are supported by +/// libsdformat, we only create bindings for the types in that `std::variant`. +/// +/// \sa ForEachParamTypeImpl +/// Usage: +/// To define a binding for Foo::Bar on cls where cls is a pybind11::class_ +/// ``` +/// forEachParamType( +/// [&cls](auto &&arg) +/// { +/// using T = std::decay_t; +/// const std::string funcName = "bar_" + computeSuffix() +/// cls.def(funcName.c_str, &Foo::Bar) +/// }); +/// ``` +static constexpr ForEachParamTypeImpl + forEachParamType = {}; + } // namespace python } // namespace SDF_VERSION_NAMESPACE } // namespace sdf diff --git a/python/src/sdf/pybind11_helpers.hh b/python/src/sdf/pybind11_helpers.hh index 2d632d72f..3e58d839a 100644 --- a/python/src/sdf/pybind11_helpers.hh +++ b/python/src/sdf/pybind11_helpers.hh @@ -36,30 +36,45 @@ namespace python void ThrowIfErrors(const sdf::Errors &_errors); +/// \brief Implementation for ErrorWrappedCast // NOTE: This currently only works for member funtions template struct ErrorWrappedCastImpl { + /// \brief Wrapper for function with `sdf::Errors&` as its first argument + /// \tparam Return Return type + /// \tparam Class Type of the class whose member function we're wrapping + /// \tparam Func Type that represents the pointer to member function (pmf) + /// \param[in] _pmf Pointer to member function to wrap + /// \return Output from calling the member function. + /// \throws PySDFErrorsException if sdf::Errors contains any errors template - static auto ErrorFirst(Func pmf) + static auto ErrorFirst(Func _pmf) { - return [pmf](Class &_self, Args... args) + return [_pmf](Class &_self, Args... args) { sdf::Errors errors; if constexpr (std::is_same_v) { - (_self.*pmf)(errors, std::forward(args)...); + (_self.*_pmf)(errors, std::forward(args)...); ThrowIfErrors(errors); } else { - auto output = (_self.*pmf)(errors, std::forward(args)...); + auto output = (_self.*_pmf)(errors, std::forward(args)...); ThrowIfErrors(errors); return output; } }; } + /// \brief Wrapper for function with `sdf::Errors&` as its last argument + /// \tparam Return Return type + /// \tparam Class Type of the class whose member function we're wrapping + /// \tparam Func Type that represents the pointer to member function (pmf) + /// \param[in] _pmf Pointer to member function to wrap + /// \return Output from calling the member function. + /// \throws PySDFErrorsException if sdf::Errors contains any errors template static auto ErrorLast(Func pmf) { @@ -80,12 +95,26 @@ struct ErrorWrappedCastImpl }; } + /// \brief Matches const member functions with sdf::Errors as their first + /// argument. + /// \tparam Return Return type + /// \tparam Class Type of the class whose member function we're wrapping + /// \param[in] _pmf Pointer to member function to wrap + /// \return Output from calling the member function. + /// \throws PySDFErrorsException if sdf::Errors contains any errors template auto operator()(Return (Class::*pmf)(Errors &, Args...) const, std::true_type) const noexcept { return ErrorFirst(pmf); } + /// \brief Matches non-const member functions with sdf::Errors as their first + /// argument. + /// \tparam Return Return type + /// \tparam Class Type of the class whose member function we're wrapping + /// \param[in] _pmf Pointer to member function to wrap + /// \return Output from calling the member function. + /// \throws PySDFErrorsException if sdf::Errors contains any errors template auto operator()(Return (Class::*pmf)(Errors &, Args...), std::false_type = {}) const noexcept @@ -93,6 +122,15 @@ struct ErrorWrappedCastImpl return ErrorFirst(pmf); } + /// \brief Matches const member functions with sdf::Errors as their last + /// argument. This is disabled if the member function has no argument other + /// than sdf::Errors to avoid ambiguity with both operator() functions + /// matching the signature. + /// \tparam Return Return type + /// \tparam Class Type of the class whose member function we're wrapping + /// \param[in] _pmf Pointer to member function to wrap + /// \return Output from calling the member function. + /// \throws PySDFErrorsException if sdf::Errors contains any errors template > @@ -102,6 +140,15 @@ struct ErrorWrappedCastImpl return ErrorLast(pmf); } + /// \brief Matches non-const member functions with sdf::Errors as their last + /// argument. This is disabled if the member function has no argument other + /// than sdf::Errors to avoid ambiguity with both operator() functions + /// matching the signature. + /// \tparam Return Return type + /// \tparam Class Type of the class whose member function we're wrapping + /// \param[in] _pmf Pointer to member function to wrap + /// \return Output from calling the member function. + /// \throws PySDFErrorsException if sdf::Errors contains any errors template > @@ -112,6 +159,17 @@ struct ErrorWrappedCastImpl } }; +/// \brief Wrap a member function such that it's called with sdf::Error as the +/// first or last argument and throw an exception if there is an error. The +/// syntax is similar to pybind11::overload_cast, but the sdf::Errors argument +/// is not included in the template arguments. +/// Usage: Two wrap a member functions +/// `Foo::Bar(sdf::Errors&, int, double)` and +/// `Foo::Baz(int, double, sdf::Errors&) const`, +/// we would do +/// `ErrorWrappedCast(&Foo::Bar)` and +/// `ErrorWrappedCast(&Foo::Baz, pybind11::const_)` +/// respectively. template static constexpr ErrorWrappedCastImpl ErrorWrappedCast = {}; @@ -119,4 +177,4 @@ static constexpr ErrorWrappedCastImpl ErrorWrappedCast = {}; } // namespace SDF_VERSION_NAMESPACE } // namespace sdf -#endif +#endif diff --git a/python/test/pyParam_TEST.py b/python/test/pyParam_TEST.py index c9a623bb3..dea6c2a8e 100644 --- a/python/test/pyParam_TEST.py +++ b/python/test/pyParam_TEST.py @@ -12,8 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from gz_test_deps.sdformat import (Element, Param, PrintConfig, - SDFErrorsException) +from gz_test_deps.sdformat import Element, Param, SDFErrorsException from gz_test_deps.math import Vector2i, Vector3d, Pose3d import unittest diff --git a/python/test/pyPrintConfig_TEST.py b/python/test/pyPrintConfig_TEST.py index 92cb23b6f..89a930e61 100644 --- a/python/test/pyPrintConfig_TEST.py +++ b/python/test/pyPrintConfig_TEST.py @@ -14,7 +14,6 @@ from gz_test_deps.sdformat import PrintConfig, SDFErrorsException import unittest -import sys class PrintConfigTEST(unittest.TestCase): From 784fca91d7ea365429ec0a6428dad91c80174cab Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Thu, 17 Aug 2023 10:26:52 -0500 Subject: [PATCH 8/9] Add one more overload in pyElement.cc, reformat. Signed-off-by: Addisu Z. Taddese --- python/src/sdf/pyElement.cc | 332 ++++++++++++++++++------------------ 1 file changed, 166 insertions(+), 166 deletions(-) diff --git a/python/src/sdf/pyElement.cc b/python/src/sdf/pyElement.cc index e0d97a1b3..935b95187 100644 --- a/python/src/sdf/pyElement.cc +++ b/python/src/sdf/pyElement.cc @@ -58,173 +58,173 @@ namespace python void defineElement(py::object module) { using PyClassElement = py::class_; - auto elemClass = - PyClassElement(module, "Element") - .def(py::init<>()) - .def("clone", ErrorWrappedCast<>(&Element::Clone, py::const_), - "Create a copy of this Element.") - .def("copy", ErrorWrappedCast(&Element::Copy), - "Copy values from an Element.") - .def("get_parent", &Element::GetParent, - "Get a pointer to this Element's parent.") - .def("set_parent", &Element::SetParent, - "Set the parent of this Element.") - .def("set_name", &Element::SetName, "Set the name of the Element") - .def("get_name", &Element::GetName, "Get the Element's name.") - .def("set_required", &Element::SetRequired, - "Set the requirement type.") - .def("get_required", &Element::GetRequired, - "Get the requirement string.") - .def("set_explicitly_set_in_file", &Element::SetExplicitlySetInFile, - "Set if the element and children where set or default in the " - "original file") - .def("get_explicitly_set_in_file", &Element::GetExplicitlySetInFile, - "Return if the element was been explicitly set in the file") - .def("to_string", - ErrorWrappedCast( - &Element::ToString, py::const_), - "Convert the element values to a string representation.", - "prefix"_a, "config"_a = PrintConfig()) - .def( - "add_attribute", - [](Element &_self, const std::string &_key, - const std::string &_type, const std::string &_defaultvalue, - bool _required, const std::string &_description = "") - { - Errors errors; - _self.AddAttribute(_key, _type, _defaultvalue, _required, - errors, _description); - ThrowIfErrors(errors); - }, - "Add an attribute value.", "key"_a, "type"_a, "default_value"_a, - "required"_a, "description"_a = "") - .def( - "add_value", - [](Element &_self, const std::string &_type, - const std::string &_defaultValue, bool _required, - const std::string &_description = "") - { - Errors errors; - _self.AddValue(_type, _defaultValue, _required, errors, + + auto elemClass = PyClassElement(module, "Element"); + elemClass.def(py::init<>()) + .def("clone", ErrorWrappedCast<>(&Element::Clone, py::const_), + "Create a copy of this Element.") + .def("copy", ErrorWrappedCast(&Element::Copy), + "Copy values from an Element.") + .def("get_parent", &Element::GetParent, + "Get a pointer to this Element's parent.") + .def("set_parent", &Element::SetParent, "Set the parent of this Element.") + .def("set_name", &Element::SetName, "Set the name of the Element") + .def("get_name", &Element::GetName, "Get the Element's name.") + .def("set_required", &Element::SetRequired, "Set the requirement type.") + .def("get_required", &Element::GetRequired, "Get the requirement string.") + .def("set_explicitly_set_in_file", &Element::SetExplicitlySetInFile, + "Set if the element and children where set or default in the " + "original file") + .def("get_explicitly_set_in_file", &Element::GetExplicitlySetInFile, + "Return if the element was been explicitly set in the file") + .def("to_string", + ErrorWrappedCast( + &Element::ToString, py::const_), + "Convert the element values to a string representation.", "prefix"_a, + "config"_a = PrintConfig()) + .def( + "to_string", + ErrorWrappedCast(&Element::ToString, py::const_), + "Convert the element values to a string representation.", "prefix"_a, + "include_default_elements"_a, "include_default_attribute"_a, + "config"_a = PrintConfig()) + .def( + "add_attribute", + [](Element &_self, const std::string &_key, const std::string &_type, + const std::string &_defaultvalue, bool _required, + const std::string &_description = "") + { + Errors errors; + _self.AddAttribute(_key, _type, _defaultvalue, _required, errors, _description); - ThrowIfErrors(errors); - }, - "Add a value to this Element", "type"_a, "default_value"_a, - "required"_a, "description"_a = "") - .def( - "add_value", - [](Element &_self, const std::string &_type, - const std::string &_defaultValue, bool _required, - const std::string &_minValue, const std::string &_maxValue, - const std::string &_description = "") - { - Errors errors; - _self.AddValue(_type, _defaultValue, _required, _minValue, - _maxValue, errors, _description); - ThrowIfErrors(errors); - }, - "Add a value to this Element", "type"_a, "default_value"_a, - "required"_a, "min_value"_a, "max_value"_a, "description"_a = "") - .def("get_attribute", - py::overload_cast(&Element::GetAttribute, - py::const_), - "Get the param of an attribute.") - .def("get_attribute_count", &Element::GetAttributeCount, - "Get the number of attributes.") - .def("get_attributes", &Element::GetAttributes, - "Get all the attribute params.") - .def("get_attribute", - py::overload_cast(&Element::GetAttribute, - py::const_), - "Get the param of an attribute.") - .def("get_element_description_count", - &Element::GetElementDescriptionCount, - "Get the number of element descriptions.") - .def("get_element_description", - py::overload_cast(&Element::GetElementDescription, - py::const_), - "Get an element description using an index") - .def("get_element_description", - py::overload_cast( - &Element::GetElementDescription, py::const_), - "Get an element description using a key") - .def("has_element_description", &Element::HasElementDescription, - "Return true if an element description exists.") - .def("has_attribute", &Element::HasAttribute, - "Return true if an attribute exists.") - .def("get_attribute_set", &Element::GetAttributeSet, - "Return true if the attribute was set (i.e. not default value)") - .def("remove_attribute", &Element::RemoveAttribute, - "Remove an attribute.") - .def("remove_all_attributes", &Element::RemoveAllAttributes, - "Removes all attributes.") - .def("get_value", &Element::GetValue, - "Get the param of the elements value") - .def("get_any", - ErrorWrappedCast(&Element::GetAny, - py::const_), - "Add a value to this Element") - .def("has_element", &Element::HasElement, - "Return true if the named element exists.") - .def("get_first_element", &Element::GetFirstElement, - "Get the first child element") - .def("get_next_element", &Element::GetNextElement, - "Get the first child Get the next sibling of this element.") - .def("find_element", - py::overload_cast(&Element::FindElement), - "Return a pointer to the child element with the provided name.") - .def("add_element", - ErrorWrappedCast(&Element::AddElement), - "Add a value to this Element") - .def("insert_element", - py::overload_cast(&Element::InsertElement), - "Add an element object.") - .def("insert_element", - py::overload_cast(&Element::InsertElement), - "Add an element object, and optionally set the given element's " - "parent to this object") - .def("remove_from_parent", &Element::RemoveFromParent, - "Remove this element from its parent.") - .def("remove_child", - ErrorWrappedCast(&Element::RemoveChild), - "Remove a child element.") - .def("clear_elements", &Element::ClearElements, - "Remove all child elements.") - .def("clear", &Element::Clear, - "Remove all child elements and reset file path and original " - "version.") - .def("update", ErrorWrappedCast<>(&Element::Update), - "Call the Update() callback on each element, as well as the " - "embedded Param.") - .def("reset", &Element::Reset, - "Call reset on each element and element description before " - "deleting all of them. Also clear out the embedded Param.") - .def("set_include_element", &Element::SetIncludeElement, - "Set the `` element that was used to load this element") - .def("get_include_element", &Element::GetIncludeElement, - "Get the `` element that was used to load this element.") - .def("set_file_path", &Element::SetFilePath, - "Set the path to the SDF document where this element came from.") - .def("file_path", &Element::FilePath, - "Get the path to the SDF document where this element came from") - .def("set_line_number", &Element::SetLineNumber, - "Set the line number of this element within the SDF document.") - .def("line_number", &Element::LineNumber, - "Get the line number of this element within the SDF document.") - .def("set_xml_path", &Element::SetXmlPath, - "Set the XML path of this element.") - .def("xml_path", &Element::XmlPath, - "Get the XML path of this element.") - .def("set_original_version", &Element::SetOriginalVersion, - "Set the spec version that this was originally parsed from.") - .def("original_version", &Element::OriginalVersion, - "Get the spec version that this was originally parsed from.") - .def("get_description", &Element::GetDescription, - "Get a text description of the element.") - .def("set_description", &Element::SetDescription, - "Set a text description for the element.") - .def("add_element_description", &Element::AddElementDescription, - "Add a new element description"); + ThrowIfErrors(errors); + }, + "Add an attribute value.", "key"_a, "type"_a, "default_value"_a, + "required"_a, "description"_a = "") + .def( + "add_value", + [](Element &_self, const std::string &_type, + const std::string &_defaultValue, bool _required, + const std::string &_description = "") + { + Errors errors; + _self.AddValue(_type, _defaultValue, _required, errors, + _description); + ThrowIfErrors(errors); + }, + "Add a value to this Element", "type"_a, "default_value"_a, + "required"_a, "description"_a = "") + .def( + "add_value", + [](Element &_self, const std::string &_type, + const std::string &_defaultValue, bool _required, + const std::string &_minValue, const std::string &_maxValue, + const std::string &_description = "") + { + Errors errors; + _self.AddValue(_type, _defaultValue, _required, _minValue, + _maxValue, errors, _description); + ThrowIfErrors(errors); + }, + "Add a value to this Element", "type"_a, "default_value"_a, + "required"_a, "min_value"_a, "max_value"_a, "description"_a = "") + .def("get_attribute", + py::overload_cast(&Element::GetAttribute, + py::const_), + "Get the param of an attribute.") + .def("get_attribute_count", &Element::GetAttributeCount, + "Get the number of attributes.") + .def("get_attributes", &Element::GetAttributes, + "Get all the attribute params.") + .def("get_attribute", + py::overload_cast(&Element::GetAttribute, py::const_), + "Get the param of an attribute.") + .def("get_element_description_count", + &Element::GetElementDescriptionCount, + "Get the number of element descriptions.") + .def("get_element_description", + py::overload_cast(&Element::GetElementDescription, + py::const_), + "Get an element description using an index") + .def("get_element_description", + py::overload_cast( + &Element::GetElementDescription, py::const_), + "Get an element description using a key") + .def("has_element_description", &Element::HasElementDescription, + "Return true if an element description exists.") + .def("has_attribute", &Element::HasAttribute, + "Return true if an attribute exists.") + .def("get_attribute_set", &Element::GetAttributeSet, + "Return true if the attribute was set (i.e. not default value)") + .def("remove_attribute", &Element::RemoveAttribute, + "Remove an attribute.") + .def("remove_all_attributes", &Element::RemoveAllAttributes, + "Removes all attributes.") + .def("get_value", &Element::GetValue, + "Get the param of the elements value") + // get_any is not supported since std::any is not supported in + // pybind11. + // get and set are defined below + .def("has_element", &Element::HasElement, + "Return true if the named element exists.") + .def("get_first_element", &Element::GetFirstElement, + "Get the first child element") + .def("get_next_element", &Element::GetNextElement, + "Get the first child Get the next sibling of this element.") + .def("find_element", + py::overload_cast(&Element::FindElement), + "Return a pointer to the child element with the provided name.") + .def("add_element", + ErrorWrappedCast(&Element::AddElement), + "Add a value to this Element") + .def("insert_element", + py::overload_cast(&Element::InsertElement), + "Add an element object.") + .def("insert_element", + py::overload_cast(&Element::InsertElement), + "Add an element object, and optionally set the given element's " + "parent to this object") + .def("remove_from_parent", &Element::RemoveFromParent, + "Remove this element from its parent.") + .def("remove_child", ErrorWrappedCast(&Element::RemoveChild), + "Remove a child element.") + .def("clear_elements", &Element::ClearElements, + "Remove all child elements.") + .def("clear", &Element::Clear, + "Remove all child elements and reset file path and original " + "version.") + .def("update", ErrorWrappedCast<>(&Element::Update), + "Call the Update() callback on each element, as well as the " + "embedded Param.") + .def("reset", &Element::Reset, + "Call reset on each element and element description before " + "deleting all of them. Also clear out the embedded Param.") + .def("set_include_element", &Element::SetIncludeElement, + "Set the `` element that was used to load this element") + .def("get_include_element", &Element::GetIncludeElement, + "Get the `` element that was used to load this element.") + .def("set_file_path", &Element::SetFilePath, + "Set the path to the SDF document where this element came from.") + .def("file_path", &Element::FilePath, + "Get the path to the SDF document where this element came from") + .def("set_line_number", &Element::SetLineNumber, + "Set the line number of this element within the SDF document.") + .def("line_number", &Element::LineNumber, + "Get the line number of this element within the SDF document.") + .def("set_xml_path", &Element::SetXmlPath, + "Set the XML path of this element.") + .def("xml_path", &Element::XmlPath, "Get the XML path of this element.") + .def("set_original_version", &Element::SetOriginalVersion, + "Set the spec version that this was originally parsed from.") + .def("original_version", &Element::OriginalVersion, + "Get the spec version that this was originally parsed from.") + .def("get_description", &Element::GetDescription, + "Get a text description of the element.") + .def("set_description", &Element::SetDescription, + "Set a text description for the element.") + .def("add_element_description", &Element::AddElementDescription, + "Add a new element description"); // Definitions for `Get`, and `Set` which will bind to `get_bool`, // `get_int`, etc. From 780059283b4a76e1098a59b1ab83aa36876eb25c Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Fri, 18 Aug 2023 13:22:12 -0500 Subject: [PATCH 9/9] Address reviewer feedback Signed-off-by: Addisu Z. Taddese --- python/test/pyElement_TEST.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/python/test/pyElement_TEST.py b/python/test/pyElement_TEST.py index 6b013ae8d..b6380d802 100644 --- a/python/test/pyElement_TEST.py +++ b/python/test/pyElement_TEST.py @@ -240,7 +240,7 @@ def test_get_templates(self): def test_clone(self): parent = Element() child = Element() - # desc = Element() + desc = Element() parent.set_name("parent") child.set_name("child") @@ -248,8 +248,8 @@ def test_clone(self): parent.insert_element(child) self.assertIsNotNone(parent.get_first_element()) - # parent.add_element_description(desc) - # self.assertEqual(parent.get_element_description_count(), 1) + parent.add_element_description(desc) + self.assertEqual(parent.get_element_description_count(), 1) parent.add_attribute("test", "string", "foo", False, "foo description") self.assertEqual(parent.get_attribute_count(), 1) @@ -273,7 +273,7 @@ def test_clone(self): self.assertEqual("/sdf/world[@name=\"default\"]", newelem.xml_path()) self.assertEqual("1.5", newelem.original_version()) self.assertIsNotNone(newelem.get_first_element()) - # self.assertEqual(newelem.get_element_description_count(), 1) + self.assertEqual(newelem.get_element_description_count(), 1) self.assertEqual(newelem.get_attribute_count(), 1) self.assertIsNotNone(newelem.get_include_element()) self.assertEqual("include", newelem.get_include_element().get_name()) @@ -382,6 +382,7 @@ def test_set(self): elem.add_value("string", "val", False, "val description") self.assertTrue(elem.set_string("hello")) + self.assertEqual(elem.get_string(), "hello") def test_copy(self): src = Element()