Migrating to Qt6/PyQt6: what are all the deprecated short-form names in Qt5?
Asked Answered
B

2

6

I'm trying to migrate a codebase from PyQt5 to PyQt6. I read in this article (see https://www.pythonguis.com/faq/pyqt5-vs-pyqt6/) that all enum members must be named using their fully qualified names. The article gives this example:

# PyQt5
widget = QCheckBox("This is a checkbox")
widget.setCheckState(Qt.Checked)
# PyQt6
widget = QCheckBox("This is a checkbox")
widget.setCheckState(Qt.CheckState.Checked)

Then the article continues:

"There are too many updated values to mention them all here. But if you're converting a codebase you can usually just search online for the short-form and the longer form will be in the results."

I get the point. This quote basically says something along the lines:

"If the Python interpreter runs into an error, and the error turns out to be a short-form enum, you'll likely find the solution online."

I get that. But this is not how I want to migrate the codebase. I want a full list of all the short-form enums and then perform a global search-and-replace for each.

Where can I find such a list?

Blither answered 2/5, 2022 at 12:21 Comment(3)
See no such a list, it's like asking for a list of all methids that return integers, you've to go through all enums and flags of each class, or create a script that will do that and eventually parse/replace them in the source.Antipus
Can I assume that all enums start with Qt. as for example Qt.Checked?Blither
No. The Qt.* is only the "global" namespace for generic flags/enums (and some helper functions), then each class has its own internal flags and enums that are specific for it, like QTabWidget.TabShape, or QAbstractItemView.ScrollMode. Maybe you could write a script that would iterate through all Qt modules and their __dict__ recursively, looking for enum objects.Antipus
B
10

I wrote a script to extract all the short-form and corresponding fully qualified enum names from the PyQt6 installation. It then does the conversions automatically:

# -*- coding: utf-8 -*-
# ================================================================================================ #
#                                       ENUM CONVERTER TOOL                                        #
# ================================================================================================ #

from typing import *
import os, argparse, inspect, re
q = "'"

help_text = '''
Copyright (c) 2022 Kristof Mulier
MIT licensed, see bottom

ENUM CONVERTER TOOL
===================
The script starts from the toplevel directory (assuming that you put this file in that directory)
and crawls through all the files and folders. In each file, it searches for old-style enums to
convert them into fully qualified names.

HOW TO USE
==========
Fill in the path to your PyQt6 installation folder. See line 57:

    pyqt6_folderpath = 'C:/Python39/Lib/site-packages/PyQt6'

Place this script in the toplevel directory of your project. Open a terminal, navigate to the
directory and invoke this script:

    $ python enum_converter_tool.py
    
WARNING
=======
This script modifies the files in your project! Make sure to backup your project before you put this
file inside. Also, you might first want to do a dry run:

    $ python enum_converter_tool.py --dry_run
    
FEATURES
========
You can invoke this script in the following ways:

    $ python enum_converter_tool.py                   No parameters. The script simply goes through
                                                      all the files and makes the replacements.
                                                      
    $ python enum_converter_tool.py --dry_run         Dry run mode. The script won't do any replace-
                                                      ments, but prints out what it could replace.
                                                      
    $ python enum_converter_tool.py --show            Print the dictionary this script creates to
                                                      convert the old-style enums into new-style.
                                                      
    $ python enum_converter_tool.py --help            Show this help info

'''

# IMPORTANT: Point at the folder where PyQt6 stub files are located. This folder will be examined to
# fill the 'enum_dict'.
# pyqt6_folderpath = 'C:/Python39/Lib/site-packages/PyQt6'
# EDIT: @Myridium suggested another way to fill this 'pyqt6_folderpath'
# variable:
import PyQt6
pyqt6_folderpath = PyQt6.__path__[0]

# Figure out where the toplevel directory is located. We assume that this converter tool is located
# in that directory. An os.walk() operation starts from this toplevel directory to find and process
# all files.
toplevel_directory = os.path.realpath(
    os.path.dirname(
        os.path.realpath(
            inspect.getfile(
                inspect.currentframe()
            )
        )
    )
).replace('\\', '/')

# Figure out the name of this script. It will be used later on to exclude oneself from the replace-
# ments.
script_name = os.path.realpath(
    inspect.getfile(inspect.currentframe())
).replace('\\', '/').split('/')[-1]

# Create the dictionary that will be filled with enums
enum_dict:Dict[str, str] = {}

def fill_enum_dict(filepath:str) -> None:
    '''
    Parse the given stub file to extract the enums and flags. Each one is inside a class, possibly a
    nested one. For example:

               ---------------------------------------------------------------------
               | class Qt(PyQt6.sip.simplewrapper):                                |
               |     class HighDpiScaleFactorRoundingPolicy(enum.Enum):            |
               |         Round = ... # type: Qt.HighDpiScaleFactorRoundingPolicy   |
               ---------------------------------------------------------------------

    The enum 'Round' is from class 'HighDpiScaleFactorRoundingPolicy' which is in turn from class
    'Qt'. The old reference style would then be:
        > Qt.Round

    The new style (fully qualified name) would be:
        > Qt.HighDpiScaleFactorRoundingPolicy.Round

    The aim of this function is to fill the 'enum_dict' with an entry like:
    enum_dict = {
        'Qt.Round' : 'Qt.HighDpiScaleFactorRoundingPolicy.Round'
    }
    '''
    content:str = ''
    with open(filepath, 'r', encoding='utf-8', newline='\n', errors='replace') as f:
        content = f.read()

    p = re.compile(r'(\w+)\s+=\s+\.\.\.\s+#\s*type:\s*([\w.]+)')
    for m in p.finditer(content):
        # Observe the enum's name, eg. 'Round'
        enum_name = m.group(1)

        # Figure out in which classes it is
        class_list = m.group(2).split('.')

        # If it belongs to just one class (no nesting), there is no point in continuing
        if len(class_list) == 1:
            continue

        # Extract the old and new enum's name
        old_enum = f'{class_list[0]}.{enum_name}'
        new_enum = ''
        for class_name in class_list:
            new_enum += f'{class_name}.'
            continue
        new_enum += enum_name

        # Add them to the 'enum_dict'
        enum_dict[old_enum] = new_enum
        continue
    return

def show_help() -> None:
    '''
    Print help info and quit.
    '''
    print(help_text)
    return

def convert_enums_in_file(filepath:str, dry_run:bool) -> None:
    '''
    Convert the enums in the given file.
    '''
    filename:str = filepath.split('/')[-1]

    # Ignore the file in some cases
    if any(filename == fname for fname in (script_name, )):
        return

    # Read the content
    content:str = ''
    with open(filepath, 'r', encoding='utf-8', newline='\n', errors='replace') as f:
        content = f.read()

    # Loop over all the keys in the 'enum_dict'. Perform a replacement in the 'content' for each of
    # them.
    for k, v in enum_dict.items():
        if k not in content:
            continue
        # Compile a regex pattern that only looks for the old enum (represented by the key of the
        # 'enum_dict') if it is surrounded by bounds. What we want to avoid is a situation like
        # this:
        #     k = 'Qt.Window'
        #     k found in 'qt.Qt.WindowType.Window'
        # In the situation above, k is found in 'qt.Qt.WindowType.Window' such that a replacement
        # will take place there, messing up the code! By surrounding k with bounds in the regex pat-
        # tern, this won't happen.
        p = re.compile(fr'\b{k}\b')

        # Substitute all occurences of k (key) in 'content' with v (value). The 'subn()' method re-
        # turns a tuple (new_string, number_of_subs_made).
        new_content, n = p.subn(v, content)
        if n == 0:
            assert new_content == content
            continue
        assert new_content != content
        print(f'{q}{filename}{q}: Replace {q}{k}{q} => {q}{v}{q} ({n})')
        content = new_content
        continue

    if dry_run:
        return

    with open(filepath, 'w', encoding='utf-8', newline='\n', errors='replace') as f:
        f.write(content)
    return

def convert_all(dry_run:bool) -> None:
    '''
    Search and replace all enums.
    '''
    for root, dirs, files in os.walk(toplevel_directory):
        for f in files:
            if not f.endswith('.py'):
                continue
            filepath = os.path.join(root, f).replace('\\', '/')
            convert_enums_in_file(filepath, dry_run)
            continue
        continue
    return

if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description = 'Convert enums to fully-qualified names',
        add_help    = False,
    )
    parser.add_argument('-h', '--help'    , action='store_true')
    parser.add_argument('-d', '--dry_run' , action='store_true')
    parser.add_argument('-s', '--show'    , action='store_true')
    args = parser.parse_args()
    if args.help:
        show_help()
    else:
        #& Check if 'pyqt6_folderpath' exists
        if not os.path.exists(pyqt6_folderpath):
            print(
                f'\nERROR:\n'
                f'Folder {q}{pyqt6_folderpath}{q} could not be found. Make sure that variable '
                f'{q}pyqt6_folderpath{q} from line 57 points to the PyQt6 installation folder.\n'
            )
        else:
            #& Fill the 'enum_dict'
            type_hint_files = [
                os.path.join(pyqt6_folderpath, _filename)
                for _filename in os.listdir(pyqt6_folderpath)
                if _filename.endswith('.pyi')
            ]
            for _filepath in type_hint_files:
                fill_enum_dict(_filepath)
                continue

            #& Perform requested action
            if args.show:
                import pprint
                pprint.pprint(enum_dict)
            elif args.dry_run:
                print('\nDRY RUN\n')
                convert_all(dry_run=True)
            else:
                convert_all(dry_run=False)
    print('\nQuit enum converter tool\n')


# MIT LICENSE
# ===========
# Copyright (c) 2022 Kristof Mulier
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
# associated documentation files (the "Software"), to deal in the Software without restriction, in-
# cluding without limitation the rights to use, copy, modify, merge, publish, distribute, sublicen-
# se, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to
# do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or substan-
# tial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRIN-
# GEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Make sure you backup your Python project. Then place this file in the toplevel directory of the project. Modify line 57 (!) such that it points to your PyQt6 installation folder.

First run the script with the --dry_run flag to make sure you agree with the replacements. Then run it without any flags.

Blither answered 17/6, 2022 at 10:50 Comment(15)
A much better solution would be to monkey-patch PyQt6 so that all the enforced scoped-enum kruft is bypassed. PySide6 natively supports both the long and short forms, which seems the only sane solution. Unfortunately, the author of PyQt6 appears resistant to such solutions. The QtPy project uses a monkey-patching approach: see qtpy/enums_compat.py.Fou
I should have perhaps qualified the situation with PySide6: it natively supports both long and short forms for unscoped enums (which is the large majority). For the small number of scoped enums, only the long-form is available. PyQt6 forces all enums to be scoped (without much justification, it would seem, as there aren't any name-clashes).Fou
Nicely written script that worked very well, and much faster than I thought when I first read the code. Unfortunately I had already hand corrected most of the enum references in my 35-module monster app, but the script found several I had missed. Thank you!Heaveho
Very nice script especially to find the scoped enums!Pimply
Glad you liked it @Heaveho and Francesco ^_^Blither
Nice script! @Fou I think the justification for the change is that Qt6 also on the C++ side moves to enum class, and so we get a similar syntax in both Python and C++ . E.g. Qt::SortOrder::AscendingOrder and Qt.SortOrder.AscendingOrder. Over the long haul this will make for example the Qt namespace a lot cleaner.Withoutdoors
@Withoutdoors I was only talking about the difference between PyQt6 and PySide6. In the meantime, PySide6 has also chosen to force unscoped enums to be scoped, so the point is now moot. Even so, there's still no real justification for enforcing scoped enums where Qt doesn't use them. The vast majority of Qt enums still aren't scoped - so if it's good enough for Qt (in the here and now), it ought to be good enough for PyQt/PySide.Fou
@Withoutdoors Also, your claim about making code cleaner seems dubious. In actual fact, it requires a lot of extra boilerplate code to deal with the loss of implicit type conversions. It might make ensuring type-safety a little easier, but that comes at a relatively large cost in the long run (especially in Python, where there's also a significant performance penalty to pay as well). The question is, how often are name clashes an insurmountable problem that requires such a far-reaching and heavyweight solution?Fou
@Fou I was mostly talking about Qt for Python (aka PySide6) and Qt C++, the officially supported versions, see also the official design guidelines wiki.qt.io/API_Design_Principles#Naming_Enum_Types_and_Values enums in global namespace vs inside a class. Btw. you still can use unscoped QEnums in PySide6 e.g. class MyEnum(IntEnum): Foo = 1 and class MyObject(QObject): QEnum(MyEnum) (Sorry don't know how to make a code block here in the comment).Withoutdoors
@Withoutdoors Well, those API Design Priciples prove my point, since they make the case for using both scoped and unscoped enums. It even goes so far as to state that using scoped enums when not strictly necessary "would add redundant line noise" - which is exactly what many Python coders object to. Qt uses both scoped and unscoped enums in its public APIs, but PySide/PyQt forces all those APIs to be scoped. There's no justification for adopting this policy, given that Qt does not follow it itself.Fou
@Fou As previously mentioned PySide doesn't force anything. QEnum just registers the enum with the Qt meta-type system, you can still use your enums the way you want, scoped or unscoped. I don't know what PyQt6 does. Where Qt moved on to scoped enums is in the global namespaces such as Qt.SortOrder. Sounds reasonable to me. In PySide6 the old syntax using unscoped enums from the Qt namespace also still works.Withoutdoors
@Withoutdoors QEnum has no relevance at all. The subject of this question is the PyQt6 treatment of Qt's public enums. PyQt6 forces all those public enums to be scoped, regardless of what Qt itself does. If PySide6 has now decided to support both long- and short-form access to those enums, that's nice, but it's no help to PyQt6 users. If you don't know PyQt6, I'm not sure why you're commenting here, because this question is purely about migrating from PyQt5 to PyQt6. This requires changing all enum access to be scoped, regardless of whether the Qt6 enums are scoped.Fou
@Fou I just wanted to share why I think Qt for Python (PySide6) introduced scoped enums which you mentioned and found superior to the PyQt6 implementation forcing scoped enums. Your comment also doesn't really help PyQt6 users and still it might be useful for people considering to chose PyQt6 vs PySide6. This comment thread is primarily about the migration script which is useful for PySide6 as well (with some modifications).Withoutdoors
Thanks for sharing @K.Mulier! May I suggest an alteration? Replacing 'line 57' with import PyQt6; pyqt_folderpath = PyQt6.__path__[0].Oswald
Hi @Myridium, thanks for your suggestion. This is a great solution! I modified my code accordingly.Blither
A
0

You can also use the PyPi package PyQtEnumConverter and it does the trick by converting all the enums from PyQt5 to PyQt6.

Ascanius answered 4/6, 2024 at 13:40 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.