Search code examples
pythonqtpermissionspyside6qmovie

How can I prevent a PermissionError when I try to delete a .GIF previously 'started' in a PySide6 QLabel?


I would be very grateful for your help overcoming this PermissionError problem when I try to delete a .GIF file that I have previously been 'playing' in my PySide6 application. Full reproduceable code + an example .gif file below.

In my PySide6 application, I create a QLabel and display a .GIF file inside.

This involves first creating a QMovie object (which takes in the relative path to the .GIF file from the current directory), and then using the QLabel.setMovie(QMovie) method to display the .GIF file inside the label. Finally, the QMovie.start() method starts the .GIF playing.

After this, I want to be able to delete the .GIF file. I have read about two Python functions for deleting files: os.remove() and pathlib.Path.unlink() .

Both of these functions work as desired in the absence of any PySide6 code. I am able to delete the .GIF file from my computer without any PermissionError.

The PermissionError appears when I try to call either of these functions after having called QMovie.start() to 'play' my .GIF file inside my PySide6 application. Here is the full PermissionError statement:

PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'test_gif_question_mark.gif'

I have used psutil to read out all the processes on my machine, and it tells me that the process python.exe is 'using' my .GIF file, and no other processes are using it.

The PermissionError does not appear if I do not call QMovie.start() i.e. I am free to delete the .GIF file after making the QMovie object, and even after calling QLabel.setMovie(QMovie) .

It is obvious that my PySide6 application opens and reads data from the .GIF file in order to display and 'play' the animation associated with it. What is not obvious to me is how to 'close' the .GIF file so that python.exe process stops 'using' the .GIF file, leaving me free to delete it.

Here is the set of things I have tried to 'close' the .GIF file:

  • Calling QMovie.stop() .

  • Calling QLabel.clear() .

  • Deleteing the QMovie object with del.

  • Deleting the QLabel object with del .

  • Closing the QApplication .

  • Deleting the QApplication using del .

  • Deleting the QMainWindow instance using del - this is the only other instance in my variables after all of the above.

None of the appraoches above prevent a PermissionError if I subsequently try to delete the .GIF file using os.remove() or Path.unlink() . Using psutil , I can see that even after all of these approaches, python.exe is still 'using' my .GIF file.

It seems to me like QMovie.stop() is not doing what I want it to do- I want it to 'let go of' and 'close' the .GIF file, so that the python.exe process is no longer using it, and I can ask Windows to delete the file.

If you know how I can force python.exe to stop using the .GIF file, or have any other solutions, I would be very grateful for your help.

Full reproduceable code:

import sys
import os
from pathlib import Path
from PySide6.QtWidgets import QMainWindow, QVBoxLayout, QWidget, QApplication, QPushButton, QLabel, QHBoxLayout
from PySide6.QtGui import QMovie

# Description: This script builds a small QApplication GUI with buttons that stimulate various steps in the process
# of using, displaying, stopping using, and finally trying to delete a .gif file.


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # Specify the path to the gif file from the current directory:
        self.path_to_gif = 'test_gif_question_mark.gif'

        # Create an instance of the QLabel class:
        self.my_label = QLabel()

        # Placeholder for the QMovie object:
        self.my_QMovie = None

        # Create a button to make the QMovie object:
        my_button_create_qmovie = QPushButton()
        my_button_create_qmovie.setText('Create QMovie')
        my_button_create_qmovie.clicked.connect(self.clicked_create_button)

        # Create a button to display the QMovie object in the QLabel:
        my_button_display_qmovie = QPushButton()
        my_button_display_qmovie.setText('Display QMovie')
        my_button_display_qmovie.clicked.connect(self.clicked_display_button)

        # Create a button to try to stop using the gif:
        my_button_stop_using_gif = QPushButton()
        my_button_stop_using_gif.setText('Try to stop using gif file')
        my_button_stop_using_gif.clicked.connect(self.clicked_stop_button)

        # Create a button to try to delete the gif:
        my_button_delete_gif = QPushButton()
        my_button_delete_gif.setText('Delete gif file')
        my_button_delete_gif.clicked.connect(self.clicked_delete_button)

        # Place the buttons in a horizontal layout:
        layout_h_1 = QHBoxLayout()
        layout_h_1.addWidget(my_button_create_qmovie)
        layout_h_1.addWidget(my_button_display_qmovie)
        layout_h_2 = QHBoxLayout()
        layout_h_2.addWidget(my_button_stop_using_gif)
        layout_h_2.addWidget(my_button_delete_gif)

        # Assign the horizontal layouts to empty widgets:
        widget_h_1 = QWidget()
        widget_h_1.setLayout(layout_h_1)
        widget_h_2 = QWidget()
        widget_h_2.setLayout(layout_h_2)

        # Place the label & buttons in a vertical layout:
        layout_v = QVBoxLayout()
        layout_v.addWidget(self.my_label)
        layout_v.addWidget(widget_h_1)
        layout_v.addWidget(widget_h_2)

        # Create a placeholder widget to hold the layout.
        widget = QWidget()
        widget.setLayout(layout_v)

        # Set the central widget of the main window:
        self.setCentralWidget(widget)

    def clicked_create_button(self):
        # Create a QMovie to display:
        self.my_QMovie = QMovie(self.path_to_gif)
        print('\nCreated QMovie object using path to .gif file')

    def clicked_display_button(self):
        # Display the gif inside the label:
        self.my_label.setMovie(self.my_QMovie)
        # Start the movie playing:
        self.my_QMovie.start()
        print('\nDisplayed and started QMovie')

    def clicked_delete_button(self):
        # Use os.remove to try to delete the gif file:
        # os.remove(self.path_to_gif)

        # Use pathlib.unlink to try to delete the gif file:
        path_gif = Path(self.path_to_gif)
        path_gif.unlink()

    def clicked_stop_button(self):
        # Try different things to get python to stop using the .gif file:
        # Try 'stopping' the QMovie object:
        self.my_QMovie.stop()

        # Try clearing the QLabel:
        self.my_label.clear()

        # Try deleting the QMovie object:
        del self.my_QMovie

        # Try deleting the QLabel:
        del self.my_label


# Run the QApplication:
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()

print('app finished')

# Try deleting the QApplication after it has finished:
del app

# Try deleting the MainWindow:
del w

# Try deleting the gif file even after the QApplication has closed:
os.remove('test_gif_question_mark.gif')

.GIF file for testing:

test_gif_question_mark.gif


Solution

  • It's a bug of PySide6. When you call the QLabel.setMovie(), as you can see in the source, the binding library assumes the label owns the movie, but the C++ implementation doesn't make it happen actually. What about reporting the bug on here?

    Anyway, this can be remedied by several ways like self.my_QMovie.deleteLater() or self.my_QMovie.setDevice(None), but the best way is making the label own the movie at the C++ side, like the following example.

    ...
    class MainWindow(QMainWindow):
        ...
        def clicked_create_button(self):
            self.my_QMovie = QMovie(self.path_to_gif, parent=self.my_label)
        ...
        def clicked_stop_button(self):
            self.my_QMovie.setParent(None)
            self.my_QMovie = None
            self.my_label.clear()
    ...
    print('app finished')
    
    # You don't need to delete the app
    del w
    os.remove('test_gif_question_mark.gif')