diff --git a/Plugin/CMakeLists.txt b/Plugin/CMakeLists.txt
index 195866004..f61fcbe5c 100644
--- a/Plugin/CMakeLists.txt
+++ b/Plugin/CMakeLists.txt
@@ -5,6 +5,7 @@ set(HEADER_FILES
${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/initModule.h
${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/PythonEnvironment.h
${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SceneLoaderPY3.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SpellingSuggestionHelper.h
${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/DataCache.h
${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/DataHelper.h
diff --git a/Plugin/src/SofaPython3/SpellingSuggestionHelper.h b/Plugin/src/SofaPython3/SpellingSuggestionHelper.h
new file mode 100644
index 000000000..88a6d8520
--- /dev/null
+++ b/Plugin/src/SofaPython3/SpellingSuggestionHelper.h
@@ -0,0 +1,55 @@
+/******************************************************************************
+* SofaPython3 plugin *
+* (c) 2021 CNRS, University of Lille, INRIA *
+* *
+* This program is free software; you can redistribute it and/or modify it *
+* under the terms of the GNU Lesser General Public License as published by *
+* the Free Software Foundation; either version 2.1 of the License, or (at *
+* your option) any later version. *
+* *
+* This program is distributed in the hope that it will be useful, but WITHOUT *
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
+* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License *
+* for more details. *
+* *
+* You should have received a copy of the GNU Lesser General Public License *
+* along with this program. If not, see . *
+*******************************************************************************
+* Contact information: contact@sofa-framework.org *
+******************************************************************************/
+#pragma once
+
+#include
+#include
+#include
+#include
+
+namespace sofapython3
+{
+
+template
+void fillVectorOfStringFrom(const Iterable& v, const UnaryOperation& op, const PickingFunction func)
+{
+ std::transform(v.begin(), v.end(), op, func);
+}
+
+template >
+std::ostream& emitSpellingMessage(std::ostream& ostream, const std::string& message, const Iterable& iterable, const std::string& name,
+ sofa::Size numEntries=5, SReal thresold=0.6_sreal,
+ PickingFunction f = [](const typename Iterable::value_type d) { return d->getName(); })
+{
+ std::vector possibleNames;
+ possibleNames.reserve(iterable.size());
+ fillVectorOfStringFrom(iterable, std::back_inserter(possibleNames), f);
+
+ auto spellingSuggestions = sofa::helper::getClosestMatch(name, possibleNames, numEntries, thresold);
+ if(!spellingSuggestions.empty())
+ {
+ for(auto& [suggestedName, score] : spellingSuggestions)
+ ostream << message << "'" << suggestedName<< "' ("<< std::to_string((int)(100*score))+"% match)" << std::endl;
+ }
+ return ostream;
+}
+
+
+}
diff --git a/Plugin/src/SofaPython3/config.h.in b/Plugin/src/SofaPython3/config.h.in
index 878c39a85..504d3537c 100644
--- a/Plugin/src/SofaPython3/config.h.in
+++ b/Plugin/src/SofaPython3/config.h.in
@@ -55,3 +55,18 @@
#else
#define SOFAPYTHON3_BIND_ATTRIBUTE_ERROR() namespace pybind11 { PYBIND11_RUNTIME_EXCEPTION(attribute_error, PyExc_AttributeError) }
#endif // PYBIND11_SOFA_VERSION >= 20801
+
+#if PYBIND11_SOFA_VERSION >= 20600
+#define SOFAPYTHON3_ADD_PYBIND_TYPE_FOR_OLD_VERSION()
+#else
+#define SOFAPYTHON3_ADD_PYBIND_TYPE_FOR_OLD_VERSION() namespace pybind11 { \
+class type : public pybind11::object { \
+public: \
+ PYBIND11_OBJECT(type, pybind11::object, PyType_Check) \
+ static pybind11::handle handle_of(pybind11::handle h) { return handle((PyObject*) Py_TYPE(h.ptr())); } \
+ static type of(pybind11::handle h) { return type(type::handle_of(h), borrowed_t{}); } \
+ template static handle handle_of(); \
+ template static type of() {return type(type::handle_of(), borrowed_t{}); } \
+}; \
+}
+#endif // PYBIND11_SOFA_VERSION >= 20801
diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp
index bb32daddd..88f3a6df4 100644
--- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp
+++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp
@@ -18,6 +18,7 @@
* Contact information: contact@sofa-framework.org *
******************************************************************************/
+#include "SofaPython3/SpellingSuggestionHelper.h"
#include
#include
@@ -44,7 +45,9 @@ using sofa::simulation::Node;
#include
+// These two lines are there to handle deprecated version of pybind.
SOFAPYTHON3_BIND_ATTRIBUTE_ERROR()
+SOFAPYTHON3_ADD_PYBIND_TYPE_FOR_OLD_VERSION()
/// Makes an alias for the pybind11 namespace to increase readability.
namespace py { using namespace pybind11; }
@@ -339,15 +342,39 @@ py::list BindingBase::__dir__(Base* self)
return list;
}
-py::object BindingBase::__getattr__(py::object self, const std::string& s)
+py::object BindingBase::__getattr__(py::object self, const std::string& attributeName)
{
- py::object res = BindingBase::GetAttr( py::cast(self), s, false );
- if( res.is_none() )
+ // Search for attribute s.
+ py::object res = BindingBase::GetAttr( py::cast(self), attributeName, false );
+
+ // If there is one, then return it
+ if( !res.is_none() )
+ return res;
+
+ // If there is none, then search into the python dictionnary
+ if( py::hasattr(self.attr("__dict__"), attributeName.c_str()) )
+ return self.attr("__dict__")[attributeName.c_str()];
+
+ // If we reach this line, this indicate that no attribute was found. Maybe it is a misspelling
+ // so let's build misspelling hints for the user.
+ Base* selfbase = py::cast(self);
+ std::stringstream tmp;
+ emitSpellingMessage(tmp, " - The data field named ", selfbase->getDataFields(), attributeName, 2, 0.6);
+ emitSpellingMessage(tmp, " - The link named ", selfbase->getLinks(), attributeName, 2, 0.6);
+
+ // Also provide spelling hints on python functions.
+ emitSpellingMessage(tmp, " - The python attribute named ", py::cast(py::type::of(self).attr("__dict__")), attributeName, 5, 0.8,
+ [](const std::pair& kv) { return py::cast(std::get<0>(kv)); });
+
+ std::stringstream message;
+ message << "Unable to find attribute: "+attributeName;
+ if(!tmp.str().empty())
{
- return self.attr("__dict__")[s.c_str()];
+ message << msgendl;
+ message << " You possibly wanted to access: " << msgendl;
+ message << tmp.rdbuf();
}
-
- return res;
+ throw py::attribute_error(message.str());
}
void BindingBase::__setattr__(py::object self, const std::string& s, py::object value)
diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp
index cb92c5adc..30850bc7d 100644
--- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp
+++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp
@@ -17,11 +17,9 @@
*******************************************************************************
* Contact information: contact@sofa-framework.org *
******************************************************************************/
-
-
/// Neede to have automatic conversion from pybind types to stl container.
#include
-#include
+#include
#include
#include
@@ -35,6 +33,9 @@ namespace simpleapi = sofa::simpleapi;
#include
using sofa::helper::logging::Message;
+#include
+using sofa::helper::getClosestMatch;
+
#include
using sofa::core::ExecParams;
@@ -59,41 +60,67 @@ using sofapython3::PythonEnvironment;
#include
#include
+#include
+
using sofa::core::objectmodel::BaseObjectDescription;
#include
#include
+// These two lines are there to handle deprecated version of pybind.
+SOFAPYTHON3_BIND_ATTRIBUTE_ERROR()
+SOFAPYTHON3_ADD_PYBIND_TYPE_FOR_OLD_VERSION()
+
/// Makes an alias for the pybind11 namespace to increase readability.
namespace py { using namespace pybind11; }
using sofa::simulation::Node;
-namespace sofapython3 {
+namespace sofapython3
+{
-bool checkParamUsage(BaseObjectDescription& desc)
+namespace
{
- bool hasFailure = false;
- std::stringstream tmp;
- tmp <<"Unknown Attribute(s): " << msgendl;
+bool checkParamUsage(BaseObjectDescription& desc, const Base* base)
+{
+ std::vector> paramErrors;
for( auto& it : desc.getAttributeMap() )
{
if (!it.second.isAccessed())
{
- hasFailure = true;
- tmp << " - \""< possibleNames;
+ if(base)
+ {
+ fillVectorOfStringFrom(base->getDataFields(), std::back_inserter(possibleNames), [](const BaseData* d){return d->getName();});
+ fillVectorOfStringFrom(base->getLinks(), std::back_inserter(possibleNames), [](const BaseLink* l){return l->getName();});
+ }
+
+ for(auto& [name, value] : paramErrors)
+ {
+ tmp << " - Unable to set attribute '"<< name <<"' with value: " << value;
+ const auto& v = getClosestMatch(name, possibleNames);
+ if(!v.empty())
+ tmp << ". Possible misspelling of attribute '" << std::get<0>(v[0]) << "' ?";
+ else
+ tmp << ".";
+ tmp << msgendl;
+ }
+
+ if(!desc.getErrors().empty())
+ tmp << desc.getErrors()[0];
throw py::type_error(tmp.str());
}
- return hasFailure;
+
+ return false;
}
py::object getItem(Node& self, std::list& path)
@@ -179,7 +206,7 @@ py::object getObject(Node &n, const std::string &name, const py::kwargs& kwargs)
msg_deprecated(&n) << "Calling the method getObject() with extra arguments is not supported anymore."
<< "To remove this message please refer to the documentation of the getObject method"
<< msgendl
- << PythonEnvironment::getPythonCallingPointString() ;
+ << PythonEnvironment::getPythonCallingPointString() ;
}
BaseObject *object = n.getObject(name);
@@ -247,7 +274,7 @@ py::object addObjectKwargs(Node* self, const std::string& type, const py::kwargs
setFieldsFromPythonValues(object.get(), kwargs);
- checkParamUsage(desc);
+ checkParamUsage(desc, object.get());
// 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
@@ -342,7 +369,7 @@ py::object addChildKwargs(Node* self, const std::string& name, const py::kwargs&
node->setInstanciationSourceFileName(finfo->filename);
node->setInstanciationSourceFilePos(finfo->line);
- checkParamUsage(desc);
+ checkParamUsage(desc, node.get());
for(auto a : kwargs)
{
@@ -398,56 +425,79 @@ py::object removeChildByName(Node& n, const std::string name)
std::unique_ptr property_children(Node* node)
{
return std::make_unique(node,
- [](Node* n) -> size_t { return n->child.size(); },
- [](Node* n, unsigned int index) -> Base::SPtr { return n->child[index]; },
- [](const Node* n, const std::string& name) { return n->getChild(name); },
- [](Node* n, unsigned int index) { n->removeChild(n->child[index]); }
- );
+ [](Node* n) -> size_t { return n->child.size(); },
+ [](Node* n, unsigned int index) -> Base::SPtr { return n->child[index]; },
+[](const Node* n, const std::string& name) { return n->getChild(name); },
+[](Node* n, unsigned int index) { n->removeChild(n->child[index]); }
+);
}
std::unique_ptr property_parents(Node* node)
{
return std::make_unique(node,
- [](Node* n) -> size_t { return n->getNbParents(); },
- [](Node* n, unsigned int index) -> Node::SPtr {
- auto p = n->getParents();
- return static_cast(p[index]);
- },
- [](const Node* n, const std::string& name) -> sofa::core::Base* {
- const auto& parents = n->getParents();
- return *std::find_if(parents.begin(),
- parents.end(),
- [name](BaseNode* child){ return child->getName() == name; });
- },
- [](Node*, unsigned int) {
- throw std::runtime_error("Removing a parent is not a supported operation. Please detach the node from the corresponding graph node.");
- });
+ [](Node* n) -> size_t { return n->getNbParents(); },
+ [](Node* n, unsigned int index) -> Node::SPtr {
+ auto p = n->getParents();
+ return static_cast(p[index]);
+},
+[](const Node* n, const std::string& name) -> sofa::core::Base* {
+ const auto& parents = n->getParents();
+ return *std::find_if(parents.begin(),
+ parents.end(),
+ [name](BaseNode* child){ return child->getName() == name; });
+},
+[](Node*, unsigned int) {
+ throw std::runtime_error("Removing a parent is not a supported operation. Please detach the node from the corresponding graph node.");
+});
}
std::unique_ptr property_objects(Node* node)
{
return std::make_unique(node,
- [](Node* n) -> size_t { return n->object.size(); },
- [](Node* n, unsigned int index) -> Base::SPtr { return (n->object[index]);},
- [](const Node* n, const std::string& name) { return n->getObject(name); },
- [](Node* n, unsigned int index) { n->removeObject(n->object[index]);}
- );
+ [](Node* n) -> size_t { return n->object.size(); },
+ [](Node* n, unsigned int index) -> Base::SPtr { return (n->object[index]);},
+[](const Node* n, const std::string& name) { return n->getObject(name); },
+[](Node* n, unsigned int index) { n->removeObject(n->object[index]);}
+);
}
-py::object __getattr__(Node& self, const std::string& name)
+py::object __getattr__(py::object pyself, const std::string& name)
{
+ Node* selfnode = py::cast(pyself);
/// Search in the object lists
- BaseObject *object = self.getObject(name);
+ BaseObject *object = selfnode->getObject(name);
if (object)
return PythonFactory::toPython(object);
/// Search in the child lists
- Node *child = self.getChild(name);
+ Node *child = selfnode->getChild(name);
if (child)
return PythonFactory::toPython(child);
/// Search in the data & link lists
- return BindingBase::GetAttr(&self, name, true);
+ py::object result = BindingBase::GetAttr(selfnode, name, false);
+ if(!result.is_none())
+ return result;
+
+ std::stringstream tmp;
+ emitSpellingMessage(tmp, " - The data field named ", selfnode->getDataFields(), name, 2, 0.8);
+ emitSpellingMessage(tmp, " - The link named ", selfnode->getDataFields(), name, 2, 0.8);
+ emitSpellingMessage(tmp, " - The object named ", selfnode->getNodeObjects(), name, 2, 0.8);
+ emitSpellingMessage(tmp, " - The child node named ", selfnode->getChildren(), name, 2, 0.8);
+
+ // Also provide spelling hints on python functions.
+ emitSpellingMessage(tmp, " - The python attribute named ", py::cast(py::type::of(pyself).attr("__dict__")), name, 5, 0.8,
+ [](const std::pair& kv) { return py::cast(std::get<0>(kv)); });
+
+ std::stringstream message;
+ message << "Unable to find attribute: "+name;
+ if(!tmp.str().empty())
+ {
+ message << msgendl;
+ message << " You possibly wanted to access: " << msgendl;
+ message << tmp.rdbuf();
+ }
+ throw pybind11::attribute_error(message.str());
}
/// gets an item using its path (path is dot-separated, relative to the object
@@ -549,6 +599,8 @@ void sendEvent(Node* self, py::object pyUserData, char* eventName)
self->propagateEvent(sofa::core::execparams::defaultInstance(), &event);
}
+}
+
void moduleAddNode(py::module &m) {
/// Register the complete parent-child relationship between Base and Node to the pybind11
/// typing system.