Bladeren bron

Ensure ctkAbstractPythonManager::executeFile set __file__

The code has also been refactored to ensure sys.path is cleaned in case of
error.

Tests have been added.

A new method named "resetErrorFlag" has been added to both CTK and PythonQt.
This method should be called before any piece of code that ends up
calling "handleError" in case of error. It ensures that the errorOccured
method won't return True when it is not expected.

See #244
Jean-Christophe Fillion-Robin 12 jaren geleden
bovenliggende
commit
8ab990c433

+ 1 - 1
CMakeExternals/PythonQt.cmake

@@ -53,7 +53,7 @@ if(${add_project})
         message(FATAL_ERROR "error: Python is required to build ${PROJECT_NAME}")
       endif()
 
-      set(revision_tag 2114405a47836b3fb16a3f66fec6a02184f32e71)
+      set(revision_tag a386dc60f71c15e67c611bc31b26cee756ed833a)
       if(${proj}_REVISION_TAG)
         set(revision_tag ${${proj}_REVISION_TAG})
       endif()

+ 42 - 1
Libs/Scripting/Python/Core/Testing/Cpp/ctkAbstractPythonManagerTest.cpp

@@ -1,4 +1,8 @@
 
+// Qt includes
+#include <QTemporaryFile>
+#include <QTextStream>
+
 // CTK includes
 #include "ctkAbstractPythonManager.h"
 #include "ctkTest.h"
@@ -43,7 +47,8 @@ private Q_SLOTS:
   void testExecuteString();
   void testExecuteString_data();
 
-  //void testExecuteFile(); // TODO
+  void testExecuteFile();
+  void testExecuteFile_data();
 
   //void testPythonAttributes(); // TODO
 };
@@ -165,5 +170,41 @@ void ctkAbstractPythonManagerTester::testExecuteString_data()
 }
 
 // ----------------------------------------------------------------------------
+void ctkAbstractPythonManagerTester::testExecuteFile()
+{
+  QFETCH(QString, stringToExecute);
+  QFETCH(bool, pythonErrorExpected);
+
+  QTemporaryFile pythonFile("testExecuteFile-XXXXXX.py");
+  QVERIFY(pythonFile.open());
+  QTextStream out(&pythonFile);
+  out << stringToExecute;
+  pythonFile.close();
+
+  this->PythonManager.executeFile(pythonFile.fileName());
+
+  QCOMPARE(this->PythonManager.pythonErrorOccured(), pythonErrorExpected);
+}
+
+// ----------------------------------------------------------------------------
+void ctkAbstractPythonManagerTester::testExecuteFile_data()
+{
+  QTest::addColumn<QString>("stringToExecute");
+  QTest::addColumn<bool>("pythonErrorExpected");
+
+  QTest::newRow("0-emptyfile") << QString("")
+                     << false;
+
+  QTest::newRow("1-helloworld") << QString("print 'Hello world'")
+                     << false;
+
+  QTest::newRow("2-syntaxerror") << QString("print '") // SyntaxError
+                     << true;
+
+  QTest::newRow("3-check __file__ attribute") << QString("print 'This file is: %s' % __file__")
+                     << false;
+}
+
+// ----------------------------------------------------------------------------
 CTK_TEST_MAIN(ctkAbstractPythonManagerTest)
 #include "moc_ctkAbstractPythonManagerTest.cpp"

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

@@ -187,6 +187,12 @@ bool ctkAbstractPythonManager::pythonErrorOccured()const
 }
 
 //-----------------------------------------------------------------------------
+void ctkAbstractPythonManager::resetErrorFlag()
+{
+  PythonQt::self()->resetErrorFlag();
+}
+
+//-----------------------------------------------------------------------------
 QStringList ctkAbstractPythonManager::pythonPaths()
 {
   return QStringList();
@@ -249,9 +255,24 @@ void ctkAbstractPythonManager::executeFile(const QString& filename)
   if (main)
     {
     QString path = QFileInfo(filename).absolutePath();
-    this->executeString(QString("import sys\nsys.path.insert(0, '%1')").arg(path));
-    this->executeString(QString("execfile('%1')").arg(filename));
-    this->executeString(QString("import sys\nif sys.path[0] == '%1': sys.path.pop(0)").arg(path));
+    // See http://nedbatchelder.com/blog/200711/rethrowing_exceptions_in_python.html
+    QStringList code = QStringList()
+        << "import sys"
+        << QString("sys.path.insert(0, '%1')").arg(path)
+        << "_updated_globals = globals()"
+        << QString("_updated_globals['__file__'] = '%1'").arg(filename)
+        << "_ctk_executeFile_exc_info = None"
+        << "try:"
+        << QString("    execfile('%1', _updated_globals)").arg(filename)
+        << "except Exception, e:"
+        << "    _ctk_executeFile_exc_info = sys.exc_info()"
+        << "finally:"
+        << "    del _updated_globals"
+        << QString("    if sys.path[0] == '%1': sys.path.pop(0)").arg(path)
+        << "    if _ctk_executeFile_exc_info:"
+        << "        raise _ctk_executeFile_exc_info[1], None, _ctk_executeFile_exc_info[2]";
+    this->executeString(code.join("\n"));
+    //PythonQt::self()->handleError(); // Clear errorOccured flag
     }
 }
 

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

@@ -110,6 +110,10 @@ public:
   /// \sa PythonQt::errorOccured()
   bool pythonErrorOccured()const;
 
+  /// Reset error flag
+  /// \sa PythonQt::resetErrorFlag()
+  void resetErrorFlag();
+
 Q_SIGNALS:
 
   /// This signal is emitted after python is pre-initialized. Observers can listen