Salome HOME
[bos #35159][EDF] (2023-T1) Following commands in Python console. Fixed intermediate... ast/spv3d 5/head
authorKonstantin Leontev <Konstantin.LEONTEV@opencascade.com>
Mon, 4 Sep 2023 16:27:24 +0000 (17:27 +0100)
committerKonstantin Leontev <Konstantin.LEONTEV@opencascade.com>
Tue, 24 Oct 2023 16:50:09 +0000 (17:50 +0100)
doc/salome/gui/input/introduction_to_gui.rst
doc/salome/gui/input/working_with_python_console.rst [new file with mode: 0644]
src/SUIT/SUIT_Tools.cxx
src/SUIT/SUIT_Tools.h
src/SalomeApp/SalomeApp_Application.cxx
src/SalomeApp/SalomeApp_Application.h
tools/PyConsole/src/PyConsole_Editor.cxx

index 026866837749d687af0d2b19335691a83c003b9a..edd30b0baa3fb3a35c1b2860c341e529ad35522a 100644 (file)
@@ -62,7 +62,8 @@ input fields.
        using_notebook.rst
        themes.rst
        using_catalog_generator.rst
-       working_with_python_scripts.rst 
+       working_with_python_scripts.rst
+       working_with_python_console.rst
        using_registry_tool.rst
        viewers_chapter.rst
        setting_preferences.rst
diff --git a/doc/salome/gui/input/working_with_python_console.rst b/doc/salome/gui/input/working_with_python_console.rst
new file mode 100644 (file)
index 0000000..e0d84ff
--- /dev/null
@@ -0,0 +1,93 @@
+.. _python_console_page: 
+
+******************
+Python Console
+******************
+
+**Python console** - Window for Python interpreter. This window functions like a standard document:
+the pop-up menu invoked by right-click in this window gives access to **Copy/Paste/SelectAll/ClearAll** options.
+
+You can run a Python script in interactive mode by typing expressions line by line or by loading a Python file
+from the main menu **File -> Load Script**.
+
+==================
+Asynchronous mode
+==================
+
+By default the console is always initialized in asynchronous mode when the Python commands are executed in the separated thread
+that does not block the main GUI loop. So, you'll see any intermediate output from the running script even if the script didn't finish yet.
+
+In the synchronous mode each Python command is executed in the main GUI thread, then GUI is blocked until the command is finished.
+It could be an issue if you run a time consuming Python script, because you won't see any output till the end of execution.
+
+If you need to run a script in synchronous mode for whatever reason, set ``PYTHON_CONSOLE_SYNC`` environment variable before Salome start:
+
+.. code-block:: console
+
+    export PYTHON_CONSOLE_SYNC=1
+
+==================
+Tracing
+==================
+
+To output in console currently executed functions, we're adding tracing code to the start and at the end
+of the command to deactivate it immediately after execution.
+
+This mechanism is turned off by default. Set ``PYCONSOLE_TRACE`` before SALOME start to activate it:
+
+.. code-block:: console
+
+    export PYCONSOLE_TRACE=1
+
+If the tracing is on while the script is running we can see functions enter and return calls
+with ``>>`` and ``<<`` marks followed by line number and function name. Tracing function prints only calls to functions
+defined in the current script to prevent printing of thousands of lines for builtin functions in some cases.
+
+Example of a script with defined functions:
+
+.. code-block:: python
+    :linenos:
+
+    #!/usr/bin/env python
+
+    import time
+
+    def sum(x, y):
+        time.sleep(3)
+        return x + y
+        
+    def sub(x, y):
+        time.sleep(3)
+        return x - y
+        
+    def prod(x, y):
+        time.sleep(3)
+        return x * y
+        
+    def div(x, y):
+        time.sleep(3)
+        return x / y
+
+    x = 5
+    y = 2
+
+    sum(x, y)
+    sub(x, y)
+    prod(x, y)
+    div(x, y)
+
+And output in the Python console:
+
+.. code-block:: console
+
+    >>> sys.setprofile(lambda frame, event, arg: print('>>', frame.f_lineno, ': ', frame.f_code.co_name) if event == 'call' and frame.f_code.co_filename == '/home/function_calls.py' and frame.f_code.co_name != '<module>' else print('<<', frame.f_lineno, ': ', frame.f_code.co_name) if event == 'return' and frame.f_code.co_filename == '/home/function_calls.py' and frame.f_code.co_name != '<module>' else None); exec(compile(open('/home/function_calls.py', 'rb').read(), '/home/function_calls.py', 'exec')); sys.setprofile(None); 
+    >> 5 :  sum
+    << 7 :  sum
+    >> 9 :  sub
+    << 11 :  sub
+    >> 13 :  prod
+    << 15 :  prod
+    >> 17 :  div
+    << 19 :  div
+    >>>
+
index 03389703e328122aba7625b859e2f76978629eed..c5802ffbdf96e668eda1fef3e9b8cd2f5c7679a5 100644 (file)
@@ -26,6 +26,7 @@
 
 #include <stdio.h>
 #include <stdarg.h>
+#include <iostream>
 
 /*!
   Traces output to log-file.
@@ -77,3 +78,47 @@ void SUIT_Tools::centerWidget( QWidget* src, const QWidget* ref )
 {
   SUIT_Tools::alignWidget( src, ref, Qt::AlignCenter );
 }
+
+/*!
+  Add tracing code to given python command if it was activated by PYCONSOLE_TRACE env variable.
+  Immediately return if PYCONSOLE_TRACE wasn't set.
+*/
+void SUIT_Tools::addTraceToPythonCommand(const QString& fileName, QString& command)
+{
+  auto isPythonTraceEnabled = []() -> bool
+  {
+    const char* envVar = std::getenv("PYCONSOLE_TRACE");
+
+    if (envVar && (envVar[0] != '\0'))
+    {
+      try
+      {
+        const long long numValue = std::stoll(envVar);
+        return numValue > 0;
+      }
+      catch(const std::exception& e)
+      {
+        std::cerr << e.what() << '\n';
+      }
+    }
+
+    return false;
+  };
+
+  static const bool isActivated = isPythonTraceEnabled();
+  if (!isActivated)
+  {
+    return;
+  }
+
+  // Using sys.setprofile() instead of sys.settrace() because we don't need any other events except of 'call' and 'return'.
+  // Another reason: the trace function for sys.settrace() must return itself, so we can't use it properly with lambda.
+  command = QString("sys.setprofile(lambda frame, event, arg: "
+    "print('>>', frame.f_lineno, ': ', frame.f_code.co_name) if event == 'call' and frame.f_code.co_filename == '%1' and frame.f_code.co_name != '<module>' else "
+    "print('<<', frame.f_lineno, ': ', frame.f_code.co_name) if event == 'return' and frame.f_code.co_filename == '%1' and frame.f_code.co_name != '<module>' else "
+    "None); "
+    "%2; "
+    "sys.setprofile(None); ").
+    arg(fileName).
+    arg(command);
+}
index cc57dffd81990d8c715610fd45cc85b022db9d00..13b3ed9d30c9aed754f66520950bbd1943fe9b22 100644 (file)
@@ -44,6 +44,8 @@ public:
   static QString fontToString( const QFont& font );
 
   static void    centerWidget( QWidget* src, const QWidget* ref );
+
+  static void addTraceToPythonCommand(const QString& fileName, QString& command);
 };
 
 #endif
index 804a98dbd8cb93309e1ed033d0bc8a6014aa9e4a..a7046eaca0523675b8d361f7ca4c88e0f9d4c681 100644 (file)
@@ -534,28 +534,7 @@ void SalomeApp_Application::onUnloadDoc( bool ask )
 /*!SLOT. Create new study and load script*/
 void SalomeApp_Application::onNewWithScript()
 {
-  QStringList filtersList;
-  filtersList.append(tr("PYTHON_FILES_FILTER"));
-  filtersList.append(tr("ALL_FILES_FILTER"));
-
-  QString anInitialPath = "";
-  if ( SUIT_FileDlg::getLastVisitedPath().isEmpty() )
-    anInitialPath = QDir::currentPath();
-
-  QString aFile = SUIT_FileDlg::getFileName( desktop(), anInitialPath, filtersList, tr( "TOT_DESK_FILE_LOAD_SCRIPT" ), true, true );
-
-  if ( !aFile.isEmpty() )
-  {
-    onNewDoc();
-
-#ifndef DISABLE_PYCONSOLE
-    QString command = QString("exec(open(\"%1\", \"rb\").read())").arg(aFile);
-    PyConsole_Console* pyConsole = pythonConsole();
-    PropertyMgr propm( this, "IsLoadedScript", true );
-    if ( pyConsole )
-      pyConsole->exec( command );
-#endif
-  }
+  execScript(true);
 }
 
 
@@ -1010,26 +989,7 @@ void SalomeApp_Application::onLoadScript( )
     return;
   }
 
-  QStringList filtersList;
-  filtersList.append(tr("PYTHON_FILES_FILTER"));
-  filtersList.append(tr("ALL_FILES_FILTER"));
-
-  QString anInitialPath = "";
-  if ( SUIT_FileDlg::getLastVisitedPath().isEmpty() )
-    anInitialPath = QDir::currentPath();
-
-  QString aFile = SUIT_FileDlg::getFileName( desktop(), anInitialPath, filtersList, tr( "TOT_DESK_FILE_LOAD_SCRIPT" ), true, true );
-
-  if ( !aFile.isEmpty() )
-  {
-#ifndef DISABLE_PYCONSOLE
-    QString command = QString("exec(compile(open('%1', 'rb').read(), '%1', 'exec'))").arg(aFile);
-    PyConsole_Console* pyConsole = pythonConsole();
-    PropertyMgr propm( this, "IsLoadedScript", true );
-    if ( pyConsole )
-      pyConsole->exec(command);
-#endif
-  }
+  execScript(false);
 }
 
 /*!Private SLOT. On save GUI state.*/
@@ -2142,6 +2102,50 @@ PyConsole_Interp* SalomeApp_Application::createPyInterp()
 
 #endif // DISABLE_PYCONSOLE
 
+/*
+  Opens a file dialog to choose a python script.
+*/
+QString SalomeApp_Application::getScriptFileName()
+{
+  QStringList filtersList;
+  filtersList.append(tr("PYTHON_FILES_FILTER"));
+  filtersList.append(tr("ALL_FILES_FILTER"));
+
+  const QString anInitialPath =
+    SUIT_FileDlg::getLastVisitedPath().isEmpty() ? QDir::currentPath() : "";
+    
+  return SUIT_FileDlg::getFileName(desktop(), anInitialPath, filtersList, tr("TOT_DESK_FILE_LOAD_SCRIPT"), true, true);
+}
+
+/*
+  Execute script in python console.
+*/
+void SalomeApp_Application::execScript(bool isNewDoc)
+{
+  const QString aFile = getScriptFileName();
+  if (aFile.isEmpty())
+  {
+    return;
+  }
+
+  if (isNewDoc)
+  {
+    onNewDoc();
+  }
+
+#ifndef DISABLE_PYCONSOLE
+  PyConsole_Console* pyConsole = pythonConsole();
+  PropertyMgr propm(this, "IsLoadedScript", true);
+  if (pyConsole)
+  {
+    QString command = QString("exec(compile(open('%1', 'rb').read(), '%1', 'exec'))").arg(aFile);
+    SUIT_Tools::addTraceToPythonCommand(aFile, command);
+
+    pyConsole->exec(command);
+  }
+#endif
+}
+
 void SalomeApp_Application::ensureShaperIsActivated()
 {
   SalomeApp_Study* study = dynamic_cast<SalomeApp_Study*>(activeStudy());
index f94e11b9c66d63aeb80b3e535617b91687f6839d..e58dd883b77226269959af85e3815a7024fb1852 100644 (file)
@@ -199,6 +199,9 @@ private:
   void                                createExtraActions();
   void                                ensureShaperIsActivated();
 
+  QString                             getScriptFileName();
+  void                                execScript(bool isNewDoc);
+
 private:
 #ifndef DISABLE_PYCONSOLE
   QPointer<SalomeApp_NoteBook>        myNoteBook;        // Notebook instance
index 3c597e7b620176c20c980b59f50f98b381f3acfd..e0e7f4a1a511c736bf2995c83eddabb78f67f6d0 100644 (file)
@@ -183,7 +183,7 @@ void PyConsole_Editor::init()
   myCmdInHistory = -1;
   myEventLoop = 0;
   myShowBanner = true;
-  myIsSync = true;
+  myIsSync = false;
   myIsSuppressOutput = false;
   myMultiLinePaste = false;
   myAutoCompletion = false;