Home > database >  Issue when executing code snippet with "exec" and inheritance
Issue when executing code snippet with "exec" and inheritance

Time:09-18

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_())

CodePudding user response:

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.

CodePudding user response:

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)
  • Related