Browse Source

ENH: ctkAbstractPythonManager: Update autocompletion to support callable with arguments

The following cases are now supported:

* d.foo_class().instantiate_bar().bar_maths(5).math_instance_member  (returns 5)
* d.foo_class().instantiate_bar().bar_maths(7).math_instance_member  (returns 7)

Updates the test file PythonAttributes-test.py with:
 - New class to test multiple arguments
 - Update of Maths() class to accept attribute

Adds ctkPythonConsole::searchUsableCharForCompletion() function

Co-authored-by: Jean-Christophe Fillion-Robin <jchris.fillionr@kitware.com>
Mayeul Chassagnard 8 years ago
parent
commit
f5611e8bae

+ 8 - 4
Libs/Scripting/Python/Core/Testing/Cpp/PythonAttributes-test.py

@@ -3,12 +3,17 @@ class Maths(object):
 
   MATHS_CLASS_MEMBER=0.1
 
-  def __init__(self,num):
-    self.math_instance_member = num
+  def __init__(self, num):
+    self.maths_instance_member = num
 
   def maths_instance_method(self):
     print("Hello from instance method")
 
+class MultipleArg(object):
+  def __init__(self, num, str, other = 0):
+    self.multipleArg_instance_member_num = num + other
+    self.multipleArg_instance_member_str = str
+    self.multipleArg_instance_member_other = other
 
 class Bar(object):
 
@@ -20,14 +25,13 @@ class Bar(object):
   def bar_instance_method(self):
     print("Hello from instance method")
 
-  def bar_maths(self,num):
+  def bar_maths(self, num = 0):
     return Maths(num)
 
   @staticmethod
   def bar_class_method():
     print("Hello from class method")
 
-
 class Foo(object):
 
   FOO_CLASS_MEMBER = 1

+ 21 - 0
Libs/Scripting/Python/Core/Testing/Cpp/ctkAbstractPythonManagerTest.cpp

@@ -316,6 +316,27 @@ void ctkAbstractPythonManagerTester::testPythonAttributes_data()
                          << "bar_instance_member"
                          << "bar_instance_method");
 
+  QTest::newRow("d.foo_class().instantiate_bar().bar_maths(5)")
+                     << "d.foo_class().instantiate_bar().bar_maths(5)"
+                     << (QStringList()
+                         << "MATHS_CLASS_MEMBER"
+                         << "maths_instance_member" // TODO: verify result is 5
+                         << "maths_instance_method");
+
+  QTest::newRow("MultipleArg( 5 + 5 , '(')")
+                     << "MultipleArg( 5 + 5 , '(')"
+                     << (QStringList()
+                         << "multipleArg_instance_member_num" // TODO: verify result is 10
+                         << "multipleArg_instance_member_str" // TODO: verify result is '('
+                         << "multipleArg_instance_member_other"); // TODO: verify result is 0
+
+  QTest::newRow("MultipleArg( 5 % 5 + 1, '\"', 0.1)")
+                     << "MultipleArg( 5 + 5 , '\"', 0.1)"
+                     << (QStringList()
+                         << "multipleArg_instance_member_num" // TODO: verify result is 1.1
+                         << "multipleArg_instance_member_str" // TODO: verify result is '"'
+                         << "multipleArg_instance_member_other"); // TODO: verify result is 0.1
+
 }
 
 // ----------------------------------------------------------------------------

+ 57 - 3
Libs/Scripting/Python/Core/ctkAbstractPythonManager.cpp

@@ -375,6 +375,56 @@ QStringList ctkAbstractPythonManager::dir_object(PyObject* object,
   return results;
 }
 
+QStringList ctkAbstractPythonManager::splitByDotOutsideParenthesis(const QString& pythonVariableName)
+{
+  QStringList tmpNames;
+  int last_pos_dot = pythonVariableName.length();
+  int numberOfParenthesisClosed = 0;
+  bool betweenSingleQuotes = false;
+  bool betweenDoubleQuotes = false;
+  for (int i = pythonVariableName.length()-1; i >= 0; --i)
+    {
+    QChar c = pythonVariableName.at(i);
+    if (c == '\'' && !betweenDoubleQuotes)
+      {
+      betweenSingleQuotes = !betweenSingleQuotes;
+      }
+    if (c == '"' && !betweenSingleQuotes)
+      {
+      betweenDoubleQuotes = !betweenDoubleQuotes;
+      }
+    // note that we must not count parenthesis if they are between quote...
+    if (!betweenSingleQuotes && !betweenDoubleQuotes)
+      {
+      if (c == '(')
+        {
+        if (numberOfParenthesisClosed>0)
+          {
+          numberOfParenthesisClosed--;
+          }
+        }
+      if (c == ')')
+        {
+        numberOfParenthesisClosed++;
+        }
+      }
+    // if we are outside parenthesis and we find a dot, then split
+    if ((c == '.' && numberOfParenthesisClosed<=0)
+        || i == 0)
+      {
+      if (i == 0) {i--;} // last case where we have to split the begging this time
+      QString textToSplit = pythonVariableName.mid(i+1,last_pos_dot-(i+1));
+      if (!textToSplit.isEmpty())
+        {
+        tmpNames.push_front(textToSplit);
+        }
+      last_pos_dot =i;
+      }
+    }
+  return tmpNames;
+}
+
+
 //----------------------------------------------------------------------------
 QStringList ctkAbstractPythonManager::pythonAttributes(const QString& pythonVariableName,
                                                        const QString& module,
@@ -420,7 +470,11 @@ QStringList ctkAbstractPythonManager::pythonAttributes(const QString& pythonVari
 
   if (!pythonVariableName.isEmpty())
     {
-    QStringList tmpNames = pythonVariableName.split('.');
+    // Split the pythonVariableName at every dot
+    // /!\ // CAREFUL to don't take dot which are between parenthesis
+    // To avoid the problem: split by dots in a smarter way!
+    QStringList tmpNames = splitByDotOutsideParenthesis(pythonVariableName);
+
     for (int i = 0; i < tmpNames.size() && object; ++i)
       {
       // fill the line step by step
@@ -430,9 +484,9 @@ QStringList ctkAbstractPythonManager::pythonAttributes(const QString& pythonVari
       line_code.append(".");
 
       QByteArray tmpName = tmpNames.at(i).toLatin1();
-      if (tmpName.contains("()")) // TODO: Make it work for arguments
+      if (tmpName.contains('(') && tmpName.contains(')'))
         {
-        tmpNames[i].remove("()");
+        tmpNames[i] = tmpNames[i].left(tmpName.indexOf('('));
         tmpName = tmpNames.at(i).toLatin1();
 
         // Attempt to instantiate the associated python class

+ 5 - 0
Libs/Scripting/Python/Core/ctkAbstractPythonManager.h

@@ -110,6 +110,11 @@ public:
   static QStringList dir_object(PyObject* object,
                                 bool appendParenthesis = false);
 
+  /// Given a python variable name, it returns the string list splited
+  /// at every dots which will be outside parenthesis
+  /// (It also takes care about the possibility that quotes can include parenthesis)
+  static QStringList splitByDotOutsideParenthesis(const QString& pythonVariableName);
+
   /// Given a python variable name, if it can be called, try to call the method or instantiate the class,
   /// lookup its attributes and return them in a string list.
   /// By default the attributes are looked up from \c __main__.

+ 52 - 24
Libs/Scripting/Python/Widgets/ctkPythonConsole.cpp

@@ -86,6 +86,8 @@ public:
 
   virtual int cursorOffset(const QString& completion);
   virtual void updateCompletionModel(const QString& completion);
+  static QString searchUsableCharForCompletion(const QString& completion);
+
 
 protected:
   bool isInUserDefinedClass(const QString &pythonFunctionPath);
@@ -264,39 +266,47 @@ int ctkPythonConsoleCompleter::parameterCountFromDocumentation(const QString& py
   return parameterCount;
 }
 
-void ctkPythonConsoleCompleter::updateCompletionModel(const QString& completion)
+//----------------------------------------------------------------------------
+QString ctkPythonConsoleCompleter::searchUsableCharForCompletion(const QString& completion)
 {
-  // Start by clearing the model
-  this->setModel(0);
-
-  // Don't try to complete the empty string
-  if (completion.isEmpty())
-    {
-    return;
-    }
-
-  bool appendParenthesis = true;
-
-  int numeberOfParenthesisClosed = 0;
+  bool betweenSingleQuotes = false;
+  bool betweenDoubleQuotes = false;
+  int numberOfParenthesisClosed = 0;
   // Search backward through the string for usable characters
   QString textToComplete;
   for (int i = completion.length()-1; i >= 0; --i)
     {
     QChar c = completion.at(i);
-    if (c.isLetterOrNumber() || c == '.' || c == '_' || c == '(' || c == ')' || c.isSymbol() || c.isPunct() || c.isSpace())
+    if (c == '\'' && !betweenDoubleQuotes)
+      {
+      betweenSingleQuotes = !betweenSingleQuotes;
+      }
+    if (c == '"' && !betweenSingleQuotes)
+      {
+      betweenDoubleQuotes = !betweenDoubleQuotes;
+      }
+    // Stop the completion if c is not a letter,number,.,_,(,) and outside parenthesis
+    if (c.isLetterOrNumber() || c == '.' || c == '_' || c == '(' || c == ')'
+        || numberOfParenthesisClosed)
       {
-      if (c == '(')
+      // Keep adding caractere to the completion if
+      // the number of '(' is always <= to the number of ')'
+      // note that we must not count parenthesis if they are between quote...
+      if (!betweenSingleQuotes && !betweenDoubleQuotes)
         {
-        if (numeberOfParenthesisClosed>0)
+        if (c == '(')
           {
-          numeberOfParenthesisClosed--;
+          if (numberOfParenthesisClosed>0)
+            {
+            numberOfParenthesisClosed--;
+            }
+          else
+            break; // stop to prepend
+          }
+        if (c == ')')
+          {
+          numberOfParenthesisClosed++;
           }
-        else
-          break; // stop to prepend
-        }
-      if (c == ')')
-        {
-        numeberOfParenthesisClosed++;
         }
       textToComplete.prepend(c);
       }
@@ -304,7 +314,25 @@ void ctkPythonConsoleCompleter::updateCompletionModel(const QString& completion)
       {
       break;
       }
-   }
+    }
+  return textToComplete;
+}
+
+//----------------------------------------------------------------------------
+void ctkPythonConsoleCompleter::updateCompletionModel(const QString& completion)
+{
+  // Start by clearing the model
+  this->setModel(0);
+
+  // Don't try to complete the empty string
+  if (completion.isEmpty())
+    {
+    return;
+    }
+
+  bool appendParenthesis = true;
+  // Search backward through the string for usable characters
+  QString textToComplete = searchUsableCharForCompletion(completion);
 
   // Split the string at the last dot, if one exists
   QString lookup;