Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: AttributeError: 'QResizeEvent' object has no attribute 'pos' #22409

Closed
Valdes-Tresanco-MS opened this issue Feb 5, 2022 · 10 comments
Closed

Comments

@Valdes-Tresanco-MS
Copy link
Contributor

Bug summary

This bug is similar to #11607. In my case, I get it when I combine PyQt5 and matplotlib 3.5.1. In any of the other combinations (PyQt5 and matplotlib==3.4.3 or PyQt6 and matplotlib==3.5.1) it works as expected.

Code for reproduction

# change this to use PyQt5 or PyQt6
qt = 'qt5'

import sys
import matplotlib
from matplotlib.figure import Figure
if qt == 'qt6':
    from PyQt6.QtWidgets import *
    from PyQt6.QtCore import *
    from PyQt6.QtGui import *
    if matplotlib.__version__ == '3.4.3':
        raise ValueError('For PyQt6 Install matplotlib 3.5.1')
    from matplotlib.backends.backend_qtagg import (FigureCanvas, NavigationToolbar2QT as NavigationToolbar)


else:
    from PyQt5.QtWidgets import *
    from PyQt5.QtCore import *
    from matplotlib.backends.backend_qt5agg import (FigureCanvasQTAgg as FigureCanvas,
                                                    NavigationToolbar2QT as NavigationToolbar)
import numpy as np

print('Matplotlib version:', matplotlib.__version__)


class ChartsBase(QMdiSubWindow):
    def __init__(self):
        super(ChartsBase, self).__init__()
        self.setMinimumSize(400, 400)

        self.mainwidgetmdi = QMainWindow()  # must be QMainWindow to handle the toolbar
        self.setWidget(self.mainwidgetmdi)

        fig = Figure()
        self.figure_canvas = FigureCanvas(fig)
        self.fig = self.figure_canvas.figure
        self.mainwidgetmdi.setCentralWidget(self.figure_canvas)
        # similar to figure canvas
        self.mpl_toolbar = NavigationToolbar(self.figure_canvas, self)
        self.mpl_toolbar.setVisible(True)
        if qt == 'qt6':
            area = Qt.ToolBarArea.BottomToolBarArea
        else:
            area = Qt.BottomToolBarArea
        self.mainwidgetmdi.addToolBar(area, self.mpl_toolbar)

        t = np.linspace(0, 10, 501)

        self.axes = self.fig.subplots(1, 1)
        line_plot_ax = self.axes.plot(t)


class MDIWindow(QMainWindow):
    count = 0

    def __init__(self):
        super().__init__()

        self.mdi = QMdiArea()
        self.setCentralWidget(self.mdi)
        bar = self.menuBar()

        file = bar.addMenu("File")
        file.addAction("New")
        file.triggered[QAction].connect(self.add_new)
        self.setWindowTitle("MDI Application with matplotlib")

    def add_new(self, p):

        if p.text() == "New":
            sub = ChartsBase()
            sub.setWindowTitle("Matplotlib example")
            self.mdi.addSubWindow(sub)
            sub.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    mdi = MDIWindow()
    mdi.show()
    sys.exit(app.exec())

Actual outcome

To reproduce:

  1. execute the app
  2. Menu "File"
  3. "New"
  4. Maximize the subwindow

When the subwindow is maximized I get this error:

Traceback (most recent call last):
  File "/home/mario/programs/miniconda/lib/python3.8/site-packages/matplotlib/backends/backend_qt.py", line 262, in enterEvent
    x, y = self.mouseEventCoords(self._get_position(event))
AttributeError: 'QResizeEvent' object has no attribute 'pos'

Expected outcome

The maximized subwindow

Additional information

What are the conditions under which this bug happens? input parameters, edge cases, etc?

Using matplotlib==3.5.1 with PyQt5. Not happen with other combinations matplotlib==3.5.1 and PyQt6 or matplotlib==3.4.3 and PyQt5

Do you know why this bug is happening?

This bug is related to the QEvents methods. QResizeEvent does have not any method to get the position in PyQt5, unlike PyQt6 which does have it

Do you maybe even know a fix?

Add a try-except clausule to this method

def enterEvent(self, event):
x, y = self.mouseEventCoords(self._get_position(event))
FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))

def enterEvent(self, event):
    try:
        x, y = self.mouseEventCoords(self._get_position(event))
    except AttributeError:
        # QResizeEvent has no attribute pos
        x = y = None
    FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))

After this, work as expected!
Happy to open a PR if it is needed!

Operating system

Linux Mint 20.2

Matplotlib Version

3.5.1

Matplotlib Backend

QtAgg

Python version

Python 3.8.5

Jupyter version

No response

Installation

pip

@anntzer
Copy link
Contributor

anntzer commented Feb 5, 2022

I cannot reproduce the problem. It also seems strange that you get a QResizeEvent in enterEvent?

@Valdes-Tresanco-MS
Copy link
Contributor Author

Yes, it seems strange to me too.
I have installed it in several different conda environments (from PyPi) to test if it was a version problem or if the piece of code I put was functional. In all cases, I get the error.
I tried with PyQt5 versions: 5.15.2, 5.15.4, and 5.15.6 and in all of them, I get the same error. I tried to debug but since enterEvent is a virtual function it's hard to keep track of it (at least for me). I printed the event type to confirm and interestingly, only in this combination do I get QResizeEvent, in the rest, I always get QEnterEvent.
With the try-except clause work as expected

I attach a small video with the procedure to reproduce the error.

test.mp4

Mario S.

@anntzer
Copy link
Contributor

anntzer commented Feb 6, 2022

That's strange :/ (but I'd think blindly ignoring the error with a try... except is wrong too, as something weird is going on).

@DechinPhy
Copy link

I've got the same issue. Do you have any idea about how to avoid this before issue fixed?

@anntzer
Copy link
Contributor

anntzer commented Feb 13, 2022

Please provide your repro. Also include the environment, all relevant versions of modules, installation methods (conda/pip/linux package), etc.

@anthepro
Copy link
Contributor

I ran into the same traceback using Matplotlib 3.5.3 and PyQt 5.15.6.

Here is the minimal (admittedly, quite non-minimal) example, run it, click on the second tab, then move the mouse over FigureCanvas:

import sys

from PyQt5.QtWidgets import QApplication, QHBoxLayout, QScrollArea, QTabWidget, QVBoxLayout, QWidget

from matplotlib.backends.backend_qtagg import FigureCanvas


if __name__ == '__main__':
    app = QApplication(sys.argv)
    layout = QVBoxLayout()

    tabs = QTabWidget()
    tabs.addTab(FigureCanvas(), 'a')
    tabs.addTab(QWidget(), 'b')

    def tabChanged(index):  # create second tab lazily
        if index != 1:
            return
        widget = tabs.widget(index)
        if widget.layout() is not None:
            return
        vboxlayout = QVBoxLayout()
        vboxlayout.addWidget(FigureCanvas())
        wrapper = QWidget()
        wrapper.setLayout(vboxlayout)
        scrollarea = QScrollArea()
        scrollarea.setWidget(wrapper)
        hboxlayout = QHBoxLayout()
        hboxlayout.addWidget(scrollarea)
        widget.setLayout(hboxlayout)
    tabs.currentChanged.connect(tabChanged)
    tabs.show()
    sys.exit(app.exec())

I did some digging and the culprit seems to be frame = sys._getframe() in FigureCanvas' resizeEvent method, which causes a memory leak, which in turn leads to the wrong event (QResizeEvent) being sent to enterEvent. Sadly, I didn't dig further than that - I have no idea why this only rarely leads to an exception - the leak is pretty consistent. Also, the exception doesn't happen using PyQt6. If I had to guess I'd say there's a race condition somewhere.

I wasn't able to reproduce the problem originally reported here, so I cannot be certain the root cause is the same, but I think it's safe enough to assume it is.

For anyone else facing this issue, the workaround I've used is:

class FigureCanvas(FigureCanvas):
    def resizeEvent(self, event):
      super().resizeEvent(event)
      import gc
      gc.collect()

Not the prettiest solution, and it does come with a slight performance penalty, but at least it manages to avoid the exception. By explicitly forcing garbage collection, we ensure frame variable is cleaned up before any other Qt code is executed. A faster option for someone who doesn't care about enter/leave events would be to override enterEvent and leaveEvent and simply ignore them. Filtering out events in enterEvent by type is not an option, since when this occurs, all subsequent enter events also receive the same QResizeEvent.

The solution I would suggest is quite simple, I would explicitly delete the variable frame once it's no longer needed in FigureCanvas' resizeEvent method:

        recursion = frame.f_code is getattr(frame.f_back, 'f_code', None)
        del frame
        if recursion:
            return

@anntzer
Copy link
Contributor

anntzer commented Aug 24, 2022

Perhaps the cleaner way to check for recursion would be something like

def resizeEvent(self, event):
    if self._in_resize_event: return  # where the attribute is initialized to False in init.
    self._in_resize_event = True
    try: ...  # the actual resize logic
    finally: self._in_resize_event = False

?

@anthepro
Copy link
Contributor

Sounds good. Should I create a PR or will you handle it?

@tacaswell
Copy link
Member

@anthepro If you are willing to open a PR that would be great!

@anntzer
Copy link
Contributor

anntzer commented Aug 25, 2022

Closed by #23729, I guess.

@anntzer anntzer closed this as completed Aug 25, 2022
@QuLogic QuLogic added this to the v3.6.0 milestone Aug 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants