1

My research has included:

PyQt reference to callable problem?

Python PyQt callback never runs - how to debug?

Passing extra arguments to PyQt slot

I am building a linux 'launcher' program which currently has two callbacks. One simply launches the clicked app, the other creates a new launcher. The first one works fine - the second one has been extremely tricky. I've done a lot to solve this.

  1. Running PyCharm debug on script and watch values for self, etc. to learn more
  2. Moving the NewLauncher function to within the InitUI method.
  3. Endlessly changing "self", "centralWidget" and other object references.
  4. Using functools partial.

The error I'm getting is "AttributeError: 'QWidget' object has no attribute 'newLauncher'"

Here's the code: (my apologies if it's too long - I was recently advised not to edit too much out).

import sys, os
import subprocess
from functools import partial

from PyQt5.QtWidgets import QFileDialog, QToolButton, QHBoxLayout, QGridLayout, QSizePolicy, QSpacerItem, QWidget, QPushButton, QFormLayout, QLineEdit, QAction, QApplication, QDesktopWidget, QMainWindow, QTabWidget, QVBoxLayout

from PyQt5.QtGui import QIcon
from PyQt5.QtCore import QSize

from ruamel.yaml import YAML


yaml = YAML()
file_object = open("/home/tsc/PycharmProjects/launcher/Matrix.yaml", "r")
code = file_object.read()
matrix = yaml.load(code)
file_object.close()


class App(QMainWindow):
    def __init__(self):
        super(App, self).__init__()

        self.initUI()

    def launch(self, filepath):
        subprocess.run(filepath)


    def newLauncher(self):
        num_butts = len(matrix)
        btn_str = 'btn' + str(num_butts)

        file_object = open("/home/tsc/PycharmProjects/launcher/Matrix.yaml", "a")
        btn_str = 'btn' + str(num_butts + 1)
        file_object.write("\n" + btn_str + ":\n")

        self.setStyleSheet('padding: 3px; background: white');
        fname, _ = QFileDialog.getOpenFileName(self, "select an executable or document to launch:", "",
                                               "all files (*.*)")

        path = fname
        fname = os.path.basename(fname)

        file_object.write("  " + "name: " + str(fname) + "\n" + "  " + "path: " + str(path) + "\n")

        self.setStyleSheet('padding: 3px; background: white');
        icon, _ = QFileDialog.getOpenFileName(self, "select an image file for the icon:", "",
                                              "all files (*.*)")

        file_object.write("  " + "icon: " + str(icon) + "\n")
        file_object.close()


    def initUI(self):
        super(App, self).__init__()

        centralWidget = QWidget()
        tabWidget = QTabWidget()

        lay = QVBoxLayout(centralWidget)

        for i in range(3):
            page = QWidget()
            pagelay = QGridLayout(page)
            bmatrix = {}

            for btn in matrix:
                name = matrix[btn]['name']
                filepath = matrix[btn]['path']
                icon = matrix[btn]['icon']
                bmatrix[btn] = QToolButton(page)
                bmatrix[btn].setIcon(QIcon(icon))
                bmatrix[btn].setIconSize(QSize(64, 64))
                bmatrix[btn].resize(100, 100)
                bmatrix[btn].clicked.connect(lambda checked, arg=filepath: self.launch(arg))

                pagelay.addWidget(bmatrix[btn])

            tabWidget.addTab(page, 'tab{}'.format(i))

        mainMenu = self.menuBar()
        fileMenu = mainMenu.addMenu('File')
        mainMenu.addMenu(fileMenu)
        newAction = QAction('&New', centralWidget)

        #1 newAction.triggered.connect(lambda checked, arg=matrix: centralWidget.newLauncher(arg)) - shows window.
        #2 newAction.triggered.connect(partial(self.NewLauncher, self)) - shows nothing, App has no NewLauncher

        fileMenu.addAction(newAction)
        editMenu = mainMenu.addMenu('Edit')

        lay.addWidget(mainMenu)
        lay.addWidget(tabWidget)

        centralWidget.setGeometry(100, 100, 1080, 630)
        centralWidget.setWindowTitle('LaunchMaster')
        qtRectangle = centralWidget.frameGeometry()
        centerPoint = QDesktopWidget().availableGeometry().center()
        qtRectangle.moveCenter(centerPoint)
        centralWidget.move(qtRectangle.topLeft())

        centralWidget.show()


if __name__ == '__main__':

    app = QApplication(sys.argv)
    ex = App()
    sys.exit(app.exec_())

And here's the yaml config file. You'll need to customize the paths, etc. if you want to test it. The interface has a menuBar and a tabWidget containing pages that themself contain the launcher buttons.

Matrix.yaml: substitute spaces for the underscores (indent is 2 chars.). I'm unsure of this markup syntax just yet, sorry for the hassle.

btn1:  
  name: firefox  
  path: firefox-esr  
  icon: /home/tsc/PycharmProjects/launcher/icons/firefox.jpeg  

btn2:  
  name: thunderbird  
  path: /home/tsc/thunderbird/thunderbird  
  icon: /home/tsc/PycharmProjects/launcher/icons/thunderbird.jpeg  
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
tscv11
  • 55
  • 1
  • 7
  • You could place the proper format of the yaml, then I fix it so that it can be seen with the markup. – eyllanesc Mar 12 '18 at 00:04
  • Sorry - I'm not sure what your point is. Are you being sarcastic? I see that you have already changed my post, so mentioning that I could have done it instead doesn't seem very helpful. Maybe you don't realize that the script works fine except for the one callback? I don't need the yaml fixed, I just don't know how to post it with the proper spacing because I haven't familiarized myself with so's markup/markdown, whatever it is. – tscv11 Mar 12 '18 at 00:20
  • `newAction.triggered.connect(partial(self.NewLauncher, self))` has at least two problems. First, you don't have a method `NewLauncher`, only a method `newLauncher`. Second, `self.newLauncher` is already a bound method, which will get the bound-in `self` as its first argument; `partial(self.newLauncher, self)` will pass in `self` twice instead, which will fail with a `TypeError` about too many arguments. – abarnert Mar 12 '18 at 00:21
  • If you check the time of my publication and of my edition you will see that I first comment, and after a while I edit it. – eyllanesc Mar 12 '18 at 00:22
  • It looks like the key is to make sure the yaml is in a "code" section. My apologies. I should have realized that. – tscv11 Mar 12 '18 at 00:29
  • The posted code doesn't cause the listed error. My guess is that it comes from one of the other versions you've tried, but it's hard to guess what that other version would look like. – abarnert Mar 12 '18 at 00:29
  • @abarnert: temporary mistake, I've tried all sorts of permutations, including what you've suggested. But to make sure I changed it back to "newAction.triggered.connect(partial(self.newLauncher))" and got the same result - no main window at all. Of course there's also the error I mentioned, which only comes up sporadically - sorry I was imprecise). – tscv11 Mar 12 '18 at 00:36
  • Why are you using `partial` with no arguments? Also, there's no way that error could come up sporadically—surely it comes up when you run one version of your code, and not when you run another version. Going through all those permutations, and then giving us one of them at random (that doesn't demonstrate the specific error you asked about) turns a perfectly deterministic bug into an un-debuggable heisenbug. – abarnert Mar 12 '18 at 00:47
  • What you need to do is give us a [minimal, complete, verifiable example](https://stackoverflow.com/help/mcve). Something without a bunch of extraneous code, and with the actual code you're asking about rather than a comment about an older version of it, and that can be run and will demonstrate exactly the error you're asking about. – abarnert Mar 12 '18 at 00:48

2 Answers2

1
  • If you are not going to need to pass some parameter it is not necessary that you use the lambda functions, so you have a regular connection.

  • On the other hand you should not call centralWidget.show (), but to show, you must also set centralWidget with setCentralWidget.

  • Another point is that you must verify if the user selected a path.

  • Another improvement to your code would be to use QProcess.startDetached() instead of subprocess.run() since it is blocking.


import sys
import os

from PyQt5.QtWidgets import QFileDialog, QToolButton, QHBoxLayout, QGridLayout, QSizePolicy, QSpacerItem, QWidget, QPushButton, QFormLayout, QLineEdit, QAction, QApplication, QDesktopWidget, QMainWindow, QTabWidget, QVBoxLayout

from PyQt5.QtGui import QIcon
from PyQt5.QtCore import QSize, QProcess

from ruamel.yaml import YAML

yaml_filename = "/home/tsc/PycharmProjects/launcher/Matrix.yaml" 


yaml = YAML()
file_object = open(yaml_filename, "r")
code = file_object.read()
matrix = yaml.load(code)
file_object.close()


class App(QMainWindow):
    def __init__(self):
        super(App, self).__init__()

        self.initUI()

    def launch(self, filepath):
        QProcess.startDetached(filepath)

    def newLauncher(self):
        fname, _ = QFileDialog.getOpenFileName(self, "select an executable or document to launch:", "",
                                               "all files (*.*)")
        if fname == "":
            return

        icon, _ = QFileDialog.getOpenFileName(self, "select an image file for the icon:", "",
                                              "all files (*.*)")
        if icon == "":
            return

        num_butts = len(matrix)
        btn_str = 'btn' + str(num_butts)
        file_object = open(yaml_filename, "a")
        btn_str = 'btn' + str(num_butts + 1)
        file_object.write("\n" + btn_str + ":\n")

        path = fname
        fname = os.path.basename(fname)
        file_object.write("  " + "name: " + str(fname) + "\n" + "  " + "path: " + str(path) + "\n")
        file_object.write("  " + "icon: " + str(icon) + "\n")
        file_object.close()


    def initUI(self):
        super(App, self).__init__()

        centralWidget = QWidget()
        tabWidget = QTabWidget()

        lay = QVBoxLayout(centralWidget)

        for i in range(3):
            page = QWidget()
            pagelay = QGridLayout(page)
            bmatrix = {}

            for btn in matrix:
                name = matrix[btn]['name']
                filepath = matrix[btn]['path']
                icon = matrix[btn]['icon']
                bmatrix[btn] = QToolButton(page)
                bmatrix[btn].setIcon(QIcon(icon))
                bmatrix[btn].setIconSize(QSize(64, 64))
                bmatrix[btn].resize(100, 100)
                bmatrix[btn].clicked.connect(lambda checked, arg=filepath: self.launch(arg))

                pagelay.addWidget(bmatrix[btn])

            tabWidget.addTab(page, 'tab{}'.format(i))

        mainMenu = self.menuBar()
        fileMenu = mainMenu.addMenu('File')
        mainMenu.addMenu(fileMenu)
        newAction = QAction('&New', centralWidget)
        newAction.triggered.connect(self.newLauncher)

        fileMenu.addAction(newAction)
        editMenu = mainMenu.addMenu('Edit')

        lay.addWidget(mainMenu)
        lay.addWidget(tabWidget)

        self.setGeometry(100, 100, 1080, 630)
        self.setWindowTitle('LaunchMaster')
        qtRectangle = self.frameGeometry()
        centerPoint = QDesktopWidget().availableGeometry().center()
        qtRectangle.moveCenter(centerPoint)
        self.move(qtRectangle.topLeft())
        self.setCentralWidget(centralWidget)

        self.show()


if __name__ == '__main__':

    app = QApplication(sys.argv)
    ex = App()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • I have tried setCentralWidget but PyCharm can't import it from QtGui, QtCore or QtWidgets. Is it from PyQt4? Now I see it shows no syntax error if setCentralWidget is preceded by "self" or presumably other objects. – tscv11 Mar 12 '18 at 00:57
  • @tscv11 Obvious, because setCentralWidget is part of the QMainWindow class, and self is an App instance that is a class that inherits from QMainWindow. Do you have another problem? – eyllanesc Mar 12 '18 at 01:03
  • @tscv11 Read the answers of: https://stackoverflow.com/questions/2709821/what-is-the-purpose-of-self – eyllanesc Mar 12 '18 at 01:05
  • Thanks for this answer. The insight that I shouldn't call centralWidget (but self instead) helped me straighten everything out. – tscv11 Mar 12 '18 at 01:14
0

The line you're asking about (I think) is this commented-out code:

newAction.triggered.connect(partial(self.NewLauncher, self))

The comment says "shows nothing, App has no NewLauncher".

If so, there are two problems here. The first is a simple typo—you wrote NewLauncher instead of newLauncher—which I'll assume you already fixed when you actually tested this. The second is a little deeper, and you might have problems with it.

self.newLauncher is a bound method. That is, it knows which self it's meant for, and when you call it, that self will be passed in as the first argument. If you then write partial(self.newLauncher, self), when that's called, it'll do the same thing as self.newLauncher(self)—that is, it'll pass in two copies of self as separate arguments.

The typo will fail quite visibly, with an AttributeError at the connect call. But the extra self will only fail, with a TypeError, inside the button-clicking signal. Which, I believe, means that PyQt will write some warning to stderr (which you may not be looking at—especially if you're on Windows and don't even have a command-line window attached) and do nothing for the click.

You probably wanted to just do this:

newAction.triggered.connect(self.newLauncher)

Occasionally, you want to pass the unbound method from the class object (App.newLauncher), and partial that to an instance:

newAction.triggered.connect(partial(App.newLauncher, self))

… but in most cases, including this one, that's just a less readable (and slower) way to do the same thing as passing a bound method.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • When I try "newAction.triggered.connect(partial(App.newLauncher, self))" I get this error: "TypeError: Qt.ConnectionType expected, not 'function'" This suggestion: "newAction.triggered.connect(self.newLauncher)" leads to no main window. I believe I have tried everything you mentioned but I am grateful for your help. – tscv11 Mar 12 '18 at 00:52
  • Your explanation of self and bound/unbound methods satisfied a curiosity I'd had for quite some time - many thanks. – tscv11 Mar 12 '18 at 01:16
  • @tscv11 Yeah, people tend to treat how methods work as "deep magic" that you shouldn't understand until you're an expert in Python internals, but I think that's a mistake. The descriptor protocol (how Python internally creates a bound method object from the expression `self.newLauncher`) is complicated, but what a bound method _is_ and how to use it is pretty simple, and worth understanding. I wrote [a blog post](https://stupidpythonideas.blogspot.com/2013/06/how-methods-work.html) about this a few years ago, but I don't know how well it explains things. – abarnert Mar 12 '18 at 01:22