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.