Skip to content

Commit 7a44ca6

Browse files
Add addObject spelling support (again) (#322)
* Diff support compatible with all CI's * Update Plugin/src/SofaPython3/SpellingSuggestionHelper.h Thanks Co-authored-by: Alex Bilger <alxbilger@users.noreply.github.com> Co-authored-by: Alex Bilger <alxbilger@users.noreply.github.com>
1 parent 8b6c5eb commit 7a44ca6

5 files changed

Lines changed: 204 additions & 54 deletions

File tree

Plugin/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ set(HEADER_FILES
55
${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/initModule.h
66
${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/PythonEnvironment.h
77
${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SceneLoaderPY3.h
8+
${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/SpellingSuggestionHelper.h
89

910
${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/DataCache.h
1011
${CMAKE_CURRENT_SOURCE_DIR}/src/SofaPython3/DataHelper.h
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/******************************************************************************
2+
* SofaPython3 plugin *
3+
* (c) 2021 CNRS, University of Lille, INRIA *
4+
* *
5+
* This program is free software; you can redistribute it and/or modify it *
6+
* under the terms of the GNU Lesser General Public License as published by *
7+
* the Free Software Foundation; either version 2.1 of the License, or (at *
8+
* your option) any later version. *
9+
* *
10+
* This program is distributed in the hope that it will be useful, but WITHOUT *
11+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
12+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License *
13+
* for more details. *
14+
* *
15+
* You should have received a copy of the GNU Lesser General Public License *
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
17+
*******************************************************************************
18+
* Contact information: contact@sofa-framework.org *
19+
******************************************************************************/
20+
#pragma once
21+
22+
#include <functional>
23+
#include <algorithm>
24+
#include <iostream>
25+
#include <sofa/helper/DiffLib.h>
26+
27+
namespace sofapython3
28+
{
29+
30+
template<class Iterable, class UnaryOperation, class PickingFunction>
31+
void fillVectorOfStringFrom(const Iterable& v, const UnaryOperation& op, const PickingFunction func)
32+
{
33+
std::transform(v.begin(), v.end(), op, func);
34+
}
35+
36+
template<class Iterable, class PickingFunction=std::function<const std::string(typename Iterable::value_type)> >
37+
std::ostream& emitSpellingMessage(std::ostream& ostream, const std::string& message, const Iterable& iterable, const std::string& name,
38+
sofa::Size numEntries=5, SReal thresold=0.6_sreal,
39+
PickingFunction f = [](const typename Iterable::value_type d) { return d->getName(); })
40+
{
41+
std::vector<std::string> possibleNames;
42+
possibleNames.reserve(iterable.size());
43+
fillVectorOfStringFrom(iterable, std::back_inserter(possibleNames), f);
44+
45+
auto spellingSuggestions = sofa::helper::getClosestMatch(name, possibleNames, numEntries, thresold);
46+
if(!spellingSuggestions.empty())
47+
{
48+
for(auto& [suggestedName, score] : spellingSuggestions)
49+
ostream << message << "'" << suggestedName<< "' ("<< std::to_string((int)(100*score))+"% match)" << std::endl;
50+
}
51+
return ostream;
52+
}
53+
54+
55+
}

Plugin/src/SofaPython3/config.h.in

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,18 @@
5555
#else
5656
#define SOFAPYTHON3_BIND_ATTRIBUTE_ERROR() namespace pybind11 { PYBIND11_RUNTIME_EXCEPTION(attribute_error, PyExc_AttributeError) }
5757
#endif // PYBIND11_SOFA_VERSION >= 20801
58+
59+
#if PYBIND11_SOFA_VERSION >= 20600
60+
#define SOFAPYTHON3_ADD_PYBIND_TYPE_FOR_OLD_VERSION()
61+
#else
62+
#define SOFAPYTHON3_ADD_PYBIND_TYPE_FOR_OLD_VERSION() namespace pybind11 { \
63+
class type : public pybind11::object { \
64+
public: \
65+
PYBIND11_OBJECT(type, pybind11::object, PyType_Check) \
66+
static pybind11::handle handle_of(pybind11::handle h) { return handle((PyObject*) Py_TYPE(h.ptr())); } \
67+
static type of(pybind11::handle h) { return type(type::handle_of(h), borrowed_t{}); } \
68+
template<typename T> static handle handle_of(); \
69+
template<typename T> static type of() {return type(type::handle_of<T>(), borrowed_t{}); } \
70+
}; \
71+
}
72+
#endif // PYBIND11_SOFA_VERSION >= 20801

bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Base.cpp

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* Contact information: contact@sofa-framework.org *
1919
******************************************************************************/
2020

21+
#include "SofaPython3/SpellingSuggestionHelper.h"
2122
#include <pybind11/pybind11.h>
2223

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

4546
#include <SofaPython3/DataHelper.h>
4647

48+
// These two lines are there to handle deprecated version of pybind.
4749
SOFAPYTHON3_BIND_ATTRIBUTE_ERROR()
50+
SOFAPYTHON3_ADD_PYBIND_TYPE_FOR_OLD_VERSION()
4851

4952
/// Makes an alias for the pybind11 namespace to increase readability.
5053
namespace py { using namespace pybind11; }
@@ -339,15 +342,39 @@ py::list BindingBase::__dir__(Base* self)
339342
return list;
340343
}
341344

342-
py::object BindingBase::__getattr__(py::object self, const std::string& s)
345+
py::object BindingBase::__getattr__(py::object self, const std::string& attributeName)
343346
{
344-
py::object res = BindingBase::GetAttr( py::cast<Base*>(self), s, false );
345-
if( res.is_none() )
347+
// Search for attribute s.
348+
py::object res = BindingBase::GetAttr( py::cast<Base*>(self), attributeName, false );
349+
350+
// If there is one, then return it
351+
if( !res.is_none() )
352+
return res;
353+
354+
// If there is none, then search into the python dictionnary
355+
if( py::hasattr(self.attr("__dict__"), attributeName.c_str()) )
356+
return self.attr("__dict__")[attributeName.c_str()];
357+
358+
// If we reach this line, this indicate that no attribute was found. Maybe it is a misspelling
359+
// so let's build misspelling hints for the user.
360+
Base* selfbase = py::cast<Base*>(self);
361+
std::stringstream tmp;
362+
emitSpellingMessage(tmp, " - The data field named ", selfbase->getDataFields(), attributeName, 2, 0.6);
363+
emitSpellingMessage(tmp, " - The link named ", selfbase->getLinks(), attributeName, 2, 0.6);
364+
365+
// Also provide spelling hints on python functions.
366+
emitSpellingMessage(tmp, " - The python attribute named ", py::cast<py::dict>(py::type::of(self).attr("__dict__")), attributeName, 5, 0.8,
367+
[](const std::pair<py::handle, py::handle>& kv) { return py::cast<std::string>(std::get<0>(kv)); });
368+
369+
std::stringstream message;
370+
message << "Unable to find attribute: "+attributeName;
371+
if(!tmp.str().empty())
346372
{
347-
return self.attr("__dict__")[s.c_str()];
373+
message << msgendl;
374+
message << " You possibly wanted to access: " << msgendl;
375+
message << tmp.rdbuf();
348376
}
349-
350-
return res;
377+
throw py::attribute_error(message.str());
351378
}
352379

353380
void BindingBase::__setattr__(py::object self, const std::string& s, py::object value)

bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Node.cpp

Lines changed: 100 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@
1717
*******************************************************************************
1818
* Contact information: contact@sofa-framework.org *
1919
******************************************************************************/
20-
21-
2220
/// Neede to have automatic conversion from pybind types to stl container.
2321
#include <pybind11/stl.h>
24-
#include <pybind11/eval.h>
22+
#include <pybind11/numpy.h>
2523

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

36+
#include <sofa/helper/DiffLib.h>
37+
using sofa::helper::getClosestMatch;
38+
3839
#include <sofa/simulation/graph/DAGNode.h>
3940
using sofa::core::ExecParams;
4041

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

63+
#include <SofaPython3/SpellingSuggestionHelper.h>
64+
6265
using sofa::core::objectmodel::BaseObjectDescription;
6366

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

70+
// These two lines are there to handle deprecated version of pybind.
71+
SOFAPYTHON3_BIND_ATTRIBUTE_ERROR()
72+
SOFAPYTHON3_ADD_PYBIND_TYPE_FOR_OLD_VERSION()
73+
6774
/// Makes an alias for the pybind11 namespace to increase readability.
6875
namespace py { using namespace pybind11; }
6976

7077
using sofa::simulation::Node;
7178

72-
namespace sofapython3 {
79+
namespace sofapython3
80+
{
7381

74-
bool checkParamUsage(BaseObjectDescription& desc)
82+
namespace
7583
{
76-
bool hasFailure = false;
77-
std::stringstream tmp;
78-
tmp <<"Unknown Attribute(s): " << msgendl;
84+
bool checkParamUsage(BaseObjectDescription& desc, const Base* base)
85+
{
86+
std::vector<std::tuple<std::string, std::string>> paramErrors;
7987
for( auto& it : desc.getAttributeMap() )
8088
{
8189
if (!it.second.isAccessed())
8290
{
83-
hasFailure = true;
84-
tmp << " - \""<<it.first <<"\" with value: \"" <<std::string(it.second) << msgendl;
91+
paramErrors.emplace_back(std::make_tuple(it.first, it.second));
8592
}
8693
}
87-
if(!desc.getErrors().empty())
88-
{
89-
hasFailure = true;
90-
tmp << desc.getErrors()[0];
91-
}
92-
if(hasFailure)
94+
95+
if(!paramErrors.empty() || !desc.getErrors().empty())
9396
{
97+
std::stringstream tmp;
98+
tmp << "Unknown Attribute(s): " << msgendl;
99+
100+
std::vector<std::string> possibleNames;
101+
if(base)
102+
{
103+
fillVectorOfStringFrom(base->getDataFields(), std::back_inserter(possibleNames), [](const BaseData* d){return d->getName();});
104+
fillVectorOfStringFrom(base->getLinks(), std::back_inserter(possibleNames), [](const BaseLink* l){return l->getName();});
105+
}
106+
107+
for(auto& [name, value] : paramErrors)
108+
{
109+
tmp << " - Unable to set attribute '"<< name <<"' with value: " << value;
110+
const auto& v = getClosestMatch(name, possibleNames);
111+
if(!v.empty())
112+
tmp << ". Possible misspelling of attribute '" << std::get<0>(v[0]) << "' ?";
113+
else
114+
tmp << ".";
115+
tmp << msgendl;
116+
}
117+
118+
if(!desc.getErrors().empty())
119+
tmp << desc.getErrors()[0];
94120
throw py::type_error(tmp.str());
95121
}
96-
return hasFailure;
122+
123+
return false;
97124
}
98125

99126
py::object getItem(Node& self, std::list<std::string>& path)
@@ -179,7 +206,7 @@ py::object getObject(Node &n, const std::string &name, const py::kwargs& kwargs)
179206
msg_deprecated(&n) << "Calling the method getObject() with extra arguments is not supported anymore."
180207
<< "To remove this message please refer to the documentation of the getObject method"
181208
<< msgendl
182-
<< PythonEnvironment::getPythonCallingPointString() ;
209+
<< PythonEnvironment::getPythonCallingPointString() ;
183210
}
184211

185212
BaseObject *object = n.getObject(name);
@@ -247,7 +274,7 @@ py::object addObjectKwargs(Node* self, const std::string& type, const py::kwargs
247274

248275
setFieldsFromPythonValues(object.get(), kwargs);
249276

250-
checkParamUsage(desc);
277+
checkParamUsage(desc, object.get());
251278

252279
// Convert the logged messages in the object's internal logging into python exception.
253280
// 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&
342369
node->setInstanciationSourceFileName(finfo->filename);
343370
node->setInstanciationSourceFilePos(finfo->line);
344371

345-
checkParamUsage(desc);
372+
checkParamUsage(desc, node.get());
346373

347374
for(auto a : kwargs)
348375
{
@@ -398,56 +425,79 @@ py::object removeChildByName(Node& n, const std::string name)
398425
std::unique_ptr<NodeIterator> property_children(Node* node)
399426
{
400427
return std::make_unique<NodeIterator>(node,
401-
[](Node* n) -> size_t { return n->child.size(); },
402-
[](Node* n, unsigned int index) -> Base::SPtr { return n->child[index]; },
403-
[](const Node* n, const std::string& name) { return n->getChild(name); },
404-
[](Node* n, unsigned int index) { n->removeChild(n->child[index]); }
405-
);
428+
[](Node* n) -> size_t { return n->child.size(); },
429+
[](Node* n, unsigned int index) -> Base::SPtr { return n->child[index]; },
430+
[](const Node* n, const std::string& name) { return n->getChild(name); },
431+
[](Node* n, unsigned int index) { n->removeChild(n->child[index]); }
432+
);
406433
}
407434

408435
std::unique_ptr<NodeIterator> property_parents(Node* node)
409436
{
410437
return std::make_unique<NodeIterator>(node,
411-
[](Node* n) -> size_t { return n->getNbParents(); },
412-
[](Node* n, unsigned int index) -> Node::SPtr {
413-
auto p = n->getParents();
414-
return static_cast<Node*>(p[index]);
415-
},
416-
[](const Node* n, const std::string& name) -> sofa::core::Base* {
417-
const auto& parents = n->getParents();
418-
return *std::find_if(parents.begin(),
419-
parents.end(),
420-
[name](BaseNode* child){ return child->getName() == name; });
421-
},
422-
[](Node*, unsigned int) {
423-
throw std::runtime_error("Removing a parent is not a supported operation. Please detach the node from the corresponding graph node.");
424-
});
438+
[](Node* n) -> size_t { return n->getNbParents(); },
439+
[](Node* n, unsigned int index) -> Node::SPtr {
440+
auto p = n->getParents();
441+
return static_cast<Node*>(p[index]);
442+
},
443+
[](const Node* n, const std::string& name) -> sofa::core::Base* {
444+
const auto& parents = n->getParents();
445+
return *std::find_if(parents.begin(),
446+
parents.end(),
447+
[name](BaseNode* child){ return child->getName() == name; });
448+
},
449+
[](Node*, unsigned int) {
450+
throw std::runtime_error("Removing a parent is not a supported operation. Please detach the node from the corresponding graph node.");
451+
});
425452
}
426453

427454
std::unique_ptr<NodeIterator> property_objects(Node* node)
428455
{
429456
return std::make_unique<NodeIterator>(node,
430-
[](Node* n) -> size_t { return n->object.size(); },
431-
[](Node* n, unsigned int index) -> Base::SPtr { return (n->object[index]);},
432-
[](const Node* n, const std::string& name) { return n->getObject(name); },
433-
[](Node* n, unsigned int index) { n->removeObject(n->object[index]);}
434-
);
457+
[](Node* n) -> size_t { return n->object.size(); },
458+
[](Node* n, unsigned int index) -> Base::SPtr { return (n->object[index]);},
459+
[](const Node* n, const std::string& name) { return n->getObject(name); },
460+
[](Node* n, unsigned int index) { n->removeObject(n->object[index]);}
461+
);
435462
}
436463

437-
py::object __getattr__(Node& self, const std::string& name)
464+
py::object __getattr__(py::object pyself, const std::string& name)
438465
{
466+
Node* selfnode = py::cast<Node*>(pyself);
439467
/// Search in the object lists
440-
BaseObject *object = self.getObject(name);
468+
BaseObject *object = selfnode->getObject(name);
441469
if (object)
442470
return PythonFactory::toPython(object);
443471

444472
/// Search in the child lists
445-
Node *child = self.getChild(name);
473+
Node *child = selfnode->getChild(name);
446474
if (child)
447475
return PythonFactory::toPython(child);
448476

449477
/// Search in the data & link lists
450-
return BindingBase::GetAttr(&self, name, true);
478+
py::object result = BindingBase::GetAttr(selfnode, name, false);
479+
if(!result.is_none())
480+
return result;
481+
482+
std::stringstream tmp;
483+
emitSpellingMessage(tmp, " - The data field named ", selfnode->getDataFields(), name, 2, 0.8);
484+
emitSpellingMessage(tmp, " - The link named ", selfnode->getDataFields(), name, 2, 0.8);
485+
emitSpellingMessage(tmp, " - The object named ", selfnode->getNodeObjects(), name, 2, 0.8);
486+
emitSpellingMessage(tmp, " - The child node named ", selfnode->getChildren(), name, 2, 0.8);
487+
488+
// Also provide spelling hints on python functions.
489+
emitSpellingMessage(tmp, " - The python attribute named ", py::cast<py::dict>(py::type::of(pyself).attr("__dict__")), name, 5, 0.8,
490+
[](const std::pair<py::handle, py::handle>& kv) { return py::cast<std::string>(std::get<0>(kv)); });
491+
492+
std::stringstream message;
493+
message << "Unable to find attribute: "+name;
494+
if(!tmp.str().empty())
495+
{
496+
message << msgendl;
497+
message << " You possibly wanted to access: " << msgendl;
498+
message << tmp.rdbuf();
499+
}
500+
throw pybind11::attribute_error(message.str());
451501
}
452502

453503
/// 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)
549599
self->propagateEvent(sofa::core::execparams::defaultInstance(), &event);
550600
}
551601

602+
}
603+
552604
void moduleAddNode(py::module &m) {
553605
/// Register the complete parent-child relationship between Base and Node to the pybind11
554606
/// typing system.

0 commit comments

Comments
 (0)