diff --git a/Plugin/src/SofaPython3/DataHelper.cpp b/Plugin/src/SofaPython3/DataHelper.cpp index 6bd1f6a8..350c87ba 100644 --- a/Plugin/src/SofaPython3/DataHelper.cpp +++ b/Plugin/src/SofaPython3/DataHelper.cpp @@ -75,15 +75,54 @@ std::string toSofaParsableString(const py::handle& p) } /// RVO optimized function. Don't care about copy on the return code. -void fillBaseObjectdescription(sofa::core::objectmodel::BaseObjectDescription& desc, - const py::dict& dict) +void fillBaseObjectdescription(sofa::core::objectmodel::BaseObjectDescription& desc, const py::dict& dict) { + for(auto kv : dict) + { + desc.setAttribute(py::str(kv.first), toSofaParsableString(kv.second)); + } +} + +void processKwargsForObjectCreation(const py::dict dict, + py::list& parametersToLink, + py::list& parametersToCopy, + sofa::core::objectmodel::BaseObjectDescription& parametersAsString) +{ + auto typeHandleBaseData = py::detail::get_type_handle(typeid(BaseData), false); + auto typeHandleLinkPath = py::detail::get_type_handle(typeid(LinkPath), false); for(auto kv : dict) { - desc.setAttribute(py::str(kv.first), toSofaParsableString(kv.second)); + if (py::isinstance(kv.second, typeHandleBaseData)) + parametersToLink.append(kv.first); + else if (py::isinstance(kv.second) || py::isinstance(kv.second, typeHandleLinkPath) ) + parametersAsString.setAttribute(py::str(kv.first), py::str(kv.second)); + //This test is only required because of the multimappings that need the data "input" during the call to canCreate but it is given as a list of strings. + //So when a list of string is passed, then we use directly the conversion to string to be able to pass it directly in the BaseObjectDescription. + //TODO: remove this once the canCreate of Mapping class doesn't need to access data input and output + else if (py::isinstance(kv.second)) + { + bool isAllStrings = true; + for(auto data : kv.second) + { + if(!py::isinstance(data)) + { + isAllStrings = false; + break; + } + } + if(isAllStrings) + parametersAsString.setAttribute(py::str(kv.first), toSofaParsableString(kv.second)); + else + parametersToCopy.append(kv.first); + } + else + parametersToCopy.append(kv.first); } + return; } + + std::ostream& operator<<(std::ostream& out, const py::buffer_info& p) { out << "buffer{"<< p.format << ", " << p.ndim << ", " << p.shape[0]; diff --git a/Plugin/src/SofaPython3/DataHelper.h b/Plugin/src/SofaPython3/DataHelper.h index d689aa3b..7352ee5d 100644 --- a/Plugin/src/SofaPython3/DataHelper.h +++ b/Plugin/src/SofaPython3/DataHelper.h @@ -210,6 +210,14 @@ void SOFAPYTHON3_API copyFromListScalar(BaseData& d, const AbstractTypeInfo& nfo std::string SOFAPYTHON3_API toSofaParsableString(const pybind11::handle& p); +/// Split the content of the dictionnary 'dict' in three set. +/// On containing the data to parent, one containing the data to copy and on containing the data to parse in the BaseObjectDescription +void SOFAPYTHON3_API processKwargsForObjectCreation(const pybind11::dict dict, + pybind11::list& parametersToLink, + pybind11::list& parametersToCopy, + sofa::core::objectmodel::BaseObjectDescription& parametersAsString); + + //pybind11::object SOFAPYTHON3_API dataToPython(BaseData* d); /// RVO optimized function. Don't care about copy on the return code. diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp index 2cfff2f3..b5836258 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp @@ -235,6 +235,115 @@ void setFieldsFromPythonValues(Base* self, const py::kwargs& dict) } } +py::object add( Node* self, const std::string& type, const py::kwargs& kwargs) +{ + std::string name {}; + if (kwargs.contains("name")) + { + name = py::str(kwargs["name"]); + if (sofapython3::isProtectedKeyword(name)) + throw py::value_error("Cannot call addObject with name " + name + ": Protected keyword"); + } + /// Prepare the description to hold the different python attributes as data field's + /// arguments then create the object. + BaseObjectDescription desc {nullptr, type.c_str()}; + py::list parametersToCopy; + py::list parametersToLink; + + //This method will sort the input kwargs. + //It will keep all strings, and list of strings as string and put them to desc so the factory can use them + //during canCreate and parse calls (important for template, src, input/output of mapping) + processKwargsForObjectCreation( kwargs, parametersToLink, parametersToCopy, desc); + + sofa::core::objectmodel::Base::SPtr createdObj; + + if (type == "Node") + { + createdObj = simpleapi::createChild(self, desc); + } + else + { + createdObj = ObjectFactory::getInstance()->createObject(self, &desc); + } + + // After calling createObject the returned value can be either a nullptr + // or non-null but with error message or non-null. + // Let's first handle the case when the returned pointer is null. + if(!createdObj) + { + std::stringstream tmp ; + for(auto& s : desc.getErrors()) + tmp << s << msgendl ; + throw py::value_error(tmp.str()); + } + + // Associates the emission location to the created object. + auto finfo = PythonEnvironment::getPythonCallingPointAsFileInfo(); + createdObj->setInstanciationSourceFileName(finfo->filename); + createdObj->setInstanciationSourceFilePos(finfo->line); + + if (name.empty()) + { + const auto resolvedName = self->getNameHelper().resolveName(createdObj->getClassName(), name, sofa::core::ComponentNameHelper::Convention::python); + createdObj->setName(resolvedName); + } + + setFieldsFromPythonValues(createdObj.get(), kwargs); + + // Convert the logged messages in the object's internal logging into python exception. + // this is not a very fast way to do that...but well...python is slow anyway. And serious + // error management has a very high priority. If performance becomes an issue we will fix it + // when needed. + if(createdObj->countLoggedMessages({Message::Error})) + { + throw py::value_error(createdObj->getLoggedMessagesAsString({Message::Error})); + } + + //Now for all the data that have not been passed by object descriptor, we pass them to the object + for(auto a : kwargs) + { + const std::string dataName = py::cast(a.first); + BaseData * d = createdObj->findData(dataName); + BaseLink * l = createdObj->findLink(dataName); + + if (d) + { + if (parametersToLink.contains(a.first)) + d->setParent(a.second.cast()); + else if(parametersToCopy.contains(a.first)) + { + try + { + PythonFactory::fromPython(d, py::cast(a.second)); + } + catch (std::exception& e) + { + msg_warning(self)<<"Creating " << type << " at " << createdObj->getPathName() <<". An exception of type \""<read(toSofaParsableString(a.second))) + throw py::value_error("Cannot convert the input \""+ toSofaParsableString(a.second)+"\" to a valid value for data " + createdObj->getPathName() + "." + dataName ); + } + } + d->setPersistent(true); + } + else if (l == nullptr && parametersToCopy.contains(a.first)) + { + // This case happens when the object overrides the method parse which + // expect some arguments in desc instead of using datas to expose variation points + desc.setAttribute(dataName,toSofaParsableString(a.second) ); + } + } + + // Let the object parse the desc one last time now that 'real' all data has been set + createdObj->parse(&desc); + // Now we check that everything has been used. If not, then throw an error. + checkParamUsage(desc, createdObj.get()); + + + return PythonFactory::toPython(createdObj.get()); +} + + class NumpyReprFixerRAII { public: @@ -270,6 +379,7 @@ class NumpyReprFixerRAII }; + /// Implement the addObject function. py::object addObjectKwargs(Node* self, const std::string& type, const py::kwargs& kwargs) { @@ -355,7 +465,7 @@ py::object addKwargs(Node* self, const py::object& callable, const py::kwargs& k if(py::isinstance(callable)) { py::str type = callable; - return addObjectKwargs(self, type, kwargs); + return add(self, type, kwargs); } if (kwargs.contains("name")) diff --git a/bindings/Sofa/tests/Simulation/Node.py b/bindings/Sofa/tests/Simulation/Node.py index f0cc5b35..ab19851f 100644 --- a/bindings/Sofa/tests/Simulation/Node.py +++ b/bindings/Sofa/tests/Simulation/Node.py @@ -63,7 +63,39 @@ def d1(): self.assertRaises(ValueError, d1) root.init() - + + def test_generic_add_type_child(self): + root = Sofa.Core.Node("rootNode") + + FirstNode = root.add("Node",name="FirstNode") + + FirstNode.addObject("RequiredPlugin", name="Sofa.Component.StateContainer") + + self.assertEqual(root.children()[0].name.value, "FirstNode") + self.assertEqual(root.FirstNode.objects()[0].type.value, "RequiredPlugin") + self.assertEqual(root.FirstNode.objects()[0].name.value, "FirstNode") + + pass + + def test_generic_add_type_object(self): + root = Sofa.Core.Node("rootNode") + + root.add("RequiredPlugin", name="Sofa.Component.StateContainer") + self.assertEqual(root.objects()[0].type.value, "RequiredPlugin") + self.assertEqual(root.objects()[0].name.value, "FirstNode") + + pass + + def test_generic_add_real_object(self): + root = Sofa.Core.Node("rootNode") + + MyNode = Sofa.Core.Node("MyNode") + + root.add(MyNode) + self.assertEqual(root.children()[0].name.value, "MyNode") + + pass + def test_addChild(self): root = Sofa.Core.Node("rootNode") root.addChild(Sofa.Core.Node("child1"))