0

I'm having some issues when trying to execute a string/file from within a QPlainTextEdit, it appears to be some sort of scoping issues. What happens is that when the code EXECUTABLE_STRING is run from the global scope, it works fine. However, when it is run from a local scope, such as through the AbstractPythonCodeWidget, it either can't find the object to do inheritance TypeError: super(type, obj): obj must be an instance or subtype of type or runs into a name error NameError: name 'Test' is not defined. Which oddly changes based on whether or not the exec(EXECUTABLE_STRING) line is commented/uncommented when run. Any help would be greatly appreciated.

import sys
from PyQt5.QtWidgets import QApplication, QPlainTextEdit
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCursor


app = QApplication(sys.argv)

EXECUTABLE_STRING = """
from PyQt5.QtWidgets import QLabel, QApplication
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCursor

class Test(QLabel):
    def __init__(self, parent=None):
        super(Test, self).__init__(parent)
        self.setText("Test")

a = Test()
a.show()
a.move(QCursor.pos())
"""

class AbstractPythonCodeWidget(QPlainTextEdit):
    def __init__(self, parent=None):
        super(AbstractPythonCodeWidget, self).__init__(parent)
        self.setPlainText(EXECUTABLE_STRING)

    def keyPressEvent(self, event):
        if event.modifiers() == Qt.ControlModifier:
            if event.key() == Qt.Key_Return:
                # this does not work
                #exec(compile(self.toPlainText(), "script", "exec"), globals(), locals())
                exec(self.toPlainText())
        return QPlainTextEdit.keyPressEvent(self, event)


w = AbstractPythonCodeWidget()
w.show()
w.move(QCursor.pos())
w.resize(512, 512)

# this works when run here, but not when run on the keypress event
# exec(EXECUTABLE_STRING)

sys.exit(app.exec_())
Emmuh
  • 1
  • 1
    Are you trying to create your own testing environment, where you can write and edit code, then test it on the fly? If so, there are strategies for doing so. I wouldn't recommend editing the code from inside the application itself; instead you can keep using your editor as usual, and build a reload functionality into your testing app. A mechanism for doing so [already exists](https://github.com/machinekoder/python-qt-live-coding), helpfully! – CrazyChucky Sep 08 '21 at 23:16
  • On a broader note—there are, in fact, a nonzero number of times when `exec` is the correct tool for the job, but they are very few and far between. It's something to always approach with caution, and look for other options. – CrazyChucky Sep 08 '21 at 23:19
  • So, I'm using `exec` solely for the purpose that it's what I knew how to use. The broader scope of the application is that inside of the application, the user can create Python scripts which can then be run when certain events are triggered. The user's scripts can either be in a raw string, or an actual file on disk depending on the settings that they've set. So I'm not too sure how helpful the live coding example would be, but I'll def check it out. – Emmuh Sep 09 '21 at 00:25

2 Answers2

0

First of all, running exec based on user input can be a security issue, but most importantly usually leads to fatal crash unless lots of precautions are taken, since you're using the same interpreter for both your program and the user code: basically, if the code of the user fails, your program fails, but that's not the only problem.

The reason for which your code doesn't run properly is actually a bit complex, and it's related to the scope of the class name, which becomes a bit complex when running exec along with super().[1]

An interesting aspect is that if you remove the arguments of super (and you should, since Python 3), the program won't raise any error.

But that won't be enough: a is a local variable, and it will be garbage collected as soon as exec is finished, and since the label is assigned to that variable, it will be destroyed along with it.

A possible solution would be to make the reference persistent, for example by assigning it to self (since self exists in the scope of the executed script). This is a working example of the EXECUTABLE_STRING:

from PyQt5.QtWidgets import QLabel
from PyQt5.QtGui import QCursor

class Test(QLabel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setText("Test")

self.a = Test()
self.a.show()
self.a.move(QCursor.pos())

As you can see, we don't need to import everything again anymore, we only import QLabel, since it wasn't imported in the main script, but everything else already exists in the scope of the script, including the current QApplication instance.

That said, all the above is only for knowledge purposes, as you should NOT use exec to run user code.

For instance, try to paste the following in the text edit, and run it:

self.document().setHtml('This should <b>NOT</b> happen!!!<br/><br/>Bye!')
self.setReadOnly(True)

from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication
QTimer.singleShot(2000, QApplication.quit)

As you can see, not only the above code is able to change the input, but also take complete control of the whole application.

You could prevent that by calling a function that would basically limit the scope of the execution:

def runCode(code):
    try:
        exec(code)
    except Exception as e:
        return e

class AbstractPythonCodeWidget(QPlainTextEdit):
    # ...
    def keyPressEvent(self, event):
        if event.modifiers() == Qt.ControlModifier:
            if event.key() == Qt.Key_Return:
                error = runCode(self.toPlainText())
                if error:
                    QMessageBox.critical(self, 'Script crash!', str(error))
        return QPlainTextEdit.keyPressEvent(self, event)

But that's just because no self is involved: you could still use w.a = Test() with the example above.

So, if you want to run user made scripts in your program, exec is probably not an acceptable solution, unless you take enough precautions.

If you don't need direct interaction between your program and the user script, a possibility could be to use the subprocess module, and run the script with another python interpreter instance.

[1] If anybody has a valid resource/answer that might shed some light on the topic, please comment.

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • This all makes sense, however, in this scenario, the expectation is that the user will need to be able to access the main application and make modifications to the data the main application is currently manipulating. So it seems that `exec` is the way to go, with a lot of precautions in place (we can also assume that the user is liable for it crashing if they use this functionality). – Emmuh Sep 09 '21 at 17:41
  • One more thing to note, is that this is a plugin, and the main application is still in Python 2.7 =\, so the `super().__init__()` workaround will not work. Is it possible to go into more detail on the scoping issues? As if I understood this all correctly I will need to have the precautions in place and add this stuff to the scope, or wait for the application to be upgraded to Python 3+ – Emmuh Sep 09 '21 at 17:46
  • @Emmuh I can understand that, but achiving that is **not** easy and should not be underestimated. What you're trying to do is possibly dangerous, and it will take a *lot* of efforts (and knowledge and experience) in order to do it properly: you cannot just do a simple `exec`. Unfortunately, as noted in the question, I don't know the details about the `super()` matter. – musicamante Sep 09 '21 at 17:52
  • I understand that it will not be easy and that there will be dangers involved with allowing users this level of access to the application. A few things to note on the subject are that this is a plugin for an application that already supports this functionality, I'm merely trying to understand how it works, and add additional functionality to it. In this case, attempting to make the scripts dynamically executed through user-driven events/signals. – Emmuh Sep 09 '21 at 18:15
  • If the application is written in Python, you could study its code and understand how it does it. It won't be easy, it will probably take a lot of time, but it will also be very educational. – musicamante Sep 09 '21 at 18:26
0

Found a similar issue that goes into more depth about how the scope works with Globals/Locals in exec here: globals and locals in python exec()

Don't want to copy/paste the entire thread, but the answer that worked for me in this post was:

d = dict(locals(), **globals())
exec (code, d, d)
Emmuh
  • 1