Column
1. Idea
PyQt is a quite flexible UI framework, but the Python version of Qt has never had a good unit testing tool for UI.
The logic layer in PyQt is connected using signals and slots. We can dynamically generate a unit test script by intercepting and reconstructing the signals and slots. Based on this idea, I wrote a unit testing tool. If there is enough demand, I will turn this module into a unit testing framework.
2. Demo
A good tool should be non-intrusive, with reasonable interfaces and standardized naming that aligns with most users’ habits. I believe a PyQt unit test case should look like this.
The main action is to trigger the button click practice according to the calling chain of the signals and slots connected in the source code, executing the logic after clicking the button.
In the code above, the entry for the unit test is the startup code for the interface, which is the test_start_main_ui
function. This code is the simplest PyQt interface startup code, with the difference being that it starts a thread for executing the unit tests.
The unit test function is like this: first, it initializes a class instance with the parameter view
. This class is used to intercept signals and execute signal actions, which I named Knife.
Next, it executes the click event of the target_button
under view
. This series of member functions is dynamically generated based on the original view’s signal-slot connection code, which will be explained in detail later.
After triggering the click event, the result is displayed on a label, and we simply assert whether this result is correct.
The GIF is a demonstration example where a number is input in the QLineEdit
, and pressing the -1s button (QPushButton
) will display the number minus one on the label on the far right. See here for the demo GUI part of the code.
3. Qt and PyQt
The signal-slot mechanism is an indispensable concept in Qt, forming the basis of Qt’s components alongside the meta-object system. However, many of these concepts originate from the ancient era of Qt, designed to compensate for the shortcomings of C++. For a strongly-typed language like Python, these elements are not as essential. For example, signals and slots are essentially the observer pattern, which can be implemented independently. My own implementation can be seen here.
The Qt meta-object system is a code generation framework that provides introspection capabilities for C++. However, Python, being a dynamic language, already offers powerful introspection at the language level. Therefore, when I use PyQt, I generally treat it as a UI library, while utilizing Python versions for other functionalities such as threading, signals and slots, and serial ports.
4. Implementation of Interception
In PyQt, the writing style for connecting signals and slots is generally like this:
<span><span>signal_instance</span><span>.</span><span>connect</span><span>(</span><span>slot_name</span><span>)</span></span>
Thus, the implementation idea for my version of the signal-slot interception function is to use regular expressions to match the source code and parse the sender of the signal and the slot function from statements that fit this pattern, then re-add the slot function into the newly generated custom signal-slot.
Reconnecting signals and slots
As previously mentioned, Python’s introspection capabilities are powerful. A very practical example is that in Python, you can dynamically obtain the source code. This feature uses the inspect library from the Python standard library, as shown below:
<span><span>import</span><span> inspect</span></span>
<span><span>print</span><span> inspect</span><span>.</span><span>getsource</span><span>(</span><span>inspect</span><span>.</span><span>isclass</span><span>)</span></span>
This code prints the source code of the isclass
function from the inspect library. The inspect module is quite magical; if you do not understand closures and coroutines, you can also call the corresponding code in this module to see.
The program also uses code.co_names to efficiently check if the string “connect” exists in the function’s source code.
5. Program Structure
Here is a portion of the program’s source code, omitting code details; the complete source code can be found in this Git repository.
A class called Knife is used to implement this. When reconstructing the new signal function, I want the calling method of the signal function to remain consistent with the calling method in the program’s source code, so a dynamic generation method is required. For dynamic generation involving class members, it is better to adopt a different writing style, such as moving the generation time from the __init__ method to the __new__ method.
The widget_instance
class contains the signals and slots, as I write GUIs using the MVC pattern. All signals and slots that need to be exported and intercepted are within one class, and this class is already an instance when passed in. The source code of this instance is dynamically parsed, and new signals are dynamically generated to load the slots.
Additionally, there is a problem where some calls may be several layers deep, like this:
<span><span>self</span><span>.</span><span>mother</span><span>.</span><span>father</span><span>.</span><span>son</span><span>.</span><span>dog</span><span>.</span><span>clicked</span><span>()</span></span>
This kind of operation requires recursive generation, as shown here.
The custom generated node class in the calling chain is SubNode, and if the slot function cannot be dynamically obtained, it will return a custom exception FailAttr.
For specifics, please check GitHub.
6. Detailed Knowledge Points
This section lists some special knowledge points.
1. getattr
, setattr
, hasattr
: Dynamically obtain methods of an object, dynamically add methods to an object, and check if an object has a specific method.
2. __new__
magic method: This method occurs before __init__
and is the true class initialization function. Note that the new
method must return a class instance, just like in the source code. Also, you cannot use instance methods in the __new__
method; you need to use staticmethod and classmethod decorators.
3. staticmethod, classmethod: Both are decorators for class methods, but the first parameter of a classmethod decorated member method is cls
, while a staticmethod does not introduce this parameter, functioning as a pure function. Both functions can be decorated with classmethod, but since cls
is not used in the parser_slots
function, I used staticmethod for decoration.
4. Using class methods to distinguish special operations is most commonly seen in Django’s ORM, which separates database operations and form definitions into class methods and member methods. Therefore, when people struggle to understand class methods and metaclasses, they can study Django’s ORM.
5. I won’t explain list comprehensions and regular expressions here.
It feels a bit lengthy; if there are other details needed, I will explain them in the next article. If there is a real need, we can consider making it a dedicated open-source project.

Long press to scan and follow the Python Chinese community,
to get more technical content!
Python Chinese Community
The spiritual home of Python Chinese developers
For cooperation and submissions, please contact WeChat:
pythonpost