0

Please note: this is on W10. This may well be significant.

Python: 3.9.4 pytest: 6.2.5 pytest-qt: 4.0.2

I've been using pytest-qt for about a week now to start developing a PyQt5 app. There have been a few baffling problems but none as baffling as this one.

My app code:

class LogTableView(QtWidgets.QTableView):    
    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)

    def resizeEvent(self, resize_event):
        super().resizeEvent(resize_event)
        # self.resizeRowsToContents()

The last line above needs to be added. Using a TDD approach I therefore start writing the test:

def test_resize_event_should_result_in_resize_rows(request, qtbot):
    t_logger.info(f'\n>>>>>> test name: {request.node.originalname}')
    table_view = logger_table.LogTableView(QtWidgets.QSplitter())
    # with unittest.mock.patch.object(table_view, 'resizeRowsToContents') as mock_resize:
    # with unittest.mock.patch('logger_table.LogTableView.resizeRowsToContents') as mock_resize:
    table_view.resizeEvent(QtGui.QResizeEvent(QtCore.QSize(10, 10), QtCore.QSize(20, 20)))

NB the commented-out lines show the kind of things I have been trying. But you can see that even just creating an object of the type LogTableView, and then calling the method, with no mocks around at all, causes the error.

On running this:

>pytest -s -v -k test_logger_table.py

I get this:

...
self = <logger_table.LogTableView object at 0x000002B672697670>
resize_event = <PyQt5.QtGui.QResizeEvent object at 0x000002B672743940>

    def resizeEvent(self, resize_event):
>       super().resizeEvent(resize_event)
E       RuntimeError: wrapped C/C++ object of type LogTableView has been deleted
...

Has anyone got any idea what this is about?

PS FWIW, out of despair, I even tried this:

super(LogTableView, self).resizeEvent(resize_event)

... same error.

mike rodent
  • 14,126
  • 11
  • 103
  • 157

1 Answers1

1

Creating a parent in the child constructor is not a very good idea.

Remember that PyQt is a binding, every reference used in Python is a wrapper for the Qt object: if the object is deleted on the C++ side, the python reference still exists, but any attempt to use its functions results in the RuntimeError above.

In your case, there's no persistent reference for the parent on the python side, only the pointer on the Qt side, which is not enough to avoid garbage collection: only parent objects take ownership in Qt (that's why you can avoid persistent references for a child Qt object), it's not the other way around. The problem is that the child believes that it has a parent (as it had one when it was created), but in the meantime that parent has been deleted, as soon as the child constructor is returned.

Just create a local variable for the parent.

def test_resize_event_should_result_in_resize_rows(request, qtbot):
    t_logger.info(f'\n>>>>>> test name: {request.node.originalname}')
    parent = QtWidgets.QSplitter()
    table_view = logger_table.LogTableView(parent)
    # ...

Besides the problem of the subject, technically speaking there's no point in using a very specific widget such as QSplitter as a parent (especially considering that in order to be properly used, the widget should be added with addWidget(), as the parenthood alone is pointless for a splitter); if you need a parent, just use a basic QWidget.

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thank you! Amazing. Yes, in fact I should have submitted the question with making a `QWidget` (as I had already tried). In fact I have already simplied things considerably: the `QTableView` constructor (which in my question serves no purpose!) actually checks that the submitted parent is a `QSplitter`. I would never have guessed that constructing the parent as a parameter of the constructor would have such an effect! – mike rodent Nov 06 '21 at 07:53
  • PS I'm not entirely sure that your explanation is correct though. I just did an experiment: in normal application code it seems OK to construct the parent as the child's parameter, in PyQt5 (although the situation normally doesn't arise in practice). I suspect that the reason this fails this time is because of the testing context: once the code progresses beyond the line creating the `LogTableView`, it is something about the way `unittest` or `pytest` operates that means that the underlying C++ object has gone (although I don't understand enough to say what exactly). – mike rodent Nov 06 '21 at 08:21
  • @mikerodent it depends on how the parent is constructed and how the child reacts to it. I cannot do deeper testing right now, but I did a couple of basic experiments yesterday and in both cases the app crashed as expected. There could be "different" ways for which it could (or couldn't) crash, the testing environment just changes some circumstances, but it *is* due to garbage collection and memory/reference management, as that's exactly what generates the exception about the C++ object being deleted. Since this cannot be considered normal usage (C++ wouldn't allow it), it's expected. – musicamante Nov 06 '21 at 16:31
  • Right, understood. I'll avoid such practices in future: worst case scenario would be inconsistent errors of this kind. I wonder if there are other practices in Python (in a PyQt5 context) which might translate to "illegal" techniques in C++... – mike rodent Nov 06 '21 at 20:39
  • @mikerodent Unfortunally I'm not experienced on C++ as I'd like to, but studying the principles and Qt source code is certainly a good start. A very useful source I've found so far is the [weboq Qt source viewer](https://code.woboq.org/qt5/qtbase/src/), as it provides really useful tooltips and references for almost any function and variable. Going through that allows you a better and deeper understanding of how Qt *actually* works, especially "under the hood" of PyQt. From there you can do some research on how C++ (and Qt) works and finally understand what "better practices" could be. – musicamante Nov 07 '21 at 02:23