From: Konstantin Leontev Date: Mon, 4 Sep 2023 16:27:24 +0000 (+0100) Subject: [bos #35159][EDF] (2023-T1) Following commands in Python console. Fixed intermediate... X-Git-Url: http://git.salome-platform.org/gitweb/?a=commitdiff_plain;h=refs%2Fheads%2Fkleontev%2F35159_Following_commands_in_Python_console;p=modules%2Fgui.git [bos #35159][EDF] (2023-T1) Following commands in Python console. Fixed intermediate print, added tracing for functions calls, added documentation. --- diff --git a/doc/salome/gui/input/introduction_to_gui.rst b/doc/salome/gui/input/introduction_to_gui.rst index 026866837..edd30b0ba 100644 --- a/doc/salome/gui/input/introduction_to_gui.rst +++ b/doc/salome/gui/input/introduction_to_gui.rst @@ -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 index 000000000..a9426655e --- /dev/null +++ b/doc/salome/gui/input/working_with_python_console.rst @@ -0,0 +1,99 @@ +.. _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 synchronous mode. It means that each Python command is executed in the main GUI thread, +then GUI is blocked until the command is finished. It was done this way to prevent crashes of PARAVIS scripts, +when VTK does not run in the main thread. + +However, it could be an issue if you run a time consuming Python script, because you won't see any output till the end of the execution. + +In this case you can switch on the asynchronous mode, when the Python commands are executed in the separated thread, +that doesn't block the main GUI loop. So, you'll see any intermediate output from the running script even if the script didn't finish yet. + +To switch asynchronous mode on, set ``PYTHON_CONSOLE_SYNC`` environment variable to any value less or equal to 0 before Salome start: + +.. code-block:: console + + export PYTHON_CONSOLE_SYNC=0 + +To return back to synchronous mode, set ``PYTHON_CONSOLE_SYNC`` to any value greater than 0. Currently undefined ``PYTHON_CONSOLE_SYNC`` +switches to sync mode as well for compatibility reasons. + +================== +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 != '' 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 != '' 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 + >>> + diff --git a/src/SUIT/SUIT_Tools.cxx b/src/SUIT/SUIT_Tools.cxx index 03389703e..c5802ffbd 100644 --- a/src/SUIT/SUIT_Tools.cxx +++ b/src/SUIT/SUIT_Tools.cxx @@ -26,6 +26,7 @@ #include #include +#include /*! 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 != '' 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 != '' else " + "None); " + "%2; " + "sys.setprofile(None); "). + arg(fileName). + arg(command); +} diff --git a/src/SUIT/SUIT_Tools.h b/src/SUIT/SUIT_Tools.h index cc57dffd8..13b3ed9d3 100644 --- a/src/SUIT/SUIT_Tools.h +++ b/src/SUIT/SUIT_Tools.h @@ -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 diff --git a/src/SalomeApp/SalomeApp_Application.cxx b/src/SalomeApp/SalomeApp_Application.cxx index 804a98dbd..a7046eaca 100644 --- a/src/SalomeApp/SalomeApp_Application.cxx +++ b/src/SalomeApp/SalomeApp_Application.cxx @@ -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(activeStudy()); diff --git a/src/SalomeApp/SalomeApp_Application.h b/src/SalomeApp/SalomeApp_Application.h index f94e11b9c..e58dd883b 100644 --- a/src/SalomeApp/SalomeApp_Application.h +++ b/src/SalomeApp/SalomeApp_Application.h @@ -199,6 +199,9 @@ private: void createExtraActions(); void ensureShaperIsActivated(); + QString getScriptFileName(); + void execScript(bool isNewDoc); + private: #ifndef DISABLE_PYCONSOLE QPointer myNoteBook; // Notebook instance diff --git a/tools/PyConsole/src/PyConsole_Console.cxx b/tools/PyConsole/src/PyConsole_Console.cxx index d2a553e04..925bfbc3e 100644 --- a/tools/PyConsole/src/PyConsole_Console.cxx +++ b/tools/PyConsole/src/PyConsole_Console.cxx @@ -93,8 +93,14 @@ void PyConsole_Console::init( PyConsole_Editor* editor ) lay->addWidget( myEditor ); // force synchronous mode + // By default we set PyConsole_Editor::myIsSync = false, so we have async as default mode. + // But in previous versions of Salome we used sync mode by default because of our testing setup + // where VTK must run in the main thread (see assert(vtkGarbageCollectorIsMainThread())). + // So, here for compatibility reasons we consider undefined PYTHON_CONSOLE_SYNC as true as well. + // One who needs to run scripts in async mode to see intermediate output during execution, + // must expicitly set PYTHON_CONSOLE_SYNC=0 before Salome start. QString synchronous = qgetenv( "PYTHON_CONSOLE_SYNC" ); - if ( !synchronous.isEmpty() && synchronous.toInt() > 0 ) + if (synchronous.isEmpty() || synchronous.toInt() > 0) setIsSync( true ); // create actions diff --git a/tools/PyConsole/src/PyConsole_Editor.cxx b/tools/PyConsole/src/PyConsole_Editor.cxx index 3c597e7b6..e0e7f4a1a 100644 --- a/tools/PyConsole/src/PyConsole_Editor.cxx +++ b/tools/PyConsole/src/PyConsole_Editor.cxx @@ -183,7 +183,7 @@ void PyConsole_Editor::init() myCmdInHistory = -1; myEventLoop = 0; myShowBanner = true; - myIsSync = true; + myIsSync = false; myIsSuppressOutput = false; myMultiLinePaste = false; myAutoCompletion = false;