HAL
python_context.cpp
Go to the documentation of this file.
2 
3 #include "gui/gui_globals.h"
11 #include "hal_core/utilities/log.h"
13 
14 #include <QDir>
15 #include <QDebug>
16 #include <QInputDialog>
17 #include <QApplication>
18 #include <QFileDialog>
19 #include <fstream>
21 
22 // Following is needed for PythonContext::checkCompleteStatement
23 #include "hal_config.h"
24 
25 #include <Python.h>
26 #include <compile.h>
27 #include <errcode.h>
28 
29 #if PY_VERSION_HEX < 0x030900a0 // Python 3.9.0
30 
31 #include <grammar.h>
32 #include <node.h>
33 #include <parsetok.h>
34 extern grammar _PyParser_Grammar;
35 
36 #endif
37 
38 namespace hal
39 {
40  PythonContext::PythonContext(QObject *parent)
41  : QObject(parent), mContext(nullptr), mSender(nullptr), mTriggerReset(false), mThread(nullptr),
42  mThreadAborted(false), mInterpreterCaller(nullptr)
43  {
44  py::initialize_interpreter();
45  initPython();
46 
47  // The Python interpreter is not thread safe, so it implements an internal
48  // Global Interpreter Lock that means only one thread can actively be
49  // executing Python code at a time (though the interpreter can switch between
50  // threads, for example if one thread is blocked for I/O).
51  // Take care to always handle the GIL correctly, or you will cause deadlocks,
52  // weird issues with threads that are running but Python believes that they
53  // don't exist, etc.
54  // DO NOT EVER run Python code while not holding the GIL!
55  // Wherever possible, use the PyGILState_Ensure / PyGILState_Release API to
56  // acquire/release the GIL.
57 
58  // We must release the GIL, otherwise any spawning thread will deadlock.
59  mMainThreadState = PyEval_SaveThread();
60 
61  }
62 
63  PythonContext::~PythonContext()
64  {
65  PyEval_RestoreThread(mMainThreadState);
66 
67  closePython();
68  py::finalize_interpreter();
69  }
70 
71  void PythonContext::setConsole(PythonConsole* c)
72  {
73  mConsole = c; // may be nullptr
74  if (c)
75  {
76  connect(mConsole,&PythonConsole::inputReceived,this,&PythonContext::handleConsoleInputReceived);
77  }
78  }
79 
80  PythonThread* PythonContext::pythonThread() const
81  {
82  return mThread;
83  }
84 
85  void PythonContext::abortThread()
86  {
87  if (!mThread) return;
88  mThreadAborted = true;
89  mThread->interrupt();
90  }
91 
92  void PythonContext::abortThreadAndWait()
93  {
94  if (!mThread) return;
95  abortThread();
96  mThread->wait();
97  }
98 
99  void PythonContext::initializeContext(py::dict* context)
100  {
101  // GIL must be held
102 
103  std::string command = "import __main__\n"
104  "import io, sys, threading, builtins\n";
105  for (auto path : utils::get_plugin_directories())
106  {
107  command += "sys.path.append('" + path.string() + "')\n";
108  }
109  command += "sys.path.append('" + utils::get_library_directory().string()
110  + "')\n"
111  "from hal_gui.console import reset\n"
112  "from hal_gui.console import clear\n"
113  "class StdOutCatcher(io.TextIOBase):\n"
114  " def __init__(self):\n"
115  " pass\n"
116  " def write(self, stuff):\n"
117  " from hal_gui import console\n"
118  " if threading.current_thread() is threading.main_thread():\n"
119  " console.redirector.write_stdout(stuff)\n"
120  " else:\n"
121  " console.redirector.thread_stdout(stuff)\n"
122  "class StdErrCatcher(io.TextIOBase):\n"
123  " def __init__(self):\n"
124  " pass\n"
125  " def write(self, stuff):\n"
126  " from hal_gui import console\n"
127  " if threading.current_thread() is threading.main_thread():\n"
128  " console.redirector.write_stderr(stuff)\n"
129  " else:\n"
130  " console.redirector.thread_stderr(stuff)\n"
131  "def redirect_input(prompt=\"Please enter input:\"):\n"
132  " if threading.current_thread() is threading.main_thread():\n"
133  " return \"input in main thread not supported\"\n"
134  " else:\n"
135  " return console.redirector.thread_stdin(prompt)\n"
136  "sys.stdout = StdOutCatcher()\n"
137  "sys.__stdout__ = sys.stdout\n"
138  "sys.stderr = StdErrCatcher()\n"
139  "sys.__stderr__ = sys.stderr\n"
140  "builtins.input = redirect_input\n"
141  "builtins.raw_input = redirect_input\n"
142  "import hal_py\n";
143 
144  py::exec(command, *context, *context);
145 
146  (*context)["netlist"] = gNetlistOwner; // assign the shared_ptr here, not the raw ptr
147 
148  if (gGuiApi)
149  {
150  (*context)["gui"] = gGuiApi;
151  }
152  }
153 
154  void PythonContext::initializeScript(py::dict *context)
155  {
156  // GIL must be held
157 
158  std::string command = "import __main__\n"
159  "import io, sys, threading\n"
160  "from hal_gui.console import reset\n"
161  "from hal_gui.console import clear\n"
162  "import hal_py\n";
163 
164  py::exec(command, *context, *context);
165 
166  (*context)["netlist"] = gNetlistOwner; // assign the shared_ptr here, not the raw ptr
167 
168  if (gGuiApi)
169  {
170  (*context)["gui"] = gGuiApi;
171  }
172  }
173 
174 
175  void PythonContext::initPython()
176  {
177  // GIL must be held
178 
179  // using namespace py::literals;
180 
181  new py::dict();
182  mContext = new py::dict(**py::globals());
183 
184  initializeContext(mContext);
185  (*mContext)["console"] = py::module::import("hal_gui.console");
186  (*mContext)["hal_gui"] = py::module::import("hal_gui");
187  }
188 
189  void PythonContext::closePython()
190  {
191  // GIL must be held
192  delete mContext;
193  mContext = nullptr;
194  }
195 
196  void PythonContext::interpretBackground(QObject* caller, const QString& input, bool multiple_expressions)
197  {
198  if (input.isEmpty())
199  {
200  return;
201  }
202 
203  if (input == "quit()")
204  {
205  forwardError("quit() cannot be used in this interpreter. Use console.reset() to restart it.\n");
206  return;
207  }
208 
209  if (input == "help()")
210  {
211  forwardError("help() cannot be used in this interpreter.\n");
212  return;
213  }
214 
215  if (input == "license()")
216  {
217  forwardError("license() cannot be used in this interpreter.\n");
218  return;
219  }
220 
221  if (mThread)
222  {
223  forwardError("python thread already running. Please retry after other thread finished.\n");
224  return;
225  }
226  log_info("python", "Python console execute: \"{}\".", input.toStdString());
227 
228  mInterpreterCaller = caller;
229  startThread(input,!multiple_expressions);
230  }
231 
232  void PythonContext::startThread(const QString& input, bool singleStatement)
233  {
234  mThread = new PythonThread(input,singleStatement,this);
235  connect(mThread,&QThread::finished,this,&PythonContext::handleThreadFinished);
236  connect(mThread,&PythonThread::stdOutput,this,&PythonContext::handleScriptOutput);
237  connect(mThread,&PythonThread::stdError,this,&PythonContext::handleScriptError);
238  connect(mThread,&PythonThread::requireInput,this,&PythonContext::handleInputRequired);
239  if (mConsole) mConsole->setReadOnly(true);
240  mLayoutLocker = new LayoutLocker;
241  mThread->start();
242  }
243 
244  void PythonContext::interpretForeground(const QString &input)
245  {
246  if (input.isEmpty())
247  {
248  return;
249  }
250 
251  log_info("python", "Python console execute: \"{}\".", input.toStdString());
252 
253  // since we've released the GIL in the constructor, re-acquire it here before
254  // running some Python code on the main thread
255  PyGILState_STATE state = PyGILState_Ensure();
256  //PyEval_RestoreThread(mMainThreadState);
257 
258  // TODO should the console also be moved to threads? Maybe actually catch Ctrl+C there
259  // as a method to interrupt? Currently you can hang the GUI by running an endless loop
260  // from the console.
261 
262  try
263  {
264  pybind11::object rc;
265  rc = py::eval<py::eval_single_statement>(input.toStdString(), *mContext, *mContext);
266  if (!rc.is_none())
267  {
268  forwardStdout(QString::fromStdString(py::str(rc).cast<std::string>()));
269  }
270  handleReset();
271  }
272  catch (py::error_already_set& e)
273  {
274  qDebug() << "interpret error set";
275  forwardError(QString::fromStdString(std::string(e.what())));
276  e.restore();
277  PyErr_Clear();
278  }
279  catch (std::exception& e)
280  {
281  qDebug() << "interpret exception";
282  forwardError(QString::fromStdString(std::string(e.what())));
283  }
284 
285  // make sure we release the GIL, otherwise we interfere with threading
286  PyGILState_Release(state);
287  //mMainThreadState = PyEval_SaveThread();
288 
289  }
290 
291  void PythonContext::interpretScript(QObject* caller, const QString& input)
292  {
293  if (mThread)
294  {
295  log_warning("python", "Not executed, python script already running");
296  return;
297  }
298 
299  //log_info("python", "Python editor execute script:\n{}\n", input.toStdString());
300 #ifdef HAL_STUDY
301  log_info("UserStudy", "Python editor execute script:\n{}\n", input.toStdString());
302 #endif
303  forwardStdout("\n");
304  forwardStdout("<Execute Python Editor content>");
305  forwardStdout("\n");
306 
307  mInterpreterCaller = caller;
308  startThread(input,false);
309  }
310 
311  void PythonContext::handleScriptOutput(const QString& txt)
312  {
313  if (!txt.isEmpty())
314  forwardStdout(txt);
315  }
316 
317  void PythonContext::handleScriptError(const QString& txt)
318  {
319  if (!txt.isEmpty())
320  forwardError(txt);
321  }
322 
323  void PythonContext::handleInputRequired(int type, const QString& prompt, const QVariant &defaultValue)
324  {
325  bool confirm;
326  if (mThread && !mThread->stdoutBuffer().isEmpty())
327  mConsole->handleStdout(mThread->flushStdout());
328 
329  switch (type) {
330  case PythonThread::ConsoleInput:
331  mConsole->handleStdout(prompt + "\n");
332  mConsole->setInputMode(true);
333  mConsole->displayPrompt();
334  mConsole->setReadOnly(false);
335  break;
336  case PythonThread::StringInput:
337  {
338  QString userInput = QInputDialog::getText(qApp->activeWindow(), "Python Script Input", prompt, QLineEdit::Normal, defaultValue.toString(), &confirm);
339  if (!confirm) userInput.clear();
340  if (mThread) mThread->setInput(userInput);
341  break;
342  }
343  case PythonThread::NumberInput:
344  {
345  int userInput = QInputDialog::getInt(qApp->activeWindow(), "Python Script Input", prompt, defaultValue.toInt());
346  if (mThread) mThread->setInput(userInput);
347  break;
348  }
349  case PythonThread::GateInput:
350  {
351  QSet<u32> gats;
352  for (const Gate* g : gNetlist->get_gates()) gats.insert(g->get_id());
354  PythonGateSelectionReceiver* pgs = new PythonGateSelectionReceiver(mThread,this);
355  GateDialog gd(gats, prompt, pgs, qApp->activeWindow());
356  Gate* gatSelect = (gd.exec() == QDialog::Accepted)
357  ? gNetlist->get_gate_by_id(gd.selectedId())
358  : nullptr;
359  if (!gd.pickerModeActivated() && mThread) mThread->setInput(QVariant::fromValue<void*>(gatSelect));
360  break;
361  }
362  case PythonThread::ModuleInput:
363  {
364  QSet<u32> mods;
366  PythonModuleSelectionReceiver* pms = new PythonModuleSelectionReceiver(mThread,this);
367  ModuleDialog md({}, prompt, false, pms, qApp->activeWindow());
368  Module* modSelect = (md.exec() == QDialog::Accepted)
369  ? gNetlist->get_module_by_id(md.selectedId())
370  : nullptr;
371  if (!md.pickerModeActivated() && mThread) mThread->setInput(QVariant::fromValue<void*>(modSelect));
372  break;
373  }
374  case PythonThread::FilenameInput:
375  {
376  QString userInput = QFileDialog::getOpenFileName(qApp->activeWindow(),prompt,".",defaultValue.toString());
377  if (mThread) mThread->setInput(userInput);
378  break;
379  }
380  default:
381  break;
382  }
383  }
384 
385  void PythonGateSelectionReceiver::handleGatesPicked(const QSet<u32>& gats)
386  {
387  Gate* gatSelect = gats.isEmpty()
388  ? nullptr
389  : gNetlist->get_gate_by_id(*gats.constBegin());
390  if (mThread) mThread->setInput(QVariant::fromValue<void*>(gatSelect));
391  this->deleteLater();
392  }
393 
394  void PythonModuleSelectionReceiver::handleModulesPicked(const QSet<u32>& mods)
395  {
396  Module* modSelect = mods.isEmpty()
397  ? nullptr
399  if (mThread) mThread->setInput(QVariant::fromValue<void*>(modSelect));
400  this->deleteLater();
401  }
402 
403  void PythonContext::handleConsoleInputReceived(const QString& input)
404  {
405  mConsole->setInputMode(false);
406  mConsole->setReadOnly(true);
407  mThread->setInput(input);
408  }
409 
410  void PythonContext::handleThreadFinished()
411  {
412  if (mConsole) mConsole->setReadOnly(false);
413  PythonEditor* calledFromEditor = dynamic_cast<PythonEditor*>(mInterpreterCaller);
414  QString errmsg;
415  if (!mThread)
416  {
417  mThreadAborted = false;
418  return;
419  }
420  if (!mThreadAborted)
421  {
422  errmsg = mThread->errorMessage();
423  if (!mThread->stdoutBuffer().isEmpty())
424  forwardStdout(mThread->stdoutBuffer());
425  if (!calledFromEditor && !mThread->result().isEmpty() && errmsg.isEmpty())
426  forwardStdout(mThread->result());
427  }
428  else
429  {
430  forwardError("\nPython thread aborted\n");
431  mThreadAborted = false;
432  }
433 
434  mThread->deleteLater();
435  mThread = 0;
436  delete mLayoutLocker;
437 
438  if (!errmsg.isEmpty())
439  forwardError(errmsg);
440 
441  if (calledFromEditor)
442  {
443  calledFromEditor->handleThreadFinished();
444  if (mTriggerReset)
445  {
446  forwardError("\nreset() can only be used in the Python console!\n");
447  mTriggerReset = false;
448  }
449  if (mTriggerClear)
450  {
451  forwardError("\nclear() can only be used in the Python console!\n");
452  mTriggerClear = false;
453  }
454  }
455  else if (mConsole)
456  {
457  handleReset();
458  handleClear();
459  mConsole->handleThreadFinished();
460  }
461 
462  if (mConsole)
463  {
464  mConsole->setInputMode(PythonConsole::Standard);
465  mConsole->displayPrompt();
466  }
467  }
468 
469  void PythonContext::forwardStdout(const QString& output)
470  {
471  if (output != "\n")
472  {
473  log_info("python", "{}", utils::rtrim(output.toStdString(), "\r\n"));
474  }
475  if (mConsole)
476  {
477  mConsole->handleStdout(output);
478  }
479  }
480 
481  void PythonContext::forwardError(const QString& output)
482  {
483  log_error("python", "{}", output.toStdString());
484  if (mConsole)
485  {
486  mConsole->handleError(output);
487  }
488  }
489 
490  std::vector<std::tuple<std::string, std::string>> PythonContext::complete(const QString& text, bool use_console_context)
491  {
492  PyGILState_STATE state = PyGILState_Ensure();
493 
494  std::vector<std::tuple<std::string, std::string>> ret_val;
495  try
496  {
497  auto namespaces = py::list();
498  if (use_console_context)
499  {
500  namespaces.append(*mContext);
501  namespaces.append(*mContext);
502  }
503  else
504  {
505  py::dict tmp_context = py::globals();
506  initializeContext(&tmp_context);
507  namespaces.append(tmp_context);
508  namespaces.append(tmp_context);
509  }
510  auto jedi = py::module::import("jedi");
511  py::object script = jedi.attr("Interpreter")(text.toStdString(), namespaces);
512  py::object list;
513  if (py::hasattr(script,"complete"))
514  list = script.attr("complete")();
515  else if (py::hasattr(script,"completions"))
516  list = script.attr("completions")();
517  else
518  log_warning("python", "Jedi autocompletion failed, neither complete() nor completions() found.");
519 
520  for (const auto& entry : list)
521  {
522  auto a = entry.attr("name_with_symbols").cast<std::string>();
523  auto b = entry.attr("complete").cast<std::string>();
524  ret_val.emplace_back(a, b);
525  }
526  }
527  catch (py::error_already_set& e)
528  {
529  forwardError(QString::fromStdString(std::string(e.what()) + "\n"));
530  e.restore();
531  PyErr_Clear();
532  }
533 
534  PyGILState_Release(state);
535 
536  return ret_val;
537  }
538 
539  int PythonContext::checkCompleteStatement(const QString& text)
540  {
541 
542  PyGILState_STATE state = PyGILState_Ensure();
543 
544  #if PY_VERSION_HEX < 0x030900a0 // Python 3.9.0
545  // PEG not yet available, use PyParser
546 
547  node* n;
548  perrdetail e;
549 
550  n = PyParser_ParseString(text.toStdString().c_str(), &_PyParser_Grammar, Py_file_input, &e);
551  if (n == NULL)
552  {
553  if (e.error == E_EOF)
554  {
555  PyGILState_Release(state);
556  return 0;
557  }
558  PyGILState_Release(state);
559  return -1;
560  }
561 
562  PyNode_Free(n);
563  PyGILState_Release(state);
564  return 1;
565 
566  #else
567 
568  // attempt to parse Python expression into AST using PEG
569  PyCompilerFlags flags {PyCF_ONLY_AST, PY_MINOR_VERSION};
570  PyObject* o = Py_CompileStringExFlags(text.toStdString().c_str(), "stdin", Py_file_input, &flags, 0);
571  // if parsing failed (-> not a complete statement), nullptr is returned
572  // (apparently no need to PyObject_Free(o) here)
573  PyGILState_Release(state);
574  return o != nullptr;
575 
576  #endif
577  }
578 
579  void PythonContext::handleReset()
580  {
581  if (mTriggerReset)
582  {
583  PyGILState_STATE state = PyGILState_Ensure();
584  closePython();
585  initPython();
586  PyGILState_Release(state);
587  scheduleClear();
588  mTriggerReset = false;
589  }
590  }
591 
592  void PythonContext::handleClear()
593  {
594  if (mTriggerClear)
595  {
596  if (mConsole)
597  {
598  mConsole->clear();
599  }
600  mTriggerClear = false;
601  }
602  }
603 
604  void PythonContext::scheduleReset()
605  {
606  mTriggerReset = true;
607  }
608 
609  void PythonContext::scheduleClear()
610  {
611  mTriggerClear = true;
612  }
613 
614  void PythonContext::updateNetlist()
615  {
616  PyGILState_STATE state = PyGILState_Ensure();
617  (*mContext)["netlist"] = gNetlistOwner; // assign the shared_ptr here, not the raw ptr
618  PyGILState_Release(state);
619  }
620 
621 } // namespace hal
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for and distribution as defined by Sections through of this document Licensor shall mean the copyright owner or entity authorized by the copyright owner that is granting the License Legal Entity shall mean the union of the acting entity and all other entities that control are controlled by or are under common control with that entity For the purposes of this definition control direct or to cause the direction or management of such whether by contract or including but not limited to software source documentation and configuration files Object form shall mean any form resulting from mechanical transformation or translation of a Source including but not limited to compiled object generated and conversions to other media types Work shall mean the work of whether in Source or Object made available under the as indicated by a copyright notice that is included in or attached to the whether in Source or Object that is based or other modifications as a an original work of authorship For the purposes of this Derivative Works shall not include works that remain separable or merely the Work and Derivative Works thereof Contribution shall mean any work of including the original version of the Work and any modifications or additions to that Work or Derivative Works that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner For the purposes of this submitted means any form of or written communication sent to the Licensor or its including but not limited to communication on electronic mailing source code control and issue tracking systems that are managed or on behalf the Licensor for the purpose of discussing and improving the but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as Not a Contribution Contributor shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work Grant of Copyright License Subject to the terms and conditions of this each Contributor hereby grants to You a non no royalty irrevocable copyright license to prepare Derivative Works publicly publicly and distribute the Work and such Derivative Works in Source or Object form Grant of Patent License Subject to the terms and conditions of this each Contributor hereby grants to You a non no royalty have offer to import
Definition: gate.h:58
const std::vector< Gate * > & get_gates() const
Definition: netlist.cpp:204
Gate * get_gate_by_id(const u32 gate_id) const
Definition: netlist.cpp:193
Module * get_module_by_id(u32 module_id) const
Definition: netlist.cpp:613
int numberSelectedGates() const
int numberSelectedModules() const
#define log_error(channel,...)
Definition: log.h:78
#define log_info(channel,...)
Definition: log.h:70
#define log_warning(channel,...)
Definition: log.h:76
string command
Definition: control.py:76
CORE_API T rtrim(const T &s, const char *to_remove=" \t\r\n")
Definition: utils.h:336
std::filesystem::path get_library_directory()
Definition: utils.cpp:128
std::vector< std::filesystem::path > get_plugin_directories()
Definition: utils.cpp:194
SelectionRelay * gSelectionRelay
Definition: plugin_gui.cpp:83
Netlist * gNetlist
Definition: plugin_gui.cpp:80
GuiApi * gGuiApi
Definition: plugin_gui.cpp:86
std::shared_ptr< Netlist > gNetlistOwner
Definition: plugin_gui.cpp:79
n
Definition: test.py:6
PinType type
grammar _PyParser_Grammar
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options)
int getInt(QWidget *parent, const QString &title, const QString &label, int value, int min, int max, int step, bool *ok, Qt::WindowFlags flags)
QString getText(QWidget *parent, const QString &title, const QString &label, QLineEdit::EchoMode mode, const QString &text, bool *ok, Qt::WindowFlags flags, Qt::InputMethodHints inputMethodHints)
QSet::const_iterator constBegin() const const
QSet::iterator insert(const T &value)
bool isEmpty() const const
void clear()
QString fromStdString(const std::string &str)
bool isEmpty() const const
std::string toStdString() const const
void finished()
int toInt(bool *ok) const const
QString toString() const const