Skip to content
Merged
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
1 change: 1 addition & 0 deletions Plugin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions Plugin/src/SofaPython3/SpellingSuggestionHelper.h
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>. *
*******************************************************************************
* Contact information: contact@sofa-framework.org *
******************************************************************************/
#pragma once

#include <functional>
#include <algorithm>
#include <iostream>
#include <sofa/helper/DiffLib.h>

namespace sofapython3
{

template<class Iterable, class UnaryOperation, class PickingFunction>
void fillVectorOfStringFrom(const Iterable& v, const UnaryOperation& op, const PickingFunction func)
{
std::transform(v.begin(), v.end(), op, func);
}

template<class Iterable, class PickingFunction=std::function<const std::string(typename Iterable::value_type)> >
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<std::string> 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;
}


}
15 changes: 15 additions & 0 deletions Plugin/src/SofaPython3/config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename T> static handle handle_of(); \
template<typename T> static type of() {return type(type::handle_of<T>(), borrowed_t{}); } \
}; \
}
#endif // PYBIND11_SOFA_VERSION >= 20801
39 changes: 33 additions & 6 deletions bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* Contact information: contact@sofa-framework.org *
******************************************************************************/

#include "SofaPython3/SpellingSuggestionHelper.h"
#include <pybind11/pybind11.h>

#include <pybind11/numpy.h>
Expand All @@ -44,7 +45,9 @@ using sofa::simulation::Node;

#include <SofaPython3/DataHelper.h>

// 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; }
Expand Down Expand Up @@ -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<Base*>(self), s, false );
if( res.is_none() )
// Search for attribute s.
py::object res = BindingBase::GetAttr( py::cast<Base*>(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<Base*>(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::dict>(py::type::of(self).attr("__dict__")), attributeName, 5, 0.8,
[](const std::pair<py::handle, py::handle>& kv) { return py::cast<std::string>(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)
Expand Down
148 changes: 100 additions & 48 deletions bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@
*******************************************************************************
* Contact information: contact@sofa-framework.org *
******************************************************************************/


/// Neede to have automatic conversion from pybind types to stl container.
#include <pybind11/stl.h>
#include <pybind11/eval.h>
#include <pybind11/numpy.h>

#include <sofa/simulation/Simulation.h>
#include <sofa/core/ComponentNameHelper.h>
Expand All @@ -35,6 +33,9 @@ namespace simpleapi = sofa::simpleapi;
#include <sofa/helper/logging/Messaging.h>
using sofa::helper::logging::Message;

#include <sofa/helper/DiffLib.h>
using sofa::helper::getClosestMatch;

#include <sofa/simulation/graph/DAGNode.h>
using sofa::core::ExecParams;

Expand All @@ -59,41 +60,67 @@ using sofapython3::PythonEnvironment;
#include <SofaPython3/Sofa/Core/Binding_NodeIterator.h>
#include <SofaPython3/Sofa/Core/Binding_PythonScriptEvent.h>

#include <SofaPython3/SpellingSuggestionHelper.h>

using sofa::core::objectmodel::BaseObjectDescription;

#include <queue>
#include <sofa/core/objectmodel/Link.h>

// 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<std::tuple<std::string, std::string>> paramErrors;
for( auto& it : desc.getAttributeMap() )
{
if (!it.second.isAccessed())
{
hasFailure = true;
tmp << " - \""<<it.first <<"\" with value: \"" <<std::string(it.second) << msgendl;
paramErrors.emplace_back(std::make_tuple(it.first, it.second));
}
}
if(!desc.getErrors().empty())
{
hasFailure = true;
tmp << desc.getErrors()[0];
}
if(hasFailure)

if(!paramErrors.empty() || !desc.getErrors().empty())
{
std::stringstream tmp;
tmp << "Unknown Attribute(s): " << msgendl;

std::vector<std::string> 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<std::string>& path)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -398,56 +425,79 @@ py::object removeChildByName(Node& n, const std::string name)
std::unique_ptr<NodeIterator> property_children(Node* node)
{
return std::make_unique<NodeIterator>(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<NodeIterator> property_parents(Node* node)
{
return std::make_unique<NodeIterator>(node,
[](Node* n) -> size_t { return n->getNbParents(); },
[](Node* n, unsigned int index) -> Node::SPtr {
auto p = n->getParents();
return static_cast<Node*>(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<Node*>(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<NodeIterator> property_objects(Node* node)
{
return std::make_unique<NodeIterator>(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<Node*>(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::dict>(py::type::of(pyself).attr("__dict__")), name, 5, 0.8,
[](const std::pair<py::handle, py::handle>& kv) { return py::cast<std::string>(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
Expand Down Expand Up @@ -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.
Expand Down