3

Build a Bulk File Rename Tool With Python and PyQt

 2 years ago
source link: https://realpython.com/bulk-file-rename-tool-python/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Build a Bulk File Rename Tool With Python and PyQt

Build a Bulk File Rename Tool With Python and PyQt

by Leodanis Pozo Ramos May 19, 2021 0 Comments

gui intermediate projects

Say you need to rename multiple files in your personal folder using a specific naming pattern. Doing that manually can be time-consuming and error-prone. So, you’re thinking of automating the file renaming process by building your own bulk file rename tool using Python. If so, then this tutorial is for you.

In this tutorial, you’ll learn how to:

  • Create the GUI for a bulk file rename tool using Qt Designer and PyQt
  • Use PyQt threads to offload the file renaming process and prevent freezing GUIs
  • Use pathlib to manage system paths and rename files
  • Update the GUI state according to the renaming process

By completing the project in this tutorial, you’ll be able to apply a rich set of skills related to PyQt, Qt Designer, PyQt threads, and working with file system paths using Python’s pathlib.

You can download the final source code for the bulk file rename tool you’ll build in this tutorial by clicking the link below:

Get the Source Code: Click here to get the source code you’ll use to build a bulk file rename tool with Python in this tutorial.

Demo: Bulk File Rename Tool With Python and PyQt

In this tutorial, you’ll build a bulk file rename tool to automate the process of renaming multiple files in a given directory in your file system. To build this application, you’ll use Python’s pathlib to manage the file renaming process and PyQt to build the application’s graphical user interface (GUI).

Here’s how your bulk file rename tool will look and work once you get to the end of this tutorial:

Once you finish building the application, you’ll be able to rename multiple files in your file system, a common task when you’re organizing your personal files and folders. In this example, the application focuses on images and Python files, but you can add other file types as you go.

Project Overview

The project you’ll build in this tutorial consists of a GUI application that loads multiple files from a given directory and allows you to rename all those files in one go using a predefined filename prefix and consecutive numbers. In this section, you’ll take a first look at the problem and a possible solution. You’ll also figure out how to lay out the project.

Laying Out the Project

To build your bulk file rename tool, you’ll create a few modules and packages and organize them in a coherent Python application layout. Your project’s root directory will look like this:

./rprename_project/
│
├── rprename/
│   │
│   ├── ui/
│   │   ├── __init__.py
│   │   ├── window.py
│   │   └── window.ui
│   │
│   ├── __init__.py
│   ├── app.py
│   ├── rename.py
│   └── views.py
│
├── README.md
├── requirements.txt
└── rprenamer.py

Here, rprename_project/ is the project’s root directory, where you’ll create the following files:

  • README.md provides a general project description and instructions on installing and running the application. Having a README.md file for your project is considered a best practice in programming, especially if you’re planning to release it as an open source solution.
  • requirements.txt provides the list of external dependencies for the project.
  • rprenamer.py provides an entry-point script to run the application.

Then you have the rprename/ directory that will hold a Python package with the following modules:

  • __init__.py enables rprename/ as a Python package.
  • app.py provides the PyQt skeleton application.
  • rename.py provides the file renaming functionalities.
  • views.py provides the application’s GUI and related functionalities.

The ui/ subdirectory will provide a package to store GUI-related code. It’ll contain the following files and modules:

  • __init__.py enables ui/ as a Python package.
  • window.py contains the Python code for the application’s main window. You’ll see how to generate this file using pyuic5.
  • window.ui holds a Qt Designer file that contains the code for the application’s main window in XML format.

Go ahead and create this directory structure with all the files and modules except for window.ui and window.py. You’ll see how to create these two files with Qt Designer and pyuic5 later in this tutorial.

To download the project’s directory structure, click the link below:

Get the Source Code: Click here to get the source code you’ll use to build a bulk file rename tool with Python in this tutorial.

Outlining the Solution

Your bulk file rename tool will be a fully functional GUI application. It’ll allow you to load several files from an existing directory and rename them using a descriptive filename prefix and consecutive numbers. To build the application, you’ll need to take the following steps:

  1. Create the application’s GUI.
  2. Provide the functionality to load and rename multiple files.
  3. Update the application’s GUI according to the file renaming process progress.

To create the application’s GUI, you’ll use Qt Designer. This tool provides a user-friendly interface to create GUIs by dragging and dropping graphical components (widgets) on a blank form. With this tool, your GUI creation process will be fast and productive.

When it comes to managing files and directories in Python, you have several options in the standard library. For example, you have os.path to work with file system paths and os to use operating system functionalities, such as renaming files with os.rename(). In this tutorial, however, you’ll use pathlib for both tasks.

Note: Python 3.4 added pathlib to the standard library. This module provides classes that represent file system paths and allow operations on them.

Typically, you’ll use pathlib.Path to manage file and directory paths in your applications. Once you have a Path object that points to a physical file or directory, you can call .rename() on that object to rename the associated file or directory.

Next, you need to code the functionality to load multiple files into your application and rename them in one go. Depending on the number of files that you need to rename, the operation can take a considerable amount of time. This might cause your application’s GUI to freeze. To prevent GUI freezing issues, you’ll offload the renaming process to a worker thread using QThread.

The file renaming process needs to be connected with the application’s GUI so the user knows what is happening at any given time. In this example, you’ll set up a progress bar to reflect the operation progress.

You’ll also write some code to ensure that the GUI gets updated according to the file renaming progress and state. There are at least two general strategies for managing the GUI update:

  1. Using conditional statements to check the state and take actions accordingly
  2. Enabling and disabling widgets depending on the application’s state.

In this project, you’ll use the second approach, which might be more intuitive and user-friendly, providing a better user experience.

Prerequisites

To complete this tutorial and get the most out of it, you should be comfortable with the following concepts:

  • Creating GUI applications with Python and PyQt
  • Using Qt Designer to create GUIs
  • Using PyQt QThread to offload long-running tasks and prevent freezing GUIs
  • Using pathlib to manage system paths and rename files

If you don’t have all the required knowledge, then that’s okay! You can start the tutorial and take some time and review the following resources whenever you need to:

Additionally, you can take a look at the following resources:

In terms of external software dependencies, your bulk file rename tool depends on PyQt v5.15.12. You can install this library from PyPI using pip as usual:

$ python -m pip install pyqt5

Finally, you should create a virtual environment to isolate your project’s dependencies as Python best practices recommend. After that, it’s time to start working on your own bulk file rename tool!

Step 1: Build the Bulk File Rename Tool’s GUI

In this section, you’ll use Qt Designer to quickly create the bulk file rename tool’s GUI. At the end of this step, you’ll have a Qt Designer’s .ui file providing the required code for the following window:

Bulk File Rename Tool Main Window GUI

You’ll also learn how to automatically translate your .ui file into Python code. You’ll use that code to provide the GUI for your application’s main window.

To download these files and all the code you’ll write in this section, click the link below:

Get the Source Code: Click here to get the source code you’ll use to build a bulk file rename tool with Python in this tutorial.

Creating the GUI With Qt Designer

To start working on your bulk file rename tool’s GUI, go ahead and fire up Qt Designer. Then create a widget-based form and run the steps shown in the following video:

Here are the steps in the above video:

  1. Create a new form using the Widget template from the New Form dialog and set its title to RP Renamer.
  2. Add a label and set its text to Last Source Directory:.
  3. Add a line edit to hold the path to the selected directory and set its .readOnly property to True.
  4. Add a push button and set its text to &Load Files.
  5. Add two labels with the text Files to Rename and Renamed Files, respectively, and set their font style to Bold.
  6. Add two list widgets to display the files to rename and the renamed files, respectively.
  7. Set a vertical layout to the labels and their corresponding list widgets.
  8. Join both arrangements using a splitter.
  9. Add a label and set its text to Filename Prefix:.
  10. Add a line edit to take the filename prefix from the user and set its placeholder text to Rename your files to….
  11. Add a label and set its text to .jpg.
  12. Add a push button and set its text to &Rename.
  13. Add a progress bar and set its value to 0.
  14. Tweak the minimumSize and maximumSize properties of labels, line edits, and push buttons to provide a coherent resizing behavior when the user resizes the window.
  15. Set a grid layout as the form’s top-level layout.

Once you’re done, save the form as window.ui under the rprename/ui/ directory. Don’t close Qt Designer. Go ahead and change the .objectName property for the following objects:

Object Value Form Window lineEdit dirEdit pushButton loadFilesButton listWdget srcFileList listWidget_2 dstFileList lineEdit_2 prefixEdit label_5 extensionLabel pushButton_2 renameFilesButton

To do that, you can select the object in the Object Inspector and change the property value in the Property Editor:

Build Bulk File Rename Change Object Names Qt Designer

You need to change the object names because you’ll use those names in your Python code, so they should be more descriptive and readable. Now go ahead and click Save on Qt Designer’s toolbar to make the new object names persistent in your window.ui file. You can also save the file by pressing Ctrl+S on your keyboard.

Once you’ve finished building the bulk file rename tool’s GUI, you can close Qt Designer since you won’t need it any longer. Next, you need to translate the content of the .ui file you just created into Python code. That’s what you’ll do in the following section.

Converting Qt Designer’s Output Into Python Code

Once you have a .ui file with a suitable GUI for your application, you need to convert the content of this file into Python code so you can load the GUI in the final application. PyQt provides a command-line tool called pyuic5 that allows you to perform this conversion.

Note: You can also load the content of a .ui file into your application directly using uic.loadUi(). This strategy, however, is mostly recommend for small dialogs.

If you take a look at the content of your window.ui file, then you’ll see that it contains XML code. That code defines all the graphical components for your application’s GUI. Here’s a fragment of the file content:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Window</class>
 <widget class="QWidget" name="Window">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>720</width>
    <height>480</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>RP Renamer</string>
  </property>
  <!-- Snip... -->

In this small fragment, the definition of Window is the top-level class. This class represents your main window. Then the XML code defines other classes and sets their properties. Since PyQt provides pyuic5 to automatically translate this XML code into Python code, you don’t need to worry about the content of your .ui files. You just need to know how to use pyuic5.

Now open a terminal on your rprename/ui/ directory and run the following command:

$ pyuic5 -o window.py window.ui

This command generates a Python module called window.py from the window.ui file and places it in your rprename/ui/ directory. The module contains the Python code for your bulk file rename tool’s GUI. Here’s a small sample of the code:

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'window.ui'
#
# Created by: PyQt5 UI code generator 5.14.1
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Window(object):
    def setupUi(self, Window):
        Window.setObjectName("Window")
        Window.resize(720, 480)
        # Snip...

    def retranslateUi(self, Window):
        _translate = QtCore.QCoreApplication.translate
        Window.setWindowTitle(_translate("Window", "RP Renamer"))
        # Snip...

Ui_Window provides all the code for generating the GUI of your bulk file rename tool. In this case, .setupUi() contains the code to create all the required widgets and also to lay them out on the GUI, while .retranslateUi() contains code for internationalization and localization, which is beyond the scope of this tutorial.

Since you’re using pyuic5 to automatically generate the Python code from an existing .ui file, you don’t need to worry about the content of window.py. Like the WARNING! comment at the top of the file states, all the changes you make to this file will disappear if you update the GUI with Qt Designer and regenerate the Python code.

Having a module with GUI-specific code strongly encourages the separation of GUI logic from business logic, which is a fundamental principle in the Model-View-Controller pattern.

At this point, your project’s layout is complete. You have all the files you need to create your bulk file rename tool. Now it’s time to put together the skeleton PyQt application using the GUI you just created.

Step 2: Create the PyQt Skeleton Application

So far, you’ve built a GUI for the bulk file rename tool using Qt Designer. You also used pyuic5 to automatically translate the .ui file content into Python code so you can use it in the application. Finally, you saved the code into window.py under the rprename/ui/ directory.

In this section, you’ll create a PyQt skeleton application and set its main window to use the GUI code you have in window.py. To download the files and all the code you’ll write in this section, click the link below:

Get the Source Code: Click here to get the source code you’ll use to build a bulk file rename tool with Python in this tutorial.

Setting Up the Bulk File Rename Tool’s Window

To create the skeleton PyQt application for your bulk file rename tool, you first need to create the application’s main window. First, fire up your favorite code editor or IDE and open the rprename/__init__.py file. Add the following content to it:

# -*- coding: utf-8 -*-
# rprename/__init__.py

"""This module provides the rprename package."""

__version__ = "0.1.0"

Other than some comments and the module docstring, the above file defines a top-level constant called __version__ to hold the application’s version number.

Next, open rprename/views.py and write the following content into it:

# -*- coding: utf-8 -*-
# rprename/views.py

"""This module provides the RP Renamer main window."""

from PyQt5.QtWidgets import QWidget

from .ui.window import Ui_Window

class Window(QWidget, Ui_Window):
    def __init__(self):
        super().__init__()
        self._setupUI()

    def _setupUI(self):
        self.setupUi(self)

In views.py, you first import QWidget from PyQt5.QtWidgets. Then you import Ui_Window from ui.window. As you saw before, this class provides the GUI for your bulk file rename tool.

Next, you create a new class called Window. This class uses multiple inheritance. It inherits from QWidget and also from your GUI class, Ui_Window. QWidget enables the base GUI functionality, and Ui_Window provides the specific GUI arrangement you want for this application.

Note: You can also use composition to create the GUI for your Window.

For example, you can define Window like this:

# rprename/views.py
# Snip...

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.ui = Ui_Window()
        self._setupUI()

    def _setupUI(self):
        self.ui.setupUi(self)

In this case, you create .ui as an instance of Ui_Window. From this point on, you need to use .ui to access the widgets on the application’s GUI.

In this tutorial, you use the multiple inheritance approach because it makes all the user interface components directly accessible without the need for the .ui attribute. For a deeper dive into this topic, check out Using a Designer UI File in Your Application.

The initializer of Window calls the base class initializer using super(). It also calls ._setupUI(), which is a non-public method that will collect all the code required for generating and setting up the GUI. Up to this point, ._setupUI(self) only calls .setupUi(), which is provided by the second parent class, Ui_Window.

Note: Python doesn’t distinguish between private and public attributes. The term non-public in the above paragraph refers to those attributes that aren’t intended to be used from outside the containing class. The Python naming convention for this kind of attribute is to use a leading underscore (_) in the name.

With the initial version of Window ready for use, you can go ahead and create a PyQt skeleton application for your bulk file rename tool.

Creating the PyQt Skeleton Application

Now that you have the application’s main window ready for use, it’s time to write the required boilerplate code to create a PyQt application. Get back to your code editor and open the rprename/app.py file. Then add the following code:

# -*- coding: utf-8 -*-
# rprename/app.py

"""This module provides the RP Renamer application."""

import sys

from PyQt5.QtWidgets import QApplication

from .views import Window

def main():
    # Create the application
    app = QApplication(sys.argv)
    # Create and show the main window
    win = Window()
    win.show()
    # Run the event loop
    sys.exit(app.exec())

In this module, you import sys to access exit(). This function allows you to cleanly exit the application when the user closes the main window. Then you import QApplication from PyQt5.QtWidgets and Window from views. The final step is to define main() as your application’s main function.

In main(), you instantiate QApplication and Window. Then you call .show() on Window. Finally, you run the application’s main loop or event loop using .exec().

Coding the Application’s Entry-Point Script

With the PyQt skeleton application in place, you can create a suitable entry-point script so you can run the application quickly. Open the rprenamer.py file from your root directory and type the following code into it:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# rprenamer.py

"""This module provides the RP Renamer entry-point script."""

from rprename.app import main

if __name__ == "__main__":
    main()

This script is fairly small. You first import main() from your app.py module. Then you use the traditional Python conditional statement that calls main() if the user runs the module as a Python script.

Now you can open a terminal and run the script to launch the application. You’ll get the following window on your screen:

Bulk File Rename Tool Main Window GUI

Cool! Your bulk file rename tool has a nice GUI that provides a button to load the files you want to rename. The top line edit will show the path to the source directory. The left-side list widget will display the list of files to be renamed, and the right-side list widget will show the renamed files.

To kick off the file renaming process, the user needs to provide a filename prefix and then click Rename. The progress bar at the bottom will show the file renaming progress.

In the next section, you’ll write the required code to provide the application’s main functionality, renaming multiple files in one go. So close the application’s window to continue adding features.

Step 3: Rename Files With pathlib and PyQt Threads

To implement the file renaming functionality for your bulk file rename tool, you’ll use Python’s pathlib and PyQt QThread. With pathlib, you’ll manage file system paths and rename files. With QThread, on the other hand, you’ll rename files in a separate thread of execution. Why?

Well, depending on the number of files that you want to rename, the renaming process might take a considerable time. Launching a long-running task in the application’s main thread can freeze the GUI, which in turn can result in a bad user experience.

To avoid GUI freezing issues, you can create a worker QThread to offload the file renaming process and make your application responsive.

Again, you can download all the code you’ll write in this section by clicking the link below:

Get the Source Code: Click here to get the source code you’ll use to build a bulk file rename tool with Python in this tutorial.

Loading and Displaying Target Files

To start renaming files, you first need a way to load those files into the application. PyQt provides a class called QFileDialog that allows you to select files or directories from your file system using a predefined dialog. Once you select the files you want to rename, you need to store their paths in a convenient data structure.

Get back to the rprename/views.py and update the code like this:

# -*- coding: utf-8 -*-
# rprename/views.py

"""This module provides the RP Renamer main window."""

from collections import deque
from pathlib import Path

from PyQt5.QtWidgets import QFileDialog, QWidget

from .ui.window_ui import Ui_Window

FILTERS = ";;".join(
    (
        "PNG Files (*.png)",
        "JPEG Files (*.jpeg)",
        "JPG Files (*.jpg)",
        "GIF Files (*.gif)",
        "Text Files (*.txt)",
        "Python Files (*.py)",
    )
)

class Window(QWidget, Ui_Window):
    # Snip...

Here, you first import deque from collections. Deques are a generalization of stacks and queues. They support efficient append and pop operations from either side of the deque. In this case, you’ll use a deque to store the paths of the files you need to rename.

You also import Path from pathlib. This class can represent concrete file or directory paths in your file system. You’ll use this class to perform different operations on files and directories. In this tutorial, you’ll use Path.rename() to rename physical files in your hard drive.

Then you import QFileDialog from PyQt5.QtWidgets. This class provides an appropriate dialog to select files from a given directory. The FILTER constant specifies different file filters as a string. These filters allow you to select from different file types when loading files into the application.

Keep views.py open and update Window like this:

 1# rprename/views.py
 2# Snip...
 3
 4class Window(QWidget, Ui_Window):
 5    def __init__(self):
 6        super().__init__()
 7        self._files = deque()
 8        self._filesCount = len(self._files)
 9        self._setupUI()
10        self._connectSignalsSlots()
11
12    def _setupUI(self):
13        self.setupUi(self)
14
15    def _connectSignalsSlots(self):
16        self.loadFilesButton.clicked.connect(self.loadFiles)
17
18    def loadFiles(self):
19        self.dstFileList.clear()
20        if self.dirEdit.text():
21            initDir = self.dirEdit.text()
22        else:
23            initDir = str(Path.home())
24        files, filter = QFileDialog.getOpenFileNames(
25            self, "Choose Files to Rename", initDir, filter=FILTERS
26        )
27        if len(files) > 0:
28            fileExtension = filter[filter.index("*") : -1]
29            self.extensionLabel.setText(fileExtension)
30            srcDirName = str(Path(files[0]).parent)
31            self.dirEdit.setText(srcDirName)
32            for file in files:
33                self._files.append(Path(file))
34                self.srcFileList.addItem(file)
35            self._filesCount = len(self._files)

Here’s what the newly added code does:

  • Line 7 creates ._files as a deque object. This attribute will store the paths to the files you want to rename.

  • Line 8 defines ._filesCount to store the number of files to be renamed.

  • Line 10 calls ._connectSignalsSlots().

  • Line 15 defines ._connectSignalsSlots(). This method will collect several signal and slot connections in a single place. Up to this point, the method connects the Load Files button’s .clicked() signal with the .loadFiles() slot. This makes it possible to trigger .loadFiles() every time the user clicks the button.

Then you define .loadFiles() to load the files that you want to rename. Here’s what it does:

  • Line 19 clears the .dstFileList list widget every time the user clicks Load Files.

  • Lines 20 to 23 define a conditional statement that checks if the Last Source Directory line edit is currently displaying any directory path. If so, then the if code block sets the initial directory, initDir, to hold that path. Otherwise, the initial directory is set to Path.home(), which returns the path to the current user’s home folder. You’ll use initDir to provide an initialization directory to the QFileOpen object.

  • Lines 24 to 26 call .getOpenFileNames() on QFileDialog. This static method takes several arguments, creates a dialog to allow the user to select one or more files, and returns a list of string-based paths to the selected files. It also returns the currently used file filter. In this case, you use the following arguments:

    • parent holds the widget that owns the dialog, which is self or the current Window object in this case.

    • caption holds the dialog’s title or caption. You use the string "Choose Files to Rename" in this example.

    • dir holds the path to the initialization directory. In other words, the path to the directory the dialog is opened in. In this example, you use initDir to initialize the dialog.

    • filter holds a file type filter so that only the files that match the filter are shown in the dialog. For example, if you set the filter to "*.jpg", then the dialog displays files that match this format.

  • Line 27 defines a conditional statement that runs a bunch of statements if the user selects at least one file from the QFileDialog.

  • Line 28 slices the current filter string to extract the file extension.

  • Line 29 sets the text of the .extensionLabel object to the file extension extracted on line 28.

  • Line 30 retrieves the paths to the directory containing the selected files. To do that, you create a Path object using the path to the first file in the list of selected files. The .parent attribute holds the path to the containing directory.

  • Line 31 sets the texts of the .dirEdit line edit to the directory path you got on line 30.

  • Lines 32 to 34 define a for loop that iterates over the list of selected files, creates a Path object for each file, and appends it to ._files. Line 34 adds each file to the .srcFileList list widget so that the user can see the currently selected files on the application’s GUI.

  • Line 35 updates ._filesCount with the current number of files in the list.

If you run the application now, then you’ll get the following behavior:

Now you can load multiple files into your bulk file rename tool by clicking Load Files. Note that you can specify the file type you want to load into the application by choosing from several file filters in the Choose Files to Rename dialog.

Note: The line numbers in the above code and in the rest of the code samples in this tutorial are intended to facilitate the explanation. They don’t match the order of lines in the final module or script.

Cool! Your project already has support for loading files of different types. Go ahead and close the application to continue coding. In the next section, you’ll implement the file renaming functionality.

Renaming Multiple Files in a Worker QThread

To perform the file renaming process, you’ll use a QThread object. Creating and setting up a worker thread allows you to offload the file renaming process from the application’s main thread. This way you prevent possible freezing GUI issues when you select a big number of files to rename.

The worker thread will perform the file renaming using pathlib.rename(). It’ll also emit custom signals to communicate with the main thread and update the application’s GUI. Go ahead and open the rprename/rename.py file in your code editor. Type in the following code:

 1# -*- coding: utf-8 -*-
 2# rprename/rename.py
 3
 4"""This module provides the Renamer class to rename multiple files."""
 5
 6import time
 7from pathlib import Path
 8
 9from PyQt5.QtCore import QObject, pyqtSignal
10
11class Renamer(QObject):
12    # Define custom signals
13    progressed = pyqtSignal(int)
14    renamedFile = pyqtSignal(Path)
15    finished = pyqtSignal()
16
17    def __init__(self, files, prefix):
18        super().__init__()
19        self._files = files
20        self._prefix = prefix
21
22    def renameFiles(self):
23        for fileNumber, file in enumerate(self._files, 1):
24            newFile = file.parent.joinpath(
25                f"{self._prefix}{str(fileNumber)}{file.suffix}"
26            )
27            file.rename(newFile)
28            time.sleep(0.1)  # Comment this line to rename files faster.
29            self.progressed.emit(fileNumber)
30            self.renamedFile.emit(newFile)
31        self.progressed.emit(0)  # Reset the progress
32        self.finished.emit()

Here’s what this code does:

  • Line 9 imports QObject and pyqtSignal() from PyQt5.QtCore. QObject allows you to create subclasses with custom signals and functionalities. With pyqtSignal(), you can create custom signals so you can emit them when a given event occurs.

  • Line 11 defines a subclass of QObject called Renamer.

  • Lines 13 to 15 define three custom signals:

    1. .progressed() will be emitted every time the class renames a new file. It returns an integer representing the number of the currently renamed file. You’ll use this number to update the progress bar in the application’s GUI.

    2. .renamedFile() will be emitted whenever the class renames a file. In this case, the signal returns the path to the renamed file. You’ll use this path to update the list of renamed files in the application’s GUI.

    3. .finished() will be emitted when the file renaming process finishes.

The class initializer on line 17 takes two required arguments:

  1. files holds the list of selected files. Each file is represented by its corresponding Path.

  2. prefix holds the filename prefix that you’ll use to rename the files.

Then you define the method that performs the file renaming process, .renameFiles(). Here’s a summary of how it works:

  • Line 23 defines a for loop to iterate over the list of selected files. The loop uses enumerate() to generate a file number as the loop goes.

  • Line 24 builds new filenames using the filename prefix fileNumber and the file extension .suffix. Then it joins the new filename with the parent directory path to create the path to file at hand, newFile.

  • Line 27 renames the current file by calling .rename() on the current file with newFile as an argument.

  • Line 28 is an optional call to time.sleep() that slows down the file renaming process so that you to see how it goes. Feel free to remove this line to run the application at its natural speed.

  • Lines 29 and 30 emit the .progressed() and .renamedFile() signals. You’ll use those signals to update the application’s GUI in the next section.

  • Line 31 emits the .progressed() using 0 as an argument. You’ll use this value to reset the progress bar after finishing the file renaming process.

  • Line 32 emits the .finished() signal when the file renaming process has finished.

Once you’ve coded Renamer, you’re ready to start renaming files. To do that, you need to create and set up a worker thread. But first, get back to rprename/views.py and update its import section like this:

# rprename/views.py
# Snip...

from PyQt5.QtCore import QThread
from PyQt5.QtWidgets import QFileDialog, QWidget

from .rename import Renamer
from .ui.window import Ui_Window
# Snip...

In the first highlighted line, you import QThread from PyQt5.QtCore. This class allows you to create and manage worker threads in PyQt applications. In the second highlighted line, you import Renamer from your rename module.

Now scroll down a little bit in your views.py file and update Window like this:

 1# rprename/views.py
 2# Snip...
 3
 4class Window(QWidget, Ui_Window):
 5    # Snip...
 6    def _connectSignalsSlots(self):
 7        self.loadFilesButton.clicked.connect(self.loadFiles)
 8        self.renameFilesButton.clicked.connect(self.renameFiles)
 9
10    def loadFiles(self):
11        # Snip..
12
13    def renameFiles(self):
14        self._runRenamerThread()
15
16    def _runRenamerThread(self):
17        prefix = self.prefixEdit.text()
18        self._thread = QThread()
19        self._renamer = Renamer(
20            files=tuple(self._files),
21            prefix=prefix,
22        )
23        self._renamer.moveToThread(self._thread)
24        # Rename
25        self._thread.started.connect(self._renamer.renameFiles)
26        # Update state
27        self._renamer.renamedFile.connect(self._updateStateWhenFileRenamed)
28        # Clean up
29        self._renamer.finished.connect(self._thread.quit)
30        self._renamer.finished.connect(self._renamer.deleteLater)
31        self._thread.finished.connect(self._thread.deleteLater)
32        # Run the thread
33        self._thread.start()
34
35    def _updateStateWhenFileRenamed(self, newFile):
36        self._files.popleft()
37        self.srcFileList.takeItem(0)
38        self.dstFileList.addItem(str(newFile))

Here, you first connect the Rename button’s .clicked() to .renameFiles(). This method calls ._runRenamerThread() to create, set up, and run the worker thread. Here’s how it works:

  • Line 17 retrieves the text in the Filename Prefix line edit. The user needs to provide this filename prefix.

  • Line 18 creates a new QThread object to offload the file renaming process.

  • Lines 19 to 22 instantiate Renamer, passing the list of files and the filename prefix as arguments to the class constructor. In this case, you turn ._files into a tuple to prevent the thread from modifying the underlying deque on the main thread.

  • Line 23 calls .moveToThread() on the Renamer instance. As its name implies, this method moves a given object to a different thread of execution. In this case, it uses ._thread as the target thread.

  • Line 25 connects the thread’s .started() signal with .renameFiles() on the Renamer instance. This makes it possible to start the file renaming process when the thread starts.

  • Line 27 connects the Renamer instance’s .renamedFile() signal with ._updateStateWhenFileRenamed(). You’ll see what this method does in a minute.

  • Lines 29 and 30 connect the Renamer instance’s .finished() signal with two slots:

    1. The thread’s .quit() slot, which quits the thread once the file renaming process is finished

    2. The Renamer instance’s .deleteLater() slot, which schedules objects for later deletion

  • Line 31 connects the thread’s .finished() signal with .deleteLater(). This makes it possible to delete the thread but only after it’s finished doing its job.

  • Line 33 starts the worker thread.

The final piece of code defines ._updateStateWhenFileRenamed(). When a file is renamed, the method removes the file from the list of files to be renamed. Then the method updates the list of Files to Rename and also the list of Renamed Files on the application’s GUI.

Here’s how the application works now:

That’s it! Your bulk file rename tool already does its job. It allows you to load several files, provide a new filename prefix, and rename all the selected files. Great job!

Now you can close the application to continue with the development process. In the next section, you’ll learn how to update the application’s GUI according to the file renaming progress.

Step 4: Update the GUI State According to the Renaming Progress

So far, your bulk file rename tool provides its main functionality. You can already use the tool to rename multiple files in your file system. However, what would happen if the user clicked Rename in the middle of the renaming process? Also, what if the user forgets to supply an appropriate filename prefix?

Another and even more visible issue is why the application’s progress bar doesn’t reflect the file renaming progress.

All these questions have to do with updating the GUI according to the application’s state at any given time. In this section, you’ll write the required code to fix or prevent the issues mentioned above. You’ll start with the progress bar.

You can download the code you’ll write in this section by clicking the link below:

Get the Source Code: Click here to get the source code you’ll use to build a bulk file rename tool with Python in this tutorial.

Updating the Progress Bar

In GUI applications, you typically use progress bars to inform your users about the progress of long-running tasks. If the users don’t get feedback on what the application is currently doing, then they might think that the application is frozen, stuck, or is having some internal issues.

In this project, you use a progress bar to provide feedback on how the file renaming process is going at any given time. To do that, get back to the rprename/views.py in your code editor and update it like this:

 1# rprename/views.py
 2# Snip...
 3
 4class Window(QWidget, Ui_Window):
 5    # Snip...
 6    def _runRenamerThread(self):
 7        # Update state
 8        self._renamer.renamedFile.connect(self._updateStateWhenFileRenamed)
 9        self._renamer.progressed.connect(self._updateProgressBar)
10        # Snip...
11
12    def _updateStateWhenFileRenamed(self, newFile):
13        # Snip...
14
15    def _updateProgressBar(self, fileNumber):
16        progressPercent = int(fileNumber / self._filesCount * 100)
17        self.progressBar.setValue(progressPercent)

Here’s what the newly added code does:

  • Line 9 connects the Renamer instance’s .progressed() signal with ._updateProgressBar().
  • Line 15 defines ._updateProgressBar(). The method takes a fileNumber as an argument. Note that .progressed() provides this file number every time a file gets renamed.
  • Line 16 computes the file renaming progress as a percentage of the total number of files, which is available in ._filesCount.
  • Line 17 updates the .value property on the progress bar using .setValue() with progressPercent as an argument.

That’s it! Go ahead and run your application again. It’ll work like this:

After these additions to Window, the progress bar of your bulk file rename tool reflects the progress of the file renaming operation, which is nice and improves your users’ experience.

Enabling and Disabling GUI Components

When you build GUI applications, you realize that some actions on the GUI are only available in certain situations. In your bulk file rename tool, for example, it doesn’t make sense to allow the user to click Rename if there’s no file already loaded into the application or if the user hasn’t provided a filename prefix.

There are at least two ways to deal with this kind of situation:

  1. Leave all the widgets enabled all the time and make sure that they don’t trigger any actions that don’t make sense in context.
  2. Enable and disable widgets depending on the application’s state.

To implement the first approach, you can use conditional statements to make sure a given action makes sense at a given moment. To implement the second approach, on the other hand, you need to figure out all the possible states your application can run into and provide methods to update the GUI accordingly.

Note: When you disable a PyQt widget or graphical component, the library grays out the widget at hand and makes it unresponsive to the user’s events, such as a click and a keypress.

In this tutorial, you’ll use the second approach, which might be more intuitive and user-friendly. To do that, you need to figure out the possible states the application can run into. Here’s a first approach to this problem:

State Description Method No files loaded The application is running, and the list of Files to Rename is empty. ._updateStateWhenNoFiles() Files loaded The list of Files to Rename contains one or more files. ._updateStateWhenFilesLoaded() Ready to rename files The list of Files to Rename contains one or several files, and the user has provided a filename prefix. ._updateStateWhenReady() Renaming files The user has clicked Rename, and the file renaming process has started. ._updateStateWhileRenaming() Renaming done The file renaming process has finished, and there are no left files to rename. ._updateStateWhenNoFiles()

Since the first and the last states in the above table are quite similar, you’ll use the same method to update the GUI. That means you only have to implement four methods.

To start coding these methods, get back to views.py and add the following code to Window:

 1# rprename/views.py
 2# Snip...
 3
 4class Window(QWidget, Ui_Window):
 5    # Snip...
 6    def _setupUI(self):
 7        self.setupUi(self)
 8        self._updateStateWhenNoFiles()
 9
10    def _updateStateWhenNoFiles(self):
11        self._filesCount = len(self._files)
12        self.loadFilesButton.setEnabled(True)
13        self.loadFilesButton.setFocus(True)
14        self.renameFilesButton.setEnabled(False)
15        self.prefixEdit.clear()
16        self.prefixEdit.setEnabled(False)
17
18   # Snip...
19   def _runRenamerThread(self):
20        # Snip...
21        self._renamer.progressed.connect(self._updateProgressBar)
22        self._renamer.finished.connect(self._updateStateWhenNoFiles)
23        # Snip...

Here’s how this update works:

  • Line 8 calls ._updateStateWhenNoFiles() from inside ._setupUI(). This updates the GUI when you launch the application.
  • Line 10 defines ._updateStateWhenNoFiles().
  • Line 11 updates the total number of files. Under this state, len(self._files) returns 0.
  • Line 12 enables the Load Files button so it accepts the user’s events.
  • Line 13 moves the focus to the Load Files button. This way, users can press Space on their keyboard to load files into the application.
  • Line 14 disables the Rename button. The button is grayed out and unresponsive.
  • Line 15 clears the Filename Prefix line edit. This removes any previously supplied filename prefix.
  • Line 16 disables the Filename Prefix line edit. This way, the user won’t be able to type in a filename prefix if there are no files in the application.
  • Line 22 connects the Renamer instance’s .finished() signal with ._updateStateWhenNoFiles(). This updates the GUI once the file renaming process finishes.

When you run the application, you can press Space on your keyboard to launch the dialog and select the files you want to rename. Also, the Filename Prefix line edit and the Rename button are disabled so you can’t perform actions on them.

Now you can code the method to update the GUI when you load files into the application. Add the following code to Window:

# rprename/views.py
# Snip...

class Window(QWidget, Ui_Window):
    # Snip...
    def loadFiles(self):
        if len(files) > 0:
            # Snip...
            self._updateStateWhenFilesLoaded()

    def _updateStateWhenFilesLoaded(self):
        self.prefixEdit.setEnabled(True)
        self.prefixEdit.setFocus(True)

When you load one or more files into the application, .loadFiles() calls ._updateStateWhenFilesLoaded(). This method enables the Filename Prefix line edit so you can enter a prefix to use for renaming files. It also moves the focus to that widget so you can provide a filename prefix immediately.

Next, you can code the method to handle the GUI update when the application is ready to rename a bunch of files. Add the following code to Window:

# rprename/views.py
# Snip...

class Window(QWidget, Ui_Window):
    # Snip...
    def _connectSignalsSlots(self):
        # Snip...
        self.prefixEdit.textChanged.connect(self._updateStateWhenReady)

    def _updateStateWhenReady(self):
        if self.prefixEdit.text():
            self.renameFilesButton.setEnabled(True)
        else:
            self.renameFilesButton.setEnabled(False)

Here, you first connect the Filename Prefix line edit’s .textChanged() signal with ._updateStateWhenReady(). This method enables or disables the Rename button according to the content of the Filename Prefix line edit. This way, when the line edit is empty, the button gets disabled, and otherwise it’s enabled.

Finally, you need to code the method to handle the GUI update when the application is renaming your files. Go ahead and add the following code to Window:

class Window(QWidget, Ui_Window):
    # Snip...
    def renameFiles(self):
        self._runRenamerThread()
        self._updateStateWhileRenaming()

    def _updateStateWhileRenaming(self):
        self.loadFilesButton.setEnabled(False)
        self.renameFilesButton.setEnabled(False)

When your user clicks Rename, the application starts the file renaming process and calls ._updateStateWhileRenaming() to update the GUI accordingly. The method disables the Load Files and Rename buttons so the user can’t click them while the renaming process is running.

That’s it! If you run the application now, then you’ll get the following behavior:

Your bulk file rename tool’s GUI now reflects the application’s state at any given time. This allows you to offer your users an intuitive, frustration-free experience. Great job!

Conclusion

Automatically renaming multiple files is a common problem when you’re organizing your personal files and folders. In this tutorial, you built a real-world GUI application to perform this task quickly and efficiently. By building this tool, you applied a wide range of skills related to creating GUI applications with PyQt and Qt Designer. You also worked with files using Python’s pathlib.

In this tutorial, you learned how to:

  • Build the GUI of a bulk file rename tool using Qt Designer
  • Use PyQt threads to offload the bulk file renaming process
  • Manage system paths and rename files with pathlib
  • Update the GUI state according to the file renaming process

You can download the final source code for the bulk file rename project by clicking the link below:

Get the Source Code: Click here to get the source code you’ll use to build a bulk file rename tool with Python in this tutorial.

Next Steps

Up to this point, you’ve built a real-world application to automate the process of renaming multiples files. Even though the application provides a minimal set of features, it’s a good starting point for you to continue adding features and learning along the way. This will help you take your skills with Python and PyQt GUI applications to the next level.

Here are some ideas you can implement to continue improving the project:

  • Add support for additional file types: You can add support for new file types, such as .bmp, .docx, .xlsx, or any other file type you need to work with. To do that, you can extend the FILTERS constant.

  • Generate date-based or random filename suffixes: You can also change the way you generate the filename suffix. Instead of using consecutive integer numbers, you can provide date-based suffixes using datetime, or you can provide random suffixes.

  • Generate an executable for the application: You can use PyInstaller or any other tool to generate an executable for your bulk file rename tool. This way, you’ll be able to share the application with your friends and colleagues.

These are just a few ideas of how to continue adding features to your bulk file rename tool. Take the challenge and build something amazing!

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Leodanis Pozo Ramos

Leodanis is an industrial engineer who loves Python and software development. He's a self-taught Python developer with 5+ years of experience.

» More about Leodanis

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills
With Unlimited Access to Real Python

lesson-locked.f5105cfd26db.svg

Join us and get access to hundreds of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Real Python Comment Policy: The most useful comments are those written with the goal of learning from or helping out other readers—after reading the whole article and all the earlier comments. Complaints and insults generally won’t make the cut here.

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Keep Learning

Related Tutorial Categories: gui intermediate projects


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK