Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions Plugin/src/SofaPython3/DataHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<py::str>(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<py::list>(kv.second))
{
bool isAllStrings = true;
for(auto data : kv.second)
{
if(!py::isinstance<py::str>(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];
Expand Down
8 changes: 8 additions & 0 deletions Plugin/src/SofaPython3/DataHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
112 changes: 111 additions & 1 deletion bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to add a second signature so we can pass real type instead of string.

py::object add( Node* self, py::type type, const py::kwargs& kwargs)
{
   if(issubchild(type, BaseObject)){
         ... 
   }else if(issubchild(type, Node)){
           ....
   }
}

But this could be in a second PR.

{
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");
}
Copy link
Contributor

@damienmarchal damienmarchal Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we consider to add a name generation mechanism (with +1) to avoid having name duplicated.
Of course this have a non linear performance hit in case of gigantic scene... but I would be in favor of having a robust normal add() and a second one considered "fast mode" for expert users.

/// 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<std::string>(a.first);
BaseData * d = createdObj->findData(dataName);
BaseLink * l = createdObj->findLink(dataName);

if (d)
{
if (parametersToLink.contains(a.first))
d->setParent(a.second.cast<BaseData*>());
else if(parametersToCopy.contains(a.first))
{
try
{
PythonFactory::fromPython(d, py::cast<py::object>(a.second));
}
catch (std::exception& e)
{
msg_warning(self)<<"Creating " << type << " at " << createdObj->getPathName() <<". An exception of type \""<<e.what()<<"\" was received while copying value input for data " << dataName << ". To fix this please reshape the list you are providing to fit the real intended data structure. \n The compatibility layer will try to create the data by converting the input to string.";

if ( !d->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:
Expand Down Expand Up @@ -270,6 +379,7 @@ class NumpyReprFixerRAII
};



/// Implement the addObject function.
py::object addObjectKwargs(Node* self, const std::string& type, const py::kwargs& kwargs)
{
Expand Down Expand Up @@ -355,7 +465,7 @@ py::object addKwargs(Node* self, const py::object& callable, const py::kwargs& k
if(py::isinstance<py::str>(callable))
{
py::str type = callable;
return addObjectKwargs(self, type, kwargs);
return add(self, type, kwargs);
}

if (kwargs.contains("name"))
Expand Down
34 changes: 33 additions & 1 deletion bindings/Sofa/tests/Simulation/Node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pass


def test_addChild(self):
root = Sofa.Core.Node("rootNode")
root.addChild(Sofa.Core.Node("child1"))
Expand Down
Loading